From f571172206de44819e83d6900542eb375520a934 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 10:37:41 +0200 Subject: [PATCH 01/21] [ADD] module l10n_fr_pos_caisse_ap_ip --- l10n_fr_pos_caisse_ap_ip/__init__.py | 2 + l10n_fr_pos_caisse_ap_ip/__manifest__.py | 25 ++ .../controllers/__init__.py | 1 + l10n_fr_pos_caisse_ap_ip/controllers/main.py | 16 ++ l10n_fr_pos_caisse_ap_ip/models/__init__.py | 1 + .../models/pos_payment_method.py | 145 ++++++++++++ .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 5 + .../static/src/js/models.js | 15 ++ .../static/src/js/payment_caisse_ap_ip.js | 216 ++++++++++++++++++ .../views/pos_payment_method.xml | 25 ++ 11 files changed, 453 insertions(+) create mode 100644 l10n_fr_pos_caisse_ap_ip/__init__.py create mode 100644 l10n_fr_pos_caisse_ap_ip/__manifest__.py create mode 100644 l10n_fr_pos_caisse_ap_ip/controllers/__init__.py create mode 100644 l10n_fr_pos_caisse_ap_ip/controllers/main.py create mode 100644 l10n_fr_pos_caisse_ap_ip/models/__init__.py create mode 100644 l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py create mode 100644 l10n_fr_pos_caisse_ap_ip/readme/CONTRIBUTORS.rst create mode 100644 l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst create mode 100644 l10n_fr_pos_caisse_ap_ip/static/src/js/models.js create mode 100644 l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js create mode 100644 l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml diff --git a/l10n_fr_pos_caisse_ap_ip/__init__.py b/l10n_fr_pos_caisse_ap_ip/__init__.py new file mode 100644 index 000000000..f7209b171 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/l10n_fr_pos_caisse_ap_ip/__manifest__.py b/l10n_fr_pos_caisse_ap_ip/__manifest__.py new file mode 100644 index 000000000..46f45c243 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2023 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'POS: Caisse-AP payment protocol for France', + 'version': '16.0.1.0.0', + 'category': 'Point of Sale', + 'license': 'AGPL-3', + 'summary': 'Add support for Caisse-AP payment protocol used in France', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'maintainers': ['alexis-via'], + 'website': 'https://github.com/OCA/l10n-france', + 'depends': ['point_of_sale'], + 'data': [ + 'views/pos_payment_method.xml', + ], + "assets": { + "point_of_sale.assets": [ + "pos_caisse_ap_ip/static/src/js/models.js", + "pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js", + ], + }, + 'installable': True, +} diff --git a/l10n_fr_pos_caisse_ap_ip/controllers/__init__.py b/l10n_fr_pos_caisse_ap_ip/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/l10n_fr_pos_caisse_ap_ip/controllers/main.py b/l10n_fr_pos_caisse_ap_ip/controllers/main.py new file mode 100644 index 000000000..675fa46f0 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/controllers/main.py @@ -0,0 +1,16 @@ +# Copyright 2023 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +from odoo import http, _ +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class PosCaisseApIpController(http.Controller): + + @http.route('/pos/caisse_ap_ip_payment_terminal_request', type='json', auth='user') + def caisse_ap_ip_payment_terminal_request(self, **kwargs): + print('caisse_ap_ip_payment_terminal_request self=', self) diff --git a/l10n_fr_pos_caisse_ap_ip/models/__init__.py b/l10n_fr_pos_caisse_ap_ip/models/__init__.py new file mode 100644 index 000000000..58690ef8d --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/models/__init__.py @@ -0,0 +1 @@ +from . import pos_payment_method diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py new file mode 100644 index 000000000..7cd82a326 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -0,0 +1,145 @@ +# Copyright 2023 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +import socket +import logging +logger = logging.getLogger(__name__) + +try: + import pycountry +except ImportError: + logger.debug('Cannot import pycountry') + +CAISSE_AP_TIMEOUT = 180 + + +class PosPaymentMethod(models.Model): + _inherit = 'pos.payment.method' + + def _get_payment_terminal_selection(self): + res = super()._get_payment_terminal_selection() + res.append(("caisse_ap_ip", _("Caisse AP over IP"))) + return res + + caisse_ap_ip_mode = fields.Selection( + [("card", "Card"), ("check", "Check")], string="Payment Mode", default="card" + ) + caisse_ap_ip_address = fields.Char(string='Caisse-AP Payment Terminal IP Address', help="IP address or DNS name of the payment terminal that support Caisse-AP protocol over IP") + caisse_ap_ip_port = fields.Integer(string='Caisse-AP Payment Terminal Port', help="TCP port of the payment terminal that support Caisse-AP protocol over IP", default=8888) + + @api.constrains('caisse_ap_ip_port') + def _check_caisse_ap_ip_port(self): + for config in self: + if config.caisse_ap_ip_port < 1 or config.caisse_ap_ip_port > 65535: + raise ValidationError(_("Port %s for the payment terminal is not a valid TCP port.") % config.caisse_ap_ip_port) + + @api.constrains('use_payment_terminal', 'caisse_ap_ip_address', 'caisse_ap_ip_port') + def _check_caisse_ap_ip(self): + for method in self: + if method.use_payment_terminal == 'caisse_ap_ip': + if not method.caisse_ap_ip_address: + raise ValidationError(_( + "Caisse-AP Payment Terminal IP Address is not set on Payment Method '%s'.") % method.display_name) + if not method.caisse_ap_ip_port: + raise ValidationError(_( + "Caisse-AP Payment Terminal Port is not set on Payment Method '%s'.") % method.display_name) + + def _prepare_caisse_ap_ip_msg(self, msg_dict): + assert isinstance(msg_dict, dict) + for tag, value in msg_dict.items(): + assert isinstance(tag, str) + assert len(tag) == 2 + assert isinstance(value, str) + assert len(value) >= 1 + assert len(value) <= 999 + msg_list = [] + # Always start with tag CZ + # the order of the other tags is unrelevant + if 'CZ' in msg_dict: + version = msg_dict.pop('CZ') + else: + version = '0300' # 0301 ?? + assert len(version) == 4 + msg_list.append(('CZ', version)) + msg_list += list(msg_dict.items()) + msg_str = ''.join(['%s%s%s' % (tag, str(len(value)).zfill(3), value) for (tag, value) in msg_list]) + return msg_str + + @api.model + def caisse_ap_ip_send_payment(self, data): + # called by JS code + print('caisse_ap_ip_send_payment data=', data) + logger.debug('caisse_ap_ip_send_payment data=%s', data) + amount = data.get('amount') + payment_method_id = data['payment_method_id'] + currency_id = data['currency_id'] + currency = self.env['res.currency'].browse(currency_id) + print('payment_method_id=', payment_method_id) + payment_method = self.browse(payment_method_id) +# p48 - identification caisse terminal +# CZ version protocole +# CJ identifiant protocole concert : aucun intérêt, mais obligatoire +# CA numéro de caisse +# CD Type action 0 débit (achat) 1 crédit (remboursement) +# BF paiement partiel 0 refusé 1 accepté + cur_speed_map = { # small speed-up, and works even if pycountry not installed + 'EUR': '978', + 'XPF': '953', + } + if currency.name in cur_speed_map: + cur_num = cur_speed_map[currency.name] + else: + try: + cur = pycountry.currencies.get(alpha_3=currency.name) + cur_num = cur.numeric # it returns a string + except Exception as e: + logger.error("pycountry doesn't support currency '%s'. Error: %s" % (cur_iso_alpha, e)) + return False + msg_dict = { + 'CJ': '012345678901', + 'CA': '01', + 'CE': cur_num, + 'BF': '0', + } + amount_compare = currency.compare_amounts(amount, 0) + if not amount_compare: + logger.error('Amount for payment terminal is 0') + return False # TODO raise Error? + elif amount_compare < 0: + msg_dict['CD'] = '1' # credit i.e. reimbursement + amount_positive = amount * -1 + else: + msg_dict['CD'] = '0' # debit i.e. regular payment + amount_positive = amount + +# Réponse sur MESSAGE_ID : CZ0040301CJ012330538600404CA00201CD001IAE00210 +# p36 Transaction +# CB = montant en centimes, longueur variable 2 à 12 +# CD = action (0 pour débit ; 1 pour remboursement ; 2 pour annulation) +# CE = devise 978 pour euro +# CH : optionnel : Numéro de référence donné lors de la transaction (En fonction du type d’action demandée par la caisse, ce numéro peut être vérifié par le terminal) + if currency.decimal_places: + amount_cent = amount_positive * (10 ** currency.decimal_places) + else: + amount_cent = amount_positive + amount_str = str(int(round(amount_cent))) + msg_dict['CB'] = amount_str + if len(amount_str) < 2: + amount_str = amount_str.zfill(2) + elif len(amount_str) > 12: + logger.error("Amount with cents '%s' is over the maximum." % amount_str) + return False + if payment_method.caisse_ap_ip_mode == 'check': + msg_dict['CC'] = '00C' + with socket.create_connection((payment_method.caisse_ap_ip_address, payment_method.caisse_ap_ip_port), timeout=CAISSE_AP_TIMEOUT) as sock: + sock.settimeout(None) + msg_str = self._prepare_caisse_ap_ip_msg(msg_dict) + logger.debug('data sent to payment terminal: %s' % msg_str) + sock.send(msg_str.encode('ascii')) + BUFFER_SIZE = 1024 + answer = sock.recv(BUFFER_SIZE) + logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) + return True diff --git a/l10n_fr_pos_caisse_ap_ip/readme/CONTRIBUTORS.rst b/l10n_fr_pos_caisse_ap_ip/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..949e75240 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Alexis de Lattre +* Pierrick Brun diff --git a/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst new file mode 100644 index 000000000..2206cb530 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module adds support for the **Caisse AP** protocol over IP in the Odoo Point of Sale. + +The `Caisse AP protocol `_ is a vendor-independent protocol used in France to communicate between a point of sale and a payment terminal. It is implemented by `Ingenico `_ payment terminals, `Verifone `_ payment terminal and other brands of payment terminals. This protocol is designed by a French association called `Association du paiement `_, abbreviated as **AP**. Note that the Caisse-AP protocol is used by Ingenico payment terminals deployed in France, but not by the same model of Ingenico payment terminals deployed in other countries! + +The Caisse-AP protocol was initially written for serial and USB. Since the Caisse AP protocol version 3.x, it also supports IP. When used over IP, the client (point of sale) and the server (payment terminal) exchange simple text data encoded as ASCII over a raw TCP socket. diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js new file mode 100644 index 000000000..534a629ca --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js @@ -0,0 +1,15 @@ +/* + Copyright 2023 Akretion (www.akretion.com) + @author: Alexis de Lattre + @author: Rémi de Lattre + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +*/ + +odoo.define("pos_caisse_ap_ip.models", function (require) { + "use strict"; + + var models = require("point_of_sale.models"); + var CaisseAPIP = require("pos_caisse_ap_ip.payment"); + models.register_payment_method("caisse_ap_ip", CaisseAPIP); + +}); diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js new file mode 100644 index 000000000..e34b75d7c --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -0,0 +1,216 @@ +/* + Copyright 2023 Akretion France (http://www.akretion.com/) + @author: Alexis de Lattre + @author: Rémi de Lattre + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +*/ + +odoo.define("pos_caisse_ap_ip.payment", function (require) { + "use strict"; + + var core = require("web.core"); + var rpc = require('web.rpc'); + var PaymentInterface = require("point_of_sale.PaymentInterface"); + const {Gui} = require("point_of_sale.Gui"); + + var _t = core._t; + + var PaymentCaisseApIp = PaymentInterface.extend({ + init: function () { + this._super.apply(this, arguments); + }, + + send_payment_request: function (cid) { + console.log('send_payment_request'); + this._super.apply(this, arguments); + // check that this.payment_method.use_payment_terminal is ?? + return this._caisse_ap_ip_pay(cid); + }, + + send_payment_cancel: function (order, cid) { + console.log('send_payment_cancel'); + this._super.apply(this, arguments); + this._show_error( + _t( + "Please press the red button on the payment terminal to cancel the transaction." + ) + ); + return Promise.reject(); + }, + + _caisse_ap_ip_pay: function (cid) { + console.log('_caisse_ap_ip_pay===== cid='); + console.log(cid); + var order = this.pos.get_order(); + var pay_line = order.selected_paymentline; + var currency = this.pos.currency; + console.log('Currency='); + console.log('THIS1===='); + console.log(this); + console.log(currency); + var data = { + amount: pay_line.amount, + currency_id: currency.id, + payment_method_id: this.payment_method.id, + payment_id: cid, + //order_id: order.name, + }; + console.log('data===='); + console.log(data); +/* if (this.payment_method.oca_payment_terminal_id) { + data.terminal_id = this.payment_method.oca_payment_terminal_id; + } */ + return this._caisse_ap_ip_request(data).then((response) => { + if (response === false) { + this._show_error( + _t( + "Failed to send the amount to pay to the payment terminal. Press the red button on the payment terminal and try again." + ) + ); + // There was an error, let the user retry. + return false; + } else if (response instanceof Object && "transaction_id" in response) { + // The response has a terminal transaction identifier: + // return a promise that polls for transaction status. + pay_line.set_payment_status("waitingCard"); + this._caisse_ap_ip_update_payment_line_terminal_transaction_status( + pay_line, + response + ); + return new Promise((resolve, reject) => { + this._caisse_ap_ip_poll_for_transaction_status( + pay_line, + resolve, + reject + ); + }); + } + + // The transaction was started, but the terminal driver + // does not report status, so we won't know the + // transaction result: we let the user enter the + // outcome manually. This is done by rejecting the + // promise as explained in the send_payment_request() + // documentation. + pay_line.set_payment_status("force_done"); + return Promise.reject(); + }); + }, + + _caisse_ap_ip_poll_for_transaction_status: function (pay_line, resolve, reject) { + var timerId = setInterval(() => { + // Query the driver status more frequently than the regular POS + // proxy, to get faster feedback when the transaction is + // complete on the terminal. + var status_params = {}; + if (this.payment_method.oca_payment_terminal_id) { + status_params.terminal_id = + this.payment_method.oca_payment_terminal_id; + } + this.pos.env.proxy.connection + .rpc("/hw_proxy/status_json", status_params, { + shadow: true, + timeout: 1000, + }) + .then((drivers_status) => { + for (var driver_name in drivers_status) { + // Look for a driver that is a payment terminal and has + // transactions. + var driver = drivers_status[driver_name]; + if (!driver.is_terminal || !("transactions" in driver)) { + continue; + } + for (var transaction_id in driver.transactions) { + var transaction = driver.transactions[transaction_id]; + if ( + transaction.transaction_id === + pay_line.terminal_transaction_id + ) { + // Look for the transaction corresponding to + // the payment line. + this._caisse_ap_ip_update_payment_line_terminal_transaction_status( + pay_line, + transaction + ); + if ( + pay_line.terminal_transaction_success !== null + ) { + resolve(pay_line.terminal_transaction_success); + // Stop the loop + clearInterval(timerId); + } + } + } + } + }) + .catch(() => { + console.error("Error querying terminal driver status"); + // We could not query the transaction status so we + // won't know the transaction result: we let the user + // enter the outcome manually. This is done by + // rejecting the promise as explained in the + // send_payment_request() documentation. + pay_line.set_payment_status("force_done"); + reject(); + // Stop the loop + clearInterval(timerId); + }); + }, 1000); + }, + + _caisse_ap_ip_update_payment_line_terminal_transaction_status: function ( + pay_line, + transaction + ) { + pay_line.terminal_transaction_id = transaction.transaction_id; + pay_line.terminal_transaction_success = transaction.success; + pay_line.terminal_transaction_status = transaction.status; + pay_line.terminal_transaction_status_details = transaction.status_details; + // Payment transaction reference, for accounting reconciliation purposes. + pay_line.transaction_id = transaction.reference; + }, + + _caisse_ap_ip_request: function (data) { + console.log('_caisse_ap_ip_payment_terminal_request data='); + console.log(data); + var self = this; + console.log('this==='); + console.log(this); + rpc.query({ + model: "pos.payment.method", + method: "caisse_ap_ip_send_payment", + args: [data], + }, { + timeout: 5000, + shadow: true, + }).then((response) => { + return response; + }) + .catch(() => { + console.error("Error starting payment transaction"); + return false; + }) + ; +/* + return this.pos.env.proxy + .message("payment_terminal_transaction_start", { + payment_info: JSON.stringify(data), + }) + .then((response) => { + return response; + }) + .catch(() => { + console.error("Error starting payment transaction"); + return false; + }); */ + }, + + _show_error: function (msg, title) { + Gui.showPopup("ErrorPopup", { + title: title || _t("Payment Terminal Error"), + body: msg, + }); + }, + }); + return PaymentCaisseApIp; +}); diff --git a/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml new file mode 100644 index 000000000..0d24aaca4 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml @@ -0,0 +1,25 @@ + + + + + + + + + pos.payment.method + + + + + + + + + + + + From ca73d28b8233eb7891795fc12bcab4895d23c6e7 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 15:41:47 +0200 Subject: [PATCH 02/21] l10n_fr_pos_caisse_ap_ip: parse answer from payment terminal in case of success --- l10n_fr_pos_caisse_ap_ip/__init__.py | 1 - l10n_fr_pos_caisse_ap_ip/__manifest__.py | 4 +- .../controllers/__init__.py | 1 - l10n_fr_pos_caisse_ap_ip/controllers/main.py | 16 --- .../models/pos_payment_method.py | 99 ++++++++++++++++++- .../static/src/js/models.js | 4 +- .../static/src/js/payment_caisse_ap_ip.js | 28 ++---- 7 files changed, 110 insertions(+), 43 deletions(-) delete mode 100644 l10n_fr_pos_caisse_ap_ip/controllers/__init__.py delete mode 100644 l10n_fr_pos_caisse_ap_ip/controllers/main.py diff --git a/l10n_fr_pos_caisse_ap_ip/__init__.py b/l10n_fr_pos_caisse_ap_ip/__init__.py index f7209b171..0650744f6 100644 --- a/l10n_fr_pos_caisse_ap_ip/__init__.py +++ b/l10n_fr_pos_caisse_ap_ip/__init__.py @@ -1,2 +1 @@ from . import models -from . import controllers diff --git a/l10n_fr_pos_caisse_ap_ip/__manifest__.py b/l10n_fr_pos_caisse_ap_ip/__manifest__.py index 46f45c243..d16af1f3e 100644 --- a/l10n_fr_pos_caisse_ap_ip/__manifest__.py +++ b/l10n_fr_pos_caisse_ap_ip/__manifest__.py @@ -17,8 +17,8 @@ ], "assets": { "point_of_sale.assets": [ - "pos_caisse_ap_ip/static/src/js/models.js", - "pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js", + "l10n_fr_pos_caisse_ap_ip/static/src/js/models.js", + "l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js", ], }, 'installable': True, diff --git a/l10n_fr_pos_caisse_ap_ip/controllers/__init__.py b/l10n_fr_pos_caisse_ap_ip/controllers/__init__.py deleted file mode 100644 index 12a7e529b..000000000 --- a/l10n_fr_pos_caisse_ap_ip/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import main diff --git a/l10n_fr_pos_caisse_ap_ip/controllers/main.py b/l10n_fr_pos_caisse_ap_ip/controllers/main.py deleted file mode 100644 index 675fa46f0..000000000 --- a/l10n_fr_pos_caisse_ap_ip/controllers/main.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2023 Akretion France (http://www.akretion.com/) -# @author: Alexis de Lattre -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -import logging -from odoo import http, _ -from odoo.http import request - -_logger = logging.getLogger(__name__) - - -class PosCaisseApIpController(http.Controller): - - @http.route('/pos/caisse_ap_ip_payment_terminal_request', type='json', auth='user') - def caisse_ap_ip_payment_terminal_request(self, **kwargs): - print('caisse_ap_ip_payment_terminal_request self=', self) diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index 7cd82a326..5846871a1 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -6,6 +6,7 @@ from odoo.exceptions import ValidationError import socket import logging +from pprint import pprint logger = logging.getLogger(__name__) try: @@ -47,7 +48,7 @@ def _check_caisse_ap_ip(self): raise ValidationError(_( "Caisse-AP Payment Terminal Port is not set on Payment Method '%s'.") % method.display_name) - def _prepare_caisse_ap_ip_msg(self, msg_dict): + def _caisse_ap_ip_prepare_msg(self, msg_dict): assert isinstance(msg_dict, dict) for tag, value in msg_dict.items(): assert isinstance(tag, str) @@ -134,12 +135,104 @@ def caisse_ap_ip_send_payment(self, data): return False if payment_method.caisse_ap_ip_mode == 'check': msg_dict['CC'] = '00C' + # TODO Try/except quand l'IP n'est pas joignable + answer = False with socket.create_connection((payment_method.caisse_ap_ip_address, payment_method.caisse_ap_ip_port), timeout=CAISSE_AP_TIMEOUT) as sock: sock.settimeout(None) - msg_str = self._prepare_caisse_ap_ip_msg(msg_dict) + msg_str = self._caisse_ap_ip_prepare_msg(msg_dict) logger.debug('data sent to payment terminal: %s' % msg_str) sock.send(msg_str.encode('ascii')) BUFFER_SIZE = 1024 answer = sock.recv(BUFFER_SIZE) - logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) + logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) + if answer: + answer_dict = self._caisse_ap_ip_parse_answer(answer) + self._caisse_ap_ip_check_answer(answer_dict, msg_dict) + if answer_dict.get('AE') == '10': + to_pos_dict = self._caisse_ap_ip_prepare_success_to_pos_dict(answer_dict) + logger.debug('JSON sent back to POS: %s', to_pos_dict) + return to_pos_dict return True + + def _caisse_ap_ip_check_answer(self, answer_dict, msg_dict): + tag_dict = { + 'CA': {'fixed_size': True, 'required': True, 'label': 'caisse'}, + 'CB': {'fixed_size': False, 'required': True, 'label': 'amount'}, + 'CD': {'fixed_size': True, 'required': True, 'label': 'action pay/reimb'}, + 'CE': {'fixed_size': True, 'required': True, 'label': 'currency'}, + 'BF': {'fixed_size': True, 'required': False, 'label': 'partial payment'}, + } + for tag, props in tag_dict.items(): + if props['required'] and not answer_dict.get(tag): + raise UserError(_("Caisse AP IP protocol: tag %s is required but it is not present in the answer. This should never happen!") % answer_dict.get(tag)) + if props['fixed_size'] and answer_dict.get(tag) and answer_dict[tag] != msg_dict[tag]: + raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=answer_dict[tag])) + elif not props['fixed_size'] and answer_dict.get(tag): + strip_answer = answer_dict[tag].lstrip('0') + if msg_dict[tag] != strip_answer: + raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=strip_answer)) + + def _caisse_ap_ip_prepare_success_to_pos_dict(self, answer_dict): + card_type_list = [] + cc_labels = { + '1': 'CB contact', + 'B': 'CB sans contact', + 'C': 'Chèque', + '2': 'Amex contact', + 'D': 'Amex sans contact', + '3': 'CB Enseigne', + '5': 'Cofinoga', + '6': 'Diners', + '7': 'CB-Pass', + '8': 'Franfinance', + '9': 'JCB', + 'A': 'Banque Accord', + 'I': 'CPEI', + 'E': 'CMCIC-Pay TPE', + 'U': 'CUP', + '0': 'Autres', + } + ci_labels = { + '0': 'indifférent', + '1':' contact', + '2': 'sans contact', + '3': 'piste', + '4': 'saisie manuelle', + } + ticket = False + if answer_dict.get('CC') and len(answer_dict['CC']) == 3: + cc_tag = answer_dict['CC'].lstrip('0') + card_type_list.append(_('Application %(label)s (code %(code)s)', label=cc_labels.get(cc_tag, _('unknown')), code=cc_tag)) + ticket = cc_labels.get(cc_tag, False) + if answer_dict.get('CI') and len(answer_dict['CI']) == 1: + card_type_list.append(_('Read mode: %(label)s (code %(code)s)', label=ci_labels.get(answer_dict['CI'], _('unknown')), code=answer_dict['CI'])) + + transaction_tags = ['AA', 'AB', 'AC', 'AI', 'CD'] + transaction_id = '|'.join(['%s-%s' % (tag, answer_dict[tag]) for tag in transaction_tags if answer_dict.get(tag)]) + + to_pos_dict = { + 'transaction_id': transaction_id, + 'card_type': ' - '.join(card_type_list), + 'ticket': ticket, + } + return to_pos_dict + + def _caisse_ap_ip_parse_answer(self, data_bytes): + print('answer=', data_bytes) + print('TYPE(answer)', type(data_bytes)) + data_str = data_bytes.decode('ascii') + logger.info('Received raw data: %s', data_str) + data_dict = {} + i = 0 + while i < len(data_str): + tag = data_str[i:i + 2] + i += 2 + size_str = data_str[i:i + 3] + size = int(size_str) + i += 3 + value = data_str[i:i + size] + data_dict[tag] = value + i += size + logger.info('Answer dict:') + pprint(data_dict) + return data_dict diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js index 534a629ca..d803a548d 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js @@ -5,11 +5,11 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ -odoo.define("pos_caisse_ap_ip.models", function (require) { +odoo.define("l10n_fr_pos_caisse_ap_ip.models", function (require) { "use strict"; var models = require("point_of_sale.models"); - var CaisseAPIP = require("pos_caisse_ap_ip.payment"); + var CaisseAPIP = require("l10n_fr_pos_caisse_ap_ip.payment"); models.register_payment_method("caisse_ap_ip", CaisseAPIP); }); diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js index e34b75d7c..4ced6ffef 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -5,7 +5,7 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ -odoo.define("pos_caisse_ap_ip.payment", function (require) { +odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { "use strict"; var core = require("web.core"); @@ -92,6 +92,7 @@ odoo.define("pos_caisse_ap_ip.payment", function (require) { // outcome manually. This is done by rejecting the // promise as explained in the send_payment_request() // documentation. + console.log('set_payment_status force_done') pay_line.set_payment_status("force_done"); return Promise.reject(); }); @@ -171,19 +172,22 @@ odoo.define("pos_caisse_ap_ip.payment", function (require) { }, _caisse_ap_ip_request: function (data) { - console.log('_caisse_ap_ip_payment_terminal_request data='); + console.log('_caisse_ap_ip_request data='); console.log(data); - var self = this; - console.log('this==='); + console.log('_caisse_ap_ip_request this==='); console.log(this); - rpc.query({ + console.log('rpc.query'); + return rpc.query({ model: "pos.payment.method", method: "caisse_ap_ip_send_payment", args: [data], }, { - timeout: 5000, + // timeout in ms + timeout: 180000, shadow: true, }).then((response) => { + console.log('rpc.query THEN response='); + console.log(response); return response; }) .catch(() => { @@ -191,18 +195,6 @@ odoo.define("pos_caisse_ap_ip.payment", function (require) { return false; }) ; -/* - return this.pos.env.proxy - .message("payment_terminal_transaction_start", { - payment_info: JSON.stringify(data), - }) - .then((response) => { - return response; - }) - .catch(() => { - console.error("Error starting payment transaction"); - return false; - }); */ }, _show_error: function (msg, title) { From 0ad273352fd4821a2c72476f4632673b36bce64b Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 16:10:06 +0200 Subject: [PATCH 03/21] l10n_fr_pos_caisse_ap_ip: add support for failures --- .../models/pos_payment_method.py | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index 5846871a1..3d0d4b61a 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError import socket import logging from pprint import pprint @@ -89,7 +89,8 @@ def caisse_ap_ip_send_payment(self, data): cur_speed_map = { # small speed-up, and works even if pycountry not installed 'EUR': '978', 'XPF': '953', - } + 'USD': '840', # Only because it is the default currency + } if currency.name in cur_speed_map: cur_num = cur_speed_map[currency.name] else: @@ -97,7 +98,7 @@ def caisse_ap_ip_send_payment(self, data): cur = pycountry.currencies.get(alpha_3=currency.name) cur_num = cur.numeric # it returns a string except Exception as e: - logger.error("pycountry doesn't support currency '%s'. Error: %s" % (cur_iso_alpha, e)) + logger.error("pycountry doesn't support currency '%s'. Error: %s" % (currency.name, e)) return False msg_dict = { 'CJ': '012345678901', @@ -149,9 +150,12 @@ def caisse_ap_ip_send_payment(self, data): answer_dict = self._caisse_ap_ip_parse_answer(answer) self._caisse_ap_ip_check_answer(answer_dict, msg_dict) if answer_dict.get('AE') == '10': - to_pos_dict = self._caisse_ap_ip_prepare_success_to_pos_dict(answer_dict) + to_pos_dict = self._caisse_ap_ip_success_to_pos_dict(answer_dict) logger.debug('JSON sent back to POS: %s', to_pos_dict) return to_pos_dict + elif answer_dict.get('AE') == '01': + error_msg = self._caisse_ap_ip_failure_error_msg(answer_dict) + raise UserError(error_msg) return True def _caisse_ap_ip_check_answer(self, answer_dict, msg_dict): @@ -172,7 +176,7 @@ def _caisse_ap_ip_check_answer(self, answer_dict, msg_dict): if msg_dict[tag] != strip_answer: raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=strip_answer)) - def _caisse_ap_ip_prepare_success_to_pos_dict(self, answer_dict): + def _caisse_ap_ip_success_to_pos_dict(self, answer_dict): card_type_list = [] cc_labels = { '1': 'CB contact', @@ -194,7 +198,7 @@ def _caisse_ap_ip_prepare_success_to_pos_dict(self, answer_dict): } ci_labels = { '0': 'indifférent', - '1':' contact', + '1': 'contact', '2': 'sans contact', '3': 'piste', '4': 'saisie manuelle', @@ -202,8 +206,9 @@ def _caisse_ap_ip_prepare_success_to_pos_dict(self, answer_dict): ticket = False if answer_dict.get('CC') and len(answer_dict['CC']) == 3: cc_tag = answer_dict['CC'].lstrip('0') - card_type_list.append(_('Application %(label)s (code %(code)s)', label=cc_labels.get(cc_tag, _('unknown')), code=cc_tag)) - ticket = cc_labels.get(cc_tag, False) + cc_label = cc_labels.get(cc_tag, _('unknown')) + card_type_list.append(_('Application %(label)s (code %(code)s)', label=cc_label, code=cc_tag)) + ticket = _('Card type: %s') % cc_label if answer_dict.get('CI') and len(answer_dict['CI']) == 1: card_type_list.append(_('Read mode: %(label)s (code %(code)s)', label=ci_labels.get(answer_dict['CI'], _('unknown')), code=answer_dict['CI'])) @@ -217,9 +222,30 @@ def _caisse_ap_ip_prepare_success_to_pos_dict(self, answer_dict): } return to_pos_dict + def _caisse_ap_ip_failure_error_msg(self, answer_dict): + error_msg = _("The payment transaction has failed.") + af_labels = { + '00': 'Inconnu', + '01': 'Transaction autorisé', + '02': 'Appel phonie', + '03': 'Forçage', + '04': 'Refusée', + '05': 'Interdite', + '06': 'Abandon', + '07': 'Non aboutie', + '08': 'Opération non effectuée Time-out saisie', + '09': 'Opération non effectuée erreur format message', + '10': 'Opération non effectuée erreur sélection', + '11': 'Opération non effectuée Abandon Opérateur', + '12': 'Opération non effectuée type d’action demandé inconnu', + '13': 'Devise non supportée', + } + if answer_dict.get('AF') and answer_dict['AF'] in af_labels: + label = af_labels[answer_dict['AF']] + error_msg = _("The payment transaction has failed: %s") % label + return error_msg + def _caisse_ap_ip_parse_answer(self, data_bytes): - print('answer=', data_bytes) - print('TYPE(answer)', type(data_bytes)) data_str = data_bytes.decode('ascii') logger.info('Received raw data: %s', data_str) data_dict = {} From 6f7866c1a15689ad163d1f2d133eb77f2593b456 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 16:47:55 +0200 Subject: [PATCH 04/21] l10n_fr_pos_caisse_ap_ip: rename fields and improve view --- .../models/pos_payment_method.py | 76 +++++++++---------- .../static/src/js/models.js | 2 +- .../static/src/js/payment_caisse_ap_ip.js | 2 +- .../views/pos_payment_method.xml | 6 +- 4 files changed, 40 insertions(+), 46 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index 3d0d4b61a..5801882a9 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -22,33 +22,31 @@ class PosPaymentMethod(models.Model): def _get_payment_terminal_selection(self): res = super()._get_payment_terminal_selection() - res.append(("caisse_ap_ip", _("Caisse AP over IP"))) + res.append(("fr-caisse_ap_ip", _("Caisse AP over IP (France only)"))) return res - caisse_ap_ip_mode = fields.Selection( + fr_caisse_ap_ip_mode = fields.Selection( [("card", "Card"), ("check", "Check")], string="Payment Mode", default="card" ) - caisse_ap_ip_address = fields.Char(string='Caisse-AP Payment Terminal IP Address', help="IP address or DNS name of the payment terminal that support Caisse-AP protocol over IP") - caisse_ap_ip_port = fields.Integer(string='Caisse-AP Payment Terminal Port', help="TCP port of the payment terminal that support Caisse-AP protocol over IP", default=8888) + fr_caisse_ap_ip_address = fields.Char(string='Caisse-AP Payment Terminal IP Address', help="IP address or DNS name of the payment terminal that support Caisse-AP protocol over IP") + fr_caisse_ap_ip_port = fields.Integer(string='Caisse-AP Payment Terminal Port', help="TCP port of the payment terminal that support Caisse-AP protocol over IP", default=8888) - @api.constrains('caisse_ap_ip_port') - def _check_caisse_ap_ip_port(self): - for config in self: - if config.caisse_ap_ip_port < 1 or config.caisse_ap_ip_port > 65535: - raise ValidationError(_("Port %s for the payment terminal is not a valid TCP port.") % config.caisse_ap_ip_port) - - @api.constrains('use_payment_terminal', 'caisse_ap_ip_address', 'caisse_ap_ip_port') - def _check_caisse_ap_ip(self): + @api.constrains('use_payment_terminal', 'fr_caisse_ap_ip_address', 'fr_caisse_ap_ip_port') + def _check_fr_caisse_ap_ip(self): for method in self: if method.use_payment_terminal == 'caisse_ap_ip': - if not method.caisse_ap_ip_address: + if not method.fr_caisse_ap_ip_address: raise ValidationError(_( "Caisse-AP Payment Terminal IP Address is not set on Payment Method '%s'.") % method.display_name) - if not method.caisse_ap_ip_port: + if not method.fr_caisse_ap_ip_port: raise ValidationError(_( "Caisse-AP Payment Terminal Port is not set on Payment Method '%s'.") % method.display_name) - def _caisse_ap_ip_prepare_msg(self, msg_dict): + if method.fr_caisse_ap_ip_port < 1 or method.fr_caisse_ap_ip_port > 65535: + raise ValidationError(_( + "Port %s for the payment terminal is not a valid TCP port.") % method.fr_caisse_ap_ip_port) + + def _fr_caisse_ap_ip_prepare_msg(self, msg_dict): assert isinstance(msg_dict, dict) for tag, value in msg_dict.items(): assert isinstance(tag, str) @@ -57,6 +55,7 @@ def _caisse_ap_ip_prepare_msg(self, msg_dict): assert len(value) >= 1 assert len(value) <= 999 msg_list = [] + # CZ tag: protocol version # Always start with tag CZ # the order of the other tags is unrelevant if 'CZ' in msg_dict: @@ -70,9 +69,8 @@ def _caisse_ap_ip_prepare_msg(self, msg_dict): return msg_str @api.model - def caisse_ap_ip_send_payment(self, data): + def fr_caisse_ap_ip_send_payment(self, data): # called by JS code - print('caisse_ap_ip_send_payment data=', data) logger.debug('caisse_ap_ip_send_payment data=%s', data) amount = data.get('amount') payment_method_id = data['payment_method_id'] @@ -80,12 +78,6 @@ def caisse_ap_ip_send_payment(self, data): currency = self.env['res.currency'].browse(currency_id) print('payment_method_id=', payment_method_id) payment_method = self.browse(payment_method_id) -# p48 - identification caisse terminal -# CZ version protocole -# CJ identifiant protocole concert : aucun intérêt, mais obligatoire -# CA numéro de caisse -# CD Type action 0 débit (achat) 1 crédit (remboursement) -# BF paiement partiel 0 refusé 1 accepté cur_speed_map = { # small speed-up, and works even if pycountry not installed 'EUR': '978', 'XPF': '953', @@ -100,6 +92,9 @@ def caisse_ap_ip_send_payment(self, data): except Exception as e: logger.error("pycountry doesn't support currency '%s'. Error: %s" % (currency.name, e)) return False + # CJ identifiant protocole concert : no interest, but required + # CA POS number + # BF partial payments: 0=refused 1=accepted msg_dict = { 'CJ': '012345678901', 'CA': '01', @@ -107,6 +102,7 @@ def caisse_ap_ip_send_payment(self, data): 'BF': '0', } amount_compare = currency.compare_amounts(amount, 0) + # CD Action type: 0=debit (regular payment) 1=credit (reimbursement) if not amount_compare: logger.error('Amount for payment terminal is 0') return False # TODO raise Error? @@ -116,13 +112,6 @@ def caisse_ap_ip_send_payment(self, data): else: msg_dict['CD'] = '0' # debit i.e. regular payment amount_positive = amount - -# Réponse sur MESSAGE_ID : CZ0040301CJ012330538600404CA00201CD001IAE00210 -# p36 Transaction -# CB = montant en centimes, longueur variable 2 à 12 -# CD = action (0 pour débit ; 1 pour remboursement ; 2 pour annulation) -# CE = devise 978 pour euro -# CH : optionnel : Numéro de référence donné lors de la transaction (En fonction du type d’action demandée par la caisse, ce numéro peut être vérifié par le terminal) if currency.decimal_places: amount_cent = amount_positive * (10 ** currency.decimal_places) else: @@ -134,31 +123,36 @@ def caisse_ap_ip_send_payment(self, data): elif len(amount_str) > 12: logger.error("Amount with cents '%s' is over the maximum." % amount_str) return False - if payment_method.caisse_ap_ip_mode == 'check': + if payment_method.fr_caisse_ap_ip_mode == 'check': msg_dict['CC'] = '00C' # TODO Try/except quand l'IP n'est pas joignable answer = False - with socket.create_connection((payment_method.caisse_ap_ip_address, payment_method.caisse_ap_ip_port), timeout=CAISSE_AP_TIMEOUT) as sock: + with socket.create_connection((payment_method.fr_caisse_ap_ip_address, payment_method.fr_caisse_ap_ip_port), timeout=CAISSE_AP_TIMEOUT) as sock: sock.settimeout(None) - msg_str = self._caisse_ap_ip_prepare_msg(msg_dict) + msg_str = self._fr_caisse_ap_ip_prepare_msg(msg_dict) logger.debug('data sent to payment terminal: %s' % msg_str) sock.send(msg_str.encode('ascii')) BUFFER_SIZE = 1024 answer = sock.recv(BUFFER_SIZE) logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) if answer: - answer_dict = self._caisse_ap_ip_parse_answer(answer) - self._caisse_ap_ip_check_answer(answer_dict, msg_dict) + answer_dict = self._fr_caisse_ap_ip_parse_answer(answer) + self._fr_caisse_ap_ip_check_answer(answer_dict, msg_dict) if answer_dict.get('AE') == '10': - to_pos_dict = self._caisse_ap_ip_success_to_pos_dict(answer_dict) + to_pos_dict = self._fr_caisse_ap_ip_success_to_pos_dict(answer_dict) logger.debug('JSON sent back to POS: %s', to_pos_dict) return to_pos_dict elif answer_dict.get('AE') == '01': - error_msg = self._caisse_ap_ip_failure_error_msg(answer_dict) + error_msg = self._fr_caisse_ap_ip_failure_error_msg(answer_dict) raise UserError(error_msg) + else: + raise UserError(_( + "Error in the communication with the payment terminal: " + "the action statuts is invalid (AE=%s). " + "This should never happen!") % answer_dict.get('AE')) return True - def _caisse_ap_ip_check_answer(self, answer_dict, msg_dict): + def _fr_caisse_ap_ip_check_answer(self, answer_dict, msg_dict): tag_dict = { 'CA': {'fixed_size': True, 'required': True, 'label': 'caisse'}, 'CB': {'fixed_size': False, 'required': True, 'label': 'amount'}, @@ -176,7 +170,7 @@ def _caisse_ap_ip_check_answer(self, answer_dict, msg_dict): if msg_dict[tag] != strip_answer: raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=strip_answer)) - def _caisse_ap_ip_success_to_pos_dict(self, answer_dict): + def _fr_caisse_ap_ip_success_to_pos_dict(self, answer_dict): card_type_list = [] cc_labels = { '1': 'CB contact', @@ -222,7 +216,7 @@ def _caisse_ap_ip_success_to_pos_dict(self, answer_dict): } return to_pos_dict - def _caisse_ap_ip_failure_error_msg(self, answer_dict): + def _fr_caisse_ap_ip_failure_error_msg(self, answer_dict): error_msg = _("The payment transaction has failed.") af_labels = { '00': 'Inconnu', @@ -245,7 +239,7 @@ def _caisse_ap_ip_failure_error_msg(self, answer_dict): error_msg = _("The payment transaction has failed: %s") % label return error_msg - def _caisse_ap_ip_parse_answer(self, data_bytes): + def _fr_caisse_ap_ip_parse_answer(self, data_bytes): data_str = data_bytes.decode('ascii') logger.info('Received raw data: %s', data_str) data_dict = {} diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js index d803a548d..de6cfeac2 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js @@ -10,6 +10,6 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.models", function (require) { var models = require("point_of_sale.models"); var CaisseAPIP = require("l10n_fr_pos_caisse_ap_ip.payment"); - models.register_payment_method("caisse_ap_ip", CaisseAPIP); + models.register_payment_method("fr-caisse_ap_ip", CaisseAPIP); }); diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js index 4ced6ffef..fadac5540 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -179,7 +179,7 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { console.log('rpc.query'); return rpc.query({ model: "pos.payment.method", - method: "caisse_ap_ip_send_payment", + method: "fr_caisse_ap_ip_send_payment", args: [data], }, { // timeout in ms diff --git a/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml index 0d24aaca4..56fc1aa27 100644 --- a/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml +++ b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml @@ -14,9 +14,9 @@ - - - + + + From 46048320e4fe36abd6994c141e652d90d58684dd Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 17:39:54 +0200 Subject: [PATCH 05/21] l10n_fr_pos_caisse_ap_ip: answer is now always json --- .../models/pos_payment_method.py | 53 ++++++++++++------- .../readme/DESCRIPTION.rst | 6 +++ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index 5801882a9..1aaa430de 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -136,21 +136,29 @@ def fr_caisse_ap_ip_send_payment(self, data): answer = sock.recv(BUFFER_SIZE) logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) if answer: - answer_dict = self._fr_caisse_ap_ip_parse_answer(answer) - self._fr_caisse_ap_ip_check_answer(answer_dict, msg_dict) - if answer_dict.get('AE') == '10': - to_pos_dict = self._fr_caisse_ap_ip_success_to_pos_dict(answer_dict) - logger.debug('JSON sent back to POS: %s', to_pos_dict) - return to_pos_dict - elif answer_dict.get('AE') == '01': - error_msg = self._fr_caisse_ap_ip_failure_error_msg(answer_dict) - raise UserError(error_msg) - else: - raise UserError(_( - "Error in the communication with the payment terminal: " - "the action statuts is invalid (AE=%s). " - "This should never happen!") % answer_dict.get('AE')) - return True + res = self._fr_caisse_ap_ip_prepare(answer, msg_dict) + else: + res = {'payment_status': 'failure'} + logger.debug('JSON sent back to POS: %s', res) + return res + + def _fr_caisse_ap_ip_answer(self, answer, msg_dict): + answer_dict = self._fr_caisse_ap_ip_parse_answer(answer) + self._fr_caisse_ap_ip_check_answer(answer_dict, msg_dict) + if answer_dict.get('AE') == '10': + res = self._fr_caisse_ap_ip_prepare_success(answer_dict) + elif answer_dict.get('AE') == '01': + res = self._fr_caisse_ap_ip_prepare_failure(answer_dict) + else: + error_msg = _( + "Error in the communication with the payment terminal: " + "the action statuts is invalid (AE=%s). " + "This should never happen!") % answer_dict.get('AE') + res = { + 'payment_status': 'failure', + 'error_message': error_msg, + } + return res def _fr_caisse_ap_ip_check_answer(self, answer_dict, msg_dict): tag_dict = { @@ -170,7 +178,7 @@ def _fr_caisse_ap_ip_check_answer(self, answer_dict, msg_dict): if msg_dict[tag] != strip_answer: raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=strip_answer)) - def _fr_caisse_ap_ip_success_to_pos_dict(self, answer_dict): + def _fr_caisse_ap_ip_prepare_success(self, answer_dict): card_type_list = [] cc_labels = { '1': 'CB contact', @@ -209,14 +217,15 @@ def _fr_caisse_ap_ip_success_to_pos_dict(self, answer_dict): transaction_tags = ['AA', 'AB', 'AC', 'AI', 'CD'] transaction_id = '|'.join(['%s-%s' % (tag, answer_dict[tag]) for tag in transaction_tags if answer_dict.get(tag)]) - to_pos_dict = { + res = { + 'payment_status': 'success', 'transaction_id': transaction_id, 'card_type': ' - '.join(card_type_list), 'ticket': ticket, } - return to_pos_dict + return res - def _fr_caisse_ap_ip_failure_error_msg(self, answer_dict): + def _fr_caisse_ap_ip_prepare_failure(self, answer_dict): error_msg = _("The payment transaction has failed.") af_labels = { '00': 'Inconnu', @@ -237,7 +246,11 @@ def _fr_caisse_ap_ip_failure_error_msg(self, answer_dict): if answer_dict.get('AF') and answer_dict['AF'] in af_labels: label = af_labels[answer_dict['AF']] error_msg = _("The payment transaction has failed: %s") % label - return error_msg + res = { + 'payment_status': 'failure', + 'error_message': error_msg, + } + return res def _fr_caisse_ap_ip_parse_answer(self, data_bytes): data_str = data_bytes.decode('ascii') diff --git a/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst index 2206cb530..e3324d964 100644 --- a/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst +++ b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst @@ -3,3 +3,9 @@ This module adds support for the **Caisse AP** protocol over IP in the Odoo Poin The `Caisse AP protocol `_ is a vendor-independent protocol used in France to communicate between a point of sale and a payment terminal. It is implemented by `Ingenico `_ payment terminals, `Verifone `_ payment terminal and other brands of payment terminals. This protocol is designed by a French association called `Association du paiement `_, abbreviated as **AP**. Note that the Caisse-AP protocol is used by Ingenico payment terminals deployed in France, but not by the same model of Ingenico payment terminals deployed in other countries! The Caisse-AP protocol was initially written for serial and USB. Since the Caisse AP protocol version 3.x, it also supports IP. When used over IP, the client (point of sale) and the server (payment terminal) exchange simple text data encoded as ASCII over a raw TCP socket. + +This module support a bi-directionnal link with the payment terminal: + +1. it sends the amount to the payment terminal +2. it waits for the end of the payment transaction +3. it parses the answer of the payment terminal which gives the payment status: in case of success, the payment line is automatically validated ; in case of failure, an error message is displayed and the Odoo user can retry or delete the payment line. From 383235584866da0d462185f448d29b8414110a79 Mon Sep 17 00:00:00 2001 From: Pierrick Brun Date: Tue, 4 Jul 2023 17:45:26 +0200 Subject: [PATCH 06/21] [UPD] javascript no polling --- .../static/src/js/models.js | 1 - .../static/src/js/payment_caisse_ap_ip.js | 201 +++++------------- 2 files changed, 50 insertions(+), 152 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js index de6cfeac2..76d0e18ee 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/models.js @@ -11,5 +11,4 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.models", function (require) { var models = require("point_of_sale.models"); var CaisseAPIP = require("l10n_fr_pos_caisse_ap_ip.payment"); models.register_payment_method("fr-caisse_ap_ip", CaisseAPIP); - }); diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js index fadac5540..bd46d2edc 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -20,183 +20,82 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { this._super.apply(this, arguments); }, - send_payment_request: function (cid) { - console.log('send_payment_request'); - this._super.apply(this, arguments); - // check that this.payment_method.use_payment_terminal is ?? - return this._caisse_ap_ip_pay(cid); - }, - send_payment_cancel: function (order, cid) { - console.log('send_payment_cancel'); this._super.apply(this, arguments); this._show_error( _t( "Please press the red button on the payment terminal to cancel the transaction." ) ); + return true; + }, + _handle_caisse_ap_ip_response: function(pay_line, response) { + if (response.payment_status == "success") { + pay_line.card_type = response.card_type; + pay_line.transaction_id = response.transaction_id; + if ("ticket" in response){ + pay_line.set_receipt_info(response.ticket); + } + return true; + } else { + return this._handle_error(response.error_message); + } + }, + _handle_caisse_ap_ip_unexpected_response: function(pay_line, response) { + // The response cannot be understood + // We let the cashier handle it manually (force or cancel) + pay_line.set_payment_status("force_done"); return Promise.reject(); }, - - _caisse_ap_ip_pay: function (cid) { - console.log('_caisse_ap_ip_pay===== cid='); - console.log(cid); + send_payment_request: function (cid) { + this._super.apply(this, arguments); + // check that this.payment_method.use_payment_terminal is ?? var order = this.pos.get_order(); var pay_line = order.selected_paymentline; var currency = this.pos.currency; - console.log('Currency='); - console.log('THIS1===='); - console.log(this); - console.log(currency); var data = { amount: pay_line.amount, currency_id: currency.id, payment_method_id: this.payment_method.id, payment_id: cid, - //order_id: order.name, }; - console.log('data===='); - console.log(data); -/* if (this.payment_method.oca_payment_terminal_id) { - data.terminal_id = this.payment_method.oca_payment_terminal_id; - } */ - return this._caisse_ap_ip_request(data).then((response) => { - if (response === false) { - this._show_error( - _t( - "Failed to send the amount to pay to the payment terminal. Press the red button on the payment terminal and try again." - ) - ); - // There was an error, let the user retry. - return false; - } else if (response instanceof Object && "transaction_id" in response) { - // The response has a terminal transaction identifier: - // return a promise that polls for transaction status. - pay_line.set_payment_status("waitingCard"); - this._caisse_ap_ip_update_payment_line_terminal_transaction_status( + pay_line.set_payment_status("waitingCard"); + return rpc.query({ + model: "pos.payment.method", + method: "fr_caisse_ap_ip_send_payment", + args: [data], + }, { + timeout: 5000, + shadow: true, + }).then((response) => { + if (response instanceof Object && "transaction_id" in response){ + // The response is a valid object + return this._handle_caisse_ap_ip_response( pay_line, response ); - return new Promise((resolve, reject) => { - this._caisse_ap_ip_poll_for_transaction_status( - pay_line, - resolve, - reject - ); - }); + } else { + return this._handle_caisse_ap_ip_unexpected_response(pay_line, response); } - - // The transaction was started, but the terminal driver - // does not report status, so we won't know the - // transaction result: we let the user enter the - // outcome manually. This is done by rejecting the - // promise as explained in the send_payment_request() - // documentation. - console.log('set_payment_status force_done') - pay_line.set_payment_status("force_done"); - return Promise.reject(); - }); - }, - - _caisse_ap_ip_poll_for_transaction_status: function (pay_line, resolve, reject) { - var timerId = setInterval(() => { - // Query the driver status more frequently than the regular POS - // proxy, to get faster feedback when the transaction is - // complete on the terminal. - var status_params = {}; - if (this.payment_method.oca_payment_terminal_id) { - status_params.terminal_id = - this.payment_method.oca_payment_terminal_id; + }).catch((error) => { + let error_msg = null; + if (error && "message" in error){ + error_msg = error.message.data.message; } - this.pos.env.proxy.connection - .rpc("/hw_proxy/status_json", status_params, { - shadow: true, - timeout: 1000, - }) - .then((drivers_status) => { - for (var driver_name in drivers_status) { - // Look for a driver that is a payment terminal and has - // transactions. - var driver = drivers_status[driver_name]; - if (!driver.is_terminal || !("transactions" in driver)) { - continue; - } - for (var transaction_id in driver.transactions) { - var transaction = driver.transactions[transaction_id]; - if ( - transaction.transaction_id === - pay_line.terminal_transaction_id - ) { - // Look for the transaction corresponding to - // the payment line. - this._caisse_ap_ip_update_payment_line_terminal_transaction_status( - pay_line, - transaction - ); - if ( - pay_line.terminal_transaction_success !== null - ) { - resolve(pay_line.terminal_transaction_success); - // Stop the loop - clearInterval(timerId); - } - } - } - } - }) - .catch(() => { - console.error("Error querying terminal driver status"); - // We could not query the transaction status so we - // won't know the transaction result: we let the user - // enter the outcome manually. This is done by - // rejecting the promise as explained in the - // send_payment_request() documentation. - pay_line.set_payment_status("force_done"); - reject(); - // Stop the loop - clearInterval(timerId); - }); - }, 1000); - }, - - _caisse_ap_ip_update_payment_line_terminal_transaction_status: function ( - pay_line, - transaction - ) { - pay_line.terminal_transaction_id = transaction.transaction_id; - pay_line.terminal_transaction_success = transaction.success; - pay_line.terminal_transaction_status = transaction.status; - pay_line.terminal_transaction_status_details = transaction.status_details; - // Payment transaction reference, for accounting reconciliation purposes. - pay_line.transaction_id = transaction.reference; + return this._handle_error(error_msg); + }); }, - _caisse_ap_ip_request: function (data) { - console.log('_caisse_ap_ip_request data='); - console.log(data); - console.log('_caisse_ap_ip_request this==='); - console.log(this); - console.log('rpc.query'); - return rpc.query({ - model: "pos.payment.method", - method: "fr_caisse_ap_ip_send_payment", - args: [data], - }, { - // timeout in ms - timeout: 180000, - shadow: true, - }).then((response) => { - console.log('rpc.query THEN response='); - console.log(response); - return response; - }) - .catch(() => { - console.error("Error starting payment transaction"); - return false; - }) - ; + _handle_error: function (msg) { + let error_msg = _t( + "Failed to send the amount to pay to the payment terminal. Press the red button on the payment terminal and try again." + ) + if(msg !== null && typeof msg === "string") { + error_msg = error_msg.concat("\n\n", msg); + } + this._show_error(error_msg); + return false; }, - _show_error: function (msg, title) { Gui.showPopup("ErrorPopup", { title: title || _t("Payment Terminal Error"), From afbc805e17797dc5370941e93fa6affd164c5c2c Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 17:47:43 +0200 Subject: [PATCH 07/21] l10n_fr_pos_caisse_ap_ip: fix bad method name --- l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index 1aaa430de..eca0ddd43 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -136,7 +136,7 @@ def fr_caisse_ap_ip_send_payment(self, data): answer = sock.recv(BUFFER_SIZE) logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) if answer: - res = self._fr_caisse_ap_ip_prepare(answer, msg_dict) + res = self._fr_caisse_ap_ip_answer(answer, msg_dict) else: res = {'payment_status': 'failure'} logger.debug('JSON sent back to POS: %s', res) From ce1cb539f59626a935c003ee671f00637401f25a Mon Sep 17 00:00:00 2001 From: Pierrick Brun Date: Tue, 4 Jul 2023 17:55:48 +0200 Subject: [PATCH 08/21] [FIX] handle errors correctly --- .../static/src/js/payment_caisse_ap_ip.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js index bd46d2edc..9c032ab18 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -68,7 +68,7 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { timeout: 5000, shadow: true, }).then((response) => { - if (response instanceof Object && "transaction_id" in response){ + if (response instanceof Object && "payment_status" in response){ // The response is a valid object return this._handle_caisse_ap_ip_response( pay_line, @@ -78,7 +78,9 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { return this._handle_caisse_ap_ip_unexpected_response(pay_line, response); } }).catch((error) => { - let error_msg = null; + let error_msg = _t( + "Failed to send the amount to pay to the payment terminal. Press the red button on the payment terminal and try again." + ) if (error && "message" in error){ error_msg = error.message.data.message; } @@ -87,13 +89,7 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { }, _handle_error: function (msg) { - let error_msg = _t( - "Failed to send the amount to pay to the payment terminal. Press the red button on the payment terminal and try again." - ) - if(msg !== null && typeof msg === "string") { - error_msg = error_msg.concat("\n\n", msg); - } - this._show_error(error_msg); + this._show_error(msg); return false; }, _show_error: function (msg, title) { From 6eacda03f50a18076cb72fba89d49320a0b1168b Mon Sep 17 00:00:00 2001 From: Pierrick Brun Date: Tue, 4 Jul 2023 18:21:25 +0200 Subject: [PATCH 09/21] [FIX] better handling of timeouts --- .../static/src/js/payment_caisse_ap_ip.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js index 9c032ab18..3e1a37f37 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -49,15 +49,17 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { }, send_payment_request: function (cid) { this._super.apply(this, arguments); - // check that this.payment_method.use_payment_terminal is ?? var order = this.pos.get_order(); var pay_line = order.selected_paymentline; var currency = this.pos.currency; + // Define the timout used in the pos and in the back-end (in ms) + const timeout = 180000; var data = { amount: pay_line.amount, currency_id: currency.id, payment_method_id: this.payment_method.id, payment_id: cid, + timeout: timeout, }; pay_line.set_payment_status("waitingCard"); return rpc.query({ @@ -65,7 +67,7 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { method: "fr_caisse_ap_ip_send_payment", args: [data], }, { - timeout: 5000, + timeout: timeout, shadow: true, }).then((response) => { if (response instanceof Object && "payment_status" in response){ @@ -78,12 +80,10 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { return this._handle_caisse_ap_ip_unexpected_response(pay_line, response); } }).catch((error) => { + // It should be a request timeout let error_msg = _t( - "Failed to send the amount to pay to the payment terminal. Press the red button on the payment terminal and try again." + "No response received from the payment terminal in the given time." ) - if (error && "message" in error){ - error_msg = error.message.data.message; - } return this._handle_error(error_msg); }); }, From a167e6f1714e0a684cb04206f84641b048994c63 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 4 Jul 2023 18:31:03 +0200 Subject: [PATCH 10/21] l10n_fr_pos_caisse_ap_ip: catch exception on TCP socket --- .../models/pos_payment_method.py | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index eca0ddd43..4a8025cd8 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -14,7 +14,7 @@ except ImportError: logger.debug('Cannot import pycountry') -CAISSE_AP_TIMEOUT = 180 +BUFFER_SIZE = 1024 class PosPaymentMethod(models.Model): @@ -76,7 +76,6 @@ def fr_caisse_ap_ip_send_payment(self, data): payment_method_id = data['payment_method_id'] currency_id = data['currency_id'] currency = self.env['res.currency'].browse(currency_id) - print('payment_method_id=', payment_method_id) payment_method = self.browse(payment_method_id) cur_speed_map = { # small speed-up, and works even if pycountry not installed 'EUR': '978', @@ -125,16 +124,29 @@ def fr_caisse_ap_ip_send_payment(self, data): return False if payment_method.fr_caisse_ap_ip_mode == 'check': msg_dict['CC'] = '00C' - # TODO Try/except quand l'IP n'est pas joignable answer = False - with socket.create_connection((payment_method.fr_caisse_ap_ip_address, payment_method.fr_caisse_ap_ip_port), timeout=CAISSE_AP_TIMEOUT) as sock: - sock.settimeout(None) - msg_str = self._fr_caisse_ap_ip_prepare_msg(msg_dict) - logger.debug('data sent to payment terminal: %s' % msg_str) - sock.send(msg_str.encode('ascii')) - BUFFER_SIZE = 1024 - answer = sock.recv(BUFFER_SIZE) - logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) + timeout_ms = data['timeout'] + # For the timeout of the TCP socket to the payment terminal, we remove + # 3 seconds from the timeout of the POS + timeout_sec = timeout_ms / 1000 - 3 + msg_str = self._fr_caisse_ap_ip_prepare_msg(msg_dict) + logger.debug('data sent to payment terminal: %s' % msg_str) + msg_bytes = msg_str.encode('ascii') + ip_addr = payment_method.fr_caisse_ap_ip_address + port = payment_method.fr_caisse_ap_ip_port + try: + with socket.create_connection((ip_addr, port), timeout=timeout_sec) as sock: + sock.settimeout(None) + sock.send(msg_bytes) + answer = sock.recv(BUFFER_SIZE) + logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) + except Exception as e: + error_msg = _("Failed to connect to the payment terminal on %(ip_addr)s:%(port)s\n%(error)s", ip_addr=ip_addr, port=port, error=e) + res = { + 'payment_status': 'failure', + 'error_message': error_msg, + } + return res if answer: res = self._fr_caisse_ap_ip_answer(answer, msg_dict) else: From 3b057807cc4c28e5403901ed391849e3bd58de6d Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 5 Jul 2023 00:13:29 +0200 Subject: [PATCH 11/21] l10n_fr_pos_caisse_ap_ip: Add fr translation Update for pre-commit --- l10n_fr_pos_caisse_ap_ip/README.rst | 1 + l10n_fr_pos_caisse_ap_ip/__manifest__.py | 24 +- l10n_fr_pos_caisse_ap_ip/i18n/fr.po | 233 ++++++++++ .../i18n/l10n_fr_pos_caisse_ap_ip.pot | 222 +++++++++ .../models/pos_payment_method.py | 430 ++++++++++++------ l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.rst | 7 + .../readme/DESCRIPTION.rst | 6 +- .../static/src/js/payment_caisse_ap_ip.js | 66 +-- .../views/pos_payment_method.xml | 21 +- 9 files changed, 809 insertions(+), 201 deletions(-) create mode 100644 l10n_fr_pos_caisse_ap_ip/README.rst create mode 100644 l10n_fr_pos_caisse_ap_ip/i18n/fr.po create mode 100644 l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot create mode 100644 l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.rst diff --git a/l10n_fr_pos_caisse_ap_ip/README.rst b/l10n_fr_pos_caisse_ap_ip/README.rst new file mode 100644 index 000000000..afb488df6 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/README.rst @@ -0,0 +1 @@ +Will be generated from readme subdir diff --git a/l10n_fr_pos_caisse_ap_ip/__manifest__.py b/l10n_fr_pos_caisse_ap_ip/__manifest__.py index d16af1f3e..13ebca32a 100644 --- a/l10n_fr_pos_caisse_ap_ip/__manifest__.py +++ b/l10n_fr_pos_caisse_ap_ip/__manifest__.py @@ -3,17 +3,17 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'POS: Caisse-AP payment protocol for France', - 'version': '16.0.1.0.0', - 'category': 'Point of Sale', - 'license': 'AGPL-3', - 'summary': 'Add support for Caisse-AP payment protocol used in France', - 'author': 'Akretion,Odoo Community Association (OCA)', - 'maintainers': ['alexis-via'], - 'website': 'https://github.com/OCA/l10n-france', - 'depends': ['point_of_sale'], - 'data': [ - 'views/pos_payment_method.xml', + "name": "POS: Caisse-AP payment protocol for France", + "version": "16.0.1.0.0", + "category": "Point of Sale", + "license": "AGPL-3", + "summary": "Add support for Caisse-AP payment protocol used in France", + "author": "Akretion,Odoo Community Association (OCA)", + "maintainers": ["alexis-via"], + "website": "https://github.com/OCA/l10n-france", + "depends": ["point_of_sale"], + "data": [ + "views/pos_payment_method.xml", ], "assets": { "point_of_sale.assets": [ @@ -21,5 +21,5 @@ "l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js", ], }, - 'installable': True, + "installable": True, } diff --git a/l10n_fr_pos_caisse_ap_ip/i18n/fr.po b/l10n_fr_pos_caisse_ap_ip/i18n/fr.po new file mode 100644 index 000000000..437933e28 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/i18n/fr.po @@ -0,0 +1,233 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_fr_pos_caisse_ap_ip +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-07-04 21:43+0000\n" +"PO-Revision-Date: 2023-07-04 21:47+0000\n" +"Last-Translator: Alexis de Lattre \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Application %(label)s (code %(code)s)" +msgstr "Application %(label)s (code %(code)s)" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in " +"the query and %(answer_val)s in the answer, but these values should be " +"identical. This should never happen!" +msgstr "" +"Protocole Caisse AP sur IP : Le tag %(label)s (%(tag)s) a pour valeur " +"%(request_val)s dans la requête et %(answer_val)s dans la réponse, alors que " +"ces valeurs devraient être identiques. Cela ne devrait jamais arriver !" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse AP IP protocol: tag %s is required but it is not present in the " +"answer from the terminal. This should never happen!" +msgstr "" +"Protocole Caisse AP sur IP : le tag %s est obligatoire mais il n'est pas " +"présent dans la réponse du terminal. Cela ne devrait jamais arriver !" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Caisse AP over IP (France only)" +msgstr "Caisse AP sur IP (France uniquement)" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,field_description:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_address +msgid "Caisse-AP Payment Terminal IP Address" +msgstr "Adresse IP du terminal de paiement Caisse-AP" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,field_description:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_port +msgid "Caisse-AP Payment Terminal Port" +msgstr "Port du terminal de paiement Caisse-AP" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse-AP payment terminal IP address is not set on payment method '%s'." +msgstr "" +"L'adresse IP du terminal de paiement Caisse-AP n'est pas définie sur " +"la méthode de paiement '%s'." + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Caisse-AP payment terminal port is not set on payment method '%s'." +msgstr "" +"Le port du terminal de paiement Caisse-AP n'est pas défini sur la " +"méthode de paiement '%s'." + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields.selection,name:l10n_fr_pos_caisse_ap_ip.selection__pos_payment_method__fr_caisse_ap_ip_mode__card +msgid "Card" +msgstr "Carte" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Card type: %s" +msgstr "Type de carte : %s" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields.selection,name:l10n_fr_pos_caisse_ap_ip.selection__pos_payment_method__fr_caisse_ap_ip_mode__check +msgid "Check" +msgstr "Vérifier" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Empty answer from payment terminal. This should never happen." +msgstr "" +"La réponse du terminal de paiement est vide. Cela ne devrait jamais arriver." + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Error in the communication with the payment terminal: the action statuts is " +"invalid (AE=%s). This should never happen!" +msgstr "" +"Erreur dans la communication avec le terminal de paiement : le statut de " +"l'action n'est pas valide (AE=%s). Cela ne devrait jamais arriver !" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Failed to connect to the payment terminal on %(ip_addr)s:%(port)s\n" +"%(error)s" +msgstr "" +"Échec de la connexion au terminal de paiement sur %(ip_addr)s:%(port)s\n" +"%(error)s" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,help:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_address +msgid "" +"IP address or DNS name of the payment terminal that support Caisse-AP " +"protocol over IP" +msgstr "" +"Adresse IP ou nom DNS du terminal de paiement qui supporte le protocole " +"Caisse-AP sur IP" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-javascript +#: code:addons/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js:0 +#, python-format +msgid "No answer from the payment terminal in the given time." +msgstr "Pas de réponse du terminal de paiement dans le délai imparti." + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,field_description:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_mode +msgid "Payment Mode" +msgstr "Mode de paiement" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-javascript +#: code:addons/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js:0 +#, python-format +msgid "Payment Terminal Error" +msgstr "Erreur du terminal de paiement" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model,name:l10n_fr_pos_caisse_ap_ip.model_pos_payment_method +msgid "Point of Sale Payment Methods" +msgstr "Modes de paiement du point de vente" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Port %s for the payment terminal is not a valid TCP port." +msgstr "Le port %s du terminal de paiement n'est pas un port TCP valide." + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-javascript +#: code:addons/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js:0 +#, python-format +msgid "Press the red button on the payment terminal to cancel the transaction." +msgstr "" +"Appuyez sur le bouton rouge du terminal de paiement pour annuler la " +"transaction." + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Read mode: %(label)s (code %(code)s)" +msgstr "Mode de lecture : %(label)s (code %(code)s)" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,help:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_port +msgid "" +"TCP port of the payment terminal that support Caisse-AP protocol over IP" +msgstr "" +"Port TCP du terminal de paiement qui supporte le protocole Caisse-AP sur IP" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "The payment transaction has failed." +msgstr "Le paiement a échoué." + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "The payment transaction has failed: %s" +msgstr "Le paiement a échoué : %s" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "You are tying to send a null amount to the payment terminal!" +msgstr "Vous essayez d'envoyer un montant nul au terminal de paiement !" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"You are tying to send amount %s cents to the payment terminal, but it is " +"over the maximum!" +msgstr "" +"Vous essayez d'envoyer le montant %s centimes au terminal de paiement, mais il " +"est supérieur au maximum !" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "unknown" +msgstr "inconnu" diff --git a/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot b/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot new file mode 100644 index 000000000..5ba41f0e0 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot @@ -0,0 +1,222 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_fr_pos_caisse_ap_ip +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-07-04 21:55+0000\n" +"PO-Revision-Date: 2023-07-04 21:55+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Application %(label)s (code %(code)s)" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in " +"the query and %(answer_val)s in the answer, but these values should be " +"identical. This should never happen!" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in " +"the request and %(answer_val)s in the answer, but these values should be " +"identical. This should never happen!" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse AP IP protocol: tag %s is required but it is not present in the " +"answer from the terminal. This should never happen!" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Caisse AP over IP (France only)" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,field_description:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_address +msgid "Caisse-AP Payment Terminal IP Address" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,field_description:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_port +msgid "Caisse-AP Payment Terminal Port" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse-AP payment terminal IP address is not set on payment method '%s'." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Caisse-AP payment terminal port is not set on payment method '%s'." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields.selection,name:l10n_fr_pos_caisse_ap_ip.selection__pos_payment_method__fr_caisse_ap_ip_mode__card +msgid "Card" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Card type: %s" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields.selection,name:l10n_fr_pos_caisse_ap_ip.selection__pos_payment_method__fr_caisse_ap_ip_mode__check +msgid "Check" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Empty answer from payment terminal. This should never happen." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Error in the communication with the payment terminal: the action statuts is " +"invalid (AE=%s). This should never happen!" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Failed to connect to the payment terminal on %(ip_addr)s:%(port)s\n" +"%(error)s" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,help:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_address +msgid "" +"IP address or DNS name of the payment terminal that support Caisse-AP " +"protocol over IP" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-javascript +#: code:addons/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js:0 +#, python-format +msgid "No answer from the payment terminal in the given time." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,field_description:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_mode +msgid "Payment Mode" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-javascript +#: code:addons/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js:0 +#, python-format +msgid "Payment Terminal Error" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model,name:l10n_fr_pos_caisse_ap_ip.model_pos_payment_method +msgid "Point of Sale Payment Methods" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Port %s for the payment terminal is not a valid TCP port." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-javascript +#: code:addons/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js:0 +#, python-format +msgid "" +"Press the red button on the payment terminal to cancel the transaction." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "Read mode: %(label)s (code %(code)s)" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#: model:ir.model.fields,help:l10n_fr_pos_caisse_ap_ip.field_pos_payment_method__fr_caisse_ap_ip_port +msgid "" +"TCP port of the payment terminal that support Caisse-AP protocol over IP" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "The payment transaction has failed." +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "The payment transaction has failed: %s" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "You are tying to send a null amount to the payment terminal!" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"You are tying to send amount %s cents to the payment terminal, but it is " +"over the maximum!" +msgstr "" + +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "unknown" +msgstr "" diff --git a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py index 4a8025cd8..da17b4b3e 100644 --- a/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -2,23 +2,24 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError, UserError -import socket import logging -from pprint import pprint +import socket + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + logger = logging.getLogger(__name__) try: import pycountry except ImportError: - logger.debug('Cannot import pycountry') + logger.debug("Cannot import pycountry") BUFFER_SIZE = 1024 class PosPaymentMethod(models.Model): - _inherit = 'pos.payment.method' + _inherit = "pos.payment.method" def _get_payment_terminal_selection(self): res = super()._get_payment_terminal_selection() @@ -28,23 +29,48 @@ def _get_payment_terminal_selection(self): fr_caisse_ap_ip_mode = fields.Selection( [("card", "Card"), ("check", "Check")], string="Payment Mode", default="card" ) - fr_caisse_ap_ip_address = fields.Char(string='Caisse-AP Payment Terminal IP Address', help="IP address or DNS name of the payment terminal that support Caisse-AP protocol over IP") - fr_caisse_ap_ip_port = fields.Integer(string='Caisse-AP Payment Terminal Port', help="TCP port of the payment terminal that support Caisse-AP protocol over IP", default=8888) + fr_caisse_ap_ip_address = fields.Char( + string="Caisse-AP Payment Terminal IP Address", + help="IP address or DNS name of the payment terminal that support " + "Caisse-AP protocol over IP", + ) + fr_caisse_ap_ip_port = fields.Integer( + string="Caisse-AP Payment Terminal Port", + help="TCP port of the payment terminal that support Caisse-AP protocol over IP", + default=8888, + ) - @api.constrains('use_payment_terminal', 'fr_caisse_ap_ip_address', 'fr_caisse_ap_ip_port') + @api.constrains( + "use_payment_terminal", "fr_caisse_ap_ip_address", "fr_caisse_ap_ip_port" + ) def _check_fr_caisse_ap_ip(self): for method in self: - if method.use_payment_terminal == 'caisse_ap_ip': + if method.use_payment_terminal == "caisse_ap_ip": if not method.fr_caisse_ap_ip_address: - raise ValidationError(_( - "Caisse-AP Payment Terminal IP Address is not set on Payment Method '%s'.") % method.display_name) + raise ValidationError( + _( + "Caisse-AP payment terminal IP address is not set on " + "payment method '%s'." + ) + % method.display_name + ) if not method.fr_caisse_ap_ip_port: - raise ValidationError(_( - "Caisse-AP Payment Terminal Port is not set on Payment Method '%s'.") % method.display_name) + raise ValidationError( + _( + "Caisse-AP payment terminal port is not set on " + "payment method '%s'." + ) + % method.display_name + ) - if method.fr_caisse_ap_ip_port < 1 or method.fr_caisse_ap_ip_port > 65535: - raise ValidationError(_( - "Port %s for the payment terminal is not a valid TCP port.") % method.fr_caisse_ap_ip_port) + if ( + method.fr_caisse_ap_ip_port < 1 + or method.fr_caisse_ap_ip_port > 65535 + ): + raise ValidationError( + _("Port %s for the payment terminal is not a valid TCP port.") + % method.fr_caisse_ap_ip_port + ) def _fr_caisse_ap_ip_prepare_msg(self, msg_dict): assert isinstance(msg_dict, dict) @@ -58,29 +84,31 @@ def _fr_caisse_ap_ip_prepare_msg(self, msg_dict): # CZ tag: protocol version # Always start with tag CZ # the order of the other tags is unrelevant - if 'CZ' in msg_dict: - version = msg_dict.pop('CZ') + if "CZ" in msg_dict: + version = msg_dict.pop("CZ") else: - version = '0300' # 0301 ?? + version = "0300" # 0301 ?? assert len(version) == 4 - msg_list.append(('CZ', version)) + msg_list.append(("CZ", version)) msg_list += list(msg_dict.items()) - msg_str = ''.join(['%s%s%s' % (tag, str(len(value)).zfill(3), value) for (tag, value) in msg_list]) + msg_str = "".join( + [ + "%s%s%s" % (tag, str(len(value)).zfill(3), value) + for (tag, value) in msg_list + ] + ) return msg_str - @api.model - def fr_caisse_ap_ip_send_payment(self, data): - # called by JS code - logger.debug('caisse_ap_ip_send_payment data=%s', data) - amount = data.get('amount') - payment_method_id = data['payment_method_id'] - currency_id = data['currency_id'] - currency = self.env['res.currency'].browse(currency_id) - payment_method = self.browse(payment_method_id) + def _fr_caisse_ap_ip_prepare_message(self, data): + self.ensure_one() + amount = data.get("amount") + currency_id = data["currency_id"] + currency = self.env["res.currency"].browse(currency_id) + data["currency"] = currency cur_speed_map = { # small speed-up, and works even if pycountry not installed - 'EUR': '978', - 'XPF': '953', - 'USD': '840', # Only because it is the default currency + "EUR": "978", + "XPF": "953", + "USD": "840", # Only because it is the default currency } if currency.name in cur_speed_map: cur_num = cur_speed_map[currency.name] @@ -89,195 +117,301 @@ def fr_caisse_ap_ip_send_payment(self, data): cur = pycountry.currencies.get(alpha_3=currency.name) cur_num = cur.numeric # it returns a string except Exception as e: - logger.error("pycountry doesn't support currency '%s'. Error: %s" % (currency.name, e)) + logger.error( + "pycountry doesn't support currency '%s'. Error: %s" + % (currency.name, e) + ) return False # CJ identifiant protocole concert : no interest, but required # CA POS number # BF partial payments: 0=refused 1=accepted msg_dict = { - 'CJ': '012345678901', - 'CA': '01', - 'CE': cur_num, - 'BF': '0', - } + "CJ": "012345678901", + "CA": "01", + "CE": cur_num, + "BF": "0", + } amount_compare = currency.compare_amounts(amount, 0) # CD Action type: 0=debit (regular payment) 1=credit (reimbursement) if not amount_compare: - logger.error('Amount for payment terminal is 0') - return False # TODO raise Error? + logger.error("Amount for payment terminal is 0") + error_msg = _( + "You are tying to send a null amount to the payment terminal!" + ) + res = { + "payment_status": "issue", + "error_message": error_msg, + } + return res elif amount_compare < 0: - msg_dict['CD'] = '1' # credit i.e. reimbursement + msg_dict["CD"] = "1" # credit i.e. reimbursement amount_positive = amount * -1 else: - msg_dict['CD'] = '0' # debit i.e. regular payment + msg_dict["CD"] = "0" # debit i.e. regular payment amount_positive = amount if currency.decimal_places: - amount_cent = amount_positive * (10 ** currency.decimal_places) + amount_cent = amount_positive * (10**currency.decimal_places) else: amount_cent = amount_positive amount_str = str(int(round(amount_cent))) - msg_dict['CB'] = amount_str + data["amount_str"] = amount_str + msg_dict["CB"] = amount_str if len(amount_str) < 2: amount_str = amount_str.zfill(2) elif len(amount_str) > 12: - logger.error("Amount with cents '%s' is over the maximum." % amount_str) - return False - if payment_method.fr_caisse_ap_ip_mode == 'check': - msg_dict['CC'] = '00C' - answer = False - timeout_ms = data['timeout'] + logger.error("Amount with cents %s is over the maximum." % amount_str) + error_msg = ( + _( + "You are tying to send amount %s cents to the payment terminal, " + "but it is over the maximum!" + ) + % amount_str + ) + res = { + "payment_status": "issue", + "error_message": error_msg, + } + return res + if self.fr_caisse_ap_ip_mode == "check": + msg_dict["CC"] = "00C" + return msg_dict + + @api.model + def fr_caisse_ap_ip_send_payment(self, data): + """Method called by the JS code of this module""" + logger.debug("fr_caisse_ap_ip_send_payment data=%s", data) + payment_method_id = data["payment_method_id"] + payment_method = self.browse(payment_method_id) + msg_dict = payment_method._fr_caisse_ap_ip_prepare_message(data) + msg_str = self._fr_caisse_ap_ip_prepare_msg(msg_dict) + msg_bytes = msg_str.encode("ascii") + timeout_ms = data["timeout"] # For the timeout of the TCP socket to the payment terminal, we remove # 3 seconds from the timeout of the POS timeout_sec = timeout_ms / 1000 - 3 - msg_str = self._fr_caisse_ap_ip_prepare_msg(msg_dict) - logger.debug('data sent to payment terminal: %s' % msg_str) - msg_bytes = msg_str.encode('ascii') ip_addr = payment_method.fr_caisse_ap_ip_address port = payment_method.fr_caisse_ap_ip_port + logger.info( + "Sending %s %s %s %s cents to payment terminal %s:%s", + msg_dict["CD"] == "1" and "reimbursement" or "payment", + msg_dict.get("CC") == "00C" and "check" or "card", + data["currency"].name, + data["amount_str"], + ip_addr, + port, + ) + logger.debug("Data about to be sent to payment terminal: %s" % msg_str) + answer = False try: with socket.create_connection((ip_addr, port), timeout=timeout_sec) as sock: sock.settimeout(None) sock.send(msg_bytes) - answer = sock.recv(BUFFER_SIZE) - logger.debug("answer received from payment terminal: %s", answer.decode('ascii')) + answer_bytes = sock.recv(BUFFER_SIZE) + answer = answer_bytes.decode("ascii") + logger.debug("Answer received from payment terminal: %s", answer) except Exception as e: - error_msg = _("Failed to connect to the payment terminal on %(ip_addr)s:%(port)s\n%(error)s", ip_addr=ip_addr, port=port, error=e) + error_msg = _( + "Failed to connect to the payment terminal on %(ip_addr)s:%(port)s\n%(error)s", + ip_addr=ip_addr, + port=port, + error=e, + ) res = { - 'payment_status': 'failure', - 'error_message': error_msg, - } + "payment_status": "issue", + "error_message": error_msg, + } return res if answer: res = self._fr_caisse_ap_ip_answer(answer, msg_dict) else: - res = {'payment_status': 'failure'} - logger.debug('JSON sent back to POS: %s', res) + res = { + "payment_status": "issue", + "error_message": _( + "Empty answer from payment terminal. This should never happen." + ), + } + logger.debug("JSON sent back to POS: %s", res) return res def _fr_caisse_ap_ip_answer(self, answer, msg_dict): answer_dict = self._fr_caisse_ap_ip_parse_answer(answer) - self._fr_caisse_ap_ip_check_answer(answer_dict, msg_dict) - if answer_dict.get('AE') == '10': + check_res = self._fr_caisse_ap_ip_check_answer(answer_dict, msg_dict) + if isinstance(check_res, dict): + return check_res + if answer_dict.get("AE") == "10": res = self._fr_caisse_ap_ip_prepare_success(answer_dict) - elif answer_dict.get('AE') == '01': + elif answer_dict.get("AE") == "01": res = self._fr_caisse_ap_ip_prepare_failure(answer_dict) else: error_msg = _( "Error in the communication with the payment terminal: " "the action statuts is invalid (AE=%s). " - "This should never happen!") % answer_dict.get('AE') + "This should never happen!" + ) % answer_dict.get("AE") res = { - 'payment_status': 'failure', - 'error_message': error_msg, - } + "payment_status": "issue", + "error_message": error_msg, + } return res def _fr_caisse_ap_ip_check_answer(self, answer_dict, msg_dict): tag_dict = { - 'CA': {'fixed_size': True, 'required': True, 'label': 'caisse'}, - 'CB': {'fixed_size': False, 'required': True, 'label': 'amount'}, - 'CD': {'fixed_size': True, 'required': True, 'label': 'action pay/reimb'}, - 'CE': {'fixed_size': True, 'required': True, 'label': 'currency'}, - 'BF': {'fixed_size': True, 'required': False, 'label': 'partial payment'}, - } + "CA": {"fixed_size": True, "required": True, "label": "caisse"}, + "CB": {"fixed_size": False, "required": True, "label": "amount"}, + "CD": {"fixed_size": True, "required": True, "label": "action pay/reimb"}, + "CE": {"fixed_size": True, "required": True, "label": "currency"}, + "BF": {"fixed_size": True, "required": False, "label": "partial payment"}, + } + fail_res = { + "payment_status": "issue", + } for tag, props in tag_dict.items(): - if props['required'] and not answer_dict.get(tag): - raise UserError(_("Caisse AP IP protocol: tag %s is required but it is not present in the answer. This should never happen!") % answer_dict.get(tag)) - if props['fixed_size'] and answer_dict.get(tag) and answer_dict[tag] != msg_dict[tag]: - raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=answer_dict[tag])) - elif not props['fixed_size'] and answer_dict.get(tag): - strip_answer = answer_dict[tag].lstrip('0') + if props["required"] and not answer_dict.get(tag): + fail_res["error_message"] = _( + "Caisse AP IP protocol: tag %s is required but it is " + "not present in the answer from the terminal. " + "This should never happen!" + ) % answer_dict.get(tag) + return fail_res + if ( + props["fixed_size"] + and answer_dict.get(tag) + and answer_dict[tag] != msg_dict[tag] + ): + fail_res["error_message"] = _( + "Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value " + "%(request_val)s in the query and %(answer_val)s in the " + "answer, but these values should be identical. " + "This should never happen!", + label=props["label"], + tag=tag, + request_val=msg_dict[tag], + answer_val=answer_dict[tag], + ) + return fail_res + elif not props["fixed_size"] and answer_dict.get(tag): + strip_answer = answer_dict[tag].lstrip("0") if msg_dict[tag] != strip_answer: - raise UserError(_("Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in the request and %(answer_val)s in the answer, but these values should be identical. This should never happen!", label=props['label'], tag=tag, request_val=msg_dict[tag], answer_val=strip_answer)) + fail_res["error_message"] = _( + "Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value " + "%(request_val)s in the request and %(answer_val)s in the " + "answer, but these values should be identical. " + "This should never happen!", + label=props["label"], + tag=tag, + request_val=msg_dict[tag], + answer_val=strip_answer, + ) + return fail_res + return True def _fr_caisse_ap_ip_prepare_success(self, answer_dict): card_type_list = [] cc_labels = { - '1': 'CB contact', - 'B': 'CB sans contact', - 'C': 'Chèque', - '2': 'Amex contact', - 'D': 'Amex sans contact', - '3': 'CB Enseigne', - '5': 'Cofinoga', - '6': 'Diners', - '7': 'CB-Pass', - '8': 'Franfinance', - '9': 'JCB', - 'A': 'Banque Accord', - 'I': 'CPEI', - 'E': 'CMCIC-Pay TPE', - 'U': 'CUP', - '0': 'Autres', - } + "1": "CB contact", + "B": "CB sans contact", + "C": "Chèque", + "2": "Amex contact", + "D": "Amex sans contact", + "3": "CB Enseigne", + "5": "Cofinoga", + "6": "Diners", + "7": "CB-Pass", + "8": "Franfinance", + "9": "JCB", + "A": "Banque Accord", + "I": "CPEI", + "E": "CMCIC-Pay TPE", + "U": "CUP", + "0": "Autres", + } ci_labels = { - '0': 'indifférent', - '1': 'contact', - '2': 'sans contact', - '3': 'piste', - '4': 'saisie manuelle', - } + "0": "indifférent", + "1": "contact", + "2": "sans contact", + "3": "piste", + "4": "saisie manuelle", + } ticket = False - if answer_dict.get('CC') and len(answer_dict['CC']) == 3: - cc_tag = answer_dict['CC'].lstrip('0') - cc_label = cc_labels.get(cc_tag, _('unknown')) - card_type_list.append(_('Application %(label)s (code %(code)s)', label=cc_label, code=cc_tag)) - ticket = _('Card type: %s') % cc_label - if answer_dict.get('CI') and len(answer_dict['CI']) == 1: - card_type_list.append(_('Read mode: %(label)s (code %(code)s)', label=ci_labels.get(answer_dict['CI'], _('unknown')), code=answer_dict['CI'])) + if answer_dict.get("CC") and len(answer_dict["CC"]) == 3: + cc_tag = answer_dict["CC"].lstrip("0") + cc_label = cc_labels.get(cc_tag, _("unknown")) + card_type_list.append( + _("Application %(label)s (code %(code)s)", label=cc_label, code=cc_tag) + ) + ticket = _("Card type: %s") % cc_label + if answer_dict.get("CI") and len(answer_dict["CI"]) == 1: + card_type_list.append( + _( + "Read mode: %(label)s (code %(code)s)", + label=ci_labels.get(answer_dict["CI"], _("unknown")), + code=answer_dict["CI"], + ) + ) - transaction_tags = ['AA', 'AB', 'AC', 'AI', 'CD'] - transaction_id = '|'.join(['%s-%s' % (tag, answer_dict[tag]) for tag in transaction_tags if answer_dict.get(tag)]) + transaction_tags = ["AA", "AB", "AC", "AI", "CD"] + transaction_id = "|".join( + [ + "%s-%s" % (tag, answer_dict[tag]) + for tag in transaction_tags + if answer_dict.get(tag) + ] + ) res = { - 'payment_status': 'success', - 'transaction_id': transaction_id, - 'card_type': ' - '.join(card_type_list), - 'ticket': ticket, - } + "payment_status": "success", + "transaction_id": transaction_id, + "card_type": " - ".join(card_type_list), + "ticket": ticket, + } + logger.info( + "Received success answer from payment terminal (card_type: %s)", + res["card_type"], + ) + logger.debug("transaction_id=%s", res["transaction_id"]) return res def _fr_caisse_ap_ip_prepare_failure(self, answer_dict): + label = None error_msg = _("The payment transaction has failed.") af_labels = { - '00': 'Inconnu', - '01': 'Transaction autorisé', - '02': 'Appel phonie', - '03': 'Forçage', - '04': 'Refusée', - '05': 'Interdite', - '06': 'Abandon', - '07': 'Non aboutie', - '08': 'Opération non effectuée Time-out saisie', - '09': 'Opération non effectuée erreur format message', - '10': 'Opération non effectuée erreur sélection', - '11': 'Opération non effectuée Abandon Opérateur', - '12': 'Opération non effectuée type d’action demandé inconnu', - '13': 'Devise non supportée', - } - if answer_dict.get('AF') and answer_dict['AF'] in af_labels: - label = af_labels[answer_dict['AF']] + "00": "Inconnu", + "01": "Transaction autorisé", + "02": "Appel phonie", + "03": "Forçage", + "04": "Refusée", + "05": "Interdite", + "06": "Abandon", + "07": "Non aboutie", + "08": "Opération non effectuée Time-out saisie", + "09": "Opération non effectuée erreur format message", + "10": "Opération non effectuée erreur sélection", + "11": "Opération non effectuée Abandon Opérateur", + "12": "Opération non effectuée type d’action demandé inconnu", + "13": "Devise non supportée", + } + if answer_dict.get("AF") and answer_dict["AF"] in af_labels: + label = af_labels[answer_dict["AF"]] error_msg = _("The payment transaction has failed: %s") % label res = { - 'payment_status': 'failure', - 'error_message': error_msg, - } + "payment_status": "failure", + "error_message": error_msg, + } + logger.info("Failure answer from payment terminal (failure report: %s)", label) return res - def _fr_caisse_ap_ip_parse_answer(self, data_bytes): - data_str = data_bytes.decode('ascii') - logger.info('Received raw data: %s', data_str) + def _fr_caisse_ap_ip_parse_answer(self, data_str): + logger.debug("Received raw data: %s", data_str) data_dict = {} i = 0 while i < len(data_str): - tag = data_str[i:i + 2] + tag = data_str[i : i + 2] i += 2 - size_str = data_str[i:i + 3] + size_str = data_str[i : i + 3] size = int(size_str) i += 3 - value = data_str[i:i + size] + value = data_str[i : i + size] data_dict[tag] = value i += size - logger.info('Answer dict:') - pprint(data_dict) + logger.debug("Answer dict: %s", data_dict) return data_dict diff --git a/l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.rst b/l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.rst new file mode 100644 index 000000000..7f99893c9 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.rst @@ -0,0 +1,7 @@ +In the menu *Point of sale > Configuration > Payment Method*, on the payment method that correspond to a payment by card: + +* select the appropriate journal, which should be a bank journal (and not a cash journa, otherwise the field *Use a payment terminal* is invisible) +* field *Use a payment terminal*: select **Caisse AP over IP (France only)** +* field *Caisse-AP Payment Terminal IP Address*: set the IP address of the payment terminal, +* field *Caisse-AP Payment Terminal Port*: set the TCP port of the payment terminal (8888 by default), +* field *Payment Mode*: set *Card* (the value *Check* is for the *Check* payment method if you use a check printer connected to the payment terminal such as Ingenico i2200) diff --git a/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst index e3324d964..e404f45b0 100644 --- a/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst +++ b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.rst @@ -2,10 +2,12 @@ This module adds support for the **Caisse AP** protocol over IP in the Odoo Poin The `Caisse AP protocol `_ is a vendor-independent protocol used in France to communicate between a point of sale and a payment terminal. It is implemented by `Ingenico `_ payment terminals, `Verifone `_ payment terminal and other brands of payment terminals. This protocol is designed by a French association called `Association du paiement `_, abbreviated as **AP**. Note that the Caisse-AP protocol is used by Ingenico payment terminals deployed in France, but not by the same model of Ingenico payment terminals deployed in other countries! -The Caisse-AP protocol was initially written for serial and USB. Since the Caisse AP protocol version 3.x, it also supports IP. When used over IP, the client (point of sale) and the server (payment terminal) exchange simple text data encoded as ASCII over a raw TCP socket. - This module support a bi-directionnal link with the payment terminal: 1. it sends the amount to the payment terminal 2. it waits for the end of the payment transaction 3. it parses the answer of the payment terminal which gives the payment status: in case of success, the payment line is automatically validated ; in case of failure, an error message is displayed and the Odoo user can retry or delete the payment line. + +The Caisse-AP protocol was initially written for serial and USB. Since the Caisse AP protocol version 3.x, it also supports IP. When used over IP, the client (point of sale) and the server (payment terminal) exchange simple text data encoded as ASCII over a raw TCP socket. + +The Caisse-AP protocol has one important drawback: as it uses a raw TCP socket, it cannot be used from pure JS code. So the JS code of the point of sale cannot generate the query to send the amount to the payment terminal by itself. In this module, the JS code of the point of sale sends a query to the Odoo server that opens a raw TCP socket to the payment terminal. It implies that, if the Odoo server is not on the LAN but somewhere on the Internet and the payment terminal has a private IP on the LAN, you will need to setup a TCP port forwarding rule on the firewall to redirect the TCP connection of the Odoo server to the payment terminal. diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js index 3e1a37f37..a01c19d2a 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js +++ b/l10n_fr_pos_caisse_ap_ip/static/src/js/payment_caisse_ap_ip.js @@ -2,6 +2,7 @@ Copyright 2023 Akretion France (http://www.akretion.com/) @author: Alexis de Lattre @author: Rémi de Lattre + @author: Pierrick Brun License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ @@ -9,7 +10,7 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { "use strict"; var core = require("web.core"); - var rpc = require('web.rpc'); + var rpc = require("web.rpc"); var PaymentInterface = require("point_of_sale.PaymentInterface"); const {Gui} = require("point_of_sale.Gui"); @@ -20,28 +21,27 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { this._super.apply(this, arguments); }, - send_payment_cancel: function (order, cid) { + send_payment_cancel: function () { this._super.apply(this, arguments); this._show_error( _t( - "Please press the red button on the payment terminal to cancel the transaction." + "Press the red button on the payment terminal to cancel the transaction." ) ); return true; }, - _handle_caisse_ap_ip_response: function(pay_line, response) { - if (response.payment_status == "success") { + _handle_caisse_ap_ip_response: function (pay_line, response) { + if (response.payment_status === "success") { pay_line.card_type = response.card_type; pay_line.transaction_id = response.transaction_id; - if ("ticket" in response){ + if ("ticket" in response) { pay_line.set_receipt_info(response.ticket); } return true; - } else { - return this._handle_error(response.error_message); } + return this._handle_error(response.error_message); }, - _handle_caisse_ap_ip_unexpected_response: function(pay_line, response) { + _handle_caisse_ap_ip_unexpected_response: function (pay_line) { // The response cannot be understood // We let the cashier handle it manually (force or cancel) pay_line.set_payment_status("force_done"); @@ -62,30 +62,32 @@ odoo.define("l10n_fr_pos_caisse_ap_ip.payment", function (require) { timeout: timeout, }; pay_line.set_payment_status("waitingCard"); - return rpc.query({ - model: "pos.payment.method", - method: "fr_caisse_ap_ip_send_payment", - args: [data], - }, { - timeout: timeout, - shadow: true, - }).then((response) => { - if (response instanceof Object && "payment_status" in response){ - // The response is a valid object - return this._handle_caisse_ap_ip_response( - pay_line, - response - ); - } else { - return this._handle_caisse_ap_ip_unexpected_response(pay_line, response); - } - }).catch((error) => { - // It should be a request timeout - let error_msg = _t( - "No response received from the payment terminal in the given time." + return rpc + .query( + { + model: "pos.payment.method", + method: "fr_caisse_ap_ip_send_payment", + args: [data], + }, + { + timeout: timeout, + shadow: true, + } ) - return this._handle_error(error_msg); - }); + .then((response) => { + if (response instanceof Object && "payment_status" in response) { + // The response is a valid object + return this._handle_caisse_ap_ip_response(pay_line, response); + } + return this._handle_caisse_ap_ip_unexpected_response(pay_line); + }) + .catch(() => { + // It should be a request timeout + const error_msg = _t( + "No answer from the payment terminal in the given time." + ); + return this._handle_error(error_msg); + }); }, _handle_error: function (msg) { diff --git a/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml index 56fc1aa27..f1f4fcd11 100644 --- a/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml +++ b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml @@ -1,22 +1,29 @@ - + - - pos.payment.method - + - - - + + + From 94937544cc69514faed289250a693dfa279baab4 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 6 Jul 2023 00:24:39 +0200 Subject: [PATCH 12/21] l10n_fr_pos_caisse_ap_ip: add file CREDITS.rst --- l10n_fr_pos_caisse_ap_ip/readme/CREDITS.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 l10n_fr_pos_caisse_ap_ip/readme/CREDITS.rst diff --git a/l10n_fr_pos_caisse_ap_ip/readme/CREDITS.rst b/l10n_fr_pos_caisse_ap_ip/readme/CREDITS.rst new file mode 100644 index 000000000..b1777e78e --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/CREDITS.rst @@ -0,0 +1 @@ +The development of this module has been financially supported by `Camptocamp `_. From ffd889ca478fa424f3540d4b4a700bc052ab8c19 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Sat, 22 Jul 2023 18:27:49 +0000 Subject: [PATCH 13/21] [UPD] Update l10n_fr_pos_caisse_ap_ip.pot --- l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot | 2 -- 1 file changed, 2 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot b/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot index 5ba41f0e0..d81581878 100644 --- a/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot +++ b/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot @@ -6,8 +6,6 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-04 21:55+0000\n" -"PO-Revision-Date: 2023-07-04 21:55+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" From 73767f1b32693d2646646422134062d6ba2d729b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sat, 22 Jul 2023 18:31:22 +0000 Subject: [PATCH 14/21] [UPD] README.rst --- l10n_fr_pos_caisse_ap_ip/README.rst | 111 ++++- .../static/description/index.html | 448 ++++++++++++++++++ 2 files changed, 558 insertions(+), 1 deletion(-) create mode 100644 l10n_fr_pos_caisse_ap_ip/static/description/index.html diff --git a/l10n_fr_pos_caisse_ap_ip/README.rst b/l10n_fr_pos_caisse_ap_ip/README.rst index afb488df6..975d76806 100644 --- a/l10n_fr_pos_caisse_ap_ip/README.rst +++ b/l10n_fr_pos_caisse_ap_ip/README.rst @@ -1 +1,110 @@ -Will be generated from readme subdir +========================================== +POS: Caisse-AP payment protocol for France +========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--france-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-france/tree/16.0/l10n_fr_pos_caisse_ap_ip + :alt: OCA/l10n-france +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-france-16-0/l10n-france-16-0-l10n_fr_pos_caisse_ap_ip + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/121/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds support for the **Caisse AP** protocol over IP in the Odoo Point of Sale. + +The `Caisse AP protocol `_ is a vendor-independent protocol used in France to communicate between a point of sale and a payment terminal. It is implemented by `Ingenico `_ payment terminals, `Verifone `_ payment terminal and other brands of payment terminals. This protocol is designed by a French association called `Association du paiement `_, abbreviated as **AP**. Note that the Caisse-AP protocol is used by Ingenico payment terminals deployed in France, but not by the same model of Ingenico payment terminals deployed in other countries! + +This module support a bi-directionnal link with the payment terminal: + +1. it sends the amount to the payment terminal +2. it waits for the end of the payment transaction +3. it parses the answer of the payment terminal which gives the payment status: in case of success, the payment line is automatically validated ; in case of failure, an error message is displayed and the Odoo user can retry or delete the payment line. + +The Caisse-AP protocol was initially written for serial and USB. Since the Caisse AP protocol version 3.x, it also supports IP. When used over IP, the client (point of sale) and the server (payment terminal) exchange simple text data encoded as ASCII over a raw TCP socket. + +The Caisse-AP protocol has one important drawback: as it uses a raw TCP socket, it cannot be used from pure JS code. So the JS code of the point of sale cannot generate the query to send the amount to the payment terminal by itself. In this module, the JS code of the point of sale sends a query to the Odoo server that opens a raw TCP socket to the payment terminal. It implies that, if the Odoo server is not on the LAN but somewhere on the Internet and the payment terminal has a private IP on the LAN, you will need to setup a TCP port forwarding rule on the firewall to redirect the TCP connection of the Odoo server to the payment terminal. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In the menu *Point of sale > Configuration > Payment Method*, on the payment method that correspond to a payment by card: + +* select the appropriate journal, which should be a bank journal (and not a cash journa, otherwise the field *Use a payment terminal* is invisible) +* field *Use a payment terminal*: select **Caisse AP over IP (France only)** +* field *Caisse-AP Payment Terminal IP Address*: set the IP address of the payment terminal, +* field *Caisse-AP Payment Terminal Port*: set the TCP port of the payment terminal (8888 by default), +* field *Payment Mode*: set *Card* (the value *Check* is for the *Check* payment method if you use a check printer connected to the payment terminal such as Ingenico i2200) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Alexis de Lattre +* Pierrick Brun + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by `Camptocamp `_. + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-alexis-via| image:: https://github.com/alexis-via.png?size=40px + :target: https://github.com/alexis-via + :alt: alexis-via + +Current `maintainer `__: + +|maintainer-alexis-via| + +This module is part of the `OCA/l10n-france `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_fr_pos_caisse_ap_ip/static/description/index.html b/l10n_fr_pos_caisse_ap_ip/static/description/index.html new file mode 100644 index 000000000..f45ffa10f --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/static/description/index.html @@ -0,0 +1,448 @@ + + + + + + +POS: Caisse-AP payment protocol for France + + + +
+

POS: Caisse-AP payment protocol for France

+ + +

Beta License: AGPL-3 OCA/l10n-france Translate me on Weblate Try me on Runbot

+

This module adds support for the Caisse AP protocol over IP in the Odoo Point of Sale.

+

The Caisse AP protocol is a vendor-independent protocol used in France to communicate between a point of sale and a payment terminal. It is implemented by Ingenico payment terminals, Verifone payment terminal and other brands of payment terminals. This protocol is designed by a French association called Association du paiement, abbreviated as AP. Note that the Caisse-AP protocol is used by Ingenico payment terminals deployed in France, but not by the same model of Ingenico payment terminals deployed in other countries!

+

This module support a bi-directionnal link with the payment terminal:

+
    +
  1. it sends the amount to the payment terminal
  2. +
  3. it waits for the end of the payment transaction
  4. +
  5. it parses the answer of the payment terminal which gives the payment status: in case of success, the payment line is automatically validated ; in case of failure, an error message is displayed and the Odoo user can retry or delete the payment line.
  6. +
+

The Caisse-AP protocol was initially written for serial and USB. Since the Caisse AP protocol version 3.x, it also supports IP. When used over IP, the client (point of sale) and the server (payment terminal) exchange simple text data encoded as ASCII over a raw TCP socket.

+

The Caisse-AP protocol has one important drawback: as it uses a raw TCP socket, it cannot be used from pure JS code. So the JS code of the point of sale cannot generate the query to send the amount to the payment terminal by itself. In this module, the JS code of the point of sale sends a query to the Odoo server that opens a raw TCP socket to the payment terminal. It implies that, if the Odoo server is not on the LAN but somewhere on the Internet and the payment terminal has a private IP on the LAN, you will need to setup a TCP port forwarding rule on the firewall to redirect the TCP connection of the Odoo server to the payment terminal.

+

Table of contents

+ +
+

Configuration

+

In the menu Point of sale > Configuration > Payment Method, on the payment method that correspond to a payment by card:

+
    +
  • select the appropriate journal, which should be a bank journal (and not a cash journa, otherwise the field Use a payment terminal is invisible)
  • +
  • field Use a payment terminal: select Caisse AP over IP (France only)
  • +
  • field Caisse-AP Payment Terminal IP Address: set the IP address of the payment terminal,
  • +
  • field Caisse-AP Payment Terminal Port: set the TCP port of the payment terminal (8888 by default),
  • +
  • field Payment Mode: set Card (the value Check is for the Check payment method if you use a check printer connected to the payment terminal such as Ingenico i2200)
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by Camptocamp.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

alexis-via

+

This module is part of the OCA/l10n-france project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From fe3e3a3a52e4dbfd5a13a4d490e40c1392b82f09 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sat, 22 Jul 2023 18:31:22 +0000 Subject: [PATCH 15/21] [ADD] icon.png --- .../static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 l10n_fr_pos_caisse_ap_ip/static/description/icon.png diff --git a/l10n_fr_pos_caisse_ap_ip/static/description/icon.png b/l10n_fr_pos_caisse_ap_ip/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 1e93f00b325e247b3a7647680c8b02f22a7a9dc6 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 22 Jul 2023 19:14:41 +0000 Subject: [PATCH 16/21] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: l10n-france-16.0/l10n-france-16.0-l10n_fr_pos_caisse_ap_ip Translate-URL: https://translation.odoo-community.org/projects/l10n-france-16-0/l10n-france-16-0-l10n_fr_pos_caisse_ap_ip/ --- l10n_fr_pos_caisse_ap_ip/i18n/fr.po | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/i18n/fr.po b/l10n_fr_pos_caisse_ap_ip/i18n/fr.po index 437933e28..197c52440 100644 --- a/l10n_fr_pos_caisse_ap_ip/i18n/fr.po +++ b/l10n_fr_pos_caisse_ap_ip/i18n/fr.po @@ -36,6 +36,16 @@ msgstr "" "%(request_val)s dans la requête et %(answer_val)s dans la réponse, alors que " "ces valeurs devraient être identiques. Cela ne devrait jamais arriver !" +#. module: l10n_fr_pos_caisse_ap_ip +#. odoo-python +#: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 +#, python-format +msgid "" +"Caisse AP IP protocol: Tag %(label)s (%(tag)s) has value %(request_val)s in " +"the request and %(answer_val)s in the answer, but these values should be " +"identical. This should never happen!" +msgstr "" + #. module: l10n_fr_pos_caisse_ap_ip #. odoo-python #: code:addons/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py:0 @@ -71,8 +81,8 @@ msgstr "Port du terminal de paiement Caisse-AP" msgid "" "Caisse-AP payment terminal IP address is not set on payment method '%s'." msgstr "" -"L'adresse IP du terminal de paiement Caisse-AP n'est pas définie sur " -"la méthode de paiement '%s'." +"L'adresse IP du terminal de paiement Caisse-AP n'est pas définie sur la " +"méthode de paiement '%s'." #. module: l10n_fr_pos_caisse_ap_ip #. odoo-python @@ -80,8 +90,8 @@ msgstr "" #, python-format msgid "Caisse-AP payment terminal port is not set on payment method '%s'." msgstr "" -"Le port du terminal de paiement Caisse-AP n'est pas défini sur la " -"méthode de paiement '%s'." +"Le port du terminal de paiement Caisse-AP n'est pas défini sur la méthode de " +"paiement '%s'." #. module: l10n_fr_pos_caisse_ap_ip #: model:ir.model.fields.selection,name:l10n_fr_pos_caisse_ap_ip.selection__pos_payment_method__fr_caisse_ap_ip_mode__card @@ -222,8 +232,8 @@ msgid "" "You are tying to send amount %s cents to the payment terminal, but it is " "over the maximum!" msgstr "" -"Vous essayez d'envoyer le montant %s centimes au terminal de paiement, mais il " -"est supérieur au maximum !" +"Vous essayez d'envoyer le montant %s centimes au terminal de paiement, mais " +"il est supérieur au maximum !" #. module: l10n_fr_pos_caisse_ap_ip #. odoo-python From c100d01f97e593974b507ace29077a1b847b7248 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 3 Sep 2023 13:34:52 +0000 Subject: [PATCH 17/21] [UPD] README.rst --- l10n_fr_pos_caisse_ap_ip/README.rst | 15 ++++--- .../static/description/index.html | 44 ++++++++++--------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/l10n_fr_pos_caisse_ap_ip/README.rst b/l10n_fr_pos_caisse_ap_ip/README.rst index 975d76806..919f0d3a9 100644 --- a/l10n_fr_pos_caisse_ap_ip/README.rst +++ b/l10n_fr_pos_caisse_ap_ip/README.rst @@ -2,10 +2,13 @@ POS: Caisse-AP payment protocol for France ========================================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e70b108063f57e755b8a0714512cf553774b5dc770658146aa6d5e2bd84dd4ae + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,11 +22,11 @@ POS: Caisse-AP payment protocol for France .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/l10n-france-16-0/l10n-france-16-0-l10n_fr_pos_caisse_ap_ip :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/121/16.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-france&target_branch=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module adds support for the **Caisse AP** protocol over IP in the Odoo Point of Sale. @@ -60,7 +63,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed +If you spotted it first, help us to smash it by providing a detailed and welcomed `feedback `_. Do not contact contributors directly about support or help with technical issues. diff --git a/l10n_fr_pos_caisse_ap_ip/static/description/index.html b/l10n_fr_pos_caisse_ap_ip/static/description/index.html index f45ffa10f..608285b5e 100644 --- a/l10n_fr_pos_caisse_ap_ip/static/description/index.html +++ b/l10n_fr_pos_caisse_ap_ip/static/description/index.html @@ -1,20 +1,20 @@ - + - + POS: Caisse-AP payment protocol for France