add pos_cashdro_refund,pos_cashdro_allow_manual
This commit is contained in:
parent
9b25650118
commit
b7e8f8967c
14 changed files with 409 additions and 0 deletions
3
pos_cashdro_allow_manual/__init__.py
Normal file
3
pos_cashdro_allow_manual/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from . import models
|
||||
23
pos_cashdro_allow_manual/__manifest__.py
Normal file
23
pos_cashdro_allow_manual/__manifest__.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
4
pos_cashdro_allow_manual/models/__init__.py
Normal file
4
pos_cashdro_allow_manual/models/__init__.py
Normal file
|
|
@ -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
|
||||
18
pos_cashdro_allow_manual/models/pos_payment_method.py
Normal file
18
pos_cashdro_allow_manual/models/pos_payment_method.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
12
pos_cashdro_allow_manual/models/pos_session.py
Normal file
12
pos_cashdro_allow_manual/models/pos_session.py
Normal file
|
|
@ -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
|
||||
45
pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js
Normal file
45
pos_cashdro_allow_manual/static/src/js/PaymentScreen.esm.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2026 Criptomart
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
Extends the PaymentScreenPaymentLines template to add a "Manual" button
|
||||
next to the Send/Retry action buttons on Cashdro payment lines.
|
||||
|
||||
The button is shown when:
|
||||
- the payment method is of type "cashdro"
|
||||
- cashdro_allow_manual_amount is True on that payment method
|
||||
- the line is in "pending" or "retry" state (the two states where
|
||||
the numpad is already active and the cashier can edit the amount)
|
||||
|
||||
Clicking it triggers the "cashdro-send-manual" event which is handled
|
||||
by CashdroAllowManualPaymentScreen in PaymentScreen.esm.js.
|
||||
-->
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t
|
||||
t-name="pos_cashdro_allow_manual.PaymentScreenPaymentLines"
|
||||
t-inherit="point_of_sale.PaymentScreenPaymentLines"
|
||||
t-inherit-mode="extension"
|
||||
owl="1"
|
||||
>
|
||||
<!--
|
||||
//div[hasclass('send_payment_request')] matches two nodes:
|
||||
1. the "Send" button shown in the "pending" state
|
||||
2. the "Retry" button shown in the "retry" state
|
||||
position="after" inserts our node after each match,
|
||||
so the Manual button appears in both states.
|
||||
-->
|
||||
<xpath expr="//div[hasclass('send_payment_request')]" position="before">
|
||||
<t
|
||||
t-if="line.payment_method.use_payment_terminal === 'cashdro'
|
||||
and line.payment_method.cashdro_allow_manual_amount"
|
||||
>
|
||||
<div
|
||||
class="button cashdro-manual-button"
|
||||
style="flex: 0 0 auto; padding: 0 12px; font-size: 0.8em; opacity: 0.75;"
|
||||
title="Confirmar importe manualmente sin usar la máquina CashDro"
|
||||
t-on-click="() => this.trigger('cashdro-send-manual', line)"
|
||||
>
|
||||
Manual
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
<!--
|
||||
Extend the t-if on the wrapper <t> that controls the delete button
|
||||
visibility so that manually-confirmed Cashdro lines keep showing the
|
||||
delete button even in 'done' state. This lets the cashier undo
|
||||
an accidental Manual press without blocking the POS session.
|
||||
|
||||
The targeted element is the <t t-if="..."> that wraps the
|
||||
delete-button <div> inside the selected payment line block.
|
||||
-->
|
||||
<xpath
|
||||
expr="//t[@t-if='line.selected']/div/t[div[hasclass('delete-button')]]"
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="t-if">!line.payment_status or !['done', 'reversed', 'waitingCard', 'waitingCapture'].includes(line.payment_status) or line.cashdro_manual_done</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
18
pos_cashdro_allow_manual/views/pos_payment_method_views.xml
Normal file
18
pos_cashdro_allow_manual/views/pos_payment_method_views.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2026 Criptomart
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record id="pos_payment_method_cashdro_allow_manual_form" model="ir.ui.view">
|
||||
<field name="name">pos.payment.method.cashdro.allow.manual.form</field>
|
||||
<field name="model">pos.payment.method</field>
|
||||
<field name="inherit_id" ref="pos_payment_method_cashdro.pos_payment_method_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="cashdro_password" position="after">
|
||||
<field
|
||||
name="cashdro_allow_manual_amount"
|
||||
attrs="{'invisible': [('use_payment_terminal', '!=', 'cashdro')]}"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
2
pos_cashdro_refund/__init__.py
Normal file
2
pos_cashdro_refund/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
21
pos_cashdro_refund/__manifest__.py
Normal file
21
pos_cashdro_refund/__manifest__.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
38
pos_cashdro_refund/static/src/js/PasswordInputPopup.js
Normal file
38
pos_cashdro_refund/static/src/js/PasswordInputPopup.js
Normal file
|
|
@ -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;
|
||||
});
|
||||
117
pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js
Normal file
117
pos_cashdro_refund/static/src/js/payment_cashdro_refund.esm.js
Normal file
|
|
@ -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);
|
||||
};
|
||||
33
pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml
Normal file
33
pos_cashdro_refund/static/src/xml/PasswordInputPopup.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2026 Criptomart - extracted from pos_payment_method_cashdro
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="PasswordInputPopup" owl="1">
|
||||
<div class="popup popup-textinput">
|
||||
<header class="title">
|
||||
<t t-esc="props.title" />
|
||||
</header>
|
||||
<div class="body">
|
||||
<p>
|
||||
<t t-esc="props.body" />
|
||||
</p>
|
||||
<input
|
||||
type="password"
|
||||
t-model="state.inputValue"
|
||||
t-ref="input"
|
||||
t-att-placeholder="props.placeholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="button confirm highlight" t-on-click="confirm">
|
||||
<t t-esc="props.confirmText" />
|
||||
</div>
|
||||
<div class="button cancel" t-on-click="cancel">
|
||||
<t t-esc="props.cancelText" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue