diff --git a/pos_payment_pax_terminal_pywebdriver/README.rst b/pos_payment_pax_terminal_pywebdriver/README.rst new file mode 100644 index 0000000000..97c9ecf00d --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/README.rst @@ -0,0 +1,201 @@ +============================= +POS Payment PAX (pywebdriver) +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:79503bce4f415229a1e017b0b843bb73d4d5d47b747b6812c3a88f821205b0b4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/18.0/pos_payment_pax_terminal_pywebdriver + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-18-0/pos-18-0-pos_payment_pax_terminal_pywebdriver + :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/pos&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Integrates **PAX payment terminals** with Odoo 18.0 Point of Sale via a +**local pywebdriver proxy** — designed for cloud-hosted Odoo where the +PAX terminal is only reachable on the store's local network. + +|PAX payment method configuration| + +How it works +------------ + +The module relays PAX terminal payments through a pywebdriver instance +running on the store PC: + +:: + + Odoo POS (browser) + │ + │ JSON-RPC silentCall + ▼ + Odoo server (backend) + │ + │ HTTP POST → /hw_proxy/pax/transaction_execute + ▼ + pywebdriver (store PC, default port 8069) + │ + │ HTTP GET (POS Link binary frame) → port 10009 + ▼ + PAX Terminal + +Supported transactions: + +================ =========================== +POS Link Command Description +================ =========================== +T00 DoSale Charge a card for a sale +T02 DoRefund Refund to a card +T04 DoVoid Void a previous transaction +================ =========================== + +When to use this module +----------------------- + +Use ``pos_payment_pax_terminal_pywebdriver`` when: + +- Odoo is **cloud-hosted** and cannot reach the terminal's LAN IP + directly +- Chrome's **Local Network Access (LNA)** enforcement (v142+) blocks + direct browser-to-terminal calls + +Use ``pos_payment_pax_terminal_redirect`` instead when Odoo is +on-premise and has direct TCP access to the terminal on port 10009. + +.. |PAX payment method configuration| image:: https://raw.githubusercontent.com/OCA/pos/18.0/pos_payment_pax_terminal_pywebdriver/static/description/img/payment_method_pax.png + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +Prerequisites +------------- + +- Odoo 18.0 +- Python ``requests`` library (standard in Odoo installations) +- **pywebdriver** installed on the store PC with the PAX plugin enabled +- PAX terminal with **POS Link HTTP** mode enabled on the local network + +Steps +----- + +1. Install and start pywebdriver on the store PC. Ensure the PAX plugin + (``pax_driver.py``) is loaded and the terminal IP is reachable from + that PC. +2. Copy or clone the ``pos_payment_pax_terminal_pywebdriver`` directory + into your Odoo addons path (e.g. ``extra_addons/pos/``). +3. Restart the Odoo server to pick up the new module. +4. Go to **Apps**, search for **POS Payment PAX (pywebdriver)**, and + click **Install**. +5. Follow the `Configuration `__ guide to set up the + payment method. + +pywebdriver setup +----------------- + +|IoT / pywebdriver hardware proxy configuration| + +The pywebdriver instance must expose the PAX endpoint at: + +:: + + POST http://:/hw_proxy/pax/transaction_execute + +The default pywebdriver port is **8069**. The PAX driver plugin must be +enabled in pywebdriver's configuration. + +.. |IoT / pywebdriver hardware proxy configuration| image:: https://raw.githubusercontent.com/OCA/pos/18.0/pos_payment_pax_terminal_pywebdriver/static/description/img/iot_config.png + +Usage +===== + +Processing a payment +-------------------- + +1. Open a POS session that has the **PAX (pywebdriver)** payment method + enabled. +2. Add items to an order and proceed to the **Payment** screen. +3. Select the **PAX Terminal (pywebdriver)** payment method and enter + the amount. +4. The terminal prompts the customer to swipe, insert, or tap their + card. +5. Once the customer completes the interaction, the POS automatically + marks the payment as **Done** and the order can be finalised. + +Refunds +------- + +Refunds work through the standard Odoo POS return flow. When a return +order is paid using this payment method, the module sends a **Return +(T00 type 02)** command to the terminal via pywebdriver. + +Fast Payments +------------- + +When **Fast Payments** is enabled (default), the payment request is sent +to the terminal as soon as the cashier selects the payment method — no +manual "Send" click is needed. Disable it to let the cashier verify the +amount first. + +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 +------- + +* Trobz + +Contributors +------------ + +- `Trobz `__ + + - Phan Hong Phuc + +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. + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_payment_pax_terminal_pywebdriver/__init__.py b/pos_payment_pax_terminal_pywebdriver/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_payment_pax_terminal_pywebdriver/__manifest__.py b/pos_payment_pax_terminal_pywebdriver/__manifest__.py new file mode 100644 index 0000000000..6cb25f1b7b --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "POS Payment PAX (pywebdriver)", + "version": "18.0.1.0.0", + "category": "Point Of Sale", + "summary": "Integrate PAX terminals with Odoo POS via a local pywebdriver proxy", + "author": "Odoo Community Association (OCA), Trobz", + "website": "https://github.com/OCA/pos", + "license": "LGPL-3", + "depends": ["point_of_sale"], + "data": [ + "views/pos_payment_method_views.xml", + ], + "assets": { + "point_of_sale._assets_pos": [ + "pos_payment_pax_terminal_pywebdriver/static/src/app/**/*", + "pos_payment_pax_terminal_pywebdriver/static/src/overrides/**/*", + ], + }, + "installable": True, +} diff --git a/pos_payment_pax_terminal_pywebdriver/i18n/fr.po b/pos_payment_pax_terminal_pywebdriver/i18n/fr.po new file mode 100644 index 0000000000..65f1c503c6 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/i18n/fr.po @@ -0,0 +1,119 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_payment_pax_terminal_pywebdriver +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-21 00:00+0000\n" +"PO-Revision-Date: 2026-07-21 00:00+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: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Cannot process a transaction with a non-positive amount." +msgstr "Impossible de traiter une transaction avec un montant non positif." + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "No PAX terminal IP is configured for this payment method. Set the Terminal IP field in the payment method settings." +msgstr "Aucune adresse IP de terminal PAX n'est configurée pour ce mode de paiement. Renseignez le champ IP du terminal dans les paramètres du mode de paiement." + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Network error contacting the Odoo server." +msgstr "Erreur réseau lors du contact avec le serveur Odoo." + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "PAX terminal declined: %(msg)s%(code)s" +msgstr "Terminal PAX refusé : %(msg)s%(code)s" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Unknown error from pywebdriver" +msgstr "Erreur inconnue de pywebdriver" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "PAX Terminal Error" +msgstr "Erreur du terminal PAX" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "OK" +msgstr "OK" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Payment declined" +msgstr "Paiement refusé" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model,name:pos_payment_pax_terminal_pywebdriver.model_pos_payment_method +msgid "Point of Sale Payment Methods" +msgstr "Modes de paiement du point de vente" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_terminal_ip +msgid "Terminal IP" +msgstr "IP du terminal" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_terminal_port +msgid "Terminal Port" +msgstr "Port du terminal" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_transaction_type +msgid "Default Transaction Type" +msgstr "Type de transaction par défaut" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__01 +msgid "Sale" +msgstr "Vente" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__02 +msgid "Return/Refund" +msgstr "Retour/Remboursement" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__05 +msgid "Auth Only" +msgstr "Autorisation uniquement" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__06 +msgid "Post-Auth" +msgstr "Post-autorisation" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_timeout +msgid "Timeout (ms)" +msgstr "Délai d'attente (ms)" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_use_https +msgid "Use HTTPS" +msgstr "Utiliser HTTPS" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_fast_payments +msgid "Fast Payments" +msgstr "Paiements rapides" diff --git a/pos_payment_pax_terminal_pywebdriver/i18n/pos_payment_pax_terminal_pywebdriver.pot b/pos_payment_pax_terminal_pywebdriver/i18n/pos_payment_pax_terminal_pywebdriver.pot new file mode 100644 index 0000000000..ea74b5c849 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/i18n/pos_payment_pax_terminal_pywebdriver.pot @@ -0,0 +1,119 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_payment_pax_terminal_pywebdriver +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-21 00:00+0000\n" +"PO-Revision-Date: 2026-07-21 00:00+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: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Cannot process a transaction with a non-positive amount." +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "No PAX terminal IP is configured for this payment method. Set the Terminal IP field in the payment method settings." +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Network error contacting the Odoo server." +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "PAX terminal declined: %(msg)s%(code)s" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Unknown error from pywebdriver" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "PAX Terminal Error" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "OK" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#. odoo-javascript +#: code:addons/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js:0 +msgid "Payment declined" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model,name:pos_payment_pax_terminal_pywebdriver.model_pos_payment_method +msgid "Point of Sale Payment Methods" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_terminal_ip +msgid "Terminal IP" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_terminal_port +msgid "Terminal Port" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_transaction_type +msgid "Default Transaction Type" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__01 +msgid "Sale" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__02 +msgid "Return/Refund" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__05 +msgid "Auth Only" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields.selection,name:pos_payment_pax_terminal_pywebdriver.selection__pos_payment_method__pax_pw_transaction_type__06 +msgid "Post-Auth" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_timeout +msgid "Timeout (ms)" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_use_https +msgid "Use HTTPS" +msgstr "" + +#. module: pos_payment_pax_terminal_pywebdriver +#: model:ir.model.fields,field_description:pos_payment_pax_terminal_pywebdriver.field_pos_payment_method__pax_pw_fast_payments +msgid "Fast Payments" +msgstr "" diff --git a/pos_payment_pax_terminal_pywebdriver/models/__init__.py b/pos_payment_pax_terminal_pywebdriver/models/__init__.py new file mode 100644 index 0000000000..58690ef8d9 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/models/__init__.py @@ -0,0 +1 @@ +from . import pos_payment_method diff --git a/pos_payment_pax_terminal_pywebdriver/models/pos_payment_method.py b/pos_payment_pax_terminal_pywebdriver/models/pos_payment_method.py new file mode 100644 index 0000000000..7c64bc5742 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/models/pos_payment_method.py @@ -0,0 +1,141 @@ +import logging + +import requests + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class PosPaymentMethod(models.Model): + _inherit = "pos.payment.method" + + def _get_payment_terminal_selection(self): + res = super()._get_payment_terminal_selection() + res.append(("pax_pywebdriver", "PAX Terminal (pywebdriver)")) + return res + + pax_pw_terminal_ip = fields.Char( + string="Terminal IP", + help="IP address of the PAX terminal reachable from pywebdriver " + "(do not include http://).", + ) + pax_pw_terminal_port = fields.Integer( + string="Terminal Port", + default=10009, + help="TCP port of the PAX terminal HTTP server (default: 10009).", + ) + pax_pw_transaction_type = fields.Selection( + [ + ("01", "Sale"), + ("02", "Return/Refund"), + ("05", "Auth Only"), + ("06", "Post-Auth"), + ], + string="Default Transaction Type", + default="01", + ) + pax_pw_timeout = fields.Integer( + string="Timeout (ms)", + default=120000, + help="Maximum time to wait for a terminal response (milliseconds).", + ) + pax_pw_use_https = fields.Boolean( + string="Use HTTPS", + default=False, + help="Enable if the PAX terminal is configured for TLS connections.", + ) + pax_pw_fast_payments = fields.Boolean( + string="Fast Payments", + default=True, + help="When enabled, the payment request is sent automatically as soon as " + "the PAX payment method is selected (no manual 'Send' click needed). " + "Disable to let the cashier review the amount before sending.", + ) + + @api.model + def _load_pos_data_fields(self, config_id): + result = super()._load_pos_data_fields(config_id) + result.extend( + [ + "pax_pw_terminal_ip", + "pax_pw_terminal_port", + "pax_pw_transaction_type", + "pax_pw_timeout", + "pax_pw_use_https", + "pax_pw_fast_payments", + ] + ) + return result + + def pax_pywebdriver_send_payment(self, data): + """ + Called via RPC from the POS JS layer. + + Forwards the payment request to pywebdriver's synchronous + /hw_proxy/pax/transaction_execute endpoint. pywebdriver then contacts + the PAX terminal on the local network and returns the parsed result. + + :param data: dict built by the JS layer — keys: + terminal_ip, terminal_port, use_https, amount (float), + currency_code, order_id, transaction_type, timeout (ms) + :return: dict with 'payment_status' ("success" | "failure" | "error") + """ + self.ensure_one() + + pywebdriver_url = ( + data.get("pywebdriver_url") or "http://127.0.0.1:8069" + ).rstrip("/") + endpoint = f"{pywebdriver_url}/hw_proxy/pax/transaction_execute" + + # Add a 10 s buffer so the HTTP client doesn't time out before PAX does + timeout_ms = data.get("timeout") or self.pax_pw_timeout or 120000 + http_timeout = timeout_ms // 1000 + 10 + + _logger.info( + "PAX pywebdriver: forwarding payment to %s — amount=%s order=%s", + endpoint, + data.get("amount"), + data.get("order_id"), + ) + + try: + resp = requests.post( + endpoint, + json={"params": {"payment_info": data}}, + timeout=http_timeout, + ) + resp.raise_for_status() + result = resp.json().get("result", {}) + except requests.exceptions.Timeout: + _logger.error("PAX pywebdriver: request to %s timed out", endpoint) + return { + "payment_status": "error", + "message": "pywebdriver request timed out", + } + except requests.exceptions.ConnectionError as exc: + _logger.error("PAX pywebdriver: cannot connect to %s — %s", endpoint, exc) + return { + "payment_status": "error", + "message": f"Cannot connect to pywebdriver at {pywebdriver_url}: {exc}", + } + except Exception as exc: + _logger.exception("PAX pywebdriver: unexpected error") + return {"payment_status": "error", "message": str(exc)} + + if result.get("success"): + return { + "payment_status": "success", + "transaction_id": result.get("transaction_id", ""), + "auth_code": result.get("auth_code", ""), + "response_message": result.get("response_message", ""), + "last_four": result.get("last_four", ""), + "card_holder_name": result.get("card_holder_name", ""), + } + + return { + "payment_status": "failure", + "response_code": result.get("response_code", ""), + "response_message": result.get("response_message") + or result.get("error", ""), + } diff --git a/pos_payment_pax_terminal_pywebdriver/pyproject.toml b/pos_payment_pax_terminal_pywebdriver/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pos_payment_pax_terminal_pywebdriver/readme/CONFIGURATION.md b/pos_payment_pax_terminal_pywebdriver/readme/CONFIGURATION.md new file mode 100644 index 0000000000..6a1f717c1a --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/readme/CONFIGURATION.md @@ -0,0 +1,43 @@ +## Create a payment method + +Go to **Point of Sale → Configuration → Payment Methods → New**: + +1. Set **Payment Terminal** to **PAX Terminal (pywebdriver)**. +2. Fill in the connection fields: + +| Field | Description | +|---|---| +| Terminal IP | LAN IP of the PAX terminal as seen from the pywebdriver PC (e.g. `192.168.1.100`) | +| Terminal Port | HTTP port on the PAX terminal (default **10009**) | +| Use HTTPS | Enable if the terminal is configured for TLS | +| Timeout (ms) | Max time to wait for a terminal response (default 120 000 ms) | +| Default Transaction Type | Usually **Sale** | +| Fast Payments | Send the payment request automatically on method selection | + +![PAX payment method form](static/description/img/payment_method_pax.png) + +## Assign to a POS configuration + +Go to **Point of Sale → Configuration → Point of Sale → [your shop]**: + +- Under **Payments**, add the PAX (pywebdriver) payment method. + +![POS configuration with PAX payment method](static/description/img/pos_config_payment_method_pax.png) + +## pywebdriver URL + +The Odoo backend forwards the payment to pywebdriver using the URL +configured in the POS hardware proxy settings. By default this is +`http://127.0.0.1:8069` (same host as Odoo). For cloud-hosted Odoo, set +the hardware proxy host to the store PC's IP or hostname where pywebdriver +is running. + +## PAX terminal setup + +The PAX terminal must have **POS Link HTTP** mode enabled. On most PAX models: + +``` +Menu → Settings → Communication → Enable POS Link → Port 10009 +``` + +Consult your PAX terminal model's administration guide for the exact steps. diff --git a/pos_payment_pax_terminal_pywebdriver/readme/CONTRIBUTORS.md b/pos_payment_pax_terminal_pywebdriver/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..bb593cb8b1 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Trobz](https://www.trobz.com) + - Phan Hong Phuc \ diff --git a/pos_payment_pax_terminal_pywebdriver/readme/DESCRIPTION.md b/pos_payment_pax_terminal_pywebdriver/readme/DESCRIPTION.md new file mode 100644 index 0000000000..fe79f460e1 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/readme/DESCRIPTION.md @@ -0,0 +1,44 @@ +Integrates **PAX payment terminals** with Odoo 18.0 Point of Sale via a +**local pywebdriver proxy** — designed for cloud-hosted Odoo where the +PAX terminal is only reachable on the store's local network. + +![PAX payment method configuration](static/description/img/payment_method_pax.png) + +## How it works + +The module relays PAX terminal payments through a pywebdriver instance +running on the store PC: + +``` +Odoo POS (browser) + │ + │ JSON-RPC silentCall + ▼ +Odoo server (backend) + │ + │ HTTP POST → /hw_proxy/pax/transaction_execute + ▼ +pywebdriver (store PC, default port 8069) + │ + │ HTTP GET (POS Link binary frame) → port 10009 + ▼ +PAX Terminal +``` + +Supported transactions: + +| POS Link Command | Description | +|---|---| +| T00 DoSale | Charge a card for a sale | +| T02 DoRefund | Refund to a card | +| T04 DoVoid | Void a previous transaction | + +## When to use this module + +Use `pos_payment_pax_terminal_pywebdriver` when: + +- Odoo is **cloud-hosted** and cannot reach the terminal's LAN IP directly +- Chrome's **Local Network Access (LNA)** enforcement (v142+) blocks direct browser-to-terminal calls + +Use `pos_payment_pax_terminal_redirect` instead when Odoo is on-premise +and has direct TCP access to the terminal on port 10009. diff --git a/pos_payment_pax_terminal_pywebdriver/readme/INSTALL.md b/pos_payment_pax_terminal_pywebdriver/readme/INSTALL.md new file mode 100644 index 0000000000..96090598c7 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/readme/INSTALL.md @@ -0,0 +1,31 @@ +## Prerequisites + +- Odoo 18.0 +- Python `requests` library (standard in Odoo installations) +- **pywebdriver** installed on the store PC with the PAX plugin enabled +- PAX terminal with **POS Link HTTP** mode enabled on the local network + +## Steps + +1. Install and start pywebdriver on the store PC. Ensure the PAX plugin + (`pax_driver.py`) is loaded and the terminal IP is reachable from that PC. +2. Copy or clone the `pos_payment_pax_terminal_pywebdriver` directory into + your Odoo addons path (e.g. `extra_addons/pos/`). +3. Restart the Odoo server to pick up the new module. +4. Go to **Apps**, search for **POS Payment PAX (pywebdriver)**, and click + **Install**. +5. Follow the [Configuration](CONFIGURATION.md) guide to set up the payment + method. + +## pywebdriver setup + +![IoT / pywebdriver hardware proxy configuration](static/description/img/iot_config.png) + +The pywebdriver instance must expose the PAX endpoint at: + +``` +POST http://:/hw_proxy/pax/transaction_execute +``` + +The default pywebdriver port is **8069**. The PAX driver plugin must be +enabled in pywebdriver's configuration. diff --git a/pos_payment_pax_terminal_pywebdriver/readme/USAGE.md b/pos_payment_pax_terminal_pywebdriver/readme/USAGE.md new file mode 100644 index 0000000000..d3ce7454e9 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/readme/USAGE.md @@ -0,0 +1,20 @@ +## Processing a payment + +1. Open a POS session that has the **PAX (pywebdriver)** payment method enabled. +2. Add items to an order and proceed to the **Payment** screen. +3. Select the **PAX Terminal (pywebdriver)** payment method and enter the amount. +4. The terminal prompts the customer to swipe, insert, or tap their card. +5. Once the customer completes the interaction, the POS automatically marks + the payment as **Done** and the order can be finalised. + +## Refunds + +Refunds work through the standard Odoo POS return flow. When a return order +is paid using this payment method, the module sends a **Return (T00 type 02)** +command to the terminal via pywebdriver. + +## Fast Payments + +When **Fast Payments** is enabled (default), the payment request is sent to +the terminal as soon as the cashier selects the payment method — no manual +"Send" click is needed. Disable it to let the cashier verify the amount first. diff --git a/pos_payment_pax_terminal_pywebdriver/static/description/img/iot_config.png b/pos_payment_pax_terminal_pywebdriver/static/description/img/iot_config.png new file mode 100644 index 0000000000..342d2efd77 Binary files /dev/null and b/pos_payment_pax_terminal_pywebdriver/static/description/img/iot_config.png differ diff --git a/pos_payment_pax_terminal_pywebdriver/static/description/img/payment_method_pax.png b/pos_payment_pax_terminal_pywebdriver/static/description/img/payment_method_pax.png new file mode 100644 index 0000000000..e69cdc1ebc Binary files /dev/null and b/pos_payment_pax_terminal_pywebdriver/static/description/img/payment_method_pax.png differ diff --git a/pos_payment_pax_terminal_pywebdriver/static/description/img/pos_config_payment_method_pax.png b/pos_payment_pax_terminal_pywebdriver/static/description/img/pos_config_payment_method_pax.png new file mode 100644 index 0000000000..0d3c3ac580 Binary files /dev/null and b/pos_payment_pax_terminal_pywebdriver/static/description/img/pos_config_payment_method_pax.png differ diff --git a/pos_payment_pax_terminal_pywebdriver/static/description/index.html b/pos_payment_pax_terminal_pywebdriver/static/description/index.html new file mode 100644 index 0000000000..f357dbeac0 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/static/description/index.html @@ -0,0 +1,545 @@ + + + + + +POS Payment PAX (pywebdriver) + + + +
+

POS Payment PAX (pywebdriver)

+ + +

Beta License: LGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

Integrates PAX payment terminals with Odoo 18.0 Point of Sale via a +local pywebdriver proxy — designed for cloud-hosted Odoo where the +PAX terminal is only reachable on the store’s local network.

+

PAX payment method configuration

+
+

How it works

+

The module relays PAX terminal payments through a pywebdriver instance +running on the store PC:

+
+Odoo POS (browser)
+    │
+    │  JSON-RPC silentCall
+    ▼
+Odoo server (backend)
+    │
+    │  HTTP POST → /hw_proxy/pax/transaction_execute
+    ▼
+pywebdriver (store PC, default port 8069)
+    │
+    │  HTTP GET (POS Link binary frame) → port 10009
+    ▼
+PAX Terminal
+
+

Supported transactions:

+ ++++ + + + + + + + + + + + + + + + + +
POS Link CommandDescription
T00 DoSaleCharge a card for a sale
T02 DoRefundRefund to a card
T04 DoVoidVoid a previous transaction
+
+
+

When to use this module

+

Use pos_payment_pax_terminal_pywebdriver when:

+
    +
  • Odoo is cloud-hosted and cannot reach the terminal’s LAN IP +directly
  • +
  • Chrome’s Local Network Access (LNA) enforcement (v142+) blocks +direct browser-to-terminal calls
  • +
+

Use pos_payment_pax_terminal_redirect instead when Odoo is +on-premise and has direct TCP access to the terminal on port 10009.

+

Table of contents

+
+ +
+ +
+
+

Prerequisites

+
    +
  • Odoo 18.0
  • +
  • Python requests library (standard in Odoo installations)
  • +
  • pywebdriver installed on the store PC with the PAX plugin enabled
  • +
  • PAX terminal with POS Link HTTP mode enabled on the local network
  • +
+
+
+

Steps

+
    +
  1. Install and start pywebdriver on the store PC. Ensure the PAX plugin +(pax_driver.py) is loaded and the terminal IP is reachable from +that PC.
  2. +
  3. Copy or clone the pos_payment_pax_terminal_pywebdriver directory +into your Odoo addons path (e.g. extra_addons/pos/).
  4. +
  5. Restart the Odoo server to pick up the new module.
  6. +
  7. Go to Apps, search for POS Payment PAX (pywebdriver), and +click Install.
  8. +
  9. Follow the Configuration guide to set up the +payment method.
  10. +
+
+
+

pywebdriver setup

+

IoT / pywebdriver hardware proxy configuration

+

The pywebdriver instance must expose the PAX endpoint at:

+
+POST http://<store-pc-ip>:<port>/hw_proxy/pax/transaction_execute
+
+

The default pywebdriver port is 8069. The PAX driver plugin must be +enabled in pywebdriver’s configuration.

+
+

Usage

+
+
+
+

Processing a payment

+
    +
  1. Open a POS session that has the PAX (pywebdriver) payment method +enabled.
  2. +
  3. Add items to an order and proceed to the Payment screen.
  4. +
  5. Select the PAX Terminal (pywebdriver) payment method and enter +the amount.
  6. +
  7. The terminal prompts the customer to swipe, insert, or tap their +card.
  8. +
  9. Once the customer completes the interaction, the POS automatically +marks the payment as Done and the order can be finalised.
  10. +
+
+
+

Refunds

+

Refunds work through the standard Odoo POS return flow. When a return +order is paid using this payment method, the module sends a Return +(T00 type 02) command to the terminal via pywebdriver.

+
+
+

Fast Payments

+

When Fast Payments is enabled (default), the payment request is sent +to the terminal as soon as the cashier selects the payment method — no +manual “Send” click is needed. Disable it to let the cashier verify the +amount first.

+
+

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

+
    +
  • Trobz
  • +
+
+
+

Contributors

+ +
+
+

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.

+

This module is part of the OCA/pos project on GitHub.

+

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

+
+
+ + diff --git a/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js b/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js new file mode 100644 index 0000000000..d1db94c2a4 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/static/src/app/payment_pax.esm.js @@ -0,0 +1,183 @@ +import {PaymentInterface} from "@point_of_sale/app/payment/payment_interface"; +import {register_payment_method} from "@point_of_sale/app/store/pos_store"; +import {deduceUrl} from "@point_of_sale/utils"; +import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog"; +import {_t} from "@web/core/l10n/translation"; + +/** + * PAX Terminal payment interface — via pywebdriver proxy. + * + * Communication flow: + * POS JS → silentCall RPC → Odoo backend (pos_payment_pax_terminal_pywebdriver) + * → HTTP POST → pywebdriver (/hw_proxy/pax/transaction_execute) + * → HTTP GET (POS Link frame) → PAX Terminal + * ← POS Link response ← + * ← parsed result JSON ← pywebdriver + * ← {payment_status, ...} ← Odoo backend + * ← silentCall result ← + * + * Use this module instead of pos_payment_pax_terminal when Odoo is cloud-hosted + * and the PAX terminal is only reachable from the local network running pywebdriver. + */ +export class PaxPywebdriverPayment extends PaymentInterface { + // ------------------------------------------------------------------------- + // PaymentInterface overrides + // ------------------------------------------------------------------------- + + get fast_payments() { + return this.payment_method_id.pax_pw_fast_payments ?? true; + } + + send_payment_request(uuid) { + super.send_payment_request(uuid); + return this._paxDoCredit(); + } + + send_payment_cancel(order, uuid) { + super.send_payment_cancel(order, uuid); + // PAX terminals process cancellation locally on the device. + // We can only reset the payment line so the cashier can retry. + const line = this._getPendingLine(); + if (line) { + line.set_payment_status("retry"); + } + return true; + } + + // ------------------------------------------------------------------------- + // Internal + // ------------------------------------------------------------------------- + + _getPendingLine() { + return this.pos.getPendingPaymentLine("pax_pywebdriver"); + } + + async _paxDoCredit() { + const order = this.pos.get_order(); + const line = order.get_selected_paymentline(); + + if (!line || line.amount <= 0) { + this._showError( + _t("Cannot process a transaction with a non-positive amount.") + ); + return false; + } + + const pm = this.payment_method_id; + if (!pm.pax_pw_terminal_ip) { + this._showError( + _t( + "No PAX terminal IP is configured for this payment method. " + + "Set the Terminal IP field in the payment method settings." + ) + ); + return false; + } + + const amountFloat = line.amount; + const orderId = order.name.replace(/\s/g, "").replaceAll("-", "").toUpperCase(); + const proxyIp = this.pos.config.proxy_ip; + const pywebdriverUrl = proxyIp ? deduceUrl(proxyIp) : "http://127.0.0.1:8069"; + + line.set_payment_status("waitingCard"); + + const result = await this.pos.data.silentCall( + "pos.payment.method", + "pax_pywebdriver_send_payment", + [[pm.id]], + { + data: { + pywebdriver_url: pywebdriverUrl, + terminal_ip: pm.pax_pw_terminal_ip, + terminal_port: pm.pax_pw_terminal_port || 10009, + use_https: pm.pax_pw_use_https || false, + amount: amountFloat, + currency_code: this._getIsoCurrencyNumeric(), + order_id: orderId, + transaction_type: pm.pax_pw_transaction_type || "01", + timeout: pm.pax_pw_timeout || 120000, + }, + } + ); + + if (!result) { + this._showError(_t("Network error contacting the Odoo server.")); + line.set_payment_status("retry"); + return false; + } + + return this._handleResult(line, result); + } + + _handleResult(line, result) { + if (!result || result.payment_status === "error") { + this._showError(result?.message || _t("Unknown error from pywebdriver")); + line.set_payment_status("retry"); + return false; + } + + if (result.payment_status === "failure") { + const code = result.response_code || ""; + const msg = result.response_message || _t("Payment declined"); + this._showError( + _t("PAX terminal declined: %(msg)s%(code)s", { + msg, + code: code ? ` (${code})` : "", + }) + ); + line.set_payment_status("retry"); + return false; + } + + // Success + if (result.transaction_id) { + line.transaction_id = result.transaction_id; + } + if (result.auth_code) { + line.card_type = result.auth_code; + } + if (result.card_holder_name) { + line.cardholder_name = result.card_holder_name; + } + + line.set_payment_status("done"); + return true; + } + + /** Map currency name to ISO 4217 numeric code (PAX expects numeric string). */ + _getIsoCurrencyNumeric() { + const map = { + USD: "840", + EUR: "978", + GBP: "826", + VND: "704", + JPY: "392", + CAD: "124", + AUD: "036", + SGD: "702", + THB: "764", + MYR: "458", + PHP: "608", + IDR: "360", + KRW: "410", + CNY: "156", + }; + return map[this.pos.currency.name] || "840"; + } + + _showError(msg, title) { + this.env.services.dialog.add(ConfirmationDialog, { + title: title || _t("PAX Terminal Error"), + body: msg, + confirmLabel: _t("OK"), + confirm: () => { + // Noop + }, + cancel: () => { + // Noop + }, + }); + } +} + +register_payment_method("pax_pywebdriver", PaxPywebdriverPayment); diff --git a/pos_payment_pax_terminal_pywebdriver/static/src/overrides/hardware_proxy/hardware_proxy_keepalive.esm.js b/pos_payment_pax_terminal_pywebdriver/static/src/overrides/hardware_proxy/hardware_proxy_keepalive.esm.js new file mode 100644 index 0000000000..de67996d58 --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/static/src/overrides/hardware_proxy/hardware_proxy_keepalive.esm.js @@ -0,0 +1,20 @@ +import {HardwareProxy} from "@point_of_sale/app/hardware_proxy/hardware_proxy_service"; +import {patch} from "@web/core/utils/patch"; + +patch(HardwareProxy.prototype, { + async keepalive() { + await super.keepalive(); + const hasPax = this.pos.models["pos.payment.method"] + .getAll() + .some( + (pm) => + pm.use_payment_terminal === "pax_pywebdriver" && + pm.pax_pw_terminal_ip + ); + if (hasPax) { + this.message("pax/status", {}).catch(() => { + // Noop — keepalive failure is non-fatal + }); + } + }, +}); diff --git a/pos_payment_pax_terminal_pywebdriver/views/pos_payment_method_views.xml b/pos_payment_pax_terminal_pywebdriver/views/pos_payment_method_views.xml new file mode 100644 index 0000000000..9c852f76aa --- /dev/null +++ b/pos_payment_pax_terminal_pywebdriver/views/pos_payment_method_views.xml @@ -0,0 +1,38 @@ + + + + pos.payment.method.form.pax.pywebdriver + pos.payment.method + + + + + + + + + + + + +