diff --git a/sale_advance_payment/README.rst b/sale_advance_payment/README.rst index 48e2c05a1b0..557f666d9f4 100644 --- a/sale_advance_payment/README.rst +++ b/sale_advance_payment/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ==================== Sale Advance Payment ==================== @@ -17,7 +13,7 @@ Sale Advance Payment .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github @@ -35,6 +31,12 @@ Sale Advance Payment The module allows to add advance payments on sales and then use them on invoices. +Additionally, it provides an optional feature to handle advance payments +that exceed the order amount. This is particularly useful for e-commerce +scenarios where tax calculations may differ between the external store +and Odoo. When enabled, overpayments are partially reconciled, leaving +the excess amount as customer credit. + **Table of contents** .. contents:: @@ -45,14 +47,39 @@ Usage To use this module, you need to: -- Go to a sale order. -- Click on "Pay Sale Advance". -- Select the Journal and specify the amount of the advanced payment. -- "Make Advance Payment". +- Go to a sale order. +- Click on "Pay Sale Advance". +- Select the Journal and specify the amount of the advanced payment. +- "Make Advance Payment". When generating the invoice, the system displays the advanced payments, select those you want to add to the invoice. +**Handling Overpayments:** + +By default, advance payments that exceed the invoice amount will be +rejected. To enable partial reconciliation of overpayments: + +1. Go to *Settings > General Settings* +2. Scroll to the *Accounting* section +3. Check *Allow Advance Payments Exceeding Order Amount* +4. Save the settings + +When enabled, advance payments larger than the order amount will be: + +- Partially reconciled up to the invoice amount +- The excess amount remains as customer credit +- Useful for e-commerce integrations with tax calculation differences + +**Example Scenarios:** + +- **E-commerce Integration**: Customer pays $120 but Odoo calculates + $100 due to tax differences +- **Prepayments**: Customer pays deposit that exceeds final invoice + amount +- **Currency Fluctuations**: Payment made in different currency with + rate variations + Known issues / Roadmap ====================== @@ -81,11 +108,11 @@ Authors Contributors ------------ -- Omar Castiñeira Saaevdra -- Daniel Reis -- Nikul Chaudhary -- Manuel Regidor -- Urvisha Desai +- Omar Castiñeira Saaevdra +- Daniel Reis +- Nikul Chaudhary +- Manuel Regidor +- Urvisha Desai Maintainers ----------- diff --git a/sale_advance_payment/__manifest__.py b/sale_advance_payment/__manifest__.py index 5ab56b1b287..9c82fbf557d 100644 --- a/sale_advance_payment/__manifest__.py +++ b/sale_advance_payment/__manifest__.py @@ -14,6 +14,7 @@ "wizard/sale_advance_payment_wzd_view.xml", "views/sale_view.xml", "views/account_payment.xml", + "views/res_config_settings.xml", "security/ir.model.access.csv", ], "installable": True, diff --git a/sale_advance_payment/models/__init__.py b/sale_advance_payment/models/__init__.py index fad33e7989b..7e6c7b09f6f 100644 --- a/sale_advance_payment/models/__init__.py +++ b/sale_advance_payment/models/__init__.py @@ -1,3 +1,5 @@ from . import payment from . import sale from . import account_move +from . import res_config_settings +from . import res_company diff --git a/sale_advance_payment/models/account_move.py b/sale_advance_payment/models/account_move.py index a638f4d8e0c..2a23a7c0ca8 100644 --- a/sale_advance_payment/models/account_move.py +++ b/sale_advance_payment/models/account_move.py @@ -7,15 +7,19 @@ class AccountMove(models.Model): _inherit = "account.move" - def action_post(self): + def _post(self, soft=True): # Automatic reconciliation of payment when invoice confirmed. - res = super().action_post() - sale_order = self.mapped("line_ids.sale_line_ids.order_id") - if sale_order and self.invoice_outstanding_credits_debits_widget is not False: - json_invoice_outstanding_data = ( - self.invoice_outstanding_credits_debits_widget.get("content", []) + res = super()._post(soft=soft) + for invoice in self: + # Get Advance Payment Account Moves + sale_orders = invoice.mapped("line_ids.sale_line_ids.order_id") + advance_payment_moves = sale_orders.account_payment_ids.move_id + # Get reconcilable payments JSON data + widget_json = invoice.invoice_outstanding_credits_debits_widget or {} + can_reconcile_lines = filter( + lambda x: x.get("move_id") in advance_payment_moves.ids, + widget_json.get("content", []), ) - for data in json_invoice_outstanding_data: - if data.get("move_id") in sale_order.account_payment_ids.move_id.ids: - self.js_assign_outstanding_line(line_id=data.get("id")) + for line in can_reconcile_lines: + invoice.js_assign_outstanding_line(line_id=line.get("id")) return res diff --git a/sale_advance_payment/models/res_company.py b/sale_advance_payment/models/res_company.py new file mode 100644 index 00000000000..5aead9bf667 --- /dev/null +++ b/sale_advance_payment/models/res_company.py @@ -0,0 +1,15 @@ +# Copyright 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + allow_advance_overpayment = fields.Boolean( + string="Allow Advance Payments Exceeding Order Amount", + help="If checked, advance payments larger than the order amount will be " + "allowed. Useful for e-commerce scenarios where tax calculations may " + "differ between the store and Odoo.", + ) diff --git a/sale_advance_payment/models/res_config_settings.py b/sale_advance_payment/models/res_config_settings.py new file mode 100644 index 00000000000..2fced40375b --- /dev/null +++ b/sale_advance_payment/models/res_config_settings.py @@ -0,0 +1,17 @@ +# Copyright 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + allow_advance_overpayment = fields.Boolean( + string="Allow Advance Payments Exceeding Order Amount", + help="If checked, advance payments larger than the order amount will be " + "allowed. Useful for e-commerce scenarios where tax calculations may " + "differ between the store and Odoo.", + related="company_id.allow_advance_overpayment", + readonly=False, + ) diff --git a/sale_advance_payment/readme/DESCRIPTION.md b/sale_advance_payment/readme/DESCRIPTION.md index 02808e96b21..337ac881cf0 100644 --- a/sale_advance_payment/readme/DESCRIPTION.md +++ b/sale_advance_payment/readme/DESCRIPTION.md @@ -1,2 +1,8 @@ The module allows to add advance payments on sales and then use them on invoices. + +Additionally, it provides an optional feature to handle advance payments +that exceed the order amount. This is particularly useful for e-commerce +scenarios where tax calculations may differ between the external store +and Odoo. When enabled, overpayments are partially reconciled, leaving +the excess amount as customer credit. diff --git a/sale_advance_payment/readme/USAGE.md b/sale_advance_payment/readme/USAGE.md index 2a4b64781f7..046018f9da3 100644 --- a/sale_advance_payment/readme/USAGE.md +++ b/sale_advance_payment/readme/USAGE.md @@ -7,3 +7,24 @@ To use this module, you need to: When generating the invoice, the system displays the advanced payments, select those you want to add to the invoice. + +**Handling Overpayments:** + +By default, advance payments that exceed the invoice amount will be rejected. +To enable partial reconciliation of overpayments: + +1. Go to *Settings > General Settings* +2. Scroll to the *Accounting* section +3. Check *Allow Advance Payments Exceeding Order Amount* +4. Save the settings + +When enabled, advance payments larger than the order amount will be: +- Partially reconciled up to the invoice amount +- The excess amount remains as customer credit +- Useful for e-commerce integrations with tax calculation differences + +**Example Scenarios:** + +- **E-commerce Integration**: Customer pays $120 but Odoo calculates $100 due to tax differences +- **Prepayments**: Customer pays deposit that exceeds final invoice amount +- **Currency Fluctuations**: Payment made in different currency with rate variations diff --git a/sale_advance_payment/static/description/index.html b/sale_advance_payment/static/description/index.html index 5c7ad2cfa0e..160946fc5e0 100644 --- a/sale_advance_payment/static/description/index.html +++ b/sale_advance_payment/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Sale Advance Payment -
+
+

Sale Advance Payment

- - -Odoo Community Association - -
-

Sale Advance Payment

-

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

The module allows to add advance payments on sales and then use them on invoices.

+

Additionally, it provides an optional feature to handle advance payments +that exceed the order amount. This is particularly useful for e-commerce +scenarios where tax calculations may differ between the external store +and Odoo. When enabled, overpayments are partially reconciled, leaving +the excess amount as customer credit.

Table of contents

    @@ -392,7 +392,7 @@

    Sale Advance Payment

-

Usage

+

Usage

To use this module, you need to:

  • Go to a sale order.
  • @@ -402,15 +402,39 @@

    Usage

When generating the invoice, the system displays the advanced payments, select those you want to add to the invoice.

+

Handling Overpayments:

+

By default, advance payments that exceed the invoice amount will be +rejected. To enable partial reconciliation of overpayments:

+
    +
  1. Go to Settings > General Settings
  2. +
  3. Scroll to the Accounting section
  4. +
  5. Check Allow Advance Payments Exceeding Order Amount
  6. +
  7. Save the settings
  8. +
+

When enabled, advance payments larger than the order amount will be:

+
    +
  • Partially reconciled up to the invoice amount
  • +
  • The excess amount remains as customer credit
  • +
  • Useful for e-commerce integrations with tax calculation differences
  • +
+

Example Scenarios:

+
    +
  • E-commerce Integration: Customer pays $120 but Odoo calculates +$100 due to tax differences
  • +
  • Prepayments: Customer pays deposit that exceeds final invoice +amount
  • +
  • Currency Fluctuations: Payment made in different currency with +rate variations
  • +
-

Known issues / Roadmap

+

Known issues / Roadmap

Split several computed values in separate fields (mls, advance_amount, amount_residual). This allows a better comprehension of logic, and a better inheritance possibility.

-

Bug Tracker

+

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 @@ -418,15 +442,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Comunitea
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -449,6 +473,5 @@

Maintainers

-
diff --git a/sale_advance_payment/tests/__init__.py b/sale_advance_payment/tests/__init__.py index 1d4adafb684..dde24a222d0 100644 --- a/sale_advance_payment/tests/__init__.py +++ b/sale_advance_payment/tests/__init__.py @@ -1 +1,2 @@ from . import test_sale_advance_payment +from . import test_advance_overpayment diff --git a/sale_advance_payment/tests/test_advance_overpayment.py b/sale_advance_payment/tests/test_advance_overpayment.py new file mode 100644 index 00000000000..e6e7505677e --- /dev/null +++ b/sale_advance_payment/tests/test_advance_overpayment.py @@ -0,0 +1,118 @@ +# Copyright 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class TestAdvanceOverpayment(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Create test data + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Customer", + } + ) + + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "type": "service", + } + ) + + # Create sale order + cls.sale_order = cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": cls.product.id, + "product_uom_qty": 1, + "price_unit": 100.0, + }, + ), + ], + } + ) + + # Confirm sale order + cls.sale_order.action_confirm() + + # Get a suitable journal + cls.journal = cls.env["account.journal"].search( + [ + ("type", "in", ("bank", "cash")), + ("company_id", "=", cls.sale_order.company_id.id), + ], + limit=1, + ) + + def _create_payment_wizard(self, amount_advance): + """Helper method to create advance payment wizard""" + return ( + self.env["account.voucher.wizard"] + .with_context( + active_id=self.sale_order.id, + active_ids=[self.sale_order.id], + ) + .create( + { + "journal_id": self.journal.id, + "amount_advance": amount_advance, + } + ) + ) + + def test_advance_overpayment_disabled_by_default(self): + """Test that overpayment is rejected by default in wizard""" + # Should raise validation error when overpayment is disabled (default) + # The validation happens during wizard creation due to @api.constrains + with self.assertRaises(ValidationError) as context: + self._create_payment_wizard(150.0) # More than order total + self.assertIn("greater than residual amount", str(context.exception)) + + def test_advance_overpayment_enabled(self): + """Test that overpayment is allowed when company setting is enabled""" + # Enable overpayment handling on company + self.sale_order.company_id.allow_advance_overpayment = True + + # Create advance payment wizard with amount larger than order + # Should not raise validation error when overpayment is enabled + try: + payment_wizard = self._create_payment_wizard(150.0) # More than order total + except ValidationError: + self.fail("raised errorwhen overpayment is enabled") + + # Create the payment + payment_wizard.make_advance_payment() + + # Verify payment was created + self.assertTrue(self.sale_order.account_payment_ids) + payment = self.sale_order.account_payment_ids[0] + self.assertEqual(payment.amount, 150.0) + + # Create invoice from order + invoice = self.sale_order._create_invoices() + invoice.action_post() + + # Verify invoice is fully paid + self.assertEqual(invoice.payment_state, "paid") + + # Verify there's remaining credit on the payment + receivable_line = payment.move_id.line_ids.filtered( + lambda x: x.account_id.account_type == "asset_receivable" + )[:1] + self.assertTrue(receivable_line) + self.assertNotEqual( + receivable_line.amount_residual_currency + if receivable_line.currency_id + else receivable_line.amount_residual, + 0, + ) diff --git a/sale_advance_payment/views/res_config_settings.xml b/sale_advance_payment/views/res_config_settings.xml new file mode 100644 index 00000000000..480492b2ab9 --- /dev/null +++ b/sale_advance_payment/views/res_config_settings.xml @@ -0,0 +1,29 @@ + + + + res.config.settings.view.form.inherit.sale.advance.payment + res.config.settings + + + + + + + + + + + + diff --git a/sale_advance_payment/wizard/sale_advance_payment_wzd.py b/sale_advance_payment/wizard/sale_advance_payment_wzd.py index 7177b672863..b60f1a3a1c1 100644 --- a/sale_advance_payment/wizard/sale_advance_payment_wzd.py +++ b/sale_advance_payment/wizard/sale_advance_payment_wzd.py @@ -56,7 +56,8 @@ def _compute_get_journal_currency(self): def check_amount(self): if self.amount_advance <= 0: raise exceptions.ValidationError(_("Amount of advance must be positive.")) - if self.env.context.get("active_id", False): + allow_overpayment = self.order_id.company_id.allow_advance_overpayment + if self.env.context.get("active_id") and not allow_overpayment: if self.payment_type == "inbound": if ( float_compare( diff --git a/sale_order_secondary_unit/views/sale_order_views.xml b/sale_order_secondary_unit/views/sale_order_views.xml index 2f8f3d4b562..5fb2082b081 100644 --- a/sale_order_secondary_unit/views/sale_order_views.xml +++ b/sale_order_secondary_unit/views/sale_order_views.xml @@ -42,6 +42,7 @@ name="secondary_uom_qty" column_invisible="parent.state in ('done', 'cancel')" groups="uom.group_uom" + optional="show" />