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..38338c2bc --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/README.rst @@ -0,0 +1,149 @@ +========================================== +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 + :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/17.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-17-0/l10n-france-17-0-l10n_fr_pos_caisse_ap_ip + :alt: Translate me on Weblate +.. |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=17.0 + :alt: Try me on Runboat + +|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 to smash 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/__init__.py b/l10n_fr_pos_caisse_ap_ip/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/__init__.py @@ -0,0 +1 @@ +from . import models 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..f44a14b52 --- /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": "17.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": [ + "l10n_fr_pos_caisse_ap_ip/static/src/app/payment_caisse_ap_ip.esm.js", + "l10n_fr_pos_caisse_ap_ip/static/src/overrides/models/models.esm.js", + ], + }, + "installable": True, +} diff --git a/l10n_fr_pos_caisse_ap_ip/i18n/es.po b/l10n_fr_pos_caisse_ap_ip/i18n/es.po new file mode 100644 index 000000000..dd5f13151 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/i18n/es.po @@ -0,0 +1,243 @@ +# 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" +"PO-Revision-Date: 2023-09-20 11:09+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\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 "Aplicación %(label)s (código %(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 "" +"Caisse AP Protocolo IP: Tag %(label)s (%(tag)s) tiene valor %(request_val)s " +"en la consulta y %(answer_val)s en la respuesta, pero estos valores deberían " +"ser idénticos. ¡Esto no debería ocurrir nunca!" + +#. 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 "" +"Caisse AP Protocolo IP: Tag %(label)s (%(tag)s) tiene valor %(request_val)s " +"en la petición y %(answer_val)s en la respuesta, pero estos valores deberían " +"ser idénticos. ¡Esto no debería ocurrir nunca!" + +#. 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 "" +"Caisse AP IP protocol: tag %s is required but it is not present in the " +"answer from the terminal. ¡Esto no debería ocurrir nunca!" + +#. 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 sobre IP (sólo en Francia)" + +#. 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 "Dirección IP del terminal de pago 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 "Terminal de pago Caisse-AP Puerto" + +#. 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 "" +"La dirección IP del terminal de pago Caisse-AP no está configurada en el " +"método de pago '%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 "" +"El puerto del terminal de pago Caisse-AP no está configurado en el método de " +"pago '%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 "Tarjeta" + +#. 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 "Tipo de tarjeta: %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 "Cheque" + +#. 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 "Respuesta vacía del terminal de pago. Esto no debería ocurrir nunca." + +#. 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 "" +"Error en la comunicación con el terminal de pago: la acción estado no es " +"válida (AE=%s).¡ Esto no debería ocurrir nunca!" + +#. 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 "" +"Error al conectar con el terminal de pago en %(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 "" +"Dirección IP o nombre DNS del terminal de pago que soporta el protocolo " +"Caisse-AP sobre 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 "No hay respuesta del terminal de pago en el tiempo dado." + +#. 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 "Forma de pago" + +#. 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 "Error de terminal de pago" + +#. 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 "Métodos de pago en el punto de venta" + +#. 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 "El puerto %s del terminal de pago no es un puerto TCP válido." + +#. 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 "Pulse el botón rojo del terminal de pago para cancelar la transacció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 "Read mode: %(label)s (code %(code)s)" +msgstr "Modo lectura: %(label)s (código %(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 "" +"Puerto TCP del terminal de pago que soporta el protocolo Caisse-AP sobre 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 "La operación de pago ha fallado." + +#. 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 "La transacción de pago ha fallado: %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 "¡Está intentando enviar un importe nulo al terminal de pago!" + +#. 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 "" +"Está intentando enviar una cantidad %s céntimos al terminal de pago, ¡pero " +"supera el máximo!" + +#. 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 "desconocido" 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..197c52440 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/i18n/fr.po @@ -0,0 +1,243 @@ +# 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 %(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 "" +"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..d81581878 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/i18n/l10n_fr_pos_caisse_ap_ip.pot @@ -0,0 +1,220 @@ +# 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" +"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/__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..12b7a4282 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/models/pos_payment_method.py @@ -0,0 +1,418 @@ +# 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 +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") + +BUFFER_SIZE = 1024 + + +class PosPaymentMethod(models.Model): + _inherit = "pos.payment.method" + + def _get_payment_terminal_selection(self): + res = super()._get_payment_terminal_selection() + res.append(("fr-caisse_ap_ip", _("Caisse AP over IP (France only)"))) + return res + + 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, + ) + + @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.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.fr_caisse_ap_ip_port: + 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 + ) + + 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) + assert len(tag) == 2 + assert isinstance(value, str) + 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: + 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( + [ + "".join([tag, str(len(value)).zfill(3), value]) + for (tag, value) in msg_list + ] + ) + return msg_str + + 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 + } + 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", + 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", + } + 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") + 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 + amount_positive = amount * -1 + else: + msg_dict["CD"] = "0" # debit i.e. regular payment + amount_positive = amount + if currency.decimal_places: + amount_cent = amount_positive * (10**currency.decimal_places) + else: + amount_cent = amount_positive + amount_str = str(int(round(amount_cent))) + 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) + 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 + 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_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, + ) + res = { + "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": "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) + 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": + 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": "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"}, + } + fail_res = { + "payment_status": "issue", + } + for tag, props in tag_dict.items(): + 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: + 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", + } + 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") + 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( + [ + "-".join([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, + } + 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"]] + error_msg = _("The payment transaction has failed: %s") % label + res = { + "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_str): + logger.debug("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.debug("Answer dict: %s", data_dict) + return data_dict diff --git a/l10n_fr_pos_caisse_ap_ip/pyproject.toml b/l10n_fr_pos_caisse_ap_ip/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.md b/l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.md new file mode 100644 index 000000000..5e27fd01d --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/CONFIGURE.md @@ -0,0 +1,15 @@ +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/CONTRIBUTORS.md b/l10n_fr_pos_caisse_ap_ip/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..b04014433 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Alexis de Lattre \<\> +- Pierrick Brun \<\> diff --git a/l10n_fr_pos_caisse_ap_ip/readme/CREDITS.md b/l10n_fr_pos_caisse_ap_ip/readme/CREDITS.md new file mode 100644 index 000000000..670e2485d --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/CREDITS.md @@ -0,0 +1,2 @@ +The development of this module has been financially supported by +[Camptocamp](https://www.camptocamp.com/). diff --git a/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.md b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.md new file mode 100644 index 000000000..00ce42809 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/readme/DESCRIPTION.md @@ -0,0 +1,40 @@ +This module adds support for the **Caisse AP** protocol over IP in the +Odoo Point of Sale. + +The [Caisse AP +protocol](https://www.associationdupaiement.fr/protocoles/protocole-caisse/) +is a vendor-independent protocol used in France to communicate between a +point of sale and a payment terminal. It is implemented by +[Ingenico](https://ingenico.com/fr/produits-et-services/terminaux-de-paiement) +payment terminals, [Verifone](https://www.verifone.com/) payment +terminal and other brands of payment terminals. This protocol is +designed by a French association called [Association du +paiement](https://www.associationdupaiement.fr/), 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. 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 000000000..3a0328b51 Binary files /dev/null and b/l10n_fr_pos_caisse_ap_ip/static/description/icon.png differ 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..a48e2c959 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/static/description/index.html @@ -0,0 +1,486 @@ + + + + + + +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 Runboat

+

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 to smash 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.

+
+
+
+ + diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/app/payment_caisse_ap_ip.esm.js b/l10n_fr_pos_caisse_ap_ip/static/src/app/payment_caisse_ap_ip.esm.js new file mode 100644 index 000000000..b70ce963b --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/static/src/app/payment_caisse_ap_ip.esm.js @@ -0,0 +1,91 @@ +/** @odoo-module */ +/* + 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). +*/ + +import {ErrorPopup} from "@point_of_sale/app/errors/popups/error_popup"; +import {PaymentInterface} from "@point_of_sale/app/payment/payment_interface"; +import {_t} from "@web/core/l10n/translation"; + +export class PaymentCaisseAPIP extends PaymentInterface { + setup() { + super.setup(...arguments); + } + + async send_payment_cancel() { + super.send_payment_cancel(...arguments); + this._show_error( + _t( + "Press the red button on the payment terminal to cancel the transaction." + ) + ); + return true; + } + + _handle_caisse_ap_ip_response(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; + } + return this._handle_error(response.error_message); + } + + _handle_caisse_ap_ip_unexpected_response(pay_line) { + // 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(); + } + + async send_payment_request(cid) { + await super.send_payment_request(...arguments); + const order = this.pos.get_order(); + const pay_line = order.selected_paymentline; + // Define the timout used in the POS and in the back-end (in ms) + const timeout = 180000; + const data = { + amount: pay_line.amount, + currency_id: this.pos.currency.id, + payment_method_id: this.payment_method.id, + payment_id: cid, + timeout: timeout, + }; + pay_line.set_payment_status("waitingCard"); + return this.env.services.orm.silent + .call("pos.payment.method", "fr_caisse_ap_ip_send_payment", [data]) + .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(msg) { + this._show_error(msg); + return false; + } + + _show_error(msg, title) { + this.env.services.popup.add(ErrorPopup, { + title: title || _t("Payment Terminal Error"), + body: msg, + }); + } +} diff --git a/l10n_fr_pos_caisse_ap_ip/static/src/overrides/models/models.esm.js b/l10n_fr_pos_caisse_ap_ip/static/src/overrides/models/models.esm.js new file mode 100644 index 000000000..b206792d2 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/static/src/overrides/models/models.esm.js @@ -0,0 +1,12 @@ +/** @odoo-module */ +/* + 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). +*/ + +import {PaymentCaisseAPIP} from "@l10n_fr_pos_caisse_ap_ip/app/payment_caisse_ap_ip.esm"; +import {register_payment_method} from "@point_of_sale/app/store/pos_store"; + +register_payment_method("fr-caisse_ap_ip", 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..ff1ea92a9 --- /dev/null +++ b/l10n_fr_pos_caisse_ap_ip/views/pos_payment_method.xml @@ -0,0 +1,35 @@ + + + + + + + pos.payment.method + + + + + + + + + + + +