From b7e8f8967c29ccb863a602ce00ead5cf7ec4a3b3 Mon Sep 17 00:00:00 2001 From: luis Date: Fri, 24 Apr 2026 13:34:06 +0200 Subject: [PATCH] add pos_cashdro_refund,pos_cashdro_allow_manual --- pos_cashdro_allow_manual/__init__.py | 3 + pos_cashdro_allow_manual/__manifest__.py | 23 ++++ pos_cashdro_allow_manual/models/__init__.py | 4 + .../models/pos_payment_method.py | 18 +++ .../models/pos_session.py | 12 ++ .../static/src/js/PaymentScreen.esm.js | 45 +++++++ .../src/xml/PaymentScreenPaymentLines.xml | 64 ++++++++++ .../views/pos_payment_method_views.xml | 18 +++ pos_cashdro_refund/__init__.py | 2 + pos_cashdro_refund/__manifest__.py | 21 ++++ .../static/src/js/PasswordInputPopup.js | 38 ++++++ .../src/js/payment_cashdro_refund.esm.js | 117 ++++++++++++++++++ .../static/src/xml/PasswordInputPopup.xml | 33 +++++ .../static/src/js/cashdro_rounding.esm.js | 11 ++ 14 files changed, 409 insertions(+) create mode 100644 pos_cashdro_allow_manual/__init__.py create mode 100644 pos_cashdro_allow_manual/__manifest__.py create mode 100644 pos_cashdro_allow_manual/models/__init__.py create mode 100644 pos_cashdro_allow_manual/models/pos_payment_method.py create mode 100644 pos_cashdro_allow_manual/models/pos_session.py create mode 100644 pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js create mode 100644 pos_cashdro_allow_manual/static/src/xml/PaymentScreenPaymentLines.xml create mode 100644 pos_cashdro_allow_manual/views/pos_payment_method_views.xml create mode 100644 pos_cashdro_refund/__init__.py create mode 100644 pos_cashdro_refund/__manifest__.py create mode 100644 pos_cashdro_refund/static/src/js/PasswordInputPopup.js create mode 100644 pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js create mode 100644 pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml diff --git a/pos_cashdro_allow_manual/__init__.py b/pos_cashdro_allow_manual/__init__.py new file mode 100644 index 0000000..9897421 --- /dev/null +++ b/pos_cashdro_allow_manual/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models diff --git a/pos_cashdro_allow_manual/__manifest__.py b/pos_cashdro_allow_manual/__manifest__.py new file mode 100644 index 0000000..c866887 --- /dev/null +++ b/pos_cashdro_allow_manual/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "POS CashDro Allow Manual Amount", + "summary": "Permite introducir manualmente el importe en pagos CashDro", + "version": "16.0.1.0.0", + "category": "Point of Sale", + "license": "AGPL-3", + "author": "Criptomart", + "depends": [ + "pos_payment_method_cashdro", + ], + "data": [ + "views/pos_payment_method_views.xml", + ], + "assets": { + "point_of_sale.assets": [ + "pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js", + "pos_cashdro_allow_manual/static/src/xml/PaymentScreenPaymentLines.xml", + ], + }, + "installable": True, +} diff --git a/pos_cashdro_allow_manual/models/__init__.py b/pos_cashdro_allow_manual/models/__init__.py new file mode 100644 index 0000000..5d0027b --- /dev/null +++ b/pos_cashdro_allow_manual/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import pos_payment_method +from . import pos_session diff --git a/pos_cashdro_allow_manual/models/pos_payment_method.py b/pos_cashdro_allow_manual/models/pos_payment_method.py new file mode 100644 index 0000000..ead4aa4 --- /dev/null +++ b/pos_cashdro_allow_manual/models/pos_payment_method.py @@ -0,0 +1,18 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class PosPaymentMethod(models.Model): + _inherit = "pos.payment.method" + + cashdro_allow_manual_amount = fields.Boolean( + string="Permitir importe manual", + help=( + "Cuando está activado, aparece un botón 'Manual' en la pantalla de " + "pago del TPV que permite confirmar el importe de la línea sin pasar " + "por la máquina CashDro. Útil ante fallos de comunicación, " + "devoluciones u otras situaciones de emergencia." + ), + default=True, + ) diff --git a/pos_cashdro_allow_manual/models/pos_session.py b/pos_cashdro_allow_manual/models/pos_session.py new file mode 100644 index 0000000..1a0bf31 --- /dev/null +++ b/pos_cashdro_allow_manual/models/pos_session.py @@ -0,0 +1,12 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class PosSession(models.Model): + _inherit = "pos.session" + + def _loader_params_pos_payment_method(self): + result = super()._loader_params_pos_payment_method() + result["search_params"]["fields"].append("cashdro_allow_manual_amount") + return result diff --git a/pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js b/pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js new file mode 100644 index 0000000..6a9df37 --- /dev/null +++ b/pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js @@ -0,0 +1,45 @@ +/** @odoo-module **/ +// Copyright 2026 Criptomart +// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import {Component} from "point_of_sale.Registries"; +import PaymentScreen from "point_of_sale.PaymentScreen"; +import {useListener} from "@web/core/utils/hooks"; + +const CashdroAllowManualPaymentScreen = (OriginalPaymentScreen) => + class extends OriginalPaymentScreen { + setup() { + super.setup(); + useListener("cashdro-send-manual", this._cashdroSendManual); + } + + /** + * Mark the Cashdro payment line as done using the amount that is already + * set on the line (entered manually via the numpad), without contacting + * the CashDro machine. + * + * Sets `cashdro_manual_done = true` on the line so the delete button + * remains visible even in `done` state, letting the cashier undo the + * operation in case of a mistake. + * + * Refuses to proceed when the amount is 0 to avoid locking the POS with + * an undeletable zero-amount payment line. + */ + async _cashdroSendManual({detail: line}) { + if (!line.get_amount()) { + this.showPopup("ErrorPopup", { + title: this.env._t("Importe no válido"), + body: this.env._t( + "Introduzca el importe en el teclado numérico antes de confirmar manualmente el pago." + ), + }); + return; + } + // Flag this line as manually confirmed so the template keeps + // showing its delete button even after the status is 'done'. + line.cashdro_manual_done = true; + line.set_payment_status("done"); + } + }; + +Component.extend(PaymentScreen, CashdroAllowManualPaymentScreen); diff --git a/pos_cashdro_allow_manual/static/src/xml/PaymentScreenPaymentLines.xml b/pos_cashdro_allow_manual/static/src/xml/PaymentScreenPaymentLines.xml new file mode 100644 index 0000000..105b80e --- /dev/null +++ b/pos_cashdro_allow_manual/static/src/xml/PaymentScreenPaymentLines.xml @@ -0,0 +1,64 @@ + + + + + + + + +
+ Manual +
+
+
+ + + !line.payment_status or !['done', 'reversed', 'waitingCard', 'waitingCapture'].includes(line.payment_status) or line.cashdro_manual_done + +
+ +
diff --git a/pos_cashdro_allow_manual/views/pos_payment_method_views.xml b/pos_cashdro_allow_manual/views/pos_payment_method_views.xml new file mode 100644 index 0000000..b1cc651 --- /dev/null +++ b/pos_cashdro_allow_manual/views/pos_payment_method_views.xml @@ -0,0 +1,18 @@ + + + + + pos.payment.method.cashdro.allow.manual.form + pos.payment.method + + + + + + + + diff --git a/pos_cashdro_refund/__init__.py b/pos_cashdro_refund/__init__.py new file mode 100644 index 0000000..efb0bb1 --- /dev/null +++ b/pos_cashdro_refund/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). diff --git a/pos_cashdro_refund/__manifest__.py b/pos_cashdro_refund/__manifest__.py new file mode 100644 index 0000000..5914bbe --- /dev/null +++ b/pos_cashdro_refund/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "POS CashDro Refund", + "summary": "Permite devoluciones (payouts) a través de la máquina CashDro desde el TPV", + "version": "16.0.1.0.0", + "category": "Point of Sale", + "license": "AGPL-3", + "author": "Criptomart", + "depends": [ + "pos_payment_method_cashdro", + ], + "assets": { + "point_of_sale.assets": [ + "pos_cashdro_refund/static/src/js/PasswordInputPopup.js", + "pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js", + "pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml", + ], + }, + "installable": True, +} diff --git a/pos_cashdro_refund/static/src/js/PasswordInputPopup.js b/pos_cashdro_refund/static/src/js/PasswordInputPopup.js new file mode 100644 index 0000000..0dfb636 --- /dev/null +++ b/pos_cashdro_refund/static/src/js/PasswordInputPopup.js @@ -0,0 +1,38 @@ +odoo.define("pos_cashdro_refund.PasswordInputPopup", function (require) { + "use strict"; + /* Copyright 2026 Criptomart - extracted from pos_payment_method_cashdro + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ + + const AbstractAwaitablePopup = require("point_of_sale.AbstractAwaitablePopup"); + const Registries = require("point_of_sale.Registries"); + const {_lt} = require("@web/core/l10n/translation"); + const {onMounted, useRef, useState} = owl; + + class PasswordInputPopup extends AbstractAwaitablePopup { + setup() { + super.setup(); + this.state = useState({inputValue: this.props.startingValue}); + this.inputRef = useRef("input"); + onMounted(this.onMounted); + } + onMounted() { + this.inputRef.el.focus(); + } + getPayload() { + return this.state.inputValue; + } + } + PasswordInputPopup.template = "PasswordInputPopup"; + PasswordInputPopup.defaultProps = { + confirmText: _lt("Ok"), + cancelText: _lt("Cancel"), + title: "", + body: "", + startingValue: "", + placeholder: "", + }; + + Registries.Component.add(PasswordInputPopup); + + return PasswordInputPopup; +}); diff --git a/pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js b/pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js new file mode 100644 index 0000000..de8bc67 --- /dev/null +++ b/pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js @@ -0,0 +1,117 @@ +/** @odoo-module */ +/* Copyright 2026 Criptomart - extracted from pos_payment_method_cashdro + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + Adds refund (payout) support to the CashDro payment terminal integration. + When the order due amount is negative (i.e. a return/refund), this module: + 1. Asks the cashier for the CashDro password to authorise the payout. + 2. Sends a type=3 (payout) request to the CashDro instead of the normal + type=4 (collection) request. + 3. Sets the payment line amount to the negative amount dispensed by the + machine (totalout). + Normal (positive) payments are delegated unchanged to the original + cashdro_send_payment_request method. +*/ + +import {PaymentCashdro} from "@pos_payment_method_cashdro/js/payment_cashdro.esm"; +import {Gui} from "point_of_sale.Gui"; +import {_t} from "web.core"; + +/** + * Dedicated refund (payout) handler added to the CashDro prototype. + * + * Keeping this as a SEPARATE named method (rather than inlining it inside an + * override of cashdro_send_payment_request) means that pos_cashdro_rounding + * can always delegate refunds here regardless of which module loaded last. + * Both modules patch cashdro_send_payment_request directly on the prototype, + * so the last writer wins for that method; but _cashdro_send_refund_request is + * only ever written by this module and is never overwritten. + */ +PaymentCashdro.prototype._cashdro_send_refund_request = async function (order) { + const payment_line = order.selected_paymentline; + const due = order.get_due(payment_line); + // Include cash rounding if applied (same logic as pos_cashdro_rounding for + // normal payments). get_rounding_applied() returns 0 when no rounding is + // configured, so this is safe without pos_cashdro_rounding installed. + const rounding = order.get_rounding_applied ? order.get_rounding_applied() : 0; + const amount = Math.round(Math.abs(due + rounding) * 100); + + // Require password authorisation before opening the drawer for a payout. + const {confirmed, payload: password} = await Gui.showPopup( + "PasswordInputPopup", + { + title: _t("CashDro Refund Authorization"), + body: _t("Enter the CashDro password to authorize this refund"), + placeholder: _t("Password"), + } + ); + if (!confirmed) { + payment_line.set_payment_status("retry"); + return false; + } + const method = payment_line.payment_method; + if (password !== method.cashdro_password) { + await Gui.showPopup("ErrorPopup", { + title: _t("Authentication Failed"), + body: _t("The CashDro password is incorrect."), + }); + payment_line.set_payment_status("retry"); + return false; + } + + const start_url = this._cashdro_payout_url({amount: amount}); + console.log(start_url); + const res = await this._cashdro_request(start_url); + console.log(res); + const operation_id = res.data || ""; + this.pos.get_order().cashdro_operation = operation_id; + + const ack_url = this._cashdro_ack_url(operation_id); + const res_ack = await this._cashdro_request(ack_url); + console.log(res_ack); + + const ask_url = this._cashdro_ask_url(operation_id); + const operation_data = await this._cashdro_request_payment(ask_url); + console.log(operation_data); + + const data = JSON.parse(operation_data.data); + payment_line.cashdro_operation_data = data; + // CashDro reports the dispensed amount in totalout for payouts. + const dispensed = data.operation.totalout / 100; + payment_line.set_amount(-dispensed); + return true; +}; + +/** + * Build the URL for a type=3 (payout/refund) operation on the CashDro. + */ +PaymentCashdro.prototype._cashdro_payout_url = function (parameters) { + const user = this.pos.get_cashier().id || this.pos.user.id; + let url = `${this._cashdro_url()}&operation=startOperation&type=3`; + url += `&posid=pos-${this.pos.pos_session.name}`; + url += `&posuser=${user}`; + url += `¶meters=${encodeURIComponent(JSON.stringify(parameters))}`; + return url; +}; + +/** + * Override cashdro_send_payment_request to route refunds to + * _cashdro_send_refund_request. + * + * NOTE: if pos_cashdro_rounding is also installed it will overwrite this + * override (because "rounding" > "refund" alphabetically), but that is fine + * because pos_cashdro_rounding also delegates negative amounts to + * _cashdro_send_refund_request. This override only matters when the rounding + * module is NOT installed. + */ +const _originalSendPaymentRequest = + PaymentCashdro.prototype.cashdro_send_payment_request; + +PaymentCashdro.prototype.cashdro_send_payment_request = async function (order) { + const payment_line = order.selected_paymentline; + const due = order.get_due(payment_line); + if (due < 0) { + return this._cashdro_send_refund_request(order); + } + return _originalSendPaymentRequest.call(this, order); +}; diff --git a/pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml b/pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml new file mode 100644 index 0000000..3e6358c --- /dev/null +++ b/pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/pos_cashdro_rounding/static/src/js/cashdro_rounding.esm.js b/pos_cashdro_rounding/static/src/js/cashdro_rounding.esm.js index e117a1f..222c9f2 100644 --- a/pos_cashdro_rounding/static/src/js/cashdro_rounding.esm.js +++ b/pos_cashdro_rounding/static/src/js/cashdro_rounding.esm.js @@ -11,6 +11,17 @@ import { PaymentCashdro } from "@pos_payment_method_cashdro/js/payment_cashdro.e PaymentCashdro.prototype.cashdro_send_payment_request = async function (order) { const payment_line = order.selected_paymentline; try { + // If pos_cashdro_refund is installed this method handles payouts. + // Rounding is not applicable to refunds so we delegate and return. + const due = order.get_due(payment_line); + if (due < 0) { + if (this._cashdro_send_refund_request) { + return this._cashdro_send_refund_request(order); + } + // Refund module not installed – nothing we can do. + payment_line.set_payment_status("retry"); + return false; + } // Cashdro treats decimals as positions in an integer we also have // to deal with floating point computing to avoid decimals at the // end or the drawer will reject our request.