Compare commits

..

No commits in common. "5b2202f7d46046d017e9d2e28a2f9814117678f9" and "40c92613d94627b78bb37a560ef73ee9883606ab" have entirely different histories.

13 changed files with 23 additions and 763 deletions

View file

@ -51,12 +51,10 @@ To check the three fixes on a PoS with a CashDro terminal:
due amount (fix 2).
#. The drawer must start the operation and ask for the amount, instead of the
*"An error occurred while connecting to the cashdro"* dialog (fixes 1 and 3).
These three fixes are not enough on their own: the payment flow of the base
module is broken on 18.0 for reasons of its own, and it hides every failure —
these ones included — behind that same dialog, without logging anything.
Install ``pos_payment_method_cashdro_flow_fix`` as well; its ``USAGE`` explains
how to read what actually went wrong.
#. With no CashDro at hand, the browser console tells fix 1 from fix 3 apart:
without this module the failure is a ``TypeError: Cannot read properties of
undefined (reading 'payment_method')``; with it, a network error against the
configured host.
The Python side is covered by the tests. The base module lives outside this
repo, so the OCA sources have to be on the addons path (mounted by
@ -78,7 +76,7 @@ the credential fields that are missing from the loader, and the JavaScript
patches are idempotent with respect to the fixed upstream code.
The JavaScript side can't be covered by automated tests, as it needs a CashDro
terminal answering on the network. See ``USAGE`` for the manual check.
terminal answering on the network.
Contributors
============

View file

@ -7,12 +7,10 @@ To check the three fixes on a PoS with a CashDro terminal:
due amount (fix 2).
#. The drawer must start the operation and ask for the amount, instead of the
*"An error occurred while connecting to the cashdro"* dialog (fixes 1 and 3).
These three fixes are not enough on their own: the payment flow of the base
module is broken on 18.0 for reasons of its own, and it hides every failure —
these ones included — behind that same dialog, without logging anything.
Install ``pos_payment_method_cashdro_flow_fix`` as well; its ``USAGE`` explains
how to read what actually went wrong.
#. With no CashDro at hand, the browser console tells fix 1 from fix 3 apart:
without this module the failure is a ``TypeError: Cannot read properties of
undefined (reading 'payment_method')``; with it, a network error against the
configured host.
The Python side is covered by the tests. The base module lives outside this
repo, so the OCA sources have to be on the addons path (mounted by

View file

@ -1,164 +0,0 @@
==============================
POS CashDro Payment Flow Fixes
==============================
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
|badge1| |badge2|
``pos_payment_method_cashdro_fix`` patches the three 17.0 leftovers that the
18.0 migration of ``pos_payment_method_cashdro`` left in the *data loading*.
This module takes care of the *payment flow* itself, which stays broken after
them.
**The blocking one.** ``_cashdro_payment_url()`` builds the ``startOperation``
url with ``this.pos.pos_session.name``. 18.0 replaced that accessor by the
``session`` getter of ``PosStore``, so ``this.pos.pos_session`` is ``undefined``
and reading ``.name`` on it raises a ``TypeError`` **on every single payment,
before any request leaves the browser**.
``cashdro_send_payment_request`` catches it with a bare ``catch`` that neither
logs nor rethrows, and shows *"An error occurred while connecting to the
cashdro"* — the same message a network failure or an unplugged drawer produce.
That is why the failure looks like a communication problem with the machine,
and why it happens with any other module of ours enabled or disabled.
**The rest.** Once the payment goes through, the flow still has these problems,
all of them fixed here:
#. **Failures are invisible.** The ``catch`` has no error binding, so nothing
reaches the browser console. Every cause — a wrong host, a busy drawer, a
bad password, an API mistake — reads as the same sentence. The error is now
logged and its message shown in the dialog.
#. **A missing host is not reported.** ``_cashdro_url()`` returns the boolean
``false``, which the callers concatenate into ``"false&operation=..."``;
``fetch`` resolves that against the Odoo server, gets a 404 page back and the
operator sees a connection error. It now raises with the method name.
#. **A started operation is never closed on failure.** If the acknowledge or the
polling fails after ``startOperation`` succeeded, the CashDro keeps the
operation open and refuses the next one: every later payment of the session
fails until the drawer is power cycled. ``finishOperation`` is now always
sent.
#. **The polling has no interval, no bound and no way to stop.** The base
implementation re-asks the drawer as fast as the network allows for as long
as the customer is inserting coins, and keeps going after a cancel. It is now
a 700 ms interval with a 5 minute deadline, stopped by a cancel or by leaving
the payment screen.
#. **A cancel is reported as failed.** ``send_payment_cancel()`` resolves to
``undefined``, which ``payment_screen`` reads as "the cancel failed" and
leaves the line stuck in ``waitingCard``, re-entering the cancel on every
click. It now returns whether the drawer took it.
#. **A reversal asks the drawer to collect the money again.**
``order.get_due()`` only discounts the payments that are already done, and
the line being reversed is not one of them, so the amount comes out positive
and the operation type is 4 (payment) instead of 3 (refund). The reversal
now uses the amount of the line, negated.
#. **The uuid of the line is ignored.** The three ``PaymentInterface`` entry
points receive the uuid of the line to act on and the base module works on
the selected one instead. The per-line *Send* and *Reverse* buttons do not
select the line first, so with two payment lines on the order the CashDro
works on the wrong one.
#. **The answers are not validated.** An HTTP error page or an envelope without
``data`` are taken as valid: the module acknowledges an empty operation id
and polls it forever. Both are now checked.
**Table of contents**
.. contents::
:local:
Usage
=====
There is nothing to configure: installing the module is enough.
Before anything else, check the payment method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18.0 added the ``payment_method_type`` field (*Integration*) to
``pos.payment.method``, and core clears ``use_payment_terminal`` whenever it is
saved as *None required*. On **Point of Sale ‣ Configuration ‣ Payment
Methods**, the CashDro method must have:
* **Integration** = *Terminal*
* **Integrate with** = *Cashdro*
* the terminal host, user and password filled in
With *Integration* left at *None required* the terminal is not wired at all:
clicking the payment method just adds a plain cash line, and nothing is ever
sent to the drawer — no dialog, no error, no traffic.
On site
~~~~~~~
#. Open a PoS session, add a product and go to the payment screen.
#. Select the CashDro payment method. The payment line must show **0**, not the
due amount (that one comes from ``pos_payment_method_cashdro_fix``).
#. The drawer must start the operation and ask for the amount.
If it still fails, the dialog now carries the reason instead of the generic
sentence, and the browser console has the full error under ``[CASHDRO]``. What
to expect:
``The CashDro payment method '...' has no terminal host configured.``
The credentials did not reach the front end. Either the fields are empty on
the payment method, or ``pos_payment_method_cashdro_fix`` is not installed.
``TypeError: Failed to fetch`` / ``NetworkError``
The browser cannot reach ``cashdro_host``. Check the host and that the till
is on the same network as the drawer. A PoS served over HTTPS also blocks a
plain ``http://`` drawer as mixed content, which shows up here.
``The CashDro answered 401/403/500 ...``
The drawer is reachable and answering: wrong user or password, or the
CashDro3WS service is failing.
``The CashDro did not return an operation id: ...``
The drawer refused ``startOperation``. Its own envelope is in the message —
usually a busy drawer with an operation still open from a previous failure.
``The CashDro did not finish the operation in time.``
``startOperation`` was accepted but the drawer never reached the finished
state within five minutes.
There are no automated tests: the flow needs a CashDro terminal answering on the
network, which is also why none of this is caught by the OCA CI.
Known issues / Roadmap
======================
Split from ``pos_payment_method_cashdro_fix`` on purpose: that module is a
temporary patch for three migration leftovers, meant to disappear as soon as
its ``upstream/*.patch`` is merged in ``OCA/pos``. This one has a longer life —
only the ``pos_session`` fix is a migration leftover, the rest are problems the
module has carried since 17.0 — so it should not go away with it.
It supersedes the ``_cashdro_url()`` patch of ``pos_payment_method_cashdro_fix``
(same method, resolved from the terminal instead of the selected line, raising
instead of returning ``false``). Once the upstream patch is merged, the
dependency has to be moved to ``pos_payment_method_cashdro`` and the
``_load_pos_data_fields`` override checked, as it is the only part of the other
module this one really needs.
Still open:
* The success path leaves the operation open on the drawer, as upstream does.
Whether the CashDro3WS expects a ``finishOperation`` after a finished
operation could not be checked without a machine, so the behaviour was left
untouched.
* ``_cashdro_url()`` does not escape the user and the password, again as
upstream does: credentials containing ``&``, ``=`` or ``#`` break the query
string.
* The whole thing wants a tour with a mocked CashDro3WS endpoint, which would
also let the OCA CI catch this class of breakage.
Contributors
============
* `Criptomart <https://criptomart.net>`_:
* Criptomart

View file

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

View file

@ -1,22 +0,0 @@
# Copyright 2026 - Today Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "POS CashDro Payment Flow Fixes",
"version": "18.0.1.0.0",
"summary": "Fix and harden the CashDro payment flow on 18.0: real errors, "
"bounded polling and correct reversals",
"category": "Point of Sale",
"license": "AGPL-3",
"author": "Criptomart",
"maintainers": ["Criptomart"],
"website": "https://git.criptomart.net/criptomart/addons-cm",
"depends": [
"pos_payment_method_cashdro",
"pos_payment_method_cashdro_fix",
],
"assets": {
"point_of_sale._assets_pos": [
"pos_payment_method_cashdro_flow_fix/static/src/js/payment_cashdro_flow.esm.js",
],
},
}

View file

@ -1,3 +0,0 @@
* `Criptomart <https://criptomart.net>`_:
* Criptomart

View file

@ -1,54 +0,0 @@
``pos_payment_method_cashdro_fix`` patches the three 17.0 leftovers that the
18.0 migration of ``pos_payment_method_cashdro`` left in the *data loading*.
This module takes care of the *payment flow* itself, which stays broken after
them.
**The blocking one.** ``_cashdro_payment_url()`` builds the ``startOperation``
url with ``this.pos.pos_session.name``. 18.0 replaced that accessor by the
``session`` getter of ``PosStore``, so ``this.pos.pos_session`` is ``undefined``
and reading ``.name`` on it raises a ``TypeError`` **on every single payment,
before any request leaves the browser**.
``cashdro_send_payment_request`` catches it with a bare ``catch`` that neither
logs nor rethrows, and shows *"An error occurred while connecting to the
cashdro"* — the same message a network failure or an unplugged drawer produce.
That is why the failure looks like a communication problem with the machine,
and why it happens with any other module of ours enabled or disabled.
**The rest.** Once the payment goes through, the flow still has these problems,
all of them fixed here:
#. **Failures are invisible.** The ``catch`` has no error binding, so nothing
reaches the browser console. Every cause — a wrong host, a busy drawer, a
bad password, an API mistake — reads as the same sentence. The error is now
logged and its message shown in the dialog.
#. **A missing host is not reported.** ``_cashdro_url()`` returns the boolean
``false``, which the callers concatenate into ``"false&operation=..."``;
``fetch`` resolves that against the Odoo server, gets a 404 page back and the
operator sees a connection error. It now raises with the method name.
#. **A started operation is never closed on failure.** If the acknowledge or the
polling fails after ``startOperation`` succeeded, the CashDro keeps the
operation open and refuses the next one: every later payment of the session
fails until the drawer is power cycled. ``finishOperation`` is now always
sent.
#. **The polling has no interval, no bound and no way to stop.** The base
implementation re-asks the drawer as fast as the network allows for as long
as the customer is inserting coins, and keeps going after a cancel. It is now
a 700 ms interval with a 5 minute deadline, stopped by a cancel or by leaving
the payment screen.
#. **A cancel is reported as failed.** ``send_payment_cancel()`` resolves to
``undefined``, which ``payment_screen`` reads as "the cancel failed" and
leaves the line stuck in ``waitingCard``, re-entering the cancel on every
click. It now returns whether the drawer took it.
#. **A reversal asks the drawer to collect the money again.**
``order.get_due()`` only discounts the payments that are already done, and
the line being reversed is not one of them, so the amount comes out positive
and the operation type is 4 (payment) instead of 3 (refund). The reversal
now uses the amount of the line, negated.
#. **The uuid of the line is ignored.** The three ``PaymentInterface`` entry
points receive the uuid of the line to act on and the base module works on
the selected one instead. The per-line *Send* and *Reverse* buttons do not
select the line first, so with two payment lines on the order the CashDro
works on the wrong one.
#. **The answers are not validated.** An HTTP error page or an envelope without
``data`` are taken as valid: the module acknowledges an empty operation id
and polls it forever. Both are now checked.

View file

@ -1,24 +0,0 @@
Split from ``pos_payment_method_cashdro_fix`` on purpose: that module is a
temporary patch for three migration leftovers, meant to disappear as soon as
its ``upstream/*.patch`` is merged in ``OCA/pos``. This one has a longer life —
only the ``pos_session`` fix is a migration leftover, the rest are problems the
module has carried since 17.0 — so it should not go away with it.
It supersedes the ``_cashdro_url()`` patch of ``pos_payment_method_cashdro_fix``
(same method, resolved from the terminal instead of the selected line, raising
instead of returning ``false``). Once the upstream patch is merged, the
dependency has to be moved to ``pos_payment_method_cashdro`` and the
``_load_pos_data_fields`` override checked, as it is the only part of the other
module this one really needs.
Still open:
* The success path leaves the operation open on the drawer, as upstream does.
Whether the CashDro3WS expects a ``finishOperation`` after a finished
operation could not be checked without a machine, so the behaviour was left
untouched.
* ``_cashdro_url()`` does not escape the user and the password, again as
upstream does: credentials containing ``&``, ``=`` or ``#`` break the query
string.
* The whole thing wants a tour with a mocked CashDro3WS endpoint, which would
also let the OCA CI catch this class of breakage.

View file

@ -1,53 +0,0 @@
There is nothing to configure: installing the module is enough.
Before anything else, check the payment method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18.0 added the ``payment_method_type`` field (*Integration*) to
``pos.payment.method``, and core clears ``use_payment_terminal`` whenever it is
saved as *None required*. On **Point of Sale ‣ Configuration ‣ Payment
Methods**, the CashDro method must have:
* **Integration** = *Terminal*
* **Integrate with** = *Cashdro*
* the terminal host, user and password filled in
With *Integration* left at *None required* the terminal is not wired at all:
clicking the payment method just adds a plain cash line, and nothing is ever
sent to the drawer — no dialog, no error, no traffic.
On site
~~~~~~~
#. Open a PoS session, add a product and go to the payment screen.
#. Select the CashDro payment method. The payment line must show **0**, not the
due amount (that one comes from ``pos_payment_method_cashdro_fix``).
#. The drawer must start the operation and ask for the amount.
If it still fails, the dialog now carries the reason instead of the generic
sentence, and the browser console has the full error under ``[CASHDRO]``. What
to expect:
``The CashDro payment method '...' has no terminal host configured.``
The credentials did not reach the front end. Either the fields are empty on
the payment method, or ``pos_payment_method_cashdro_fix`` is not installed.
``TypeError: Failed to fetch`` / ``NetworkError``
The browser cannot reach ``cashdro_host``. Check the host and that the till
is on the same network as the drawer. A PoS served over HTTPS also blocks a
plain ``http://`` drawer as mixed content, which shows up here.
``The CashDro answered 401/403/500 ...``
The drawer is reachable and answering: wrong user or password, or the
CashDro3WS service is failing.
``The CashDro did not return an operation id: ...``
The drawer refused ``startOperation``. Its own envelope is in the message —
usually a busy drawer with an operation still open from a previous failure.
``The CashDro did not finish the operation in time.``
``startOperation`` was accepted but the drawer never reached the finished
state within five minutes.
There are no automated tests: the flow needs a CashDro terminal answering on the
network, which is also why none of this is caught by the OCA CI.

View file

@ -1,353 +0,0 @@
/* Copyright 2026 - Today Criptomart
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
Payment flow of `pos_payment_method_cashdro` reworked for 18.0. See the
README for the reasoning behind each patch; the short version is that the
base module builds the *startOperation* url with the 17.0 session accessor,
which throws before any request leaves the browser, and then hides every
failure — that one included — behind the same generic dialog.
*/
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { PaymentCashdro } from "@pos_payment_method_cashdro/js/payment_cashdro.esm";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
// The CashDro answers `askOperation` with the state "F" once the operation is
// finished. Anything else means "keep asking", which we do on a timer instead
// of in a tight loop, and give up on after a while.
const POLL_INTERVAL_MS = 700;
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
/**
* Raised when the polling is stopped on purpose (a cancel, or leaving the
* payment screen). It is not an error to report to the operator.
*/
class CashdroAborted extends Error {}
patch(PaymentCashdro.prototype, {
/**
* @override
*/
setup() {
super.setup(...arguments);
// Set while `_cashdro_request_payment` is asking the drawer, so that a
// cancel can actually stop it.
this.cashdro_polling = false;
},
// --------------------------------------------------------------------------
// Entry points of the PaymentInterface
//
// The three of them get the uuid of the line to act on, and the base module
// ignores it in favour of the selected one: the per-line Send/Reverse
// buttons don't select the line first, so with two payment lines on the
// order the CashDro ends up working on the wrong one. None of them can
// delegate to `super`, as the base body is what we are replacing.
// --------------------------------------------------------------------------
/**
* @override
*/
send_payment_request(uuid) {
const order = this.pos.get_order();
const line = this._cashdro_line(order, uuid);
line?.set_payment_status("waiting");
return this.cashdro_send_payment_request(order, line, false);
},
/**
* @override
*/
send_payment_reversal(uuid) {
const order = this.pos.get_order();
const line = this._cashdro_line(order, uuid);
line?.set_payment_status("reversing");
return this.cashdro_send_payment_request(order, line, true);
},
/**
* @override
* `payment_screen` reads the returned value: anything falsy means "the
* cancel failed" and leaves the line in `waitingCard` for good, with
* `deletePaymentLine` re-entering here on every click. The base method
* returns `undefined` even when the operation was finished just fine.
*/
async send_payment_cancel(order) {
// Stop the polling first: `cashdro_finish_operation` closes the
// operation on the drawer, and asking about it afterwards is pointless.
this.cashdro_polling = false;
const operation = (order || this.pos.get_order())?.cashdro_operation;
if (!operation) {
return true;
}
try {
await this.cashdro_finish_operation(operation);
} catch (error) {
console.error("[CASHDRO] finishOperation failed", error);
return false;
}
return true;
},
/**
* @override
* Leaving the payment screen has to stop the polling too, or it keeps
* hitting the drawer for the rest of the session.
*/
close() {
this.cashdro_polling = false;
return super.close(...arguments);
},
// --------------------------------------------------------------------------
// Payment flow
// --------------------------------------------------------------------------
/**
* @override
* Same three steps as the base module (startOperation, acknowledge, poll),
* with the failures made visible and the started operation always closed.
*
* @param {Object} order
* @param {Object} [line] the line to pay, defaults to the selected one
* @param {Boolean} [reversal]
* @returns {Promise<Boolean>}
*/
async cashdro_send_payment_request(order, line = null, reversal = false) {
const payment_line = line || order.get_selected_paymentline();
let operation_id = "";
try {
const amount = this._cashdro_amount(payment_line, reversal);
const res = await this._cashdro_request(this._cashdro_payment_url({ amount }));
operation_id = res.data || "";
if (!operation_id) {
// The drawer answers a refused operation (busy, bad
// credentials, unknown type) with an envelope carrying no
// data. Acknowledging and polling an empty id gets us nowhere.
throw new Error(
_t("The CashDro did not return an operation id: %s", JSON.stringify(res))
);
}
order.cashdro_operation = operation_id;
await this._cashdro_request(this._cashdro_ack_url(operation_id));
const operation_data = await this._cashdro_request_payment(
this._cashdro_ask_url(operation_id)
);
const data = JSON.parse(operation_data.data);
payment_line.cashdro_operation_data = data;
// `totalin` is what the customer put in the drawer, so it only says
// anything when we were collecting. On a reversal core zeroes the
// line itself, and on a refund the line already holds the amount
// the drawer was asked to give back.
if (!reversal && amount > 0) {
payment_line.set_amount(data.operation.totalin / 100);
}
} catch (error) {
if (error instanceof CashdroAborted) {
// `send_payment_cancel` already closed the operation and core
// takes care of the payment line.
return false;
}
console.error("[CASHDRO] payment request failed", error);
// An operation that was started and never finished keeps the drawer
// busy: it refuses the next `startOperation`, so every later
// payment of the session fails until it is power cycled.
if (operation_id) {
await this.cashdro_finish_operation(operation_id).catch((finish_error) =>
console.error("[CASHDRO] could not close the operation", finish_error)
);
}
payment_line?.set_payment_status("retry");
this.env.services.dialog.add(AlertDialog, {
title: _t("Error"),
body: _t(
"An error occurred while connecting to the cashdro.\n\n%s",
error.message || error
),
});
return false;
}
return true;
},
/**
* @override
* The order is not always around any more when the operation has to be
* closed (a cancel right after validating, for instance), and the caller
* wants to know whether the drawer took it.
*/
async cashdro_finish_operation(operation) {
if (!operation) {
return false;
}
const res = await this._cashdro_request(this._cashdro_finish_url(operation));
const order = this.pos.get_order();
if (order) {
order.cashdro_operation = false;
}
return res;
},
// --------------------------------------------------------------------------
// Amounts
// --------------------------------------------------------------------------
/**
* The amount, in cents, to ask the drawer for: positive collects money,
* negative gives it back. Split out of `cashdro_send_payment_request` so
* that `pos_payment_method_cashdro_rounding` has one single place to hook
* into, whatever the flow that reaches the drawer.
*
* @param {Object} line the payment line being paid or reversed
* @param {Boolean} reversal
* @returns {Number}
*/
_cashdro_amount(line, reversal) {
// `get_due()` only discounts the payments that are already `is_done()`,
// and `sendPaymentReverse` sets the line to "reversing" before getting
// here: the line being given back still counts as pending, so the due
// comes out positive and the base module asks the drawer to *collect*
// the amount a second time instead of dispensing it.
const amount = reversal ? -line.get_amount() : line.pos_order_id.get_due();
// The CashDro treats decimals as positions in an integer, and the
// floating point noise has to be absorbed or the drawer rejects the
// request.
return Math.round(amount * 100);
},
// --------------------------------------------------------------------------
// API communication
// --------------------------------------------------------------------------
/**
* @override
* Two changes over the base method (and over the one in
* `pos_payment_method_cashdro_fix`, which this one supersedes): the payment
* method comes from the terminal itself instead of the selected line —
* `send_payment_cancel` runs when the line is already deselected or
* removed — and a missing host is an error instead of the string "false"
* silently travelling into `fetch`, where it resolves against the Odoo
* server and looks exactly like a dead drawer.
*/
_cashdro_url() {
const method = this.payment_method_id;
if (!method?.cashdro_host) {
throw new Error(
_t(
"The CashDro payment method '%s' has no terminal host configured.",
method?.name || ""
)
);
}
// Cashdro machines don't support safe POST calls, so we're sending
// all the data quite unsafely constantly...
let url = `${method.cashdro_host}/Cashdro3WS/index.php`;
url += `?name=${method.cashdro_user}`;
url += `&password=${method.cashdro_password}`;
return url;
},
/**
* @override
* `this.pos.pos_session` is the 17.0 accessor, renamed in 18.0 to the
* `session` getter of `PosStore`. Reading `.name` on the resulting
* `undefined` throws a TypeError on every single payment, before any
* request reaches the drawer, and the base module turns it into the
* generic "error connecting to the cashdro" dialog.
*/
_cashdro_payment_url(parameters) {
const user = this.pos.get_cashier().id || this.pos.user.id;
// Type 4 for payment, 3 for refund
const operationType = parameters.amount > 0 ? 4 : 3;
parameters = { ...parameters, amount: Math.abs(parameters.amount) };
let url = `${this._cashdro_url()}&operation=startOperation&type=${operationType}`;
url += `&posid=pos-${this.pos.session.name}`;
url += `&posuser=${user}`;
url += `&parameters=${encodeURIComponent(JSON.stringify(parameters))}`;
return url;
},
/**
* @override
* The base method assumes any answer is a valid JSON envelope, so an HTTP
* error page comes back as a bare `SyntaxError` with no hint of what the
* drawer actually said.
*/
async _cashdro_request(url) {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
throw new Error(
_t("The CashDro answered %(status)s %(statusText)s", {
status: response.status,
statusText: response.statusText,
})
);
}
const body = await response.text();
try {
return JSON.parse(body);
} catch {
throw new Error(_t("Unexpected answer from the CashDro: %s", body.slice(0, 200)));
}
},
/**
* @override
* The base implementation re-asks the drawer with no delay, no bound and no
* way to stop: it floods the CashDro3WS endpoint for as long as the
* customer is inserting coins — which the embedded web service answers by
* dropping connections — and keeps polling a finished operation after a
* cancel.
*
* @param {String} request_url
* @returns {Promise}
*/
_cashdro_request_payment(request_url) {
this.cashdro_polling = true;
const deadline = Date.now() + POLL_TIMEOUT_MS;
return new Promise((resolve, reject) => {
const ask = async () => {
if (!this.cashdro_polling) {
return reject(new CashdroAborted("Polling stopped"));
}
if (Date.now() > deadline) {
this.cashdro_polling = false;
return reject(
new Error(_t("The CashDro did not finish the operation in time."))
);
}
try {
const responseData = await this._cashdro_request(request_url);
const data = JSON.parse(responseData.data);
if (data.operation.state === "F") {
this.cashdro_polling = false;
return resolve(responseData);
}
setTimeout(ask, POLL_INTERVAL_MS);
} catch (error) {
this.cashdro_polling = false;
reject(error);
}
};
ask();
});
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* The line the terminal has been asked to act on. 18.0 identifies it by
* uuid; the selected line is only a fallback for the base module's own
* calls.
*
* @param {Object} order
* @param {String} [uuid]
* @returns {Object}
*/
_cashdro_line(order, uuid) {
return (uuid && order.get_paymentline_by_uuid(uuid)) || order.get_selected_paymentline();
},
});

View file

@ -1,52 +0,0 @@
# pos_payment_method_cashdro_rounding
Sends the CashDro the amount the customer is really expected to pay — the due
amount with the PoS **cash rounding** applied — instead of the plain due amount
the OCA module asks for.
## Why
A CashDro can only give change with the coins it holds. When the PoS is
configured with a cash rounding (`pos.config.cash_rounding`, typically to 0.05
so that 1 and 2 cent coins are not needed), asking the drawer for the unrounded
due leaves it trying to return change it does not have.
Core already computes that amount: `PosOrder.getDefaultAmountDueToPayIn()` is
what a payment line defaults to when it is created. We simply reuse it, so the
drawer is asked for exactly what the payment screen shows.
`shouldRound()` decides whether the rounding applies at all — it takes
`only_round_cash_method` into account, and the CashDro method is a cash one, so
in practice it is always true when a cash rounding is configured.
## How
One single patch on `PaymentCashdro.prototype._cashdro_amount()`, the hook
`pos_payment_method_cashdro_flow_fix` extracted for this. Everything else of the
OCA payment flow (request, acknowledge, polling, error handling) is untouched.
Two things this deliberately does **not** do:
- It does not touch the **reversals**. There the amount to send is the one of
the line being given back, not the remaining of the order, and it is already
the rounded figure that was collected. Rounding it a second time against the
order total would ask the drawer to collect the total all over again.
- It does not touch `_cashdro_payment_url()`, which is what it patched until
`18.0.1.1.0`. That method is shared by the payment and the reversal paths,
which is exactly how the reversal bug above got in, and it collides with the
fix modules that also patch it — the outcome then depending on the asset load
order.
## Dependencies
Needs `pos_payment_method_cashdro_flow_fix` for the `_cashdro_amount()` hook,
and through it `pos_payment_method_cashdro_fix`: without them the OCA module
does not work on 18.0 at all, so there is nothing to round.
## Testing
There are no automated tests — the flow needs a CashDro answering on the
network. To check it by hand, configure a cash rounding of 0.05 on the PoS, take
an order to a total that is not a multiple of 0.05 (e.g. 17.43) and confirm the
drawer asks for 17.45. See the `USAGE` of
`pos_payment_method_cashdro_flow_fix` for how to read a failure.

View file

@ -1,19 +1,18 @@
# Copyright Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "POS CashDro Rounding",
"version": "18.0.1.1.0",
"version": "18.0.1.0.0",
"summary": "Apply cash/currency rounding to CashDro payment request amount.",
"category": "Point of Sale",
"license": "AGPL-3",
"author": "Criptomart",
"depends": [
"point_of_sale",
"pos_payment_method_cashdro",
"pos_payment_method_cashdro_flow_fix",
],
"assets": {
"point_of_sale._assets_pos": [
"pos_payment_method_cashdro_rounding/static/src/js/cashdro_rounding.esm.js",
],
},
"installable": True,
}

View file

@ -17,29 +17,21 @@ patch(PaymentCashdro.prototype, {
* really expected to pay: the due amount with the cash rounding applied,
* which is the very same amount core defaults a payment line to.
*
* The hook belongs to `pos_payment_method_cashdro_flow_fix`, where the
* amount is computed. This module used to patch `_cashdro_payment_url`
* instead, which is shared with the reversals: there the amount to send is
* the one of the line being given back, not the remaining of the order, so
* reversing a paid order asked the drawer to collect the rounded total all
* over again.
* Only the url builder is patched so the rest of the payment flow of the
* base module (request, acknowledge, polling and error handling) is kept.
*/
_cashdro_amount(line, reversal) {
const order = line.pos_order_id;
// `shouldRound` and `getDefaultAmountDueToPayIn` came with the 18.0 tax
// totals rework. On a build that predates them, fall back instead of
// breaking the payment with a TypeError the operator would read as one
// more connection error.
const rounds =
!reversal &&
typeof order.getDefaultAmountDueToPayIn === "function" &&
order.shouldRound?.(line.payment_method_id);
if (!rounds) {
return super._cashdro_amount(...arguments);
_cashdro_payment_url(parameters) {
const order = this.pos.get_order();
const line = order.get_selected_paymentline();
if (!line || !order.shouldRound(line.payment_method_id)) {
return super._cashdro_payment_url(...arguments);
}
// CashDro treats decimals as positions in an integer, so we round the
// product to avoid the trailing decimals of the floating point
// computation or the drawer would reject our request.
return Math.round(order.getDefaultAmountDueToPayIn(line.payment_method_id) * 100);
const amount = Math.round(
order.getDefaultAmountDueToPayIn(line.payment_method_id) * 100
);
return super._cashdro_payment_url({ ...parameters, amount });
},
});