From 276c70e2c8a3ec4daa9ecea25e7270a4a3955ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abraham=20Jes=C3=BAs?= Date: Tue, 8 Sep 2026 12:26:17 +0200 Subject: [PATCH] [ADD] pos_payment_method_fee: add module --- pos_payment_method_fee/README.rst | 119 +++++ pos_payment_method_fee/__init__.py | 1 + pos_payment_method_fee/__manifest__.py | 22 + pos_payment_method_fee/i18n/es.po | 368 ++++++++++++++ .../i18n/pos_payment_method_fee.pot | 352 +++++++++++++ pos_payment_method_fee/models/__init__.py | 5 + pos_payment_method_fee/models/pos_payment.py | 61 +++ .../models/pos_payment_fee_line.py | 185 +++++++ .../models/pos_payment_method.py | 30 ++ .../models/pos_payment_method_fee.py | 112 +++++ pos_payment_method_fee/models/pos_session.py | 29 ++ pos_payment_method_fee/readme/CONFIGURE.rst | 18 + .../readme/CONTRIBUTORS.rst | 4 + pos_payment_method_fee/readme/DESCRIPTION.rst | 5 + pos_payment_method_fee/readme/USAGE.rst | 6 + .../security/ir.model.access.csv | 5 + .../static/description/index.html | 471 ++++++++++++++++++ pos_payment_method_fee/tests/__init__.py | 5 + pos_payment_method_fee/tests/common.py | 120 +++++ .../tests/test_pos_payment.py | 112 +++++ .../tests/test_pos_payment_fee_line.py | 365 ++++++++++++++ .../tests/test_pos_payment_method.py | 84 ++++ .../tests/test_pos_payment_method_fee.py | 127 +++++ .../tests/test_pos_session.py | 135 +++++ .../views/pos_payment_fee_line_views.xml | 89 ++++ .../views/pos_payment_method_fee_views.xml | 34 ++ .../odoo/addons/pos_payment_method_fee | 1 + setup/pos_payment_method_fee/setup.py | 6 + 28 files changed, 2871 insertions(+) create mode 100644 pos_payment_method_fee/README.rst create mode 100644 pos_payment_method_fee/__init__.py create mode 100644 pos_payment_method_fee/__manifest__.py create mode 100644 pos_payment_method_fee/i18n/es.po create mode 100644 pos_payment_method_fee/i18n/pos_payment_method_fee.pot create mode 100644 pos_payment_method_fee/models/__init__.py create mode 100644 pos_payment_method_fee/models/pos_payment.py create mode 100644 pos_payment_method_fee/models/pos_payment_fee_line.py create mode 100644 pos_payment_method_fee/models/pos_payment_method.py create mode 100644 pos_payment_method_fee/models/pos_payment_method_fee.py create mode 100644 pos_payment_method_fee/models/pos_session.py create mode 100644 pos_payment_method_fee/readme/CONFIGURE.rst create mode 100644 pos_payment_method_fee/readme/CONTRIBUTORS.rst create mode 100644 pos_payment_method_fee/readme/DESCRIPTION.rst create mode 100644 pos_payment_method_fee/readme/USAGE.rst create mode 100644 pos_payment_method_fee/security/ir.model.access.csv create mode 100644 pos_payment_method_fee/static/description/index.html create mode 100644 pos_payment_method_fee/tests/__init__.py create mode 100644 pos_payment_method_fee/tests/common.py create mode 100644 pos_payment_method_fee/tests/test_pos_payment.py create mode 100644 pos_payment_method_fee/tests/test_pos_payment_fee_line.py create mode 100644 pos_payment_method_fee/tests/test_pos_payment_method.py create mode 100644 pos_payment_method_fee/tests/test_pos_payment_method_fee.py create mode 100644 pos_payment_method_fee/tests/test_pos_session.py create mode 100644 pos_payment_method_fee/views/pos_payment_fee_line_views.xml create mode 100644 pos_payment_method_fee/views/pos_payment_method_fee_views.xml create mode 120000 setup/pos_payment_method_fee/odoo/addons/pos_payment_method_fee create mode 100644 setup/pos_payment_method_fee/setup.py diff --git a/pos_payment_method_fee/README.rst b/pos_payment_method_fee/README.rst new file mode 100644 index 0000000000..4cc9adca77 --- /dev/null +++ b/pos_payment_method_fee/README.rst @@ -0,0 +1,119 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +POS Payment Method Fees +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a4d4bff3ef27de510be6cb421b036d7440070332b0b6e91b58d08a5ff925e02f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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/16.0/pos_payment_method_fee + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-16-0/pos-16-0-pos_payment_method_fee + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Models transactional financial fees per POS payment method. +Fees can be defined for each available payment method. +The corresponding fee amount is calculated automatically during order payment. +This module facilitates the management of surcharges associated with credit cards or other payment methods. +The applied fee amounts are posted into accounting. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure payment methods and their fees in Point of Sale: + +1. Go to *Point of Sale -> Configuration -> Payment Methods*. +2. Create a new payment method or open an existing one. +3. Configure the **Fee Grouping Policy**: + + * **Grouped**: Fees are summarized per fee rule into a single journal entry at session close. + * **Detailed**: One journal entry is generated per transaction fee at session close. + +4. Under the **Transaction Fees** section, add one or more fee rules: + + * **Fee Type**: Select *Fixed* or *Percentage*. + * **Amount**: Set the fixed amount or percentage value. + * **Posting Policy**: Select *Immediate* (posted immediately upon payment) or *At Session Close* (posted when POS session is closed). + * Configure the **Fee Account**, **Counterpart Account**, and **Posting Journal**. + +5. Save the payment method. +6. Ensure the payment method is added to the relevant **POS Configuration** (*Point of Sale -> Configuration -> Point of Sale*). + +Usage +===== + +Usage in Point of Sale session: + +1. Open a POS session and select an order. +2. Select a payment method configured with fee rules. +3. The system automatically calculates and registers the corresponding fee line. +4. Validate the payment and close the session according to the configured posting policy. + +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 +~~~~~~~ + +* BINHEX Systems Solutions S.L. + +Contributors +~~~~~~~~~~~~ + +* `BINHEX Systems Solutions `_: + + * Ariel Torres Galvez + * Abraham J. Febres + +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_method_fee/__init__.py b/pos_payment_method_fee/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pos_payment_method_fee/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_payment_method_fee/__manifest__.py b/pos_payment_method_fee/__manifest__.py new file mode 100644 index 0000000000..e06c625c18 --- /dev/null +++ b/pos_payment_method_fee/__manifest__.py @@ -0,0 +1,22 @@ +{ + "name": "POS Payment Method Fees", + "summary": "Models transactional financial fees per POS payment method, " + "with automatic posting and complete traceability.", + "version": "16.0.1.0.0", + "category": "Point of Sale", + "license": "LGPL-3", + "author": "BINHEX Systems Solutions S.L., Odoo Community Association (OCA)", + "website": "https://github.com/OCA/pos", + "depends": [ + "point_of_sale", + "account", + ], + "data": [ + "security/ir.model.access.csv", + "views/pos_payment_method_fee_views.xml", + "views/pos_payment_fee_line_views.xml", + ], + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/pos_payment_method_fee/i18n/es.po b/pos_payment_method_fee/i18n/es.po new file mode 100644 index 0000000000..a84198a141 --- /dev/null +++ b/pos_payment_method_fee/i18n/es.po @@ -0,0 +1,368 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_payment_method_fee +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-08 09:10+0000\n" +"PO-Revision-Date: 2026-09-08 09:10+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_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__counterpart_account_id +msgid "" +"Account credited (or debited on refunds) as counterpart of the fee expense, " +"typically the outstanding/bank account of the payment method." +msgstr "" +"Cuenta abonada (o cargada en devoluciones) como contrapartida del gasto de la comisión, " +"típicamente la cuenta bancaria o de pagos pendientes del método de pago." + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__journal_id +msgid "" +"Accounting journal used to post the journal entries generated by this fee " +"rule." +msgstr "" +"Diario contable utilizado para asentar las anotaciones generadas por esta " +"regla de comisión." + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__active +msgid "Active" +msgstr "Activo" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__amount +msgid "Amount" +msgstr "Importe" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__posting_policy__session_close +msgid "At Session Close" +msgstr "Al cierre de sesión" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__amount_base +msgid "Base Amount" +msgstr "Importe base" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__company_id +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__company_id +msgid "Company" +msgstr "Compañía" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__counterpart_account_id +msgid "Counterpart Account" +msgstr "Cuenta de contrapartida" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__create_uid +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__create_date +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__currency_id +msgid "Currency" +msgstr "Moneda" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method__fee_grouping_policy__detailed +msgid "Detailed" +msgstr "Detallado" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__display_name +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_fee_line__state__draft +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Draft" +msgstr "Borrador" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__account_id +msgid "Expense account used to book this financial cost." +msgstr "Cuenta de gasto utilizada para registrar este coste financiero." + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__account_id +msgid "Fee Account" +msgstr "Cuenta de comisión" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__fee_amount +msgid "Fee Amount" +msgstr "Importe de la comisión" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method__fee_grouping_policy +msgid "Fee Grouping Policy" +msgstr "Política de agrupación de comisiones" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment__fee_line_ids +msgid "Fee Lines" +msgstr "Líneas de comisiones" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__fee_rule_id +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Fee Rule" +msgstr "Regla de comisión" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__fee_type +msgid "Fee Type" +msgstr "Tipo de comisión" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__fee_type__fixed +msgid "Fixed" +msgstr "Fijo" + +#. module: pos_payment_method_fee +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Group By" +msgstr "Agrupar por" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method__fee_grouping_policy__grouped +msgid "Grouped" +msgstr "Agrupado" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__id +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__id +msgid "ID" +msgstr "ID" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__posting_policy__immediate +msgid "Immediate" +msgstr "Inmediata" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__posting_policy +msgid "" +"Immediate: the accounting entry is generated right after the payment is registered.\n" +"At Session Close: the fee is calculated immediately but the accounting entry is generated (and optionally grouped) when the POS session is closed." +msgstr "" +"Inmediata: el asiento contable se genera justo después de registrar el pago.\n" +"Al cierre de sesión: la comisión se calcula inmediatamente pero el asiento contable se genera (y opcionalmente agrupa) al cerrar la sesión TPV." + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__account_move_line_id +msgid "Journal Item" +msgstr "Apunte contable" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line____last_update +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee____last_update +msgid "Last Modified on" +msgstr "Última modificación el" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__write_uid +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__write_date +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__name +msgid "Name" +msgstr "Nombre" + +#. module: pos_payment_method_fee +#: model_terms:ir.actions.act_window,help:pos_payment_method_fee.action_pos_payment_fee_line +msgid "No fee lines registered yet." +msgstr "Aún no hay líneas de comisiones registradas." + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "POS Fee - %s - Payment #%s" +msgstr "Comisión TPV - %s - Pago #%s" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "POS Fees (grouped) - %s - Session %s" +msgstr "Comisiones TPV (agrupadas) - %s - Sesión %s" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment_fee_line +msgid "POS Payment Fee Line" +msgstr "Línea de comisión de pago TPV" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment_method_fee +msgid "POS Payment Method Fee Rule" +msgstr "Regla de comisión de método de pago TPV" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__payment_id +msgid "Payment" +msgstr "Pago" + +#. module: pos_payment_method_fee +#: model:ir.actions.act_window,name:pos_payment_method_fee.action_pos_payment_fee_line +#: model:ir.ui.menu,name:pos_payment_method_fee.menu_pos_payment_fee_line +msgid "Payment Fee Lines" +msgstr "Líneas de comisiones de pago" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__payment_method_id +msgid "Payment Method" +msgstr "Método de pago" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__fee_type__percentage +msgid "Percentage" +msgstr "Porcentaje" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__amount +msgid "" +"Percentage value (e.g. 1.5 for 1.5%%) when Fee Type is 'Percentage', or the " +"fixed amount charged per transaction when Fee Type is 'Fixed'." +msgstr "" +"Valor porcentual (ej. 1,5 para 1,5%%) si el tipo de comisión es 'Porcentaje', o el " +"importe fijo cobrado por transacción si el tipo de comisión es 'Fijo'." + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "Please configure a Posting Journal on fee rule \"%s\"." +msgstr "Configure un diario de contabilización en la regla de comisión \"%s\"." + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "" +"Please configure both the Fee Account and the Counterpart Account on fee " +"rule \"%s\"." +msgstr "" +"Configure tanto la cuenta de comisión como la cuenta de contrapartida en la regla " +"de comisión \"%s\"." + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment_method +msgid "Point of Sale Payment Methods" +msgstr "Métodos de pago en el punto de venta" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment +msgid "Point of Sale Payments" +msgstr "Pagos en el punto de venta" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_session +msgid "Point of Sale Session" +msgstr "Sesión TPV" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_fee_line__state__posted +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Posted" +msgstr "Publicado" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__journal_id +msgid "Posting Journal" +msgstr "Diario de contabilización" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__posting_policy +msgid "Posting Policy" +msgstr "Política de contabilización" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__session_id +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Session" +msgstr "Sesión" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__state +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Status" +msgstr "Estado" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_method_fee.py:0 +#, python-format +msgid "The fixed fee \"%s\" must be a positive amount." +msgstr "La comisión fija \"%s\" debe ser un importe positivo." + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_method_fee.py:0 +#, python-format +msgid "The percentage fee \"%s\" must be between 0 and 100." +msgstr "El porcentaje de comisión \"%s\" debe estar entre 0 y 100." + +#. module: pos_payment_method_fee +#: model_terms:ir.actions.act_window,help:pos_payment_method_fee.action_pos_payment_fee_line +msgid "" +"This list displays full traceability of financial costs (fees)\n" +" calculated per POS payment transaction." +msgstr "" +"Esta lista muestra la trazabilidad completa de los costes financieros (comisiones)\n" +" calculados por transacción de pago TPV." + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment__total_fee_amount +msgid "Total Fees" +msgstr "Total comisiones" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method__fee_ids +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_method_form_inherit_fee +msgid "Transaction Fees" +msgstr "Comisiones por transacción" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method__fee_grouping_policy +msgid "" +"Used only when a fee rule's Posting Policy is 'At Session Close'.\n" +"Grouped: fees are summarized per fee rule into a single journal entry (recommended for high transaction volume).\n" +"Detailed: one journal entry is generated per transaction fee (recommended for audit / fiscal traceability)." +msgstr "" +"Se utiliza solo cuando la política de contabilización de la regla es 'Al cierre de sesión'.\n" +"Agrupado: las comisiones se resumen por regla en un único asiento contable (recomendado para un volumen alto de transacciones).\n" +"Detallado: se genera un asiento contable por cada comisión de transacción (recomendado para trazabilidad fiscal y auditoría)." diff --git a/pos_payment_method_fee/i18n/pos_payment_method_fee.pot b/pos_payment_method_fee/i18n/pos_payment_method_fee.pot new file mode 100644 index 0000000000..8e615a6f2b --- /dev/null +++ b/pos_payment_method_fee/i18n/pos_payment_method_fee.pot @@ -0,0 +1,352 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_payment_method_fee +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-08 09:17+0000\n" +"PO-Revision-Date: 2026-09-08 09:17+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_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__counterpart_account_id +msgid "" +"Account credited (or debited on refunds) as counterpart of the fee expense, " +"typically the outstanding/bank account of the payment method." +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__journal_id +msgid "" +"Accounting journal used to post the journal entries generated by this fee " +"rule." +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__active +msgid "Active" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__amount +msgid "Amount" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__posting_policy__session_close +msgid "At Session Close" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__amount_base +msgid "Base Amount" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__company_id +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__company_id +msgid "Company" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__counterpart_account_id +msgid "Counterpart Account" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__create_uid +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__create_uid +msgid "Created by" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__create_date +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__create_date +msgid "Created on" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__currency_id +msgid "Currency" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method__fee_grouping_policy__detailed +msgid "Detailed" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__display_name +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__display_name +msgid "Display Name" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_fee_line__state__draft +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Draft" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__account_id +msgid "Expense account used to book this financial cost." +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__account_id +msgid "Fee Account" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__fee_amount +msgid "Fee Amount" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method__fee_grouping_policy +msgid "Fee Grouping Policy" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment__fee_line_ids +msgid "Fee Lines" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__fee_rule_id +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Fee Rule" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__fee_type +msgid "Fee Type" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__fee_type__fixed +msgid "Fixed" +msgstr "" + +#. module: pos_payment_method_fee +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Group By" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method__fee_grouping_policy__grouped +msgid "Grouped" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__id +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__id +msgid "ID" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__posting_policy__immediate +msgid "Immediate" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__posting_policy +msgid "" +"Immediate: the accounting entry is generated right after the payment is registered.\n" +"At Session Close: the fee is calculated immediately but the accounting entry is generated (and optionally grouped) when the POS session is closed." +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__account_move_line_id +msgid "Journal Item" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line____last_update +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee____last_update +msgid "Last Modified on" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__write_uid +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__write_date +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__write_date +msgid "Last Updated on" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__name +msgid "Name" +msgstr "" + +#. module: pos_payment_method_fee +#: model_terms:ir.actions.act_window,help:pos_payment_method_fee.action_pos_payment_fee_line +msgid "No fee lines registered yet." +msgstr "" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "POS Fee - %s - Payment #%s" +msgstr "" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "POS Fees (grouped) - %s - Session %s" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment_fee_line +msgid "POS Payment Fee Line" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment_method_fee +msgid "POS Payment Method Fee Rule" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__payment_id +msgid "Payment" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.actions.act_window,name:pos_payment_method_fee.action_pos_payment_fee_line +#: model:ir.ui.menu,name:pos_payment_method_fee.menu_pos_payment_fee_line +msgid "Payment Fee Lines" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__payment_method_id +msgid "Payment Method" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_method_fee__fee_type__percentage +msgid "Percentage" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method_fee__amount +msgid "" +"Percentage value (e.g. 1.5 for 1.5%%) when Fee Type is 'Percentage', or the " +"fixed amount charged per transaction when Fee Type is 'Fixed'." +msgstr "" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "Please configure a Posting Journal on fee rule \"%s\"." +msgstr "" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_fee_line.py:0 +#, python-format +msgid "" +"Please configure both the Fee Account and the Counterpart Account on fee " +"rule \"%s\"." +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment_method +msgid "Point of Sale Payment Methods" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_payment +msgid "Point of Sale Payments" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model,name:pos_payment_method_fee.model_pos_session +msgid "Point of Sale Session" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields.selection,name:pos_payment_method_fee.selection__pos_payment_fee_line__state__posted +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Posted" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__journal_id +msgid "Posting Journal" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__posting_policy +msgid "Posting Policy" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method_fee__sequence +msgid "Sequence" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__session_id +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Session" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_fee_line__state +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_fee_line_search +msgid "Status" +msgstr "" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_method_fee.py:0 +#, python-format +msgid "The fixed fee \"%s\" must be a positive amount." +msgstr "" + +#. module: pos_payment_method_fee +#. odoo-python +#: code:addons/pos_payment_method_fee/models/pos_payment_method_fee.py:0 +#, python-format +msgid "The percentage fee \"%s\" must be between 0 and 100." +msgstr "" + +#. module: pos_payment_method_fee +#: model_terms:ir.actions.act_window,help:pos_payment_method_fee.action_pos_payment_fee_line +msgid "" +"This list displays full traceability of financial costs (fees)\n" +" calculated per POS payment transaction." +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment__total_fee_amount +msgid "Total Fees" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,field_description:pos_payment_method_fee.field_pos_payment_method__fee_ids +#: model_terms:ir.ui.view,arch_db:pos_payment_method_fee.view_pos_payment_method_form_inherit_fee +msgid "Transaction Fees" +msgstr "" + +#. module: pos_payment_method_fee +#: model:ir.model.fields,help:pos_payment_method_fee.field_pos_payment_method__fee_grouping_policy +msgid "" +"Used only when a fee rule's Posting Policy is 'At Session Close'.\n" +"Grouped: fees are summarized per fee rule into a single journal entry (recommended for high transaction volume).\n" +"Detailed: diff --git a/pos_payment_method_fee/models/__init__.py b/pos_payment_method_fee/models/__init__.py new file mode 100644 index 0000000000..4c8292c0d3 --- /dev/null +++ b/pos_payment_method_fee/models/__init__.py @@ -0,0 +1,5 @@ +from . import pos_payment_method_fee +from . import pos_payment_fee_line +from . import pos_payment_method +from . import pos_payment +from . import pos_session diff --git a/pos_payment_method_fee/models/pos_payment.py b/pos_payment_method_fee/models/pos_payment.py new file mode 100644 index 0000000000..d957d9e006 --- /dev/null +++ b/pos_payment_method_fee/models/pos_payment.py @@ -0,0 +1,61 @@ +from odoo import api, fields, models +from odoo.tools import float_is_zero + + +class PosPayment(models.Model): + _inherit = "pos.payment" + + fee_line_ids = fields.One2many( + comodel_name="pos.payment.fee.line", + inverse_name="payment_id", + string="Fee Lines", + ) + + total_fee_amount = fields.Monetary( + string="Total Fees", + compute="_compute_total_fee_amount", + store=True, + ) + + @api.depends("fee_line_ids.fee_amount") + def _compute_total_fee_amount(self): + for payment in self: + payment.total_fee_amount = sum(payment.fee_line_ids.mapped("fee_amount")) + + @api.model_create_multi + def create(self, vals_list): + payments = super().create(vals_list) + payments._create_payment_method_fees() + return payments + + def _create_payment_method_fees(self): + """Fee engine entry point: evaluate every active fee rule + configured on the payment's method and generate the corresponding + ``pos.payment.fee.line`` records. This never touches the ticket, + the order amounts or the taxes: fees act exclusively as an + internal financial cost.""" + FeeLine = self.env["pos.payment.fee.line"] + for payment in self: + payment_method = payment.payment_method_id + if not payment_method or not payment_method.fee_ids: + continue + session = payment.session_id + if not session: + continue + precision = payment.currency_id.decimal_places if payment.currency_id else 2 + for fee_rule in payment_method.fee_ids.filtered(lambda f: f.active): + fee_amount = fee_rule._compute_fee_amount(payment.amount) + if float_is_zero(fee_amount, precision_digits=precision): + continue + fee_line = FeeLine.create( + { + "payment_id": payment.id, + "fee_rule_id": fee_rule.id, + "amount_base": payment.amount, + "fee_amount": fee_amount, + "session_id": session.id, + "state": "draft", + } + ) + if fee_rule.posting_policy == "immediate": + fee_line._post_immediate() diff --git a/pos_payment_method_fee/models/pos_payment_fee_line.py b/pos_payment_method_fee/models/pos_payment_fee_line.py new file mode 100644 index 0000000000..eb35d2040e --- /dev/null +++ b/pos_payment_method_fee/models/pos_payment_fee_line.py @@ -0,0 +1,185 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.fields import Command +from odoo.tools import float_is_zero + + +class PosPaymentFeeLine(models.Model): + _name = "pos.payment.fee.line" + _description = "POS Payment Fee Line" + _order = "id" + + payment_id = fields.Many2one( + comodel_name="pos.payment", + string="Payment", + required=True, + ondelete="cascade", + index=True, + ) + + fee_rule_id = fields.Many2one( + comodel_name="pos.payment.method.fee", + string="Fee Rule", + required=True, + ondelete="restrict", + index=True, + ) + + amount_base = fields.Monetary(required=True) + + fee_amount = fields.Monetary(required=True) + + account_move_line_id = fields.Many2one( + comodel_name="account.move.line", + string="Journal Item", + readonly=True, + copy=False, + ) + + session_id = fields.Many2one( + comodel_name="pos.session", + string="Session", + required=True, + index=True, + ondelete="cascade", + ) + + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("posted", "Posted"), + ], + string="Status", + default="draft", + required=True, + copy=False, + ) + + company_id = fields.Many2one( + related="fee_rule_id.company_id", store=True, readonly=True + ) + + currency_id = fields.Many2one( + related="payment_id.currency_id", store=True, readonly=True + ) + + # ------------------------------------------------------------------ + # Posting engine + # ------------------------------------------------------------------ + @api.model + def _prepare_account_move_vals(self, amount, fee_rule, label): + """Build the ``account.move`` values for a (grouped or single) fee + amount. A negative amount (net refunds) flips debit/credit so the + entry stays balanced and financially correct.""" + company = fee_rule.company_id or self.env.company + journal = fee_rule.journal_id + if not journal: + raise UserError( + _('Please configure a Posting Journal on fee rule "%(name)s".') + % {"name": fee_rule.name} + ) + debit_account = fee_rule.account_id + credit_account = fee_rule.counterpart_account_id + if not debit_account or not credit_account: + raise UserError( + _( + "Please configure both the Fee Account and the " + 'Counterpart Account on fee rule "%(name)s".' + ) + % {"name": fee_rule.name} + ) + if amount < 0: + debit_account, credit_account = credit_account, debit_account + amount = -amount + return { + "journal_id": journal.id, + "company_id": company.id, + "date": fields.Date.today(), + "ref": label, + "line_ids": [ + Command.create( + { + "name": label, + "account_id": debit_account.id, + "debit": amount, + "credit": 0.0, + } + ), + Command.create( + { + "name": label, + "account_id": credit_account.id, + "debit": 0.0, + "credit": amount, + } + ), + ], + } + + @api.model + def _post_fee_amount(self, amount, fee_rule, label): + move = ( + self.env["account.move"] + .sudo() + .create(self._prepare_account_move_vals(amount, fee_rule, label)) + ) + move._post() + return move + + def _post_immediate(self): + """Post a single fee line right away (Immediate posting policy).""" + self.ensure_one() + self.state = "posted" + precision = self.currency_id.decimal_places if self.currency_id else 2 + if not float_is_zero(self.fee_amount, precision_digits=precision): + label = _("POS Fee - %(fee_rule)s - Payment #%(payment)s") % { + "fee_rule": self.fee_rule_id.name, + "payment": self.payment_id.id, + } + move = self._post_fee_amount(self.fee_amount, self.fee_rule_id, label) + move_line = move.line_ids.filtered( + lambda l: l.account_id == self.fee_rule_id.account_id + )[:1] + self.account_move_line_id = move_line.id + + def action_post_session_close_fees(self): + """Entry point called at POS session close for all draft fee lines + whose rule posting_policy is 'session_close'. Splits the recordset + per payment method grouping policy (grouped vs detailed).""" + grouped_lines = self.filtered( + lambda l: l.fee_rule_id.payment_method_id.fee_grouping_policy != "detailed" + ) + detailed_lines = self - grouped_lines + grouped_lines._post_grouped_fees() + detailed_lines._post_detailed_fees() + + def _post_grouped_fees(self): + """Group homogeneous fees (same fee rule) into a single journal + entry, as required for high transaction volume retail scenarios.""" + groups = {} + for line in self: + groups.setdefault(line.fee_rule_id, self.browse()) + groups[line.fee_rule_id] |= line + for fee_rule_id, lines in groups.items(): + currency = lines[:1].currency_id + precision = currency.decimal_places if currency else 2 + total = sum(lines.mapped("fee_amount")) + if float_is_zero(total, precision_digits=precision): + lines.write({"state": "posted"}) + continue + session = lines.session_id[:1] + label = _("POS Fees (grouped) - %(fee_rule)s - Session %(session)s") % { + "fee_rule": fee_rule_id.name, + "session": session.name, + } + move = lines._post_fee_amount(total, fee_rule_id, label) + move_line = move.line_ids.filtered( + lambda l: l.account_id == fee_rule_id.account_id + )[:1] + lines.write({"account_move_line_id": move_line.id, "state": "posted"}) + + def _post_detailed_fees(self): + """Post one journal entry per fee line, preserving full per + transaction traceability (audit friendly).""" + for line in self: + line._post_immediate() diff --git a/pos_payment_method_fee/models/pos_payment_method.py b/pos_payment_method_fee/models/pos_payment_method.py new file mode 100644 index 0000000000..bfa2bfd59b --- /dev/null +++ b/pos_payment_method_fee/models/pos_payment_method.py @@ -0,0 +1,30 @@ +from odoo import fields, models + + +class PosPaymentMethod(models.Model): + _inherit = "pos.payment.method" + + fee_ids = fields.One2many( + comodel_name="pos.payment.method.fee", + inverse_name="payment_method_id", + string="Transaction Fees", + ) + + fee_grouping_policy = fields.Selection( + selection=[ + ("grouped", "Grouped"), + ("detailed", "Detailed"), + ], + default="grouped", + required=True, + help="Used only when a fee rule's Posting Policy is 'At Session " + "Close'.\n" + "Grouped: fees are summarized per fee rule into a single " + "journal entry (recommended for high transaction volume).\n" + "Detailed: one journal entry is generated per transaction " + "fee (recommended for audit / fiscal traceability).", + ) + + def _is_write_forbidden(self, fields): + whitelisted_fields = {"fee_ids", "fee_grouping_policy"} + return super()._is_write_forbidden(fields - whitelisted_fields) diff --git a/pos_payment_method_fee/models/pos_payment_method_fee.py b/pos_payment_method_fee/models/pos_payment_method_fee.py new file mode 100644 index 0000000000..02d7ba31b3 --- /dev/null +++ b/pos_payment_method_fee/models/pos_payment_method_fee.py @@ -0,0 +1,112 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PosPaymentMethodFee(models.Model): + _name = "pos.payment.method.fee" + _description = "POS Payment Method Fee Rule" + _order = "sequence, id" + + name = fields.Char(required=True) + + payment_method_id = fields.Many2one( + comodel_name="pos.payment.method", + string="Payment Method", + required=True, + ondelete="cascade", + index=True, + ) + + fee_type = fields.Selection( + selection=[ + ("percentage", "Percentage"), + ("fixed", "Fixed"), + ], + required=True, + default="percentage", + ) + + amount = fields.Float( + required=True, + digits="Product Price", + help="Percentage value (e.g. 1.5 for 1.5%%) when Fee Type is " + "'Percentage', or the fixed amount charged per transaction " + "when Fee Type is 'Fixed'.", + ) + + account_id = fields.Many2one( + comodel_name="account.account", + string="Fee Account", + required=True, + help="Expense account used to book this financial cost.", + ) + + counterpart_account_id = fields.Many2one( + comodel_name="account.account", + string="Counterpart Account", + required=True, + help="Account credited (or debited on refunds) as counterpart of " + "the fee expense, typically the outstanding/bank account of " + "the payment method.", + ) + + journal_id = fields.Many2one( + comodel_name="account.journal", + string="Posting Journal", + required=True, + help="Accounting journal used to post the journal entries " + "generated by this fee rule.", + ) + + posting_policy = fields.Selection( + selection=[ + ("immediate", "Immediate"), + ("session_close", "At Session Close"), + ], + required=True, + default="session_close", + help="Immediate: the accounting entry is generated right after " + "the payment is registered.\n" + "At Session Close: the fee is calculated immediately but the " + "accounting entry is generated (and optionally grouped) when " + "the POS session is closed.", + ) + + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + required=True, + default=lambda self: self.env.company, + ) + + sequence = fields.Integer(default=10) + + active = fields.Boolean(default=True) + + @api.constrains("fee_type", "amount") + def _check_amount(self): + for rule in self: + if rule.fee_type == "percentage" and not (0 <= rule.amount <= 100): + raise ValidationError( + _('The percentage fee "%(name)s" must be between 0 and 100.') + % {"name": rule.name} + ) + if rule.fee_type == "fixed" and rule.amount < 0: + raise ValidationError( + _('The fixed fee "%(name)s" must be a positive amount.') + % {"name": rule.name} + ) + + def _compute_fee_amount(self, amount_base): + """Calculate the fee amount for a given transaction base amount. + + A negative ``amount_base`` (refund) yields a negative fee amount so + that the financial cost is reversed symmetrically. + """ + self.ensure_one() + if self.fee_type == "percentage": + return amount_base * (self.amount / 100.0) + if self.fee_type == "fixed": + sign = 1.0 if amount_base >= 0 else -1.0 + return sign * abs(self.amount) + return 0.0 diff --git a/pos_payment_method_fee/models/pos_session.py b/pos_payment_method_fee/models/pos_session.py new file mode 100644 index 0000000000..3804d9ec9d --- /dev/null +++ b/pos_payment_method_fee/models/pos_session.py @@ -0,0 +1,29 @@ +from odoo import models + + +class PosSession(models.Model): + _inherit = "pos.session" + + def action_pos_session_closing_control(self, *args, **kwargs): + """Run standard Odoo closing and then processes + (and posts) pending session fees whose posting policy is + 'session_close'. Does not modify any POS closing core logic.""" + res = super().action_pos_session_closing_control(*args, **kwargs) + for session in self: + session._process_pos_payment_method_fees() + return res + + def _process_pos_payment_method_fees(self): + """Batch fee generation: searches draft fee lines for this session + with posting_policy 'session_close' and delegates to the posting + engine (grouped or detailed per payment method).""" + self.ensure_one() + fee_lines = self.env["pos.payment.fee.line"].search( + [ + ("session_id", "=", self.id), + ("state", "=", "draft"), + ("fee_rule_id.posting_policy", "=", "session_close"), + ] + ) + if fee_lines: + fee_lines.action_post_session_close_fees() diff --git a/pos_payment_method_fee/readme/CONFIGURE.rst b/pos_payment_method_fee/readme/CONFIGURE.rst new file mode 100644 index 0000000000..60d8cbffc8 --- /dev/null +++ b/pos_payment_method_fee/readme/CONFIGURE.rst @@ -0,0 +1,18 @@ +To configure payment methods and their fees in Point of Sale: + +1. Go to *Point of Sale -> Configuration -> Payment Methods*. +2. Create a new payment method or open an existing one. +3. Configure the **Fee Grouping Policy**: + + * **Grouped**: Fees are summarized per fee rule into a single journal entry at session close. + * **Detailed**: One journal entry is generated per transaction fee at session close. + +4. Under the **Transaction Fees** section, add one or more fee rules: + + * **Fee Type**: Select *Fixed* or *Percentage*. + * **Amount**: Set the fixed amount or percentage value. + * **Posting Policy**: Select *Immediate* (posted immediately upon payment) or *At Session Close* (posted when POS session is closed). + * Configure the **Fee Account**, **Counterpart Account**, and **Posting Journal**. + +5. Save the payment method. +6. Ensure the payment method is added to the relevant **POS Configuration** (*Point of Sale -> Configuration -> Point of Sale*). diff --git a/pos_payment_method_fee/readme/CONTRIBUTORS.rst b/pos_payment_method_fee/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..67b07b8056 --- /dev/null +++ b/pos_payment_method_fee/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `BINHEX Systems Solutions `_: + + * Ariel Torres Galvez + * Abraham J. Febres diff --git a/pos_payment_method_fee/readme/DESCRIPTION.rst b/pos_payment_method_fee/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..51743f4645 --- /dev/null +++ b/pos_payment_method_fee/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +Models transactional financial fees per POS payment method. +Fees can be defined for each available payment method. +The corresponding fee amount is calculated automatically during order payment. +This module facilitates the management of surcharges associated with credit cards or other payment methods. +The applied fee amounts are posted into accounting. diff --git a/pos_payment_method_fee/readme/USAGE.rst b/pos_payment_method_fee/readme/USAGE.rst new file mode 100644 index 0000000000..9c9f415c94 --- /dev/null +++ b/pos_payment_method_fee/readme/USAGE.rst @@ -0,0 +1,6 @@ +Usage in Point of Sale session: + +1. Open a POS session and select an order. +2. Select a payment method configured with fee rules. +3. The system automatically calculates and registers the corresponding fee line. +4. Validate the payment and close the session according to the configured posting policy. diff --git a/pos_payment_method_fee/security/ir.model.access.csv b/pos_payment_method_fee/security/ir.model.access.csv new file mode 100644 index 0000000000..9cc75cb787 --- /dev/null +++ b/pos_payment_method_fee/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_pos_payment_method_fee_manager,pos.payment.method.fee.manager,model_pos_payment_method_fee,point_of_sale.group_pos_manager,1,1,1,1 +access_pos_payment_method_fee_user,pos.payment.method.fee.user,model_pos_payment_method_fee,point_of_sale.group_pos_user,1,0,0,0 +access_pos_payment_fee_line_manager,pos.payment.fee.line.manager,model_pos_payment_fee_line,point_of_sale.group_pos_manager,1,1,1,1 +access_pos_payment_fee_line_user,pos.payment.fee.line.user,model_pos_payment_fee_line,point_of_sale.group_pos_user,1,0,1,0 diff --git a/pos_payment_method_fee/static/description/index.html b/pos_payment_method_fee/static/description/index.html new file mode 100644 index 0000000000..34c8495282 --- /dev/null +++ b/pos_payment_method_fee/static/description/index.html @@ -0,0 +1,471 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

POS Payment Method Fees

+ +

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

+

Models transactional financial fees per POS payment method. +Fees can be defined for each available payment method. +The corresponding fee amount is calculated automatically during order payment. +This module facilitates the management of surcharges associated with credit cards or other payment methods. +The applied fee amounts are posted into accounting.

+

Table of contents

+ +
+

Configuration

+

To configure payment methods and their fees in Point of Sale:

+
    +
  1. Go to Point of Sale -> Configuration -> Payment Methods.
  2. +
  3. Create a new payment method or open an existing one.
  4. +
  5. Configure the Fee Grouping Policy:
      +
    • Grouped: Fees are summarized per fee rule into a single journal entry at session close.
    • +
    • Detailed: One journal entry is generated per transaction fee at session close.
    • +
    +
  6. +
  7. Under the Transaction Fees section, add one or more fee rules:
      +
    • Fee Type: Select Fixed or Percentage.
    • +
    • Amount: Set the fixed amount or percentage value.
    • +
    • Posting Policy: Select Immediate (posted immediately upon payment) or At Session Close (posted when POS session is closed).
    • +
    • Configure the Fee Account, Counterpart Account, and Posting Journal.
    • +
    +
  8. +
  9. Save the payment method.
  10. +
  11. Ensure the payment method is added to the relevant POS Configuration (Point of Sale -> Configuration -> Point of Sale).
  12. +
+
+
+

Usage

+

Usage in Point of Sale session:

+
    +
  1. Open a POS session and select an order.
  2. +
  3. Select a payment method configured with fee rules.
  4. +
  5. The system automatically calculates and registers the corresponding fee line.
  6. +
  7. Validate the payment and close the session according to the configured posting policy.
  8. +
+
+
+

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

+
    +
  • BINHEX Systems Solutions S.L.
  • +
+
+
+

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_method_fee/tests/__init__.py b/pos_payment_method_fee/tests/__init__.py new file mode 100644 index 0000000000..08c067f205 --- /dev/null +++ b/pos_payment_method_fee/tests/__init__.py @@ -0,0 +1,5 @@ +from . import test_pos_payment_method_fee +from . import test_pos_payment_method +from . import test_pos_payment +from . import test_pos_payment_fee_line +from . import test_pos_session diff --git a/pos_payment_method_fee/tests/common.py b/pos_payment_method_fee/tests/common.py new file mode 100644 index 0000000000..07b720c824 --- /dev/null +++ b/pos_payment_method_fee/tests/common.py @@ -0,0 +1,120 @@ +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +class TestPosPaymentMethodFeeCommon(AccountTestInvoicingCommon): + """Shared fixtures: accounting accounts, journal, test POS payment + methods, and an open POS session for orders and payments without UI + dependency.""" + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.fee_expense_account = cls.company_data["default_account_expense"] + cls.counterpart_account = cls.company_data["default_account_assets"] + cls.misc_journal = cls.company_data["default_journal_misc"] + cls.bank_journal = cls.company_data["default_journal_bank"] + + cls.pos_config = cls.env["pos.config"].create( + { + "name": "Test Shop - Fees", + } + ) + + cls.payment_method = cls.env["pos.payment.method"].create( + { + "name": "Visa Test", + "journal_id": cls.bank_journal.id, + "company_id": cls.env.company.id, + } + ) + + cls.payment_method_b = cls.env["pos.payment.method"].create( + { + "name": "Mastercard Test", + "journal_id": cls.bank_journal.id, + "company_id": cls.env.company.id, + } + ) + + cls.pos_config.write( + { + "payment_method_ids": [ + (4, cls.payment_method.id), + (4, cls.payment_method_b.id), + ], + } + ) + + cls.pos_session = cls.env["pos.session"].create( + { + "config_id": cls.pos_config.id, + } + ) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + def _create_order(self, amount_total=100.0, session=None): + return self.env["pos.order"].create( + { + "session_id": (session or self.pos_session).id, + "amount_total": amount_total, + "amount_tax": 0.0, + "amount_paid": amount_total, + "amount_return": 0.0, + } + ) + + def _create_payment(self, order, amount, payment_method=None): + return self.env["pos.payment"].create( + { + "pos_order_id": order.id, + "payment_method_id": (payment_method or self.payment_method).id, + "amount": amount, + } + ) + + def _create_fee_rule(self, **kwargs): + vals = { + "name": "Test Fee", + "payment_method_id": self.payment_method.id, + "fee_type": "percentage", + "amount": 1.5, + "account_id": self.fee_expense_account.id, + "counterpart_account_id": self.counterpart_account.id, + "journal_id": self.misc_journal.id, + "posting_policy": "session_close", + } + vals.update(kwargs) + return self.env["pos.payment.method.fee"].create(vals) + + def _new_fee_rule(self, **overrides): + # Required fields forbid NULL at DB level, so an in-memory (unsaved) + # record is used to simulate an incomplete/invalid configuration. + vals = { + "name": "Test Fee", + "payment_method_id": self.payment_method.id, + "fee_type": "fixed", + "amount": 1.0, + "account_id": self.fee_expense_account.id, + "counterpart_account_id": self.counterpart_account.id, + "journal_id": self.misc_journal.id, + "company_id": self.env.company.id, + } + vals.update(overrides) + return self.env["pos.payment.method.fee"].new(vals) + + def _create_draft_fee_line(self, payment, fee_rule, fee_amount, amount_base=100.0): + # Bypasses the zero-amount skip in `_create_payment_method_fees` so + # zero/offsetting amounts can be tested directly. + return self.env["pos.payment.fee.line"].create( + { + "payment_id": payment.id, + "fee_rule_id": fee_rule.id, + "amount_base": amount_base, + "fee_amount": fee_amount, + "session_id": self.pos_session.id, + "state": "draft", + } + ) diff --git a/pos_payment_method_fee/tests/test_pos_payment.py b/pos_payment_method_fee/tests/test_pos_payment.py new file mode 100644 index 0000000000..3d6104198a --- /dev/null +++ b/pos_payment_method_fee/tests/test_pos_payment.py @@ -0,0 +1,112 @@ +from odoo.tests import tagged + +from .common import TestPosPaymentMethodFeeCommon + + +@tagged("post_install", "-at_install") +class TestCreatePaymentMethodFees(TestPosPaymentMethodFeeCommon): + """`pos.payment._create_payment_method_fees`: fee line generation + triggered on payment creation, and every guard condition.""" + + def test_payment_method_without_fees_no_lines(self): + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertFalse(payment.fee_line_ids) + + def test_inactive_fee_rule_not_applied(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + fee_rule.active = False + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertFalse(payment.fee_line_ids) + + def test_zero_fee_does_not_create_line(self): + self._create_fee_rule(fee_type="fixed", amount=0.0) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertFalse(payment.fee_line_ids) + + def test_multiple_fees_creates_one_line_per_rule(self): + self._create_fee_rule( + name="Fee A - percentage", fee_type="percentage", amount=1.5 + ) + self._create_fee_rule(name="Fee B - fixed", fee_type="fixed", amount=0.30) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertEqual(len(payment.fee_line_ids), 2) + + def test_multiple_payment_methods_independent_fees(self): + self._create_fee_rule(payment_method_id=self.payment_method.id) + order = self._create_order(100.0) + payment_a = self._create_payment( + order, 100.0, payment_method=self.payment_method + ) + payment_b = self._create_payment( + order, 50.0, payment_method=self.payment_method_b + ) + self.assertTrue(payment_a.fee_line_ids) + self.assertFalse(payment_b.fee_line_ids) + + def test_order_amounts_not_altered_by_fees(self): + """Fees must never alter the receipt, paid amount, or taxes: they act + exclusively as an internal financial cost.""" + self._create_fee_rule(fee_type="percentage", amount=5.0) + order = self._create_order(200.0) + payment = self._create_payment(order, 200.0) + self.assertEqual(order.amount_total, 200.0) + self.assertEqual(order.amount_tax, 0.0) + self.assertEqual(payment.amount, 200.0) + self.assertTrue(payment.fee_line_ids) + + def test_fee_line_fields_are_populated_correctly(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = payment.fee_line_ids + self.assertEqual(fee_line.fee_rule_id, fee_rule) + self.assertEqual(fee_line.amount_base, 100.0) + self.assertAlmostEqual(fee_line.fee_amount, 1.0, places=2) + self.assertEqual(fee_line.session_id, self.pos_session) + self.assertEqual(fee_line.state, "draft") + + def test_immediate_policy_posts_fee_line_right_away(self): + self._create_fee_rule(posting_policy="immediate", fee_type="fixed", amount=1.0) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertEqual(payment.fee_line_ids.state, "posted") + + def test_session_close_policy_leaves_fee_line_draft(self): + self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertEqual(payment.fee_line_ids.state, "draft") + + +@tagged("post_install", "-at_install") +class TestTotalFeeAmountCompute(TestPosPaymentMethodFeeCommon): + """`pos.payment._compute_total_fee_amount`.""" + + def test_total_fee_amount_sums_all_fee_lines(self): + self._create_fee_rule( + name="Fee A - percentage", fee_type="percentage", amount=1.5 + ) + self._create_fee_rule(name="Fee B - fixed", fee_type="fixed", amount=0.30) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertAlmostEqual(payment.total_fee_amount, 1.80, places=2) + + def test_total_fee_amount_zero_without_fee_lines(self): + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertEqual(payment.total_fee_amount, 0.0) + + def test_total_fee_amount_recomputes_when_fee_line_added(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertAlmostEqual(payment.total_fee_amount, 1.0, places=2) + + self._create_draft_fee_line(payment, fee_rule, 2.0) + self.assertAlmostEqual(payment.total_fee_amount, 3.0, places=2) diff --git a/pos_payment_method_fee/tests/test_pos_payment_fee_line.py b/pos_payment_method_fee/tests/test_pos_payment_fee_line.py new file mode 100644 index 0000000000..9f93730fe3 --- /dev/null +++ b/pos_payment_method_fee/tests/test_pos_payment_fee_line.py @@ -0,0 +1,365 @@ +from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import tagged + +from .common import TestPosPaymentMethodFeeCommon + + +@tagged("post_install", "-at_install") +class TestPrepareAccountMoveVals(TestPosPaymentMethodFeeCommon): + """Unit tests for `pos.payment.fee.line._prepare_account_move_vals`.""" + + def setUp(self): + super().setUp() + self.fee_line_model = self.env["pos.payment.fee.line"] + + def test_positive_amount_debits_fee_account(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + vals = self.fee_line_model._prepare_account_move_vals( + 5.0, fee_rule, "Test Label" + ) + self.assertEqual(vals["journal_id"], self.misc_journal.id) + self.assertEqual(vals["company_id"], fee_rule.company_id.id) + self.assertEqual(vals["ref"], "Test Label") + self.assertEqual(vals["date"], fields.Date.today()) + + debit_line, credit_line = vals["line_ids"][0][2], vals["line_ids"][1][2] + self.assertEqual(debit_line["account_id"], self.fee_expense_account.id) + self.assertEqual(debit_line["debit"], 5.0) + self.assertEqual(debit_line["credit"], 0.0) + self.assertEqual(credit_line["account_id"], self.counterpart_account.id) + self.assertEqual(credit_line["debit"], 0.0) + self.assertEqual(credit_line["credit"], 5.0) + + def test_negative_amount_flips_debit_credit(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + vals = self.fee_line_model._prepare_account_move_vals( + -5.0, fee_rule, "Refund Label" + ) + debit_line, credit_line = vals["line_ids"][0][2], vals["line_ids"][1][2] + + # Amount is stored positive, but accounts are swapped. + self.assertEqual(debit_line["account_id"], self.counterpart_account.id) + self.assertEqual(debit_line["debit"], 5.0) + self.assertEqual(credit_line["account_id"], self.fee_expense_account.id) + self.assertEqual(credit_line["credit"], 5.0) + + def test_company_id_falls_back_to_env_company(self): + fee_rule = self._new_fee_rule(company_id=False) + vals = self.fee_line_model._prepare_account_move_vals(5.0, fee_rule, "Label") + self.assertEqual(vals["company_id"], self.env.company.id) + + def test_missing_journal_raises_user_error(self): + fee_rule = self._new_fee_rule(journal_id=False) + with self.assertRaisesRegex(UserError, "Posting Journal"): + self.fee_line_model._prepare_account_move_vals(5.0, fee_rule, "Label") + + def test_missing_fee_account_raises_user_error(self): + fee_rule = self._new_fee_rule(account_id=False) + with self.assertRaisesRegex(UserError, "Fee Account"): + self.fee_line_model._prepare_account_move_vals(5.0, fee_rule, "Label") + + def test_missing_counterpart_account_raises_user_error(self): + fee_rule = self._new_fee_rule(counterpart_account_id=False) + with self.assertRaisesRegex(UserError, "Counterpart Account"): + self.fee_line_model._prepare_account_move_vals(5.0, fee_rule, "Label") + + def test_missing_both_fee_and_counterpart_accounts_raises_user_error(self): + fee_rule = self._new_fee_rule(account_id=False, counterpart_account_id=False) + with self.assertRaisesRegex(UserError, "Fee Account"): + self.fee_line_model._prepare_account_move_vals(5.0, fee_rule, "Label") + + +@tagged("post_install", "-at_install") +class TestPostFeeAmount(TestPosPaymentMethodFeeCommon): + """`pos.payment.fee.line._post_fee_amount`.""" + + def test_creates_posted_move(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + move = self.env["pos.payment.fee.line"]._post_fee_amount( + 3.0, fee_rule, "Direct Label" + ) + self.assertEqual(move.state, "posted") + self.assertEqual(move.ref, "Direct Label") + fee_move_line = move.line_ids.filtered( + lambda l: l.account_id == self.fee_expense_account + ) + self.assertAlmostEqual(fee_move_line.debit, 3.0, places=2) + + def test_propagates_configuration_errors(self): + fee_rule = self._new_fee_rule(journal_id=False) + with self.assertRaises(UserError): + self.env["pos.payment.fee.line"]._post_fee_amount(3.0, fee_rule, "Label") + + +@tagged("post_install", "-at_install") +class TestPostImmediate(TestPosPaymentMethodFeeCommon): + """`pos.payment.fee.line._post_immediate`.""" + + def test_immediate_posting_creates_move(self): + self._create_fee_rule(posting_policy="immediate", fee_type="fixed", amount=1.0) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = payment.fee_line_ids + self.assertEqual(len(fee_line), 1) + self.assertEqual(fee_line.state, "posted") + self.assertTrue(fee_line.account_move_line_id) + + move = fee_line.account_move_line_id.move_id + self.assertEqual(move.state, "posted") + self.assertAlmostEqual(sum(move.line_ids.mapped("debit")), 1.0, places=2) + self.assertAlmostEqual(sum(move.line_ids.mapped("credit")), 1.0, places=2) + + def test_sets_matching_move_line(self): + fee_rule = self._create_fee_rule( + posting_policy="immediate", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = self._create_draft_fee_line(payment, fee_rule, 1.0) + + fee_line._post_immediate() + + self.assertEqual(fee_line.state, "posted") + self.assertTrue(fee_line.account_move_line_id) + self.assertEqual( + fee_line.account_move_line_id.account_id, self.fee_expense_account + ) + + def test_zero_amount_skips_move_creation(self): + fee_rule = self._create_fee_rule( + posting_policy="immediate", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = self._create_draft_fee_line(payment, fee_rule, 0.0) + + fee_line._post_immediate() + + self.assertEqual(fee_line.state, "posted") + self.assertFalse(fee_line.account_move_line_id) + + def test_refund_flips_debit_credit(self): + self._create_fee_rule( + posting_policy="immediate", fee_type="percentage", amount=2.0 + ) + order = self._create_order(-100.0) + payment = self._create_payment(order, -100.0) + fee_line = payment.fee_line_ids + self.assertAlmostEqual(fee_line.fee_amount, -2.0, places=2) + + move = fee_line.account_move_line_id.move_id + fee_account_line = move.line_ids.filtered( + lambda l: l.account_id == self.fee_expense_account + ) + # On a refund, the fee is reversed: the fee account is credited + # instead of debited. + self.assertAlmostEqual(fee_account_line.credit, 2.0, places=2) + self.assertAlmostEqual(fee_account_line.debit, 0.0, places=2) + + +@tagged("post_install", "-at_install") +class TestActionPostSessionCloseFees(TestPosPaymentMethodFeeCommon): + """`action_post_session_close_fees`: dispatch to grouped/detailed.""" + + def test_mixed_policies_routed_to_correct_posting_method(self): + """A grouped payment method and a detailed one, each with a + session_close fee, must be routed to their own posting method.""" + fee_rule_grouped = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + fee_rule_detailed = self._create_fee_rule( + name="Detailed Fee", + payment_method_id=self.payment_method_b.id, + posting_policy="session_close", + fee_type="fixed", + amount=1.0, + ) + self.payment_method.fee_grouping_policy = "grouped" + self.payment_method_b.fee_grouping_policy = "detailed" + + order1 = self._create_order(100.0) + order2 = self._create_order(50.0) + payment1 = self._create_payment(order1, 100.0) + payment2 = self._create_payment( + order2, 50.0, payment_method=self.payment_method + ) + payment3 = self._create_payment( + order1, 100.0, payment_method=self.payment_method_b + ) + payment4 = self._create_payment( + order2, 50.0, payment_method=self.payment_method_b + ) + + fee_lines = ( + payment1.fee_line_ids + | payment2.fee_line_ids + | payment3.fee_line_ids + | payment4.fee_line_ids + ) + self.assertEqual(len(fee_lines), 4) + + fee_lines.action_post_session_close_fees() + self.assertTrue(all(line.state == "posted" for line in fee_lines)) + + grouped_moves = fee_lines.filtered( + lambda l: l.fee_rule_id == fee_rule_grouped + ).mapped("account_move_line_id.move_id") + detailed_moves = fee_lines.filtered( + lambda l: l.fee_rule_id == fee_rule_detailed + ).mapped("account_move_line_id.move_id") + self.assertEqual( + len(grouped_moves), 1, "Grouped payment method must yield a single move" + ) + self.assertEqual( + len(detailed_moves), + 2, + "Detailed payment method must yield one move per line", + ) + + def test_all_grouped_lines_routed_to_grouped_posting(self): + fee_rule = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + self.payment_method.fee_grouping_policy = "grouped" + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + line_a = self._create_draft_fee_line(payment, fee_rule, 1.0) + line_b = self._create_draft_fee_line(payment, fee_rule, 1.0) + + (line_a | line_b).action_post_session_close_fees() + + moves = (line_a | line_b).mapped("account_move_line_id.move_id") + self.assertEqual(len(moves), 1) + + def test_all_detailed_lines_routed_to_detailed_posting(self): + fee_rule = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + self.payment_method.fee_grouping_policy = "detailed" + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + line_a = self._create_draft_fee_line(payment, fee_rule, 1.0) + line_b = self._create_draft_fee_line(payment, fee_rule, 1.0) + + (line_a | line_b).action_post_session_close_fees() + + moves = (line_a | line_b).mapped("account_move_line_id.move_id") + self.assertEqual(len(moves), 2) + + +@tagged("post_install", "-at_install") +class TestPostGroupedFees(TestPosPaymentMethodFeeCommon): + """`pos.payment.fee.line._post_grouped_fees`.""" + + def test_groups_same_fee_rule_into_a_single_move(self): + self.payment_method.fee_grouping_policy = "grouped" + + order1 = self._create_order(100.0) + order2 = self._create_order(50.0) + payment1 = self._create_payment(order1, 100.0) + payment2 = self._create_payment(order2, 50.0) + + self.pos_session._process_pos_payment_method_fees() + + fee_lines = payment1.fee_line_ids | payment2.fee_line_ids + self.assertTrue(all(line.state == "posted" for line in fee_lines)) + + moves = fee_lines.mapped("account_move_line_id.move_id") + self.assertEqual( + len(moves), 1, "Grouped policy must create a single journal entry" + ) + self.assertAlmostEqual(sum(moves.line_ids.mapped("debit")), 2.0, places=2) + + def test_different_fee_rules_are_grouped_independently(self): + """Two different fee rules on the same payment method must generate + two independent grouped journal entries.""" + self._create_fee_rule( + name="Fee Percentage", + posting_policy="session_close", + fee_type="percentage", + amount=1.0, + ) + self._create_fee_rule( + name="Fee Fixed", + posting_policy="session_close", + fee_type="fixed", + amount=0.5, + ) + self.payment_method.fee_grouping_policy = "grouped" + + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + self.assertEqual(len(payment.fee_line_ids), 2) + + self.pos_session._process_pos_payment_method_fees() + + moves = payment.fee_line_ids.mapped("account_move_line_id.move_id") + self.assertEqual(len(moves), 2) + + def test_zero_total_skips_move_creation(self): + fee_rule = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + line_a = self._create_draft_fee_line(payment, fee_rule, 2.0) + line_b = self._create_draft_fee_line(payment, fee_rule, -2.0) + + (line_a | line_b)._post_grouped_fees() + + self.assertTrue(all(line.state == "posted" for line in (line_a, line_b))) + self.assertFalse(line_a.account_move_line_id) + self.assertFalse(line_b.account_move_line_id) + + def test_sets_move_line_and_state_on_all_grouped_lines(self): + fee_rule = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + line_a = self._create_draft_fee_line(payment, fee_rule, 1.0) + line_b = self._create_draft_fee_line(payment, fee_rule, 1.0) + + (line_a | line_b)._post_grouped_fees() + + self.assertTrue(line_a.account_move_line_id) + self.assertEqual(line_a.account_move_line_id, line_b.account_move_line_id) + + +@tagged("post_install", "-at_install") +class TestPostDetailedFees(TestPosPaymentMethodFeeCommon): + """`pos.payment.fee.line._post_detailed_fees`.""" + + def test_posts_one_move_per_line(self): + self.payment_method.fee_grouping_policy = "detailed" + + order1 = self._create_order(100.0) + order2 = self._create_order(50.0) + payment1 = self._create_payment(order1, 100.0) + payment2 = self._create_payment(order2, 50.0) + + self.pos_session._process_pos_payment_method_fees() + + fee_lines = payment1.fee_line_ids | payment2.fee_line_ids + self.assertTrue(all(line.state == "posted" for line in fee_lines)) + + moves = fee_lines.mapped("account_move_line_id.move_id") + self.assertEqual( + len(moves), 2, "Detailed policy must create one entry per fee line" + ) + + def test_direct_call_posts_each_line_independently(self): + fee_rule = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + line_a = self._create_draft_fee_line(payment, fee_rule, 1.0) + line_b = self._create_draft_fee_line(payment, fee_rule, 2.0) + + (line_a | line_b)._post_detailed_fees() + + self.assertNotEqual( + line_a.account_move_line_id.move_id, line_b.account_move_line_id.move_id + ) diff --git a/pos_payment_method_fee/tests/test_pos_payment_method.py b/pos_payment_method_fee/tests/test_pos_payment_method.py new file mode 100644 index 0000000000..8e93d84345 --- /dev/null +++ b/pos_payment_method_fee/tests/test_pos_payment_method.py @@ -0,0 +1,84 @@ +from odoo.exceptions import UserError +from odoo.tests import tagged + +from .common import TestPosPaymentMethodFeeCommon + + +@tagged("post_install", "-at_install") +class TestPosPaymentMethodFeeGrouping(TestPosPaymentMethodFeeCommon): + """`fee_grouping_policy` field on `pos.payment.method`.""" + + def test_default_grouping_policy(self): + method = self.env["pos.payment.method"].create( + { + "name": "Cash Test", + "journal_id": self.bank_journal.id, + } + ) + self.assertEqual(method.fee_grouping_policy, "grouped") + + def test_grouping_policy_can_be_set_to_detailed(self): + method = self.env["pos.payment.method"].create( + { + "name": "Cash Test", + "journal_id": self.bank_journal.id, + "fee_grouping_policy": "detailed", + } + ) + self.assertEqual(method.fee_grouping_policy, "detailed") + + def test_fee_ids_relation(self): + fee_rule = self._create_fee_rule() + self.assertIn(fee_rule, self.payment_method.fee_ids) + + +@tagged("post_install", "-at_install") +class TestPosPaymentMethodWriteForbidden(TestPosPaymentMethodFeeCommon): + """`_is_write_forbidden` override: fee fields stay editable even while + the payment method has open POS sessions, everything else stays + protected.""" + + def test_open_session_is_detected_on_payment_method(self): + # Sanity check for the scenario under test: common setUp links + # `payment_method` to an open `pos_session`. + self.assertIn(self.pos_session, self.payment_method.open_session_ids) + + def test_writing_fee_grouping_policy_allowed_with_open_session(self): + self.payment_method.write({"fee_grouping_policy": "detailed"}) + self.assertEqual(self.payment_method.fee_grouping_policy, "detailed") + + def test_writing_fee_ids_allowed_with_open_session(self): + self.payment_method.write( + { + "fee_ids": [ + ( + 0, + 0, + { + "name": "New Fee", + "fee_type": "fixed", + "amount": 1.0, + "account_id": self.fee_expense_account.id, + "counterpart_account_id": self.counterpart_account.id, + "journal_id": self.misc_journal.id, + }, + ) + ] + } + ) + self.assertTrue(self.payment_method.fee_ids) + + def test_writing_other_fields_still_forbidden_with_open_session(self): + with self.assertRaises(UserError): + self.payment_method.write({"name": "Renamed Visa"}) + + def test_writing_other_fields_allowed_without_open_session(self): + method = self.env["pos.payment.method"].create( + { + "name": "Unlinked Method", + "journal_id": self.bank_journal.id, + } + ) + self.assertFalse(method.open_session_ids) + method.write({"name": "Renamed Unlinked"}) + self.assertEqual(method.name, "Renamed Unlinked") diff --git a/pos_payment_method_fee/tests/test_pos_payment_method_fee.py b/pos_payment_method_fee/tests/test_pos_payment_method_fee.py new file mode 100644 index 0000000000..d12c2b81de --- /dev/null +++ b/pos_payment_method_fee/tests/test_pos_payment_method_fee.py @@ -0,0 +1,127 @@ +from odoo.exceptions import ValidationError +from odoo.tests import tagged + +from .common import TestPosPaymentMethodFeeCommon + + +@tagged("post_install", "-at_install") +class TestFeeRuleDefaults(TestPosPaymentMethodFeeCommon): + """Field defaults on `pos.payment.method.fee`.""" + + def test_company_id_defaults_to_current_company(self): + fee_rule = self._create_fee_rule() + self.assertEqual(fee_rule.company_id, self.env.company) + + def test_active_defaults_to_true(self): + fee_rule = self._create_fee_rule() + self.assertTrue(fee_rule.active) + + def test_sequence_defaults_to_ten(self): + fee_rule = self._create_fee_rule() + self.assertEqual(fee_rule.sequence, 10) + + def test_posting_policy_defaults_to_session_close(self): + fee_rule = self.env["pos.payment.method.fee"].create( + { + "name": "Default Policy Fee", + "payment_method_id": self.payment_method.id, + "fee_type": "fixed", + "amount": 1.0, + "account_id": self.fee_expense_account.id, + "counterpart_account_id": self.counterpart_account.id, + "journal_id": self.misc_journal.id, + } + ) + self.assertEqual(fee_rule.posting_policy, "session_close") + + def test_fee_type_defaults_to_percentage(self): + fee_rule = self.env["pos.payment.method.fee"].create( + { + "name": "Default Type Fee", + "payment_method_id": self.payment_method.id, + "amount": 1.0, + "account_id": self.fee_expense_account.id, + "counterpart_account_id": self.counterpart_account.id, + "journal_id": self.misc_journal.id, + } + ) + self.assertEqual(fee_rule.fee_type, "percentage") + + def test_fee_ids_relation(self): + fee_rule = self._create_fee_rule() + self.assertIn(fee_rule, self.payment_method.fee_ids) + + +@tagged("post_install", "-at_install") +class TestFeeRuleConstraints(TestPosPaymentMethodFeeCommon): + """`_check_amount` validation: every branch and boundary.""" + + def test_percentage_above_100_raises(self): + with self.assertRaisesRegex(ValidationError, "between 0 and 100"): + self._create_fee_rule(fee_type="percentage", amount=150.0) + + def test_percentage_below_0_raises(self): + with self.assertRaisesRegex(ValidationError, "between 0 and 100"): + self._create_fee_rule(fee_type="percentage", amount=-1.0) + + def test_percentage_lower_boundary_is_valid(self): + fee_rule = self._create_fee_rule(fee_type="percentage", amount=0.0) + self.assertEqual(fee_rule.amount, 0.0) + + def test_percentage_upper_boundary_is_valid(self): + fee_rule = self._create_fee_rule(fee_type="percentage", amount=100.0) + self.assertEqual(fee_rule.amount, 100.0) + + def test_fixed_negative_raises(self): + with self.assertRaisesRegex(ValidationError, "positive amount"): + self._create_fee_rule(fee_type="fixed", amount=-1.0) + + def test_fixed_zero_is_valid(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=0.0) + self.assertEqual(fee_rule.amount, 0.0) + + def test_fixed_positive_is_valid(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=3.5) + self.assertEqual(fee_rule.amount, 3.5) + + def test_constraint_is_revalidated_on_write(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=1.0) + with self.assertRaises(ValidationError): + fee_rule.write({"amount": -5.0}) + + def test_changing_fee_type_revalidates_amount(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=150.0) + with self.assertRaises(ValidationError): + fee_rule.write({"fee_type": "percentage"}) + + +@tagged("post_install", "-at_install") +class TestComputeFeeAmount(TestPosPaymentMethodFeeCommon): + """`_compute_fee_amount`: percentage, fixed, refund sign and unknown + fee type fallback.""" + + def test_percentage_fee_calculation(self): + fee_rule = self._create_fee_rule(fee_type="percentage", amount=2.0) + self.assertAlmostEqual(fee_rule._compute_fee_amount(150.0), 3.0, places=2) + + def test_fixed_fee_calculation(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=0.5) + self.assertAlmostEqual(fee_rule._compute_fee_amount(150.0), 0.5, places=2) + + def test_fixed_fee_sign_on_refund(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=0.5) + self.assertAlmostEqual(fee_rule._compute_fee_amount(-50.0), -0.5, places=2) + + def test_percentage_fee_sign_on_refund(self): + fee_rule = self._create_fee_rule(fee_type="percentage", amount=2.0) + self.assertAlmostEqual(fee_rule._compute_fee_amount(-50.0), -1.0, places=2) + + def test_fixed_fee_zero_base_amount_keeps_positive_sign(self): + fee_rule = self._create_fee_rule(fee_type="fixed", amount=0.5) + self.assertAlmostEqual(fee_rule._compute_fee_amount(0.0), 0.5, places=2) + + def test_unknown_fee_type_returns_zero(self): + # `fee_type` is a Selection, but `.new()` skips selection validation + # so the fallback branch can be exercised directly. + fee_rule = self._new_fee_rule(fee_type="unknown") + self.assertEqual(fee_rule._compute_fee_amount(100.0), 0.0) diff --git a/pos_payment_method_fee/tests/test_pos_session.py b/pos_payment_method_fee/tests/test_pos_session.py new file mode 100644 index 0000000000..5c1d07f54b --- /dev/null +++ b/pos_payment_method_fee/tests/test_pos_session.py @@ -0,0 +1,135 @@ +from unittest.mock import patch + +from odoo.tests import tagged + +from .common import TestPosPaymentMethodFeeCommon + + +@tagged("post_install", "-at_install") +class TestProcessPosPaymentMethodFees(TestPosPaymentMethodFeeCommon): + """`pos.session._process_pos_payment_method_fees`: scoping and + filtering of draft session_close fee lines.""" + + def test_processes_only_draft_session_close_lines(self): + fee_rule_session_close = self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + fee_rule_immediate = self._create_fee_rule( + name="Immediate Fee", + posting_policy="immediate", + fee_type="fixed", + amount=1.0, + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + + draft_line = payment.fee_line_ids.filtered( + lambda l: l.fee_rule_id == fee_rule_session_close + ) + immediate_line = payment.fee_line_ids.filtered( + lambda l: l.fee_rule_id == fee_rule_immediate + ) + self.assertEqual(draft_line.state, "draft") + self.assertEqual(immediate_line.state, "posted") + + self.pos_session._process_pos_payment_method_fees() + + self.assertEqual(draft_line.state, "posted") + self.assertTrue(draft_line.account_move_line_id) + # Already-posted immediate line must be left untouched. + self.assertTrue(immediate_line.account_move_line_id) + + def test_does_not_process_lines_from_another_session(self): + self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + other_config = self.env["pos.config"].create({"name": "Other Shop"}) + other_session = self.env["pos.session"].create({"config_id": other_config.id}) + + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = payment.fee_line_ids + fee_line.session_id = other_session.id + + self.pos_session._process_pos_payment_method_fees() + + self.assertEqual( + fee_line.state, + "draft", + "Fee lines belonging to another session must not be processed", + ) + + def test_does_not_reprocess_already_posted_lines(self): + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = payment.fee_line_ids + self.pos_session._process_pos_payment_method_fees() + move_line = fee_line.account_move_line_id + self.assertTrue(move_line) + + # A second call must be a no-op: no new move for the same line. + self.pos_session._process_pos_payment_method_fees() + self.assertEqual(fee_line.account_move_line_id, move_line) + + def test_noop_when_no_draft_fee_lines(self): + # No fee rules configured: must not raise despite no draft lines. + self.pos_session._process_pos_payment_method_fees() + + def test_ensure_one_is_enforced(self): + other_config = self.env["pos.config"].create({"name": "Other Shop 2"}) + other_session = self.env["pos.session"].create({"config_id": other_config.id}) + with self.assertRaises(ValueError): + (self.pos_session | other_session)._process_pos_payment_method_fees() + + +@tagged("post_install", "-at_install") +class TestActionPosSessionClosingControl(TestPosPaymentMethodFeeCommon): + """`pos.session.action_pos_session_closing_control`: delegation to the + fee posting engine after the core POS closing logic runs.""" + + @patch( + "odoo.addons.point_of_sale.models.pos_session.PosSession" + ".action_pos_session_closing_control" + ) + def test_closing_control_delegates_to_fee_processing(self, super_mock): + super_mock.return_value = "super result" + self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + order = self._create_order(100.0) + payment = self._create_payment(order, 100.0) + fee_line = payment.fee_line_ids + self.assertEqual(fee_line.state, "draft") + + result = self.pos_session.action_pos_session_closing_control() + + super_mock.assert_called_once() + self.assertEqual(result, "super result") + self.assertEqual(fee_line.state, "posted") + self.assertTrue(fee_line.account_move_line_id) + + @patch( + "odoo.addons.point_of_sale.models.pos_session.PosSession" + ".action_pos_session_closing_control" + ) + def test_closing_control_processes_every_session_in_the_recordset(self, super_mock): + super_mock.return_value = True + self._create_fee_rule( + posting_policy="session_close", fee_type="fixed", amount=1.0 + ) + other_config = self.env["pos.config"].create({"name": "Other Shop 3"}) + other_session = self.env["pos.session"].create({"config_id": other_config.id}) + + order1 = self._create_order(100.0, session=self.pos_session) + payment1 = self._create_payment(order1, 100.0) + order2 = self._create_order(50.0, session=self.pos_session) + payment2 = self._create_payment(order2, 50.0) + # Attach the second payment's fee line to the other session to + # simulate an independent draft fee pending on it. + payment2.fee_line_ids.session_id = other_session.id + + sessions = self.pos_session | other_session + sessions.action_pos_session_closing_control() + + self.assertEqual(payment1.fee_line_ids.state, "posted") + self.assertEqual(payment2.fee_line_ids.state, "posted") diff --git a/pos_payment_method_fee/views/pos_payment_fee_line_views.xml b/pos_payment_method_fee/views/pos_payment_fee_line_views.xml new file mode 100644 index 0000000000..7e24696039 --- /dev/null +++ b/pos_payment_method_fee/views/pos_payment_fee_line_views.xml @@ -0,0 +1,89 @@ + + + + + pos.payment.fee.line.tree + pos.payment.fee.line + + + + + + + + + + + + + + + + pos.payment.fee.line.search + pos.payment.fee.line + + + + + + + + + + + + + + + + + + Payment Fee Lines + pos.payment.fee.line + tree + + +

+ No fee lines registered yet. +

+

+ This list displays full traceability of financial costs (fees) + calculated per POS payment transaction. +

+
+
+ + + +
diff --git a/pos_payment_method_fee/views/pos_payment_method_fee_views.xml b/pos_payment_method_fee/views/pos_payment_method_fee_views.xml new file mode 100644 index 0000000000..6d797777ec --- /dev/null +++ b/pos_payment_method_fee/views/pos_payment_method_fee_views.xml @@ -0,0 +1,34 @@ + + + + + pos.payment.method.form.inherit.fee + pos.payment.method + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/pos_payment_method_fee/odoo/addons/pos_payment_method_fee b/setup/pos_payment_method_fee/odoo/addons/pos_payment_method_fee new file mode 120000 index 0000000000..f475244586 --- /dev/null +++ b/setup/pos_payment_method_fee/odoo/addons/pos_payment_method_fee @@ -0,0 +1 @@ +../../../../pos_payment_method_fee \ No newline at end of file diff --git a/setup/pos_payment_method_fee/setup.py b/setup/pos_payment_method_fee/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pos_payment_method_fee/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)