From 193381287a0417c0fe1b4530921de6244c79698f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Alan=20Ramos=20Rodr=C3=ADguez?= Date: Fri, 18 Sep 2026 09:10:48 -0600 Subject: [PATCH 1/3] [ADD] account_early_payment_base: common ground of the early payment discounts settled by a credit note Both account_supplier_early_payment_discount and the new customer side need the same fields on account.move (last day to pay, discount amount, credit note), the same Apply Early Payment button and the business day helpers of res.company. Defined twice they override each other, so they move here and the extending modules override _get_early_payment_values, _early_payment_can_apply and _early_payment_adjust_credit_note for the move types they handle. A credit note flagged is_early_payment_refund is reconciled with its invoice when posted. Co-Authored-By: Claude Fable 5.1 --- account_early_payment_base/README.rst | 98 ++++ account_early_payment_base/__init__.py | 1 + account_early_payment_base/__manifest__.py | 17 + .../i18n/account_early_payment_base.pot | 122 +++++ account_early_payment_base/i18n/es.po | 126 +++++ account_early_payment_base/models/__init__.py | 2 + .../models/account_move.py | 108 +++++ .../models/res_company.py | 38 ++ account_early_payment_base/pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 2 + .../readme/DESCRIPTION.md | 6 + account_early_payment_base/readme/USAGE.md | 6 + .../static/description/index.html | 448 ++++++++++++++++++ account_early_payment_base/tests/__init__.py | 1 + .../tests/test_early_payment_base.py | 66 +++ .../views/account_move_views.xml | 28 ++ 16 files changed, 1072 insertions(+) create mode 100644 account_early_payment_base/README.rst create mode 100644 account_early_payment_base/__init__.py create mode 100644 account_early_payment_base/__manifest__.py create mode 100644 account_early_payment_base/i18n/account_early_payment_base.pot create mode 100644 account_early_payment_base/i18n/es.po create mode 100644 account_early_payment_base/models/__init__.py create mode 100644 account_early_payment_base/models/account_move.py create mode 100644 account_early_payment_base/models/res_company.py create mode 100644 account_early_payment_base/pyproject.toml create mode 100644 account_early_payment_base/readme/CONTRIBUTORS.md create mode 100644 account_early_payment_base/readme/DESCRIPTION.md create mode 100644 account_early_payment_base/readme/USAGE.md create mode 100644 account_early_payment_base/static/description/index.html create mode 100644 account_early_payment_base/tests/__init__.py create mode 100644 account_early_payment_base/tests/test_early_payment_base.py create mode 100644 account_early_payment_base/views/account_move_views.xml diff --git a/account_early_payment_base/README.rst b/account_early_payment_base/README.rst new file mode 100644 index 00000000000..a1750356ce4 --- /dev/null +++ b/account_early_payment_base/README.rst @@ -0,0 +1,98 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=========================== +Early Payment Discount Base +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1137e57a5c49641188846285b624d5ae66c5fcf7b7bba1c944ec056f6846381e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--payment-lightgray.png?logo=github + :target: https://github.com/OCA/account-payment/tree/19.0/account_early_payment_base + :alt: OCA/account-payment +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-payment-19-0/account-payment-19-0-account_early_payment_base + :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/account-payment&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Technical module shared by the early payment discounts settled by a +credit note: ``account_supplier_early_payment_discount`` (vendor bills) +and ``account_customer_early_payment_discount`` (customer invoices). It +holds, on ``account.move``, the last day to pay, the amount of the +discount, the credit note that settles it and the *Apply Early Payment* +button, plus the business day helpers of ``res.company``, so both +modules can be installed together. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Install ``account_supplier_early_payment_discount`` or +``account_customer_early_payment_discount``; this module does nothing on +its own. Extending modules override ``_get_early_payment_values``, +``_early_payment_can_apply`` and ``_early_payment_adjust_credit_note`` +for the move types they handle. A credit note flagged +``is_early_payment_refund`` is reconciled with its invoice when posted. + +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 +------- + +* Jarsa + +Contributors +------------ + +- Jarsa + + - Alan Ramos + +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/account-payment `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_early_payment_base/__init__.py b/account_early_payment_base/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/account_early_payment_base/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_early_payment_base/__manifest__.py b/account_early_payment_base/__manifest__.py new file mode 100644 index 00000000000..cb6c6733711 --- /dev/null +++ b/account_early_payment_base/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2026 Jarsa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Early Payment Discount Base", + "summary": "Common ground of the early payment discounts settled by a credit note", + "version": "19.0.1.0.0", + "category": "Accounting", + "website": "https://github.com/OCA/account-payment", + "author": "Jarsa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "development_status": "Beta", + "depends": ["account", "resource"], + "data": [ + "views/account_move_views.xml", + ], + "installable": True, +} diff --git a/account_early_payment_base/i18n/account_early_payment_base.pot b/account_early_payment_base/i18n/account_early_payment_base.pot new file mode 100644 index 00000000000..fa4d5327509 --- /dev/null +++ b/account_early_payment_base/i18n/account_early_payment_base.pot @@ -0,0 +1,122 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_early_payment_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-18 15:09+0000\n" +"PO-Revision-Date: 2026-09-18 15:09+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: account_early_payment_base +#: model_terms:ir.ui.view,arch_db:account_early_payment_base.view_move_form +msgid "" +"A credit note for the early payment discount will be created and applied to " +"this invoice. Continue?" +msgstr "" + +#. module: account_early_payment_base +#: model_terms:ir.ui.view,arch_db:account_early_payment_base.view_move_form +msgid "Apply Early Payment" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model,name:account_early_payment_base.model_res_company +msgid "Companies" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_credit_note_id +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_credit_note_id +msgid "Credit note that settles the early payment discount of this invoice." +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__display_name +#: model:ir.model.fields,field_description:account_early_payment_base.field_res_company__display_name +msgid "Display Name" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_amount +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_amount +msgid "Early Payment Amount" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_can_apply +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_can_apply +msgid "Early Payment Can Apply" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_credit_note_id +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_credit_note_id +msgid "Early Payment Credit Note" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_date +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_date +msgid "Early Payment Date" +msgstr "" + +#. module: account_early_payment_base +#. odoo-python +#: code:addons/account_early_payment_base/models/account_move.py:0 +msgid "Early payment discount of %s" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__id +#: model:ir.model.fields,field_description:account_early_payment_base.field_res_company__id +msgid "ID" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__is_early_payment_refund +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__is_early_payment_refund +msgid "Is Early Payment Refund" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model,name:account_early_payment_base.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_date +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_date +msgid "Last day to pay to earn the early payment discount." +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_can_apply +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_can_apply +msgid "The discount is earned and its credit note is not issued yet." +msgstr "" + +#. module: account_early_payment_base +#. odoo-python +#: code:addons/account_early_payment_base/models/account_move.py:0 +msgid "The early payment discount of %s cannot be applied." +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__is_early_payment_refund +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__is_early_payment_refund +msgid "This credit note settles an early payment discount." +msgstr "" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_amount +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_amount +msgid "What the early payment discount takes off this invoice." +msgstr "" diff --git a/account_early_payment_base/i18n/es.po b/account_early_payment_base/i18n/es.po new file mode 100644 index 00000000000..347fd243388 --- /dev/null +++ b/account_early_payment_base/i18n/es.po @@ -0,0 +1,126 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_early_payment_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-18 15:09+0000\n" +"PO-Revision-Date: 2026-09-18 15:09+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: account_early_payment_base +#: model_terms:ir.ui.view,arch_db:account_early_payment_base.view_move_form +msgid "" +"A credit note for the early payment discount will be created and applied to " +"this invoice. Continue?" +msgstr "" +"Se creará una nota de crédito por el descuento de pronto pago y se aplicará " +"a esta factura. ¿Continuar?" + +#. module: account_early_payment_base +#: model_terms:ir.ui.view,arch_db:account_early_payment_base.view_move_form +msgid "Apply Early Payment" +msgstr "Aplicar pronto pago" + +#. module: account_early_payment_base +#: model:ir.model,name:account_early_payment_base.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_credit_note_id +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_credit_note_id +msgid "Credit note that settles the early payment discount of this invoice." +msgstr "" +"Nota de crédito que liquida el descuento por pronto pago de esta factura." + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__display_name +#: model:ir.model.fields,field_description:account_early_payment_base.field_res_company__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_amount +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_amount +msgid "Early Payment Amount" +msgstr "Importe del pronto pago" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_can_apply +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_can_apply +msgid "Early Payment Can Apply" +msgstr "Pronto pago aplicable" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_credit_note_id +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_credit_note_id +msgid "Early Payment Credit Note" +msgstr "Nota de crédito de pronto pago" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__early_payment_date +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__early_payment_date +msgid "Early Payment Date" +msgstr "Fecha límite de pronto pago" + +#. module: account_early_payment_base +#. odoo-python +#: code:addons/account_early_payment_base/models/account_move.py:0 +msgid "Early payment discount of %s" +msgstr "Descuento por pronto pago de %s" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__id +#: model:ir.model.fields,field_description:account_early_payment_base.field_res_company__id +msgid "ID" +msgstr "ID" + +#. module: account_early_payment_base +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_bank_statement_line__is_early_payment_refund +#: model:ir.model.fields,field_description:account_early_payment_base.field_account_move__is_early_payment_refund +msgid "Is Early Payment Refund" +msgstr "Es nota de crédito de pronto pago" + +#. module: account_early_payment_base +#: model:ir.model,name:account_early_payment_base.model_account_move +msgid "Journal Entry" +msgstr "Asiento contable" + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_date +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_date +msgid "Last day to pay to earn the early payment discount." +msgstr "Último día para pagar y ganar el descuento por pronto pago." + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_can_apply +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_can_apply +msgid "The discount is earned and its credit note is not issued yet." +msgstr "El descuento ya se ganó y su nota de crédito aún no se emite." + +#. module: account_early_payment_base +#. odoo-python +#: code:addons/account_early_payment_base/models/account_move.py:0 +msgid "The early payment discount of %s cannot be applied." +msgstr "No se puede aplicar el descuento por pronto pago de %s." + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__is_early_payment_refund +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__is_early_payment_refund +msgid "This credit note settles an early payment discount." +msgstr "Esta nota de crédito liquida un descuento por pronto pago." + +#. module: account_early_payment_base +#: model:ir.model.fields,help:account_early_payment_base.field_account_bank_statement_line__early_payment_amount +#: model:ir.model.fields,help:account_early_payment_base.field_account_move__early_payment_amount +msgid "What the early payment discount takes off this invoice." +msgstr "Lo que el descuento por pronto pago resta a esta factura." diff --git a/account_early_payment_base/models/__init__.py b/account_early_payment_base/models/__init__.py new file mode 100644 index 00000000000..3cd5ec9bff4 --- /dev/null +++ b/account_early_payment_base/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_move +from . import res_company diff --git a/account_early_payment_base/models/account_move.py b/account_early_payment_base/models/account_move.py new file mode 100644 index 00000000000..a1da578d923 --- /dev/null +++ b/account_early_payment_base/models/account_move.py @@ -0,0 +1,108 @@ +# Copyright 2026 Jarsa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class AccountMove(models.Model): + _inherit = "account.move" + + early_payment_date = fields.Date( + compute="_compute_early_payment", + store=True, + help="Last day to pay to earn the early payment discount.", + ) + early_payment_amount = fields.Monetary( + compute="_compute_early_payment", + store=True, + help="What the early payment discount takes off this invoice.", + ) + early_payment_can_apply = fields.Boolean( + compute="_compute_early_payment_can_apply", + help="The discount is earned and its credit note is not issued yet.", + ) + early_payment_credit_note_id = fields.Many2one( + "account.move", + readonly=True, + copy=False, + help="Credit note that settles the early payment discount of this invoice.", + ) + is_early_payment_refund = fields.Boolean( + readonly=True, + copy=False, + help="This credit note settles an early payment discount.", + ) + + @api.depends("move_type", "invoice_date", "company_id") + def _compute_early_payment(self): + """Extending modules add their dependencies and compute the values in + `_get_early_payment_values` for the move types they handle.""" + for move in self: + values = move._get_early_payment_values() + move.early_payment_date = values.get("date", False) + move.early_payment_amount = values.get("amount", 0.0) + + @api.depends("early_payment_amount", "early_payment_credit_note_id.state", "state") + def _compute_early_payment_can_apply(self): + for move in self: + move.early_payment_can_apply = move._early_payment_can_apply() + + def _get_early_payment_values(self): + """Date and amount of the discount of this move, empty when none.""" + self.ensure_one() + return {} + + def _early_payment_can_apply(self): + """Whether the discount is earned and still to settle.""" + self.ensure_one() + credit_note = self.early_payment_credit_note_id + return bool( + self.state == "posted" + and self.early_payment_amount > 0 + and (not credit_note or credit_note.state == "cancel") + ) + + def _early_payment_credit_note_values(self): + """Defaults of the credit note that settles the discount.""" + self.ensure_one() + today = fields.Date.context_today(self) + return { + "invoice_date": today, + "date": today, + "ref": self.env._("Early payment discount of %s", self.name), + "invoice_payment_term_id": False, + "is_early_payment_refund": True, + } + + def _early_payment_adjust_credit_note(self, credit_note): + """Turn the full reversal into the discount: extending modules scale + or drop its lines and post it when their flow says so.""" + self.ensure_one() + + def _post(self, soft=True): + posted = super()._post(soft=soft) + for move in posted.filtered("is_early_payment_refund"): + invoice = move.reversed_entry_id + (invoice.line_ids + move.line_ids).filtered( + lambda line: line.account_type + in ("asset_receivable", "liability_payable") + and not line.reconciled + ).reconcile() + return posted + + def action_apply_early_payment(self): + """Settle the early payment discount with a credit note. + + :return: the credit note, adjusted (and posted) by the extending module. + """ + self.ensure_one() + if not self.early_payment_can_apply: + raise UserError( + self.env._( + "The early payment discount of %s cannot be applied.", self.name + ) + ) + credit_note = self._reverse_moves([self._early_payment_credit_note_values()]) + self._early_payment_adjust_credit_note(credit_note) + self.early_payment_credit_note_id = credit_note + return credit_note diff --git a/account_early_payment_base/models/res_company.py b/account_early_payment_base/models/res_company.py new file mode 100644 index 00000000000..289fc8d700c --- /dev/null +++ b/account_early_payment_base/models/res_company.py @@ -0,0 +1,38 @@ +# Copyright 2026 Jarsa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import datetime, time, timedelta + +import pytz + +from odoo import models + + +class ResCompany(models.Model): + _inherit = "res.company" + + def _early_payment_calendar(self): + """Working hours of the company, the ones that tell a business day.""" + self.ensure_one() + return self.resource_calendar_id or self.env["resource.calendar"].search( + [("company_id", "in", [self.id, False])], limit=1 + ) + + def _add_business_days(self, start, days): + """Date `days` business days after `start`. + + A business day is a day the working calendar of the company has hours + on, so its holidays are skipped along with the weekends. `start` + itself never counts as one of the days. + """ + self.ensure_one() + calendar = self._early_payment_calendar() + if not calendar: + return start + timedelta(days=days) + begin = pytz.timezone(calendar.tz).localize( + datetime.combine(start + timedelta(days=1), time.min) + ) + return calendar.plan_days(days, begin, compute_leaves=True).date() + + def _is_business_day(self, date): + self.ensure_one() + return self._add_business_days(date - timedelta(days=1), 1) == date diff --git a/account_early_payment_base/pyproject.toml b/account_early_payment_base/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/account_early_payment_base/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_early_payment_base/readme/CONTRIBUTORS.md b/account_early_payment_base/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..5501ed298f7 --- /dev/null +++ b/account_early_payment_base/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Jarsa + - Alan Ramos \ diff --git a/account_early_payment_base/readme/DESCRIPTION.md b/account_early_payment_base/readme/DESCRIPTION.md new file mode 100644 index 00000000000..46f77950975 --- /dev/null +++ b/account_early_payment_base/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +Technical module shared by the early payment discounts settled by a credit +note: `account_supplier_early_payment_discount` (vendor bills) and +`account_customer_early_payment_discount` (customer invoices). It holds, on +`account.move`, the last day to pay, the amount of the discount, the credit +note that settles it and the *Apply Early Payment* button, plus the business +day helpers of `res.company`, so both modules can be installed together. diff --git a/account_early_payment_base/readme/USAGE.md b/account_early_payment_base/readme/USAGE.md new file mode 100644 index 00000000000..2753ad79457 --- /dev/null +++ b/account_early_payment_base/readme/USAGE.md @@ -0,0 +1,6 @@ +Install `account_supplier_early_payment_discount` or +`account_customer_early_payment_discount`; this module does nothing on its +own. Extending modules override `_get_early_payment_values`, +`_early_payment_can_apply` and `_early_payment_adjust_credit_note` for the +move types they handle. A credit note flagged `is_early_payment_refund` is +reconciled with its invoice when posted. diff --git a/account_early_payment_base/static/description/index.html b/account_early_payment_base/static/description/index.html new file mode 100644 index 00000000000..bf84bb0a5ca --- /dev/null +++ b/account_early_payment_base/static/description/index.html @@ -0,0 +1,448 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Early Payment Discount Base

+ +

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

+

Technical module shared by the early payment discounts settled by a +credit note: account_supplier_early_payment_discount (vendor bills) +and account_customer_early_payment_discount (customer invoices). It +holds, on account.move, the last day to pay, the amount of the +discount, the credit note that settles it and the Apply Early Payment +button, plus the business day helpers of res.company, so both +modules can be installed together.

+

Table of contents

+ +
+

Usage

+

Install account_supplier_early_payment_discount or +account_customer_early_payment_discount; this module does nothing on +its own. Extending modules override _get_early_payment_values, +_early_payment_can_apply and _early_payment_adjust_credit_note +for the move types they handle. A credit note flagged +is_early_payment_refund is reconciled with its invoice when posted.

+
+
+

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

+
    +
  • Jarsa
  • +
+
+
+

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/account-payment project on GitHub.

+

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

+
+
+
+
+ + diff --git a/account_early_payment_base/tests/__init__.py b/account_early_payment_base/tests/__init__.py new file mode 100644 index 00000000000..1ace6600bde --- /dev/null +++ b/account_early_payment_base/tests/__init__.py @@ -0,0 +1 @@ +from . import test_early_payment_base diff --git a/account_early_payment_base/tests/test_early_payment_base.py b/account_early_payment_base/tests/test_early_payment_base.py new file mode 100644 index 00000000000..41b07c427d4 --- /dev/null +++ b/account_early_payment_base/tests/test_early_payment_base.py @@ -0,0 +1,66 @@ +# Copyright 2026 Jarsa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from datetime import date + +from odoo import Command +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestEarlyPaymentBase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.company + cls.company.resource_calendar_id = cls.env["resource.calendar"].create( + {"name": "Monday to Friday", "company_id": cls.company.id} + ) + cls.env["resource.calendar.leaves"].create( + { + "name": "Holiday", + "calendar_id": cls.company.resource_calendar_id.id, + "date_from": "2026-09-16 00:00:00", + "date_to": "2026-09-16 23:59:59", + } + ) + + def test_business_days_skip_weekends_and_holidays(self): + # Friday the 11th: Mon 14, Tue 15, (16 off), Thu 17, Fri 18, Mon 21 ... + self.assertEqual( + self.company._add_business_days(date(2026, 9, 11), 3), date(2026, 9, 17) + ) + self.assertEqual( + self.company._add_business_days(date(2026, 9, 11), 8), date(2026, 9, 24) + ) + self.assertTrue(self.company._is_business_day(date(2026, 9, 17))) + self.assertFalse(self.company._is_business_day(date(2026, 9, 16))) + self.assertFalse(self.company._is_business_day(date(2026, 9, 19))) + + def test_without_calendar_the_days_are_calendar_days(self): + self.company.resource_calendar_id = False + self.env["resource.calendar"].search([]).write( + {"company_id": self.env["res.company"].create({"name": "Other"}).id} + ) + self.assertEqual( + self.company._add_business_days(date(2026, 9, 11), 8), date(2026, 9, 19) + ) + + def test_nothing_applies_on_its_own(self): + invoice = self.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": self.env["res.partner"].create({"name": "Customer"}).id, + "invoice_date": "2026-09-11", + "invoice_line_ids": [ + Command.create({"name": "Line", "quantity": 1, "price_unit": 100.0}) + ], + } + ) + invoice.action_post() + self.assertFalse(invoice.early_payment_date) + self.assertEqual(invoice.early_payment_amount, 0.0) + self.assertFalse(invoice.early_payment_can_apply) + with self.assertRaises(UserError): + invoice.action_apply_early_payment() diff --git a/account_early_payment_base/views/account_move_views.xml b/account_early_payment_base/views/account_move_views.xml new file mode 100644 index 00000000000..b608c133208 --- /dev/null +++ b/account_early_payment_base/views/account_move_views.xml @@ -0,0 +1,28 @@ + + + + account.move.form.inherit.early.payment + account.move + + + +