diff --git a/purchase_uninvoiced_amount_force_invoiced_line/README.rst b/purchase_uninvoiced_amount_force_invoiced_line/README.rst new file mode 100644 index 00000000000..5e5a67d756e --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/README.rst @@ -0,0 +1,95 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================================== +Purchase Uninvoiced Amount Force Invoiced Line +============================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:17be8e577d852d28c6c34a63337c78ee25165180efd64934ae199ddf171b67e1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fpurchase--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/purchase-workflow/tree/19.0/purchase_uninvoiced_amount_force_invoiced_line + :alt: OCA/purchase-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/purchase-workflow-19-0/purchase-workflow-19-0-purchase_uninvoiced_amount_force_invoiced_line + :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/purchase-workflow&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Glue module that sets uninvoiced amount to 0 when force invoiced is +enabled on purchase order lines. + +**Table of contents** + +.. contents:: + :local: + +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 +------- + +* ForgeFlow + +Contributors +------------ + +- `ForgeFlow `__: + + - Joan Sisquella + +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-JoanSForgeFlow| image:: https://github.com/JoanSForgeFlow.png?size=40px + :target: https://github.com/JoanSForgeFlow + :alt: JoanSForgeFlow + +Current `maintainer `__: + +|maintainer-JoanSForgeFlow| + +This module is part of the `OCA/purchase-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_uninvoiced_amount_force_invoiced_line/__init__.py b/purchase_uninvoiced_amount_force_invoiced_line/__init__.py new file mode 100644 index 00000000000..6f937c0c0cd --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/purchase_uninvoiced_amount_force_invoiced_line/__manifest__.py b/purchase_uninvoiced_amount_force_invoiced_line/__manifest__.py new file mode 100644 index 00000000000..e724455182b --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Purchase Uninvoiced Amount Force Invoiced Line", + "version": "19.0.1.0.0", + "category": "Purchases", + "license": "AGPL-3", + "summary": "Glue module between uninvoiced amount line and force invoiced line", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "maintainers": ["JoanSForgeFlow"], + "website": "https://github.com/OCA/purchase-workflow", + "depends": [ + "purchase_order_uninvoiced_amount_line", + "purchase_invoice_status_line", + ], + "data": [], + "installable": True, +} diff --git a/purchase_uninvoiced_amount_force_invoiced_line/i18n/it.po b/purchase_uninvoiced_amount_force_invoiced_line/i18n/it.po new file mode 100644 index 00000000000..fa2b0947a64 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/i18n/it.po @@ -0,0 +1,22 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_uninvoiced_amount_force_invoiced_line +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-09-17 15:42+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: purchase_uninvoiced_amount_force_invoiced_line +#: model:ir.model,name:purchase_uninvoiced_amount_force_invoiced_line.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "Riga ordine di acquisto" diff --git a/purchase_uninvoiced_amount_force_invoiced_line/i18n/purchase_uninvoiced_amount_force_invoiced_line.pot b/purchase_uninvoiced_amount_force_invoiced_line/i18n/purchase_uninvoiced_amount_force_invoiced_line.pot new file mode 100644 index 00000000000..7040dae0d41 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/i18n/purchase_uninvoiced_amount_force_invoiced_line.pot @@ -0,0 +1,19 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_uninvoiced_amount_force_invoiced_line +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_uninvoiced_amount_force_invoiced_line +#: model:ir.model,name:purchase_uninvoiced_amount_force_invoiced_line.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "" diff --git a/purchase_uninvoiced_amount_force_invoiced_line/models/__init__.py b/purchase_uninvoiced_amount_force_invoiced_line/models/__init__.py new file mode 100644 index 00000000000..d70b8833432 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import purchase_order_line diff --git a/purchase_uninvoiced_amount_force_invoiced_line/models/purchase_order_line.py b/purchase_uninvoiced_amount_force_invoiced_line/models/purchase_order_line.py new file mode 100644 index 00000000000..b4dcfec3e75 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/models/purchase_order_line.py @@ -0,0 +1,34 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + @api.depends( + "product_qty", + "qty_received", + "qty_invoiced", + "price_subtotal", + "force_invoiced", + "invoice_status", + ) + def _compute_amount_uninvoiced(self): + super()._compute_amount_uninvoiced() + for line in self: + if line.force_invoiced or line.invoice_status == "invoiced": + line.amount_uninvoiced = 0.0 + else: + # Ensure invoiced + uninvoiced = ordered + # (ignore qty_received to avoid "limbo" amounts) + qty = line.product_qty - line.qty_invoiced + price_unit = ( + line.price_subtotal / line.product_qty + if line.product_qty + else line.price_unit + ) + amount_uninvoiced = max(0, qty * price_unit) + line.amount_uninvoiced = line.currency_id.round(amount_uninvoiced) + return True diff --git a/purchase_uninvoiced_amount_force_invoiced_line/pyproject.toml b/purchase_uninvoiced_amount_force_invoiced_line/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/purchase_uninvoiced_amount_force_invoiced_line/readme/CONTRIBUTORS.md b/purchase_uninvoiced_amount_force_invoiced_line/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..0060b85dbf9 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [ForgeFlow](https://www.forgeflow.com): + - Joan Sisquella +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/purchase_uninvoiced_amount_force_invoiced_line/readme/DESCRIPTION.md b/purchase_uninvoiced_amount_force_invoiced_line/readme/DESCRIPTION.md new file mode 100644 index 00000000000..dff481299e1 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Glue module that sets uninvoiced amount to 0 when force invoiced is +enabled on purchase order lines. diff --git a/purchase_uninvoiced_amount_force_invoiced_line/static/description/icon.png b/purchase_uninvoiced_amount_force_invoiced_line/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/purchase_uninvoiced_amount_force_invoiced_line/static/description/icon.png differ diff --git a/purchase_uninvoiced_amount_force_invoiced_line/static/description/index.html b/purchase_uninvoiced_amount_force_invoiced_line/static/description/index.html new file mode 100644 index 00000000000..c1cd78e6e3f --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Purchase Uninvoiced Amount Force Invoiced Line

+ +

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

+

Glue module that sets uninvoiced amount to 0 when force invoiced is +enabled on purchase order lines.

+

Table of contents

+ +
+

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

+
    +
  • ForgeFlow
  • +
+
+
+

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.

+

Current maintainer:

+

JoanSForgeFlow

+

This module is part of the OCA/purchase-workflow project on GitHub.

+

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

+
+
+
+
+ + diff --git a/purchase_uninvoiced_amount_force_invoiced_line/tests/__init__.py b/purchase_uninvoiced_amount_force_invoiced_line/tests/__init__.py new file mode 100644 index 00000000000..1e694dfd845 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_purchase_uninvoiced_amount_force_invoiced_line diff --git a/purchase_uninvoiced_amount_force_invoiced_line/tests/test_purchase_uninvoiced_amount_force_invoiced_line.py b/purchase_uninvoiced_amount_force_invoiced_line/tests/test_purchase_uninvoiced_amount_force_invoiced_line.py new file mode 100644 index 00000000000..a913d526396 --- /dev/null +++ b/purchase_uninvoiced_amount_force_invoiced_line/tests/test_purchase_uninvoiced_amount_force_invoiced_line.py @@ -0,0 +1,164 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import Command +from odoo.tests import Form + +from odoo.addons.base.tests.common import BaseCommon + + +class TestPurchaseUninvoicedAmountForceInvoicedLine(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product_1 = cls.env["product.product"].create( + {"name": "Product 1", "type": "consu", "purchase_method": "receive"} + ) + cls.product_2 = cls.env["product.product"].create( + {"name": "Product 2", "type": "consu", "purchase_method": "purchase"} + ) + + def _create_purchase_order(self, lines_data): + purchase_form = Form(self.env["purchase.order"]) + purchase_form.partner_id = self.partner + for line_data in lines_data: + with purchase_form.order_line.new() as line_form: + line_form.product_id = line_data["product"] + line_form.product_qty = line_data["qty"] + line_form.price_unit = line_data["price"] + purchase = purchase_form.save() + purchase.button_confirm() + for i, line_data in enumerate(lines_data): + if "received" in line_data: + purchase.order_line[i].qty_received = line_data["received"] + return purchase + + def test_force_invoiced_false_normal_calculation(self): + purchase = self._create_purchase_order( + [{"product": self.product_1, "qty": 10, "received": 8, "price": 50.0}] + ) + line = purchase.order_line[0] + line.force_invoiced = False + self.assertEqual(line.amount_uninvoiced, 500.0) + + def test_force_invoiced_true_zero_amount(self): + purchase = self._create_purchase_order( + [{"product": self.product_1, "qty": 10, "received": 8, "price": 50.0}] + ) + line = purchase.order_line[0] + line.force_invoiced = True + self.assertEqual(line.amount_uninvoiced, 0.0) + + def test_force_invoiced_with_partial_invoice(self): + purchase = self._create_purchase_order( + [{"product": self.product_1, "qty": 10, "received": 10, "price": 100.0}] + ) + line = purchase.order_line[0] + invoice = self.env["account.move"].create( + { + "move_type": "in_invoice", + "partner_id": self.partner.id, + "purchase_id": purchase.id, + "invoice_date": "2025-01-01", + "invoice_line_ids": [ + Command.create( + { + "product_id": line.product_id.id, + "quantity": 5, + "price_unit": 100.0, + "purchase_line_id": line.id, + }, + ) + ], + } + ) + invoice.action_post() + line.force_invoiced = False + self.assertEqual(line.amount_uninvoiced, 500.0) + line.force_invoiced = True + self.assertEqual(line.amount_uninvoiced, 0.0) + + def test_multiple_lines_mixed_force_invoiced(self): + purchase = self._create_purchase_order( + [ + {"product": self.product_1, "qty": 5, "price": 80.0}, + {"product": self.product_2, "qty": 3, "price": 120.0}, + ] + ) + line1, line2 = purchase.order_line + line1.force_invoiced = False + line2.force_invoiced = True + self.assertEqual(line1.amount_uninvoiced, 400.0) + self.assertEqual(line2.amount_uninvoiced, 0.0) + + def test_purchase_policy_with_force_invoiced(self): + purchase = self._create_purchase_order( + [{"product": self.product_2, "qty": 8, "price": 75.0}] + ) + line = purchase.order_line[0] + line.force_invoiced = False + self.assertEqual(line.amount_uninvoiced, 600.0) + line.force_invoiced = True + self.assertEqual(line.amount_uninvoiced, 0.0) + + def test_force_invoiced_toggle(self): + purchase = self._create_purchase_order( + [{"product": self.product_1, "qty": 6, "price": 90.0}] + ) + line = purchase.order_line[0] + line.force_invoiced = False + self.assertEqual(line.amount_uninvoiced, 540.0) + line.force_invoiced = True + self.assertEqual(line.amount_uninvoiced, 0.0) + line.force_invoiced = False + self.assertEqual(line.amount_uninvoiced, 540.0) + + def test_receive_policy_ignores_qty_received_until_invoiced(self): + purchase = self._create_purchase_order( + [{"product": self.product_1, "qty": 10, "received": 0, "price": 50.0}] + ) + line = purchase.order_line[0] + self.assertEqual(line.amount_uninvoiced, 500.0) + line.qty_received = 8 + self.assertEqual(line.amount_uninvoiced, 500.0) + inv_partial = self.env["account.move"].create( + { + "move_type": "in_invoice", + "partner_id": self.partner.id, + "purchase_id": purchase.id, + "invoice_date": "2025-01-02", + "invoice_line_ids": [ + Command.create( + { + "product_id": line.product_id.id, + "quantity": 3, + "price_unit": 50.0, + "purchase_line_id": line.id, + }, + ) + ], + } + ) + inv_partial.action_post() + self.assertEqual(line.amount_uninvoiced, 350.0) + line.qty_received = 10 + self.assertEqual(line.amount_uninvoiced, 350.0) + inv_rest = self.env["account.move"].create( + { + "move_type": "in_invoice", + "partner_id": self.partner.id, + "purchase_id": purchase.id, + "invoice_date": "2025-01-03", + "invoice_line_ids": [ + Command.create( + { + "product_id": line.product_id.id, + "quantity": 7, + "price_unit": 50.0, + "purchase_line_id": line.id, + }, + ) + ], + } + ) + inv_rest.action_post() + self.assertEqual(line.amount_uninvoiced, 0.0) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000000..811189d76f6 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +odoo-addon-purchase_order_uninvoiced_amount_line @ git+https://github.com/OCA/purchase-workflow.git@refs/pull/2998/head#subdirectory=purchase_order_uninvoiced_amount_line +odoo-addon-purchase_invoice_status_line @ git+https://github.com/OCA/purchase-workflow.git@refs/pull/3036/head#subdirectory=purchase_invoice_status_line +odoo-addon-purchase_order_line_menu @ git+https://github.com/OCA/purchase-workflow.git@refs/pull/2846/head#subdirectory=purchase_order_line_menu