From 183c3ca6adebfa0023136ed5da4b4c9274cdf5ef Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:56:16 +0500 Subject: [PATCH 01/10] [ADD] feat: implement POS order split payment functionality with configurable limits --- tg_pos_splitpayment/__init__.py | 1 + tg_pos_splitpayment/__manifest__.py | 16 ++ tg_pos_splitpayment/models/__init__.py | 3 + tg_pos_splitpayment/models/pos_config.py | 6 + tg_pos_splitpayment/models/pos_session.py | 9 ++ .../models/res_config_settings.py | 6 + .../static/src/app/actionpad/actionpad.js | 27 ++++ .../static/src/app/actionpad/actionpad.xml | 13 ++ .../split_payment_popup.js | 52 ++++++ .../split_payment_popup.xml | 27 ++++ .../screens/payment_screen/payment_screen.js | 19 +++ .../split_payment_screen.js | 149 ++++++++++++++++++ .../split_payment_screen.xml | 66 ++++++++ .../views/res_config_settings_views.xml | 17 ++ 14 files changed, 411 insertions(+) create mode 100644 tg_pos_splitpayment/__init__.py create mode 100644 tg_pos_splitpayment/__manifest__.py create mode 100644 tg_pos_splitpayment/models/__init__.py create mode 100644 tg_pos_splitpayment/models/pos_config.py create mode 100644 tg_pos_splitpayment/models/pos_session.py create mode 100644 tg_pos_splitpayment/models/res_config_settings.py create mode 100644 tg_pos_splitpayment/static/src/app/actionpad/actionpad.js create mode 100644 tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml create mode 100644 tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.js create mode 100644 tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.xml create mode 100644 tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js create mode 100644 tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js create mode 100644 tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.xml create mode 100644 tg_pos_splitpayment/views/res_config_settings_views.xml diff --git a/tg_pos_splitpayment/__init__.py b/tg_pos_splitpayment/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/tg_pos_splitpayment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/tg_pos_splitpayment/__manifest__.py b/tg_pos_splitpayment/__manifest__.py new file mode 100644 index 00000000..79f08e69 --- /dev/null +++ b/tg_pos_splitpayment/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "POS Split Payment", + "version": "17.0.1.0.0", + "author": "IT-Projects LLC", + "license": "LGPL-3", + "depends": ["point_of_sale", "pos_restaurant"], + "data": [ + "views/res_config_settings_views.xml", + ], + "assets": { + "point_of_sale._assets_pos": [ + "tg_pos_splitpayment/static/src/**/*", + ], + }, + "installable": True, +} diff --git a/tg_pos_splitpayment/models/__init__.py b/tg_pos_splitpayment/models/__init__.py new file mode 100644 index 00000000..58d88f10 --- /dev/null +++ b/tg_pos_splitpayment/models/__init__.py @@ -0,0 +1,3 @@ +from . import pos_config +from . import res_config_settings +from . import pos_session diff --git a/tg_pos_splitpayment/models/pos_config.py b/tg_pos_splitpayment/models/pos_config.py new file mode 100644 index 00000000..b0dbdd4a --- /dev/null +++ b/tg_pos_splitpayment/models/pos_config.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class PosConfig(models.Model): + _inherit = "pos.config" + + pos_max_split_orders = fields.Integer("Max Orders to Split", default=3) diff --git a/tg_pos_splitpayment/models/pos_session.py b/tg_pos_splitpayment/models/pos_session.py new file mode 100644 index 00000000..818961ff --- /dev/null +++ b/tg_pos_splitpayment/models/pos_session.py @@ -0,0 +1,9 @@ +from odoo import models + +class PosSession(models.Model): + _inherit = "pos.session" + + def _loader_params_pos_config(self): + result = super()._loader_params_pos_config() + result['search_params']['fields'].append('pos_max_split_orders') + return result diff --git a/tg_pos_splitpayment/models/res_config_settings.py b/tg_pos_splitpayment/models/res_config_settings.py new file mode 100644 index 00000000..767642dd --- /dev/null +++ b/tg_pos_splitpayment/models/res_config_settings.py @@ -0,0 +1,6 @@ +from odoo import fields, models + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + pos_max_split_orders = fields.Integer(related="pos_config_id.pos_max_split_orders", readonly=False) diff --git a/tg_pos_splitpayment/static/src/app/actionpad/actionpad.js b/tg_pos_splitpayment/static/src/app/actionpad/actionpad.js new file mode 100644 index 00000000..2b4cfbca --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/actionpad/actionpad.js @@ -0,0 +1,27 @@ +/** @odoo-module */ + +import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad"; +import { patch } from "@web/core/utils/patch"; +import { SplitPaymentPopup } from "../popups/split_payment_popup/split_payment_popup"; +import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; +import { _t } from "@web/core/l10n/translation"; + +patch(ActionpadWidget.prototype, { + async onClickSplitPayment() { + const order = this.pos.get_order(); + if (order.get_orderlines().length === 0) { + this.env.services.popup.add(ErrorPopup, { + title: "Empty Order", + body: "There are no products in the order to split.", + }); + return; + } + const { confirmed, payload: splitParts } = await this.env.services.popup.add(SplitPaymentPopup); + if (confirmed) { + this.pos.showScreen("SplitPaymentScreen", { + order: order, + splitParts: splitParts, + }); + } + } +}); diff --git a/tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml b/tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml new file mode 100644 index 00000000..645fe20f --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml @@ -0,0 +1,13 @@ + + + + + + + + + Split Payment + + + + diff --git a/tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.js b/tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.js new file mode 100644 index 00000000..1db7aad8 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.js @@ -0,0 +1,52 @@ +/** @odoo-module */ + +import { AbstractAwaitablePopup } from "@point_of_sale/app/popup/abstract_awaitable_popup"; +import { _t } from "@web/core/l10n/translation"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { useState } from "@odoo/owl"; + +export class SplitPaymentPopup extends AbstractAwaitablePopup { + static template = "tg_pos_splitpayment.SplitPaymentPopup"; + static defaultProps = { + title: _t("Split Payment"), + }; + + setup() { + super.setup(); + this.pos = usePos(); + this.maxSplit = this.pos.config.pos_max_split_orders || 3; + this.state = useState({ + splitParts: 3 > this.maxSplit ? this.maxSplit : 3, + errorMessage: "", + }); + } + + getPayload() { + return this.state.splitParts; + } + + confirm() { + if (this.state.splitParts < 2) { + this.state.errorMessage = _t("Minimum 2 orders required."); + return; + } + if (this.state.splitParts > this.maxSplit) { + this.state.errorMessage = _t(`Maximum allowed is ${this.maxSplit}.`); + return; + } + this.state.errorMessage = ""; + super.confirm(); + } + + increment() { + if (this.state.splitParts < this.maxSplit) { + this.state.splitParts++; + } + } + + decrement() { + if (this.state.splitParts > 2) { + this.state.splitParts--; + } + } +} diff --git a/tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.xml b/tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.xml new file mode 100644 index 00000000..2add313f --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/popups/split_payment_popup/split_payment_popup.xml @@ -0,0 +1,27 @@ + + + + + + + + + How many orders to split into? + + - + + + + + + + + + + diff --git a/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js b/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js new file mode 100644 index 00000000..21c1ebbc --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen"; +import { patch } from "@web/core/utils/patch"; + +patch(PaymentScreen.prototype, { + async _finalizeValidation() { + await super._finalizeValidation(...arguments); + if (this.pos.tgNextSplitOrders && this.pos.tgNextSplitOrders.length > 0) { + const nextOrderUid = this.pos.tgNextSplitOrders.shift(); + // get order from pos.orders + const nextOrder = this.pos.orders.find(o => o.uid === nextOrderUid); + if (nextOrder) { + this.pos.set_order(nextOrder); + this.pos.showScreen("ProductScreen"); + } + } + } +}); diff --git a/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js b/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js new file mode 100644 index 00000000..7b74fef5 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js @@ -0,0 +1,149 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { Component, useState } from "@odoo/owl"; +import { Numpad } from "@point_of_sale/app/generic_components/numpad/numpad"; + +export class SplitPaymentScreen extends Component { + static template = "tg_pos_splitpayment.SplitPaymentScreen"; + static components = { Numpad }; + + setup() { + super.setup(); + this.pos = usePos(); + this.originalOrder = this.props.order; + this.totalAmount = this.originalOrder.get_total_with_tax(); + + const splitParts = this.props.splitParts || 3; + const splits = []; + for (let i = 0; i < splitParts; i++) { + splits.push({ + id: i, + name: `Order ${i + 1}`, + amount: 0, + isFirst: i === 0, + amountStr: "0", + }); + } + splits[0].amount = this.totalAmount; + splits[0].amountStr = this.env.utils.formatCurrency(this.totalAmount); + + this.state = useState({ + splits: splits, + selectedId: 1, // default to select second order + }); + } + + get selectedSplit() { + return this.state.splits.find(s => s.id === this.state.selectedId); + } + + selectSplit(id) { + if (id !== 0) { + this.state.selectedId = id; + } + } + + updateAmount(val) { + const split = this.selectedSplit; + if (!split || split.isFirst) return; + + if (val === "Backspace") { + split.amountStr = split.amountStr.slice(0, -1); + if (split.amountStr === "") split.amountStr = "0"; + } else if (val === "-") { + if (split.amountStr.startsWith("-")) { + split.amountStr = split.amountStr.substring(1); + } else { + split.amountStr = "-" + split.amountStr; + } + } else if (val === "." || val === ",") { + if (!split.amountStr.includes(".")) { + split.amountStr += "."; + } + } else { + if (split.amountStr === "0") { + split.amountStr = val; + } else { + split.amountStr += val; + } + } + + split.amount = parseFloat(split.amountStr) || 0; + this._recalculateFirstOrder(); + } + + _recalculateFirstOrder() { + let otherTotal = 0; + for (let i = 1; i < this.state.splits.length; i++) { + otherTotal += this.state.splits[i].amount; + } + this.state.splits[0].amount = this.totalAmount - otherTotal; + } + + async confirm() { + // Enforce proportional split + const originalLines = this.originalOrder.get_orderlines(); + const splitsToCreate = this.state.splits.filter(s => s.amount !== 0); + + if (splitsToCreate.length === 0) { + this.pos.showScreen("ProductScreen"); + return; + } + + const newOrders = []; + + for (let i = 0; i < splitsToCreate.length; i++) { + const split = splitsToCreate[i]; + const ratio = split.amount / this.totalAmount; + + let order; + if (i === 0) { + // modify original order + order = this.originalOrder; + } else { + // Create new order + order = this.pos.add_new_order(); + } + newOrders.push(order); + } + + // Apply quantities + for (let j = 0; j < originalLines.length; j++) { + const line = originalLines[j]; + const originalQty = line.get_quantity(); + + for (let i = 0; i < splitsToCreate.length; i++) { + const split = splitsToCreate[i]; + const ratio = split.amount / this.totalAmount; + const newQty = originalQty * ratio; + + if (i === 0) { + // Update original line + line.set_quantity(newQty); + } else { + // Add line to new order by cloning + const newLine = line.clone(); + newLine.order = newOrders[i]; + newLine.set_quantity(newQty); + newOrders[i].add_orderline(newLine); + } + } + } + + // Set next orders to show + if (newOrders.length > 1) { + this.pos.tgNextSplitOrders = newOrders.slice(1).map(o => o.uid); + } + + this.pos.set_order(newOrders[0]); + this.pos.showScreen("ProductScreen"); + } + + back() { + this.pos.showScreen("ProductScreen"); + } +} + +registry.category("pos_screens").add("SplitPaymentScreen", SplitPaymentScreen); diff --git a/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.xml b/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.xml new file mode 100644 index 00000000..92082a57 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.xml @@ -0,0 +1,66 @@ + + + + + + + + Back + + + Split Payment + + + Confirm Split + + + + + + + + + + + + (Calculated automatically) + + + + + + + + + + + 1 + 2 + 3 + + + 4 + 5 + 6 + + + 7 + 8 + 9 + + + - + 0 + . + ⌫ + + + + + + + + diff --git a/tg_pos_splitpayment/views/res_config_settings_views.xml b/tg_pos_splitpayment/views/res_config_settings_views.xml new file mode 100644 index 00000000..a65afd97 --- /dev/null +++ b/tg_pos_splitpayment/views/res_config_settings_views.xml @@ -0,0 +1,17 @@ + + + + res.config.settings.view.form.inherit.tg.pos.splitpayment + res.config.settings + + + + + + + + + + + + From a2786a8412f51ceb21f75b568a53598396d27f36 Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:11:50 +0500 Subject: [PATCH 02/10] [FIX] feat: add odoo/enterprise repository to external-repos list --- external-repos.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 external-repos.txt diff --git a/external-repos.txt b/external-repos.txt new file mode 100644 index 00000000..0a65c17d --- /dev/null +++ b/external-repos.txt @@ -0,0 +1 @@ +https://github.com/odoo/enterprise website_sale_renting From 103590c21dc93b20ce6d7eb883ac1d80cea665e8 Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:14:50 +0500 Subject: [PATCH 03/10] [REF] refactor: rename external-repos.txt to external_repos.txt --- external-repos.txt => external_repos.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename external-repos.txt => external_repos.txt (100%) diff --git a/external-repos.txt b/external_repos.txt similarity index 100% rename from external-repos.txt rename to external_repos.txt From dec989e9aff733ddec7a898a58b02c6f72c9e76f Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:14:16 +0500 Subject: [PATCH 04/10] [FIX] refactor: move split payment trigger to control button and migrate split order logic to receipt screen --- tg_pos_splitpayment/models/pos_session.py | 3 +- .../static/src/app/actionpad/actionpad.xml | 13 --------- .../split_payment_button.js} | 28 +++++++++++++++---- .../control_buttons/split_payment_button.xml | 10 +++++++ .../receipt_screen.js} | 12 ++++---- 5 files changed, 41 insertions(+), 25 deletions(-) delete mode 100644 tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml rename tg_pos_splitpayment/static/src/app/{actionpad/actionpad.js => control_buttons/split_payment_button.js} (52%) create mode 100644 tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.xml rename tg_pos_splitpayment/static/src/app/screens/{payment_screen/payment_screen.js => receipt_screen/receipt_screen.js} (59%) diff --git a/tg_pos_splitpayment/models/pos_session.py b/tg_pos_splitpayment/models/pos_session.py index 818961ff..23c2e2d0 100644 --- a/tg_pos_splitpayment/models/pos_session.py +++ b/tg_pos_splitpayment/models/pos_session.py @@ -5,5 +5,6 @@ class PosSession(models.Model): def _loader_params_pos_config(self): result = super()._loader_params_pos_config() - result['search_params']['fields'].append('pos_max_split_orders') + if result['search_params'].get('fields'): + result['search_params']['fields'].append('pos_max_split_orders') return result diff --git a/tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml b/tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml deleted file mode 100644 index 645fe20f..00000000 --- a/tg_pos_splitpayment/static/src/app/actionpad/actionpad.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - Split Payment - - - - diff --git a/tg_pos_splitpayment/static/src/app/actionpad/actionpad.js b/tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.js similarity index 52% rename from tg_pos_splitpayment/static/src/app/actionpad/actionpad.js rename to tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.js index 2b4cfbca..a3a4ac34 100644 --- a/tg_pos_splitpayment/static/src/app/actionpad/actionpad.js +++ b/tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.js @@ -1,15 +1,24 @@ /** @odoo-module */ -import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad"; -import { patch } from "@web/core/utils/patch"; +import { usePos } from "@point_of_sale/app/store/pos_hook"; +import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen"; +import { Component } from "@odoo/owl"; import { SplitPaymentPopup } from "../popups/split_payment_popup/split_payment_popup"; import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; -import { _t } from "@web/core/l10n/translation"; -patch(ActionpadWidget.prototype, { - async onClickSplitPayment() { +export class SplitPaymentButton extends Component { + static template = "tg_pos_splitpayment.SplitPaymentButton"; + + setup() { + this.pos = usePos(); + } + _isDisabled() { const order = this.pos.get_order(); - if (order.get_orderlines().length === 0) { + return !order || order.get_orderlines().length === 0; + } + async click() { + const order = this.pos.get_order(); + if (this._isDisabled()) { this.env.services.popup.add(ErrorPopup, { title: "Empty Order", body: "There are no products in the order to split.", @@ -24,4 +33,11 @@ patch(ActionpadWidget.prototype, { }); } } +} + +ProductScreen.addControlButton({ + component: SplitPaymentButton, + condition: function () { + return true; + }, }); diff --git a/tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.xml b/tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.xml new file mode 100644 index 00000000..2998e837 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/control_buttons/split_payment_button.xml @@ -0,0 +1,10 @@ + + + + + + + Split + + + diff --git a/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js b/tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js similarity index 59% rename from tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js rename to tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js index 21c1ebbc..cb9415e6 100644 --- a/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js +++ b/tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js @@ -1,19 +1,21 @@ /** @odoo-module */ -import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen"; +import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen"; import { patch } from "@web/core/utils/patch"; -patch(PaymentScreen.prototype, { - async _finalizeValidation() { - await super._finalizeValidation(...arguments); +patch(ReceiptScreen.prototype, { + orderDone() { if (this.pos.tgNextSplitOrders && this.pos.tgNextSplitOrders.length > 0) { const nextOrderUid = this.pos.tgNextSplitOrders.shift(); - // get order from pos.orders const nextOrder = this.pos.orders.find(o => o.uid === nextOrderUid); if (nextOrder) { + this.pos.removeOrder(this.currentOrder); this.pos.set_order(nextOrder); + this.pos.resetProductScreenSearch(); this.pos.showScreen("ProductScreen"); + return; } } + super.orderDone(...arguments); } }); From 2063b0413bc5f4be7b86699db57bea2602c97b3d Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:18:41 +0500 Subject: [PATCH 05/10] [REF] chore: remove website_sale_renting entry from external_repos.txt --- external_repos.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_repos.txt b/external_repos.txt index 0a65c17d..0a811149 100644 --- a/external_repos.txt +++ b/external_repos.txt @@ -1 +1 @@ -https://github.com/odoo/enterprise website_sale_renting +https://github.com/odoo/enterprise From 3ed36c4316462d71a527dafbbf3e5bfb576088bf Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:19:54 +0500 Subject: [PATCH 06/10] [FIX] chore: pin odoo/enterprise repository to version 17.0 --- external_repos.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_repos.txt b/external_repos.txt index 0a811149..bddfc3d9 100644 --- a/external_repos.txt +++ b/external_repos.txt @@ -1 +1 @@ -https://github.com/odoo/enterprise +https://github.com/odoo/enterprise 17.0 From 5ae93b9f84bd3fd478b541f23df11c62508cea43 Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:28:01 +0500 Subject: [PATCH 07/10] [REF] chore: add website_sale_renting to enterprise external repositories --- external_repos.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_repos.txt b/external_repos.txt index bddfc3d9..0cf835fa 100644 --- a/external_repos.txt +++ b/external_repos.txt @@ -1 +1 @@ -https://github.com/odoo/enterprise 17.0 +https://github.com/odoo/enterprise 17.0 website_sale_renting From 596f5f1b57a234a6db3c56b81bec385c30bed7c9 Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:30:25 +0500 Subject: [PATCH 08/10] [REF] chore: add sale_renting to external_repos.txt enterprise dependencies --- external_repos.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_repos.txt b/external_repos.txt index 0cf835fa..d17c4f68 100644 --- a/external_repos.txt +++ b/external_repos.txt @@ -1 +1 @@ -https://github.com/odoo/enterprise 17.0 website_sale_renting +https://github.com/odoo/enterprise 17.0 website_sale_renting,sale_renting From ab6798780c6519b7654de7753f5c620e5df36c4d Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:33:18 +0500 Subject: [PATCH 09/10] [REF] feat: add web_gantt to odoo/enterprise external repositories list --- external_repos.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external_repos.txt b/external_repos.txt index d17c4f68..a4923277 100644 --- a/external_repos.txt +++ b/external_repos.txt @@ -1 +1 @@ -https://github.com/odoo/enterprise 17.0 website_sale_renting,sale_renting +https://github.com/odoo/enterprise 17.0 website_sale_renting,sale_renting,web_gantt From cd0bd51d9502ef31d5d4203e8a2eac62b185d7a2 Mon Sep 17 00:00:00 2001 From: SecretAgentNull <181582849+SecretAgentNull@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:37:08 +0500 Subject: [PATCH 10/10] [FIX] feat: implement split payment order validation and workflow restrictions in PoS --- .../static/src/app/navbar/navbar.js | 19 ++++++ .../screens/payment_screen/payment_screen.js | 19 ++++++ .../screens/payment_screen/payment_screen.xml | 8 +++ .../screens/receipt_screen/receipt_screen.js | 41 ++++++++---- .../split_payment_screen.js | 37 ++++++++--- .../screens/ticket_screen/ticket_screen.js | 35 +++++++++++ .../static/src/app/store/models.js | 23 +++++++ .../static/src/app/store/pos_store.js | 62 +++++++++++++++++++ 8 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 tg_pos_splitpayment/static/src/app/navbar/navbar.js create mode 100644 tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js create mode 100644 tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.xml create mode 100644 tg_pos_splitpayment/static/src/app/screens/ticket_screen/ticket_screen.js create mode 100644 tg_pos_splitpayment/static/src/app/store/models.js create mode 100644 tg_pos_splitpayment/static/src/app/store/pos_store.js diff --git a/tg_pos_splitpayment/static/src/app/navbar/navbar.js b/tg_pos_splitpayment/static/src/app/navbar/navbar.js new file mode 100644 index 00000000..94b39646 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/navbar/navbar.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { Navbar } from "@point_of_sale/app/navbar/navbar"; +import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; +import { _t } from "@web/core/l10n/translation"; + +patch(Navbar.prototype, { + async closeSession() { + if (this.pos.orders.some(o => o.tgSplitGroupId && !o.finalized)) { + await this.popup.add(ErrorPopup, { + title: _t("Cannot Close Session"), + body: _t("You cannot close a PoS session until all the split orders are paid."), + }); + return; + } + return super.closeSession(...arguments); + } +}); diff --git a/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js b/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js new file mode 100644 index 00000000..69d3b436 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen"; +import { patch } from "@web/core/utils/patch"; + +patch(PaymentScreen.prototype, { + async _finalizeValidation() { + // Mark the split group as paid + const splitGroupId = this.currentOrder.tgSplitGroupId; + if (splitGroupId) { + this.pos.orders.forEach(o => { + if (o.tgSplitGroupId === splitGroupId) { + o.isSplitGroupPaid = true; + } + }); + } + await super._finalizeValidation(...arguments); + } +}); diff --git a/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.xml b/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.xml new file mode 100644 index 00000000..ef06858f --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/screens/payment_screen/payment_screen.xml @@ -0,0 +1,8 @@ + + + + + {'d-none': currentOrder.tgSplitGroupId and !currentOrder.finalized} + + + diff --git a/tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js b/tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js index cb9415e6..16787ad0 100644 --- a/tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js +++ b/tg_pos_splitpayment/static/src/app/screens/receipt_screen/receipt_screen.js @@ -2,20 +2,39 @@ import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen"; import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; +import { _t } from "@web/core/l10n/translation"; patch(ReceiptScreen.prototype, { - orderDone() { - if (this.pos.tgNextSplitOrders && this.pos.tgNextSplitOrders.length > 0) { - const nextOrderUid = this.pos.tgNextSplitOrders.shift(); - const nextOrder = this.pos.orders.find(o => o.uid === nextOrderUid); - if (nextOrder) { - this.pos.removeOrder(this.currentOrder); - this.pos.set_order(nextOrder); - this.pos.resetProductScreenSearch(); - this.pos.showScreen("ProductScreen"); - return; - } + async orderDone() { + const splitGroupId = this.currentOrder.tgSplitGroupId; + const nextOrder = splitGroupId ? this.pos.orders.find(o => o.tgSplitGroupId === splitGroupId && !o.finalized && o.uid !== this.currentOrder.uid) : null; + + if (nextOrder) { + await this.env.services.popup.add(ErrorPopup, { + title: _t("Cannot Create New Order"), + body: _t("You cannot create a new order until all the split orders are paid."), + }); + return; } super.orderDone(...arguments); + }, + async resumeOrder() { + const splitGroupId = this.currentOrder.tgSplitGroupId; + const nextOrder = splitGroupId ? this.pos.orders.find(o => o.tgSplitGroupId === splitGroupId && !o.finalized && o.uid !== this.currentOrder.uid) : null; + if (nextOrder) { + this.pos.orders.forEach(o => { + if (o.tgSplitGroupId === splitGroupId) { + o.isSplitGroupPaid = true; + } + }); + + this.pos.removeOrder(this.currentOrder); + this.pos.set_order(nextOrder); + this.pos.resetProductScreenSearch(); + this.pos.showScreen("PaymentScreen"); + return; + } + return super.resumeOrder(...arguments); } }); diff --git a/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js b/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js index 7b74fef5..ec83b917 100644 --- a/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js +++ b/tg_pos_splitpayment/static/src/app/screens/split_payment_screen/split_payment_screen.js @@ -113,32 +113,55 @@ export class SplitPaymentScreen extends Component { for (let j = 0; j < originalLines.length; j++) { const line = originalLines[j]; const originalQty = line.get_quantity(); + const unit = line.get_unit(); + const rounding = unit ? unit.rounding : 0.01; + + let remainingQty = originalQty; for (let i = 0; i < splitsToCreate.length; i++) { const split = splitsToCreate[i]; const ratio = split.amount / this.totalAmount; - const newQty = originalQty * ratio; + + let newQty; + if (i === splitsToCreate.length - 1) { + newQty = remainingQty; + } else { + const exactQty = originalQty * ratio; + newQty = Math.round(exactQty / rounding) * rounding; + newQty = parseFloat(newQty.toFixed(5)); // fix float precision + remainingQty -= newQty; + remainingQty = parseFloat(remainingQty.toFixed(5)); + } if (i === 0) { // Update original line line.set_quantity(newQty); } else { - // Add line to new order by cloning - const newLine = line.clone(); - newLine.order = newOrders[i]; - newLine.set_quantity(newQty); - newOrders[i].add_orderline(newLine); + if (newQty !== 0) { + // Add line to new order by cloning + const newLine = line.clone(); + newLine.order = newOrders[i]; + newLine.set_quantity(newQty); + newOrders[i].add_orderline(newLine); + } } } } + // Assign split group ID to all related orders + const splitGroupId = this.originalOrder.uid; + for (const order of newOrders) { + order.tgSplitGroupId = splitGroupId; + order.isSplitGroupPaid = false; + } + // Set next orders to show if (newOrders.length > 1) { this.pos.tgNextSplitOrders = newOrders.slice(1).map(o => o.uid); } this.pos.set_order(newOrders[0]); - this.pos.showScreen("ProductScreen"); + this.pos.showScreen("PaymentScreen"); } back() { diff --git a/tg_pos_splitpayment/static/src/app/screens/ticket_screen/ticket_screen.js b/tg_pos_splitpayment/static/src/app/screens/ticket_screen/ticket_screen.js new file mode 100644 index 00000000..d20466b7 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/screens/ticket_screen/ticket_screen.js @@ -0,0 +1,35 @@ +/** @odoo-module */ + +import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen"; +import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; +import { _t } from "@web/core/l10n/translation"; + +patch(TicketScreen.prototype, { + shouldHideDeleteButton(order) { + if (order.tgSplitGroupId && !order.finalized) { + return true; + } + return super.shouldHideDeleteButton(...arguments); + }, + onCreateNewOrder() { + if (this.pos.orders.some(o => o.tgSplitGroupId && !o.finalized)) { + this.env.services.popup.add(ErrorPopup, { + title: _t("Cannot Create New Order"), + body: _t("You cannot create a new order until all the split orders are paid."), + }); + return; + } + return super.onCreateNewOrder(...arguments); + }, + async onDeleteOrder(order) { + const splitGroupId = order.tgSplitGroupId; + if (splitGroupId) { + const relatedOrders = this.pos.orders.filter(o => o.tgSplitGroupId === splitGroupId && o.uid !== order.uid); + for (const relOrder of relatedOrders) { + this.pos.removeOrder(relOrder); + } + } + return super.onDeleteOrder(order); + } +}); diff --git a/tg_pos_splitpayment/static/src/app/store/models.js b/tg_pos_splitpayment/static/src/app/store/models.js new file mode 100644 index 00000000..a0de5e0a --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/store/models.js @@ -0,0 +1,23 @@ +/** @odoo-module */ + +import { Order } from "@point_of_sale/app/store/models"; +import { patch } from "@web/core/utils/patch"; + +patch(Order.prototype, { + setup() { + super.setup(...arguments); + this.tgSplitGroupId = this.tgSplitGroupId || null; + this.isSplitGroupPaid = this.isSplitGroupPaid || false; + }, + export_as_JSON() { + const json = super.export_as_JSON(...arguments); + json.tgSplitGroupId = this.tgSplitGroupId; + json.isSplitGroupPaid = this.isSplitGroupPaid; + return json; + }, + init_from_JSON(json) { + super.init_from_JSON(...arguments); + this.tgSplitGroupId = json.tgSplitGroupId; + this.isSplitGroupPaid = json.isSplitGroupPaid; + } +}); diff --git a/tg_pos_splitpayment/static/src/app/store/pos_store.js b/tg_pos_splitpayment/static/src/app/store/pos_store.js new file mode 100644 index 00000000..e931d330 --- /dev/null +++ b/tg_pos_splitpayment/static/src/app/store/pos_store.js @@ -0,0 +1,62 @@ +/** @odoo-module */ + +import { PosStore } from "@point_of_sale/app/store/pos_store"; +import { Order } from "@point_of_sale/app/store/models"; +import { patch } from "@web/core/utils/patch"; +import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup"; +import { _t } from "@web/core/l10n/translation"; + +patch(Order.prototype, { + export_as_JSON() { + const json = super.export_as_JSON(...arguments); + if (this.tgSplitGroupId) { + json.tgSplitGroupId = this.tgSplitGroupId; + } + if (this.isSplitGroupPaid !== undefined) { + json.isSplitGroupPaid = this.isSplitGroupPaid; + } + return json; + }, + init_from_JSON(json) { + super.init_from_JSON(...arguments); + if (json.tgSplitGroupId) { + this.tgSplitGroupId = json.tgSplitGroupId; + } + if (json.isSplitGroupPaid !== undefined) { + this.isSplitGroupPaid = json.isSplitGroupPaid; + } + } +}); + +patch(PosStore.prototype, { + add_new_order() { + if (this.orders.some(o => o.tgSplitGroupId && !o.finalized)) { + this.env.services.popup.add(ErrorPopup, { + title: _t("Cannot Create New Order"), + body: _t("You cannot create a new order until all the split orders are paid."), + }); + return null; + } + return super.add_new_order(...arguments); + }, + showScreen(name, props) { + const unpaidSplitOrder = this.orders.find(o => o.tgSplitGroupId && !o.finalized); + if (name === "ProductScreen" && unpaidSplitOrder) { + if (!this.get_order() || this.get_order().uid !== unpaidSplitOrder.uid) { + this.set_order(unpaidSplitOrder); + } + return super.showScreen("PaymentScreen", props); + } + return super.showScreen(...arguments); + }, + async closePos() { + if (this.orders.some(o => o.tgSplitGroupId && !o.finalized)) { + this.env.services.popup.add(ErrorPopup, { + title: _t("Cannot Close Session"), + body: _t("You cannot close a PoS session until all the split orders are paid."), + }); + return; + } + return super.closePos(...arguments); + } +});
How many orders to split into?