add pos_cashdro_refund,pos_cashdro_allow_manual

This commit is contained in:
Luis 2026-04-24 13:34:06 +02:00
parent 9b25650118
commit b7e8f8967c
14 changed files with 409 additions and 0 deletions

View file

@ -0,0 +1,2 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

View 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,
}

View 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;
});

View 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 += `&parameters=${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);
};

View 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>