diff --git a/setup/stock_account_move_reset_to_draft/odoo/addons/stock_account_move_reset_to_draft b/setup/stock_account_move_reset_to_draft/odoo/addons/stock_account_move_reset_to_draft new file mode 120000 index 00000000000..230823545af --- /dev/null +++ b/setup/stock_account_move_reset_to_draft/odoo/addons/stock_account_move_reset_to_draft @@ -0,0 +1 @@ +../../../../stock_account_move_reset_to_draft \ No newline at end of file diff --git a/setup/stock_account_move_reset_to_draft/setup.py b/setup/stock_account_move_reset_to_draft/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/stock_account_move_reset_to_draft/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_account_move_reset_to_draft/README.rst b/stock_account_move_reset_to_draft/README.rst new file mode 100644 index 00000000000..d05cbca7b57 --- /dev/null +++ b/stock_account_move_reset_to_draft/README.rst @@ -0,0 +1,107 @@ +================================= +Stock account move reset to draft +================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:607846dfb2ebfa561ccc45ab5422470475824616c748ea2ef707adc4ae5d7eb7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-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--invoicing-lightgray.png?logo=github + :target: https://github.com/OCA/account-invoicing/tree/16.0/stock_account_move_reset_to_draft + :alt: OCA/account-invoicing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-invoicing-16-0/account-invoicing-16-0-stock_account_move_reset_to_draft + :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-invoicing&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to restore a vendor bill to draft if SVLs linked to any invoice line +have been generated (stock_account not allowed). + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +When a purchase invoice is created for a different price than the one for which the +SVLs of the incoming picking were created and confirmed, SVLs are created for the +price difference, but from that moment on it is no longer possible to restore it +to draft (stock_account not allowed). + +Usage +===== + +#. Create a product category with Costing Method: Average Cost (AVCO) +#. Create a product linked to the category created before. +#. Create a purchase order and adds a product line with quantity 1 and price 10. +#. Confirms the purchase order and validates the incoming picking. +#. Creates an invoice from the purchase order. +#. Changes the invoice line price to 12 and confirm the invoice. +#. It is possible to reset the invoice to draft (the SVL of the difference will be removed). + +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 +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Víctor Martínez + * Pedro M. Baeza + +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-victoralmau| image:: https://github.com/victoralmau.png?size=40px + :target: https://github.com/victoralmau + :alt: victoralmau + +Current `maintainer `__: + +|maintainer-victoralmau| + +This module is part of the `OCA/account-invoicing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_account_move_reset_to_draft/__init__.py b/stock_account_move_reset_to_draft/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_account_move_reset_to_draft/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_account_move_reset_to_draft/__manifest__.py b/stock_account_move_reset_to_draft/__manifest__.py new file mode 100644 index 00000000000..3aace77086e --- /dev/null +++ b/stock_account_move_reset_to_draft/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Stock account move reset to draft", + "author": "Tecnativa, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-invoicing", + "version": "16.0.1.0.0", + # Real dependency is stock_account but we need purchase_stock in tests + "depends": ["purchase_stock"], + "license": "AGPL-3", + "category": "Warehouse Management", + "installable": True, + "maintainers": ["victoralmau"], +} diff --git a/stock_account_move_reset_to_draft/i18n/es.po b/stock_account_move_reset_to_draft/i18n/es.po new file mode 100644 index 00000000000..be76f1e2128 --- /dev/null +++ b/stock_account_move_reset_to_draft/i18n/es.po @@ -0,0 +1,21 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_account_move_reset_to_draft +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-10-11 15:02+0000\n" +"PO-Revision-Date: 2024-10-11 15:02+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: stock_account_move_reset_to_draft +#: model:ir.model,name:stock_account_move_reset_to_draft.model_account_move +msgid "Journal Entry" +msgstr "Asiento contable" diff --git a/stock_account_move_reset_to_draft/i18n/stock_account_move_reset_to_draft.pot b/stock_account_move_reset_to_draft/i18n/stock_account_move_reset_to_draft.pot new file mode 100644 index 00000000000..ee2c52a4c2f --- /dev/null +++ b/stock_account_move_reset_to_draft/i18n/stock_account_move_reset_to_draft.pot @@ -0,0 +1,21 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_account_move_reset_to_draft +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-10-11 15:02+0000\n" +"PO-Revision-Date: 2024-10-11 15:02+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: stock_account_move_reset_to_draft +#: model:ir.model,name:stock_account_move_reset_to_draft.model_account_move +msgid "Journal Entry" +msgstr "" diff --git a/stock_account_move_reset_to_draft/models/__init__.py b/stock_account_move_reset_to_draft/models/__init__.py new file mode 100644 index 00000000000..06d06dffb2a --- /dev/null +++ b/stock_account_move_reset_to_draft/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import account_move diff --git a/stock_account_move_reset_to_draft/models/account_move.py b/stock_account_move_reset_to_draft/models/account_move.py new file mode 100644 index 00000000000..80cd9feeac2 --- /dev/null +++ b/stock_account_move_reset_to_draft/models/account_move.py @@ -0,0 +1,72 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_is_zero + + +class AccountMove(models.Model): + _inherit = "account.move" + + def button_draft(self): + """If it is a purchase invoice, we will create a new SVL for each line with + the sum of the value in opposite sign. + """ + for item in self.sudo().filtered( + lambda x: x.is_inbound + and any(line.stock_valuation_layer_ids for line in x.line_ids) + ): + for line in item.line_ids.filtered("stock_valuation_layer_ids"): + origin_svls = line.stock_valuation_layer_ids.stock_valuation_layer_id + if ( + len( + origin_svls.stock_valuation_layer_ids.account_move_line_id.filtered( + lambda x: x.parent_state == "posted" + ) + ) + > 1 + ): + raise UserError( + _( + "Inventory valuation records are intertwined for %(line_name)s.", + line_name=line.display_name, + ) + ) + for origin_svl in origin_svls: + if origin_svl.quantity != origin_svl.remaining_qty: + raise UserError( + _( + "The inventory has already been (partially) consumed " + "for %(line_name)s.", + line_name=line.display_name, + ) + ) + svls = origin_svl.stock_valuation_layer_ids + value = sum(svls.mapped("value")) + if not float_is_zero( + value, precision_rounding=line.currency_id.rounding + ): + origin_svl.remaining_value -= value + revert_svl = svls[0].copy({"value": -value}) + revert_svl._validate_accounting_entries() + product = line.product_id.with_company(item.company_id.id) + if product.cost_method == "average": + product.sudo().with_context(disable_auto_svl=True).write( + {"standard_price": product.value_svl / product.quantity_svl} + ) + return super().button_draft() + + def _compute_show_reset_to_draft_button(self): + """Overwrite the value only if it is already posted and with SVLs. + We use the same fields for filtering that account uses for the + show_reset_to_draft_button field. + """ + _self = self.sudo().filtered( + lambda x: not x.restrict_mode_hash_table + and x.state in ("posted", "cancel") + and any(line.stock_valuation_layer_ids for line in x.line_ids) + ) + for item in self: + item.show_reset_to_draft_button = True + return super(AccountMove, self - _self)._compute_show_reset_to_draft_button() diff --git a/stock_account_move_reset_to_draft/readme/CONTEXT.rst b/stock_account_move_reset_to_draft/readme/CONTEXT.rst new file mode 100644 index 00000000000..ca56e358080 --- /dev/null +++ b/stock_account_move_reset_to_draft/readme/CONTEXT.rst @@ -0,0 +1,4 @@ +When a purchase invoice is created for a different price than the one for which the +SVLs of the incoming picking were created and confirmed, SVLs are created for the +price difference, but from that moment on it is no longer possible to restore it +to draft (stock_account not allowed). diff --git a/stock_account_move_reset_to_draft/readme/CONTRIBUTORS.rst b/stock_account_move_reset_to_draft/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..5fb71305308 --- /dev/null +++ b/stock_account_move_reset_to_draft/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Tecnativa `_: + + * Víctor Martínez + * Pedro M. Baeza diff --git a/stock_account_move_reset_to_draft/readme/DESCRIPTION.rst b/stock_account_move_reset_to_draft/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..f41988e7966 --- /dev/null +++ b/stock_account_move_reset_to_draft/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to restore a vendor bill to draft if SVLs linked to any invoice line +have been generated (stock_account not allowed). diff --git a/stock_account_move_reset_to_draft/readme/USAGE.rst b/stock_account_move_reset_to_draft/readme/USAGE.rst new file mode 100644 index 00000000000..fb5294a4dfa --- /dev/null +++ b/stock_account_move_reset_to_draft/readme/USAGE.rst @@ -0,0 +1,7 @@ +#. Create a product category with Costing Method: Average Cost (AVCO) +#. Create a product linked to the category created before. +#. Create a purchase order and adds a product line with quantity 1 and price 10. +#. Confirms the purchase order and validates the incoming picking. +#. Creates an invoice from the purchase order. +#. Changes the invoice line price to 12 and confirm the invoice. +#. It is possible to reset the invoice to draft (the SVL of the difference will be removed). diff --git a/stock_account_move_reset_to_draft/static/description/icon.png b/stock_account_move_reset_to_draft/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/stock_account_move_reset_to_draft/static/description/icon.png differ diff --git a/stock_account_move_reset_to_draft/static/description/index.html b/stock_account_move_reset_to_draft/static/description/index.html new file mode 100644 index 00000000000..9add9c93dec --- /dev/null +++ b/stock_account_move_reset_to_draft/static/description/index.html @@ -0,0 +1,448 @@ + + + + + +Stock account move reset to draft + + + +
+

Stock account move reset to draft

+ + +

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

+

This module allows to restore a vendor bill to draft if SVLs linked to any invoice line +have been generated (stock_account not allowed).

+

Table of contents

+ +
+

Use Cases / Context

+

When a purchase invoice is created for a different price than the one for which the +SVLs of the incoming picking were created and confirmed, SVLs are created for the +price difference, but from that moment on it is no longer possible to restore it +to draft (stock_account not allowed).

+
+
+

Usage

+
    +
  1. Create a product category with Costing Method: Average Cost (AVCO)
  2. +
  3. Create a product linked to the category created before.
  4. +
  5. Create a purchase order and adds a product line with quantity 1 and price 10.
  6. +
  7. Confirms the purchase order and validates the incoming picking.
  8. +
  9. Creates an invoice from the purchase order.
  10. +
  11. Changes the invoice line price to 12 and confirm the invoice.
  12. +
  13. It is possible to reset the invoice to draft (the SVL of the difference will be removed).
  14. +
+
+
+

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

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Víctor Martínez
    • +
    • Pedro M. Baeza
    • +
    +
  • +
+
+
+

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:

+

victoralmau

+

This module is part of the OCA/account-invoicing project on GitHub.

+

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

+
+
+
+ + diff --git a/stock_account_move_reset_to_draft/tests/__init__.py b/stock_account_move_reset_to_draft/tests/__init__.py new file mode 100644 index 00000000000..048f1c6cc24 --- /dev/null +++ b/stock_account_move_reset_to_draft/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_stock_account_move_reset_to_draft diff --git a/stock_account_move_reset_to_draft/tests/test_stock_account_move_reset_to_draft.py b/stock_account_move_reset_to_draft/tests/test_stock_account_move_reset_to_draft.py new file mode 100644 index 00000000000..dde7aae717d --- /dev/null +++ b/stock_account_move_reset_to_draft/tests/test_stock_account_move_reset_to_draft.py @@ -0,0 +1,162 @@ +# Copyright 2024 Tecnativa - Víctor Martínez +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + + +from odoo.exceptions import UserError +from odoo.tests import Form +from odoo.tools import mute_logger + +from odoo.addons.base.tests.common import BaseCommon + + +class TestStockAccountMoveResetToDraft(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.category = cls.env["product.category"].create( + { + "name": "Test product", + "property_cost_method": "average", + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test product", + "categ_id": cls.category.id, + } + ) + cls.partner = cls.env["res.partner"].create({"name": "Test partner"}) + + @mute_logger("odoo.models.unlink") + def test_purchase_order_flow_01(self): + order_form = Form(self.env["purchase.order"]) + order_form.partner_id = self.partner + with order_form.order_line.new() as line_form: + line_form.product_id = self.product + line_form.price_unit = 10 + line_form.taxes_id.clear() + order = order_form.save() + order.button_confirm() + res = order.picking_ids.button_validate() + wizard = self.env[res["res_model"]].with_context(**res["context"]).create({}) + wizard.process() + self.assertEqual(order.picking_ids.state, "done") + res_invoice = order.action_create_invoice() + invoice = self.env[res_invoice["res_model"]].browse(res_invoice["res_id"]) + self.assertEqual(invoice.state, "draft") + invoice.invoice_date = order.date_approve + invoice.invoice_line_ids.price_unit = 12 + # Upon confirmation, a SVL will be created for the difference (2=12-10) + invoice.action_post() + self.assertEqual(invoice.state, "posted") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 1) + svl_1 = invoice.invoice_line_ids.stock_valuation_layer_ids + self.assertEqual(svl_1.value, 2) + self.assertEqual( + sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 2 + ) + self.assertTrue(invoice.show_reset_to_draft_button) + # Switch to draft, a SVL will be created for the difference + invoice.button_draft() + self.assertEqual(invoice.state, "draft") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2) + self.assertEqual(svl_1.value, 2) + svl_1_negative = invoice.invoice_line_ids.stock_valuation_layer_ids - svl_1 + self.assertEqual(svl_1_negative.value, -2) + self.assertEqual( + sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0 + ) + # Confirm again, no new SVLs are generated + invoice.action_post() + self.assertEqual(invoice.state, "posted") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2) + self.assertEqual(svl_1.value, 2) + self.assertEqual(svl_1_negative.value, -2) + self.assertTrue(invoice.show_reset_to_draft_button) + self.assertEqual( + sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0 + ) + # Change to draft and change the price to 10 so that SVL is not generated + invoice.button_draft() + self.assertEqual(invoice.state, "draft") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2) + self.assertEqual(svl_1.value, 2) + self.assertEqual(svl_1_negative.value, -2) + invoice.invoice_line_ids.price_unit = 10 + invoice.action_post() + self.assertEqual(invoice.state, "posted") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2) + self.assertEqual(svl_1.value, 2) + self.assertEqual(svl_1_negative.value, -2) + self.assertTrue(invoice.show_reset_to_draft_button) + self.assertEqual( + sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0 + ) + + @mute_logger("odoo.models.unlink") + def test_purchase_order_flow_02(self): + # PO for a product: 2 pcs at EUR10 + order_form = Form(self.env["purchase.order"]) + order_form.partner_id = self.partner + with order_form.order_line.new() as line_form: + line_form.product_id = self.product + line_form.price_unit = 10 + line_form.product_qty = 2 + line_form.taxes_id.clear() + order = order_form.save() + order.button_confirm() + # Receive 1 pc and create a backorder + picking = order.picking_ids + picking.move_ids_without_package.quantity_done = 1 + res = picking.button_validate() + wizard = self.env[res["res_model"]].with_context(**res["context"]).create({}) + wizard.process() + # Receive 1 pc + extra_picking = order.picking_ids - picking + res = extra_picking.button_validate() + wizard = self.env[res["res_model"]].with_context(**res["context"]).create({}) + wizard.process() + # Create a bill for 2 pcs at EUR12 and post + res_invoice = order.action_create_invoice() + invoice = self.env[res_invoice["res_model"]].browse(res_invoice["res_id"]) + self.assertEqual(invoice.state, "draft") + invoice.invoice_date = order.date_approve + invoice.invoice_line_ids.price_unit = 12 + invoice.action_post() + self.assertEqual(invoice.state, "posted") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2) + svls_1 = invoice.invoice_line_ids.stock_valuation_layer_ids + self.assertEqual(sum(svls_1.mapped("value")), 24) + self.assertTrue(invoice.show_reset_to_draft_button) + # Reset the bill to draft + invoice.button_draft() + self.assertEqual(invoice.state, "draft") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 4) + svls_1_negative = invoice.invoice_line_ids.stock_valuation_layer_ids - svls_1 + self.assertEqual(sum(svls_1.mapped("value")), 24) + self.assertEqual(sum(svls_1_negative.mapped("value")), -24) + # Change the bill content to 1 pc at EUR15 and post + invoice.invoice_line_ids.quantity = 1 + invoice.invoice_line_ids.price_unit = 8 + invoice.action_post() + self.assertEqual(invoice.state, "posted") + self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 4) + # Create another bill for 1 pc at EUR8 and post + res_invoice = order.action_create_invoice() + invoice_extra = self.env[res_invoice["res_model"]].browse(res_invoice["res_id"]) + self.assertEqual(invoice_extra.state, "draft") + invoice_extra.invoice_date = order.date_approve + invoice_extra.invoice_line_ids.price_unit = 8 + invoice_extra.action_post() + self.assertEqual(invoice_extra.state, "posted") + self.assertEqual( + len(invoice_extra.invoice_line_ids.stock_valuation_layer_ids), 1 + ) + self.assertEqual( + invoice_extra.invoice_line_ids.stock_valuation_layer_ids.value, 8 + ) + self.assertTrue(invoice.show_reset_to_draft_button) + # Reset the first bill to draft -> User error to prevent valuation inconsistencies + with self.assertRaises(UserError): + invoice.button_draft() + # Delivery 1 pc