first commit
This commit is contained in:
commit
5e80a2669e
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# OS / IDE files
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Local virtual environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
37
README.md
Normal file
37
README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# POS Ojol Discount Amount
|
||||||
|
|
||||||
|
Adds a custom global discount line workflow tailored for Ojol (online delivery apps like GoFood, GrabFood, ShopeeFood) in Odoo 19 Point of Sale.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Pricelist Integration**: Uses Ojol's specific menu pricing when active.
|
||||||
|
- **Global Order Discount**: Allows the cashier to apply a single, global order-level discount amount via a custom dialog (by pressing the discount/Rp button).
|
||||||
|
- **Automated Discount Line**: Generates a single negative-amount "Diskon Ojol" service line that scales across your order.
|
||||||
|
- **Configurable Per POS Config**: Set a specific discount product per POS configuration in the settings interface.
|
||||||
|
- **Robust Field & Model Protection**: Seamlessly loads all necessary relational schemas (`company_id`, etc.) and handles special service product templates, preventing initialization crashes in POS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Go to **Point of Sale** > **Configuration** > **Settings**.
|
||||||
|
2. Locate the **Ojol Discount** section.
|
||||||
|
3. Select or create an **Ojol Discount Product**.
|
||||||
|
- *Note: This product should be a Service product with the same tax configuration as your menu items to ensure correct tax calculations.*
|
||||||
|
4. Save the settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Open a new POS Session.
|
||||||
|
2. Select the Ojol pricelist.
|
||||||
|
3. Add items to the cart.
|
||||||
|
4. Click the custom discount button (`Rp`) on the numpad.
|
||||||
|
5. Enter the target discount amount in the custom popup dialog and click apply.
|
||||||
|
6. A negative-priced order line with the selected **Ojol Discount Product** will be appended to the cart.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
Released under the [LGPL-3](https://www.gnu.org/licenses/lgpl-3.0.html) License.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
33
__manifest__.py
Normal file
33
__manifest__.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "POS Ojol Discount Amount",
|
||||||
|
"version": "19.0.2.0.0",
|
||||||
|
"category": "Point of Sale",
|
||||||
|
"summary": "Global Ojol discount line in POS — configurable discount product per POS config",
|
||||||
|
"author": "Suherdy Yacob",
|
||||||
|
"description": """
|
||||||
|
Adds a configurable Ojol discount workflow to the POS App.
|
||||||
|
When the Ojol pricelist is active:
|
||||||
|
- Products are priced at their full Ojol pricelist price.
|
||||||
|
- The cashier presses the "Rp" numpad button to enter a total order discount.
|
||||||
|
- A single "Diskon Ojol" negative-price line is added to the order.
|
||||||
|
- The Ojol discount product is configurable per POS config in Settings.
|
||||||
|
""",
|
||||||
|
"depends": ["point_of_sale"],
|
||||||
|
"data": [
|
||||||
|
"views/pos_config_views.xml",
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"point_of_sale._assets_pos": [
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/models/product_pricelist.js",
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/models/pos_config.js",
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/models/pos_order.js",
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/models/orderline.js",
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/components/ojol_discount_dialog.xml",
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/components/ojol_discount_dialog.js",
|
||||||
|
"pos_ojol_discount_amount/static/src/overrides/screens/product_screen.js",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"installable": True,
|
||||||
|
"auto_install": False,
|
||||||
|
"license": "LGPL-3"
|
||||||
|
}
|
||||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
4
models/__init__.py
Normal file
4
models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import pos_order
|
||||||
|
from . import pos_config
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import product_template
|
||||||
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/pos_order.cpython-312.pyc
Normal file
BIN
models/__pycache__/pos_order.cpython-312.pyc
Normal file
Binary file not shown.
14
models/pos_config.py
Normal file
14
models/pos_config.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class PosConfig(models.Model):
|
||||||
|
_inherit = 'pos.config'
|
||||||
|
|
||||||
|
ojol_discount_product_id = fields.Many2one(
|
||||||
|
'product.product',
|
||||||
|
string='Ojol Discount Product',
|
||||||
|
help='Product used to create the Ojol discount line in POS orders. '
|
||||||
|
'Should be a service product with the same tax configuration as menu items.',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
19
models/pos_order.py
Normal file
19
models/pos_order.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class PosOrderLine(models.Model):
|
||||||
|
_inherit = 'pos.order.line'
|
||||||
|
|
||||||
|
is_ojol_discount = fields.Boolean(
|
||||||
|
string='Is Ojol Discount Line',
|
||||||
|
default=False,
|
||||||
|
help='If True, this line is the global Ojol discount line.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _load_pos_data_fields(self, config):
|
||||||
|
fields_list = super()._load_pos_data_fields(config)
|
||||||
|
for f in ('is_ojol_discount',):
|
||||||
|
if f not in fields_list:
|
||||||
|
fields_list.append(f)
|
||||||
|
return fields_list
|
||||||
15
models/product_template.py
Normal file
15
models/product_template.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, models
|
||||||
|
from odoo.fields import Domain
|
||||||
|
|
||||||
|
class ProductTemplate(models.Model):
|
||||||
|
_inherit = 'product.template'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _load_pos_data_domain(self, data, config):
|
||||||
|
domain = super()._load_pos_data_domain(data, config)
|
||||||
|
if config.ojol_discount_product_id:
|
||||||
|
# Ensure the discount product's template is loaded regardless of other domain constraints
|
||||||
|
discount_tmpl_domain = [('id', '=', config.ojol_discount_product_id.product_tmpl_id.id)]
|
||||||
|
domain = Domain.OR([domain, discount_tmpl_domain])
|
||||||
|
return domain
|
||||||
13
models/res_config_settings.py
Normal file
13
models/res_config_settings.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
pos_ojol_discount_product_id = fields.Many2one(
|
||||||
|
'product.product',
|
||||||
|
related='pos_config_id.ojol_discount_product_id',
|
||||||
|
string='Ojol Discount Product',
|
||||||
|
readonly=False,
|
||||||
|
help='Product used to create the Ojol discount line in POS orders.',
|
||||||
|
)
|
||||||
75
static/src/overrides/components/ojol_discount_dialog.js
Normal file
75
static/src/overrides/components/ojol_discount_dialog.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component, useState, onMounted } from "@odoo/owl";
|
||||||
|
import { Dialog } from "@web/core/dialog/dialog";
|
||||||
|
import { formatCurrency } from "@web/core/currency";
|
||||||
|
|
||||||
|
export class OjolDiscountDialog extends Component {
|
||||||
|
static template = "pos_ojol_discount_amount.OjolDiscountDialog";
|
||||||
|
static components = { Dialog };
|
||||||
|
static props = {
|
||||||
|
/** Called with the final amount (number) or null (cancel) */
|
||||||
|
getPayload: Function,
|
||||||
|
close: Function,
|
||||||
|
/** Current discount amount already applied (0 if none) */
|
||||||
|
currentAmount: { type: Number, optional: true },
|
||||||
|
/** Currency id for formatting */
|
||||||
|
currencyId: { type: Number, optional: true },
|
||||||
|
};
|
||||||
|
static defaultProps = {
|
||||||
|
currentAmount: 0,
|
||||||
|
currencyId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({
|
||||||
|
rawInput: this.props.currentAmount > 0 ? String(this.props.currentAmount) : "",
|
||||||
|
parsed: this.props.currentAmount || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const input = document.getElementById("ojol_discount_input");
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatAmount(amount) {
|
||||||
|
if (this.props.currencyId) {
|
||||||
|
return formatCurrency(amount, this.props.currencyId);
|
||||||
|
}
|
||||||
|
return `Rp ${amount.toLocaleString("id-ID")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(ev) {
|
||||||
|
const raw = ev.target.value;
|
||||||
|
this.state.rawInput = raw;
|
||||||
|
const parsed = parseFloat(raw);
|
||||||
|
this.state.parsed = isNaN(parsed) || parsed < 0 ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeydown(ev) {
|
||||||
|
if (ev.key === "Enter" && this.state.parsed > 0) {
|
||||||
|
this.onConfirm();
|
||||||
|
} else if (ev.key === "Escape") {
|
||||||
|
this.onCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirm() {
|
||||||
|
this.props.getPayload(this.state.parsed);
|
||||||
|
this.props.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove() {
|
||||||
|
this.props.getPayload(0);
|
||||||
|
this.props.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.props.getPayload(null);
|
||||||
|
this.props.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
static/src/overrides/components/ojol_discount_dialog.xml
Normal file
52
static/src/overrides/components/ojol_discount_dialog.xml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="pos_ojol_discount_amount.OjolDiscountDialog">
|
||||||
|
<Dialog title="'Diskon Ojol'" size="'md'" contentClass="'ojol-discount-dialog p-3'">
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<p class="mb-1 text-muted">
|
||||||
|
Enter the total discount amount for this Ojol order.
|
||||||
|
This will appear as a separate <strong>Diskon Ojol</strong> line.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text fw-bold">Rp</span>
|
||||||
|
<input
|
||||||
|
id="ojol_discount_input"
|
||||||
|
type="number"
|
||||||
|
class="form-control form-control-lg text-end"
|
||||||
|
t-att-value="state.rawInput"
|
||||||
|
min="0"
|
||||||
|
step="1000"
|
||||||
|
placeholder="0"
|
||||||
|
t-on-input="onInput"
|
||||||
|
t-on-keydown="onKeydown"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-if="state.parsed > 0">
|
||||||
|
<div class="alert alert-info py-2 mb-0">
|
||||||
|
Discount: <strong t-esc="formatAmount(state.parsed)"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-if="props.currentAmount > 0">
|
||||||
|
<div class="text-muted small">
|
||||||
|
Current discount: <span t-esc="formatAmount(props.currentAmount)"/>
|
||||||
|
(will be replaced)
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-set-slot="footer">
|
||||||
|
<button class="btn btn-secondary" t-on-click="onCancel">Cancel</button>
|
||||||
|
<t t-if="props.currentAmount > 0">
|
||||||
|
<button class="btn btn-outline-danger" t-on-click="onRemove">Remove Discount</button>
|
||||||
|
</t>
|
||||||
|
<button class="btn btn-primary" t-on-click="onConfirm" t-att-disabled="state.parsed <= 0">
|
||||||
|
Apply Discount
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
</Dialog>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
16
static/src/overrides/components/orderline.xml
Normal file
16
static/src/overrides/components/orderline.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
<t t-inherit="point_of_sale.Orderline" t-inherit-mode="extension">
|
||||||
|
<xpath expr="//li[@t-if='vals.discount']" position="replace">
|
||||||
|
<li t-if="vals.discount" class="price-per-unit">
|
||||||
|
<i class="fa fa-tag pe-1"/>
|
||||||
|
<t t-if="line.order_id and line.order_id.pricelist_id and line.order_id.pricelist_id.name == 'Ojol'">
|
||||||
|
<em>Discount <t t-esc="vals.discount"/> </em> off on <t t-esc="vals.noDiscountPrice"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<em><t t-esc="vals.discount" />% </em> discount off on <t t-esc="vals.noDiscountPrice"/>
|
||||||
|
</t>
|
||||||
|
</li>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
35
static/src/overrides/models/orderline.js
Normal file
35
static/src/overrides/models/orderline.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
||||||
|
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register extra fields on PosOrderline so the model store
|
||||||
|
* knows about our custom columns.
|
||||||
|
*/
|
||||||
|
patch(PosOrderline, {
|
||||||
|
extraFields: {
|
||||||
|
...(PosOrderline.extraFields || {}),
|
||||||
|
is_ojol_discount: {
|
||||||
|
model: "pos.order.line",
|
||||||
|
name: "is_ojol_discount",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
patch(PosOrder.prototype, {
|
||||||
|
/**
|
||||||
|
* Override getTotalDiscount to include the Ojol global discount line.
|
||||||
|
*/
|
||||||
|
getTotalDiscount() {
|
||||||
|
const base = super.getTotalDiscount();
|
||||||
|
const ojolLine = this.lines.find((l) => l.is_ojol_discount);
|
||||||
|
if (ojolLine) {
|
||||||
|
return base + Math.abs(ojolLine.price_unit * ojolLine.qty);
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
},
|
||||||
|
});
|
||||||
16
static/src/overrides/models/pos_config.js
Normal file
16
static/src/overrides/models/pos_config.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { PosConfig } from "@point_of_sale/app/models/pos_config";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(PosConfig, {
|
||||||
|
extraFields: {
|
||||||
|
...(PosConfig.extraFields || {}),
|
||||||
|
ojol_discount_product_id: {
|
||||||
|
model: "pos.config",
|
||||||
|
name: "ojol_discount_product_id",
|
||||||
|
type: "many2one",
|
||||||
|
relation: "product.product",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
68
static/src/overrides/models/pos_order.js
Normal file
68
static/src/overrides/models/pos_order.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(PosOrder.prototype, {
|
||||||
|
/**
|
||||||
|
* Returns the existing Ojol discount orderline, or null.
|
||||||
|
*/
|
||||||
|
getOjolDiscountLine() {
|
||||||
|
return this.lines.find((l) => l.is_ojol_discount) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current Ojol discount amount (0 if none).
|
||||||
|
*/
|
||||||
|
getOjolDiscount() {
|
||||||
|
const line = this.getOjolDiscountLine();
|
||||||
|
return line ? Math.abs(line.price_unit * line.qty) : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update the Ojol global discount line.
|
||||||
|
* @param {number} amount Positive discount amount (Rp). Pass 0 to remove.
|
||||||
|
*/
|
||||||
|
setOjolDiscount(amount) {
|
||||||
|
// Remove any existing Ojol discount line
|
||||||
|
const existing = this.getOjolDiscountLine();
|
||||||
|
if (existing) {
|
||||||
|
existing.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discProduct = this.config.ojol_discount_product_id;
|
||||||
|
if (!discProduct) {
|
||||||
|
console.warn(
|
||||||
|
"[OjolDiscount] No Ojol discount product configured in POS settings."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The discount product may be a product.product or product.template
|
||||||
|
// config field is Many2one to product.product
|
||||||
|
const productVariant = discProduct;
|
||||||
|
const productTmpl = productVariant.product_tmpl_id;
|
||||||
|
|
||||||
|
if (!productTmpl) {
|
||||||
|
console.warn("[OjolDiscount] Cannot resolve product template for discount product.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create negative-price discount line
|
||||||
|
this.models["pos.order.line"].create({
|
||||||
|
order_id: this,
|
||||||
|
product_id: productVariant,
|
||||||
|
product_tmpl_id: productTmpl,
|
||||||
|
price_unit: -Math.abs(amount),
|
||||||
|
qty: 1,
|
||||||
|
discount: 0,
|
||||||
|
price_type: "manual",
|
||||||
|
full_product_name: "Diskon Ojol",
|
||||||
|
is_ojol_discount: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
49
static/src/overrides/models/product_pricelist.js
Normal file
49
static/src/overrides/models/product_pricelist.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { ProductPricelist } from "@point_of_sale/app/models/product_pricelist";
|
||||||
|
import { ProductTemplate } from "@point_of_sale/app/models/product_template";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch ProductPricelist to ensure rule indexes are always up-to-date.
|
||||||
|
* The lazy-recompute guard (_indexedItemCount) ensures that if items are loaded
|
||||||
|
* after the pricelist is set up, the indexes will be recomputed on first use.
|
||||||
|
*/
|
||||||
|
patch(ProductPricelist.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this._indexedItemCount = this.item_ids?.length || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryRulesIds(categoryIds) {
|
||||||
|
if (this._indexedItemCount !== (this.item_ids?.length || 0)) {
|
||||||
|
this.computeRuleIndexes();
|
||||||
|
this._indexedItemCount = this.item_ids?.length || 0;
|
||||||
|
}
|
||||||
|
return super.getCategoryRulesIds(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
getGlobalRulesIds() {
|
||||||
|
if (this._indexedItemCount !== (this.item_ids?.length || 0)) {
|
||||||
|
this.computeRuleIndexes();
|
||||||
|
this._indexedItemCount = this.item_ids?.length || 0;
|
||||||
|
}
|
||||||
|
return super.getGlobalRulesIds(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRulesByProductId(productId) {
|
||||||
|
if (this._indexedItemCount !== (this.item_ids?.length || 0)) {
|
||||||
|
this.computeRuleIndexes();
|
||||||
|
this._indexedItemCount = this.item_ids?.length || 0;
|
||||||
|
}
|
||||||
|
return super.getRulesByProductId(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRulesByTmplId(tmplId) {
|
||||||
|
if (this._indexedItemCount !== (this.item_ids?.length || 0)) {
|
||||||
|
this.computeRuleIndexes();
|
||||||
|
this._indexedItemCount = this.item_ids?.length || 0;
|
||||||
|
}
|
||||||
|
return super.getRulesByTmplId(...arguments);
|
||||||
|
},
|
||||||
|
});
|
||||||
66
static/src/overrides/screens/product_screen.js
Normal file
66
static/src/overrides/screens/product_screen.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { OjolDiscountDialog } from "../components/ojol_discount_dialog";
|
||||||
|
import { makeAwaitable, ask } from "@point_of_sale/app/utils/make_awaitable_dialog";
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
|
||||||
|
patch(ProductScreen.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
this.dialog = useService("dialog");
|
||||||
|
},
|
||||||
|
|
||||||
|
getNumpadButtons() {
|
||||||
|
const buttons = super.getNumpadButtons();
|
||||||
|
const discountButton = buttons.find((b) => b.value === "discount");
|
||||||
|
if (discountButton && this.pos.selectedOrder?.pricelist_id?.name === "Ojol") {
|
||||||
|
discountButton.text = "Rp";
|
||||||
|
discountButton.disabled = false;
|
||||||
|
}
|
||||||
|
return buttons;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override numpad click: intercept "discount" mode in Ojol pricelist
|
||||||
|
* to show the global discount dialog instead of per-line discount.
|
||||||
|
*/
|
||||||
|
async onNumpadClick(buttonValue) {
|
||||||
|
const order = this.pos.selectedOrder;
|
||||||
|
if (
|
||||||
|
buttonValue === "discount" &&
|
||||||
|
order?.pricelist_id?.name === "Ojol"
|
||||||
|
) {
|
||||||
|
await this._showOjolDiscountDialog(order);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return super.onNumpadClick(buttonValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _showOjolDiscountDialog(order) {
|
||||||
|
if (!this.pos.config.ojol_discount_product_id) {
|
||||||
|
await ask(this.dialog, {
|
||||||
|
title: _t("Configuration Missing"),
|
||||||
|
body: _t("Ojol discount product is not configured. Please configure it in POS Settings first."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAmount = order.getOjolDiscount();
|
||||||
|
const currencyId = this.pos.currency?.id;
|
||||||
|
|
||||||
|
const amount = await makeAwaitable(this.dialog, OjolDiscountDialog, {
|
||||||
|
currentAmount,
|
||||||
|
currencyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// null = user cancelled, undefined = closed without action
|
||||||
|
if (amount === null || amount === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
order.setOjolDiscount(amount);
|
||||||
|
},
|
||||||
|
});
|
||||||
35
views/pos_config_views.xml
Normal file
35
views/pos_config_views.xml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Direct configuration on Point of Sale (pos.config form) -->
|
||||||
|
<record id="view_pos_config_form_ojol" model="ir.ui.view">
|
||||||
|
<field name="name">pos.config.form.ojol</field>
|
||||||
|
<field name="model">pos.config</field>
|
||||||
|
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//page[@name='configuration']" position="inside">
|
||||||
|
<group string="Ojol Discount" name="ojol_discount_group">
|
||||||
|
<field name="ojol_discount_product_id"
|
||||||
|
options="{'no_create': True}"
|
||||||
|
placeholder="Select Ojol Discount Product…"/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Configuration on Global Settings (res.config.settings form) -->
|
||||||
|
<record id="res_config_settings_view_form_ojol" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.ojol</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority" eval="95"/>
|
||||||
|
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//setting[@id='pos_global_discount']" position="after">
|
||||||
|
<setting string="Ojol Global Discount" help="Configure the dedicated service product used for order-level Ojol discounts.">
|
||||||
|
<field name="pos_ojol_discount_product_id"
|
||||||
|
options="{'no_create': True}"
|
||||||
|
placeholder="Select Ojol Discount Product…"/>
|
||||||
|
</setting>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user