From fe8efb7f92ea1e85ca7dbebdbce4b98f2e8d95d7 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 9 Aug 2024 07:56:16 +0000 Subject: [PATCH 01/17] [ADD] stock_valuation_fifo_lot --- .../odoo/addons/stock_valuation_fifo_lot | 1 + setup/stock_valuation_fifo_lot/setup.py | 6 + stock_valuation_fifo_lot/README.rst | 93 ++++ stock_valuation_fifo_lot/__init__.py | 3 + stock_valuation_fifo_lot/__manifest__.py | 21 + stock_valuation_fifo_lot/models/__init__.py | 5 + stock_valuation_fifo_lot/models/product.py | 122 +++++ stock_valuation_fifo_lot/models/stock_move.py | 97 ++++ .../models/stock_valuation_layer.py | 13 + .../readme/CONTRIBUTORS.rst | 5 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 433 ++++++++++++++++++ .../views/stock_valuation_layer_views.xml | 23 + 13 files changed, 823 insertions(+) create mode 120000 setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot create mode 100644 setup/stock_valuation_fifo_lot/setup.py create mode 100644 stock_valuation_fifo_lot/README.rst create mode 100644 stock_valuation_fifo_lot/__init__.py create mode 100644 stock_valuation_fifo_lot/__manifest__.py create mode 100644 stock_valuation_fifo_lot/models/__init__.py create mode 100644 stock_valuation_fifo_lot/models/product.py create mode 100644 stock_valuation_fifo_lot/models/stock_move.py create mode 100644 stock_valuation_fifo_lot/models/stock_valuation_layer.py create mode 100644 stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst create mode 100644 stock_valuation_fifo_lot/readme/DESCRIPTION.rst create mode 100644 stock_valuation_fifo_lot/static/description/index.html create mode 100644 stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml diff --git a/setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot b/setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot new file mode 120000 index 00000000..bf429093 --- /dev/null +++ b/setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot @@ -0,0 +1 @@ +../../../../stock_valuation_fifo_lot \ No newline at end of file diff --git a/setup/stock_valuation_fifo_lot/setup.py b/setup/stock_valuation_fifo_lot/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/stock_valuation_fifo_lot/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_valuation_fifo_lot/README.rst b/stock_valuation_fifo_lot/README.rst new file mode 100644 index 00000000..b8809519 --- /dev/null +++ b/stock_valuation_fifo_lot/README.rst @@ -0,0 +1,93 @@ +======================== +Stock Valuation Fifo Lot +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c2a4a8addfa492b1a12ab3bd17b857715a3f32304bd2839b9e69ced26533f74a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_valuation_fifo_lot + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_valuation_fifo_lot + :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/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is used to calculate FIFO cost by lot. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**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 +~~~~~~~ + +* Ecosoft + +Contributors +~~~~~~~~~~~~ + +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +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-newtratip| image:: https://github.com/newtratip.png?size=40px + :target: https://github.com/newtratip + :alt: newtratip + +Current `maintainer `__: + +|maintainer-newtratip| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_valuation_fifo_lot/__init__.py b/stock_valuation_fifo_lot/__init__.py new file mode 100644 index 00000000..8ebc8a7c --- /dev/null +++ b/stock_valuation_fifo_lot/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import models diff --git a/stock_valuation_fifo_lot/__manifest__.py b/stock_valuation_fifo_lot/__manifest__.py new file mode 100644 index 00000000..586b5f48 --- /dev/null +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + "name": "Stock Valuation Fifo Lot", + "version": "16.0.1.0.0", + "category": "Warehouse Management", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "Ecosoft, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "depends": [ + "stock_account", + "stock_no_negative", + ], + "data": [ + "views/stock_valuation_layer_views.xml", + ], + "installable": True, + "maintainers": ["newtratip"], +} diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py new file mode 100644 index 00000000..c5490433 --- /dev/null +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import product +from . import stock_move +from . import stock_valuation_layer diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py new file mode 100644 index 00000000..6d652d5d --- /dev/null +++ b/stock_valuation_fifo_lot/models/product.py @@ -0,0 +1,122 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models +from odoo.tools import float_is_zero + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def _sort_by_all_candidates(self, all_candidates, sort_by): + """Hook function for other sort by""" + return all_candidates + + def _get_all_candidates(self, company, sort_by=None): + all_candidates = ( + self.env["stock.valuation.layer"] + .sudo() + .search( + [ + ("product_id", "=", self.id), + ("remaining_qty", ">", 0), + ("company_id", "=", company.id), + ] + ) + ) + if sort_by == "lot_create_date": + + def sorting_key(candidate): + if len(candidate.lot_ids) > 1: + return min(candidate.lot_ids.mapped("create_date")) + elif candidate.lot_ids: + return candidate.lot_ids[0].create_date + else: + return candidate.create_date + + all_candidates = all_candidates.sorted(key=sorting_key) + elif sort_by is not None: + all_candidates = self._sort_by_all_candidates(all_candidates, sort_by) + return all_candidates + + def _run_fifo(self, quantity, company): + self.ensure_one() + move_id = self._context.get("used_in_move_id") + if self.tracking == "none" or not move_id: + vals = super()._run_fifo(quantity, company) + else: + move = self.env["stock.move"].browse(move_id) + move_lines = move._get_out_move_lines() + tmp_value = 0 + tmp_remaining_qty = 0 + for move_line in move_lines: + # Find back incoming stock valuation layers + # (called candidates here) to value `quantity`. + qty_to_take_on_candidates = move_line.product_uom_id._compute_quantity( + move_line.qty_done, move.product_id.uom_id + ) + candidates = self._get_all_candidates(company).filtered( + lambda l: move_line.lot_id in l.lot_ids + ) + for candidate in candidates: + qty_taken_on_candidate = min( + qty_to_take_on_candidates, candidate.remaining_qty + ) + + candidate_unit_cost = ( + candidate.remaining_value / candidate.remaining_qty + ) + value_taken_on_candidate = ( + qty_taken_on_candidate * candidate_unit_cost + ) + value_taken_on_candidate = candidate.currency_id.round( + value_taken_on_candidate + ) + new_remaining_value = ( + candidate.remaining_value - value_taken_on_candidate + ) + + candidate_vals = { + "remaining_qty": candidate.remaining_qty + - qty_taken_on_candidate, + "remaining_value": new_remaining_value, + } + + candidate.write(candidate_vals) + + qty_to_take_on_candidates -= qty_taken_on_candidate + tmp_value += value_taken_on_candidate + + if float_is_zero( + qty_to_take_on_candidates, + precision_rounding=self.uom_id.rounding, + ): + break + + if candidates and qty_to_take_on_candidates > 0: + tmp_value += abs(candidate.unit_cost * -qty_to_take_on_candidates) + tmp_remaining_qty += qty_to_take_on_candidates + + # Calculate standard price (Sorted by lot created date) + all_candidates = self._get_all_candidates( + company, sort_by="lot_create_date" + ) + new_standard_price = 0.0 + if all_candidates: + new_standard_price = all_candidates[0].unit_cost + elif candidates: + new_standard_price = candidate.unit_cost + + # Update standard price + if new_standard_price and self.cost_method == "fifo": + self.sudo().with_company(company.id).with_context( + disable_auto_svl=True + ).standard_price = new_standard_price + + # Value + vals = { + "remaining_qty": -tmp_remaining_qty, + "value": -tmp_value, + "unit_cost": tmp_value / (quantity + tmp_remaining_qty), + } + return vals diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py new file mode 100644 index 00000000..11690bfd --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -0,0 +1,97 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _prepare_common_svl_vals(self): + """ + Prepare lots/serial numbers on stock valuation report + """ + self.ensure_one() + res = super()._prepare_common_svl_vals() + res.update( + { + "lot_ids": [(6, 0, self.lot_ids.ids)], + } + ) + return res + + def _create_out_svl(self, forced_quantity=None): + """ + Send context current move to _create_out_svl function + """ + layers = self.env["stock.valuation.layer"] + for move in self: + move = move.with_context(used_in_move_id=move.id) + layer = super(StockMove, move)._create_out_svl( + forced_quantity=forced_quantity + ) + layers |= layer + return layers + + def _create_in_svl(self, forced_quantity=None): + """ + 1. Check stock move - Multiple lot on the stock move is not + allowed for incoming transfer + 2. Change product standard price to first available lot price + """ + layers = self.env["stock.valuation.layer"] + for move in self: + layer = super(StockMove, move)._create_in_svl( + forced_quantity=forced_quantity + ) + # Calculate standard price (Sorted by lot created date) + if ( + move.product_id.cost_method == "fifo" + and move.product_id.tracking != "none" + ): + all_candidates = move.product_id._get_all_candidates( + move.company_id, sort_by="lot_create_date" + ) + if all_candidates: + move.product_id.sudo().with_company( + move.company_id.id + ).with_context( + disable_auto_svl=True + ).standard_price = all_candidates[ + 0 + ].unit_cost + layers |= layer + return layers + + def _get_price_unit(self): + """ + No PO, Get price unit from lot price + """ + self.ensure_one() + price_unit = super()._get_price_unit() + if ( + not self.purchase_line_id + and self.product_id.cost_method == "fifo" + and len(self.lot_ids) == 1 + ): + candidates = ( + self.env["stock.valuation.layer"] + .sudo() + .search( + [ + ("product_id", "=", self.product_id.id), + ( + "lot_ids", + "in", + self.lot_ids.ids, + ), + ("quantity", ">", 0), + ("value", ">", 0), + ("company_id", "=", self.company_id.id), + ], + limit=1, + ) + ) + if candidates: + price_unit = candidates[0].unit_cost + return price_unit diff --git a/stock_valuation_fifo_lot/models/stock_valuation_layer.py b/stock_valuation_fifo_lot/models/stock_valuation_layer.py new file mode 100644 index 00000000..0b2c10d4 --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_valuation_layer.py @@ -0,0 +1,13 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import fields, models + + +class StockValuationLayer(models.Model): + _inherit = "stock.valuation.layer" + + lot_ids = fields.Many2many( + comodel_name="stock.lot", + string="Lots/Serial Numbers", + ) diff --git a/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..eef9b340 --- /dev/null +++ b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian diff --git a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst new file mode 100644 index 00000000..e1787438 --- /dev/null +++ b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module is used to calculate FIFO cost by lot. diff --git a/stock_valuation_fifo_lot/static/description/index.html b/stock_valuation_fifo_lot/static/description/index.html new file mode 100644 index 00000000..cd386759 --- /dev/null +++ b/stock_valuation_fifo_lot/static/description/index.html @@ -0,0 +1,433 @@ + + + + + +Stock Valuation Fifo Lot + + + +
+

Stock Valuation Fifo Lot

+ + +

Alpha License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

This module is used to calculate FIFO cost by lot.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

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

+
    +
  • Ecosoft
  • +
+
+
+

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:

+

newtratip

+

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

+

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

+
+
+
+ + diff --git a/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml new file mode 100644 index 00000000..1901131c --- /dev/null +++ b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml @@ -0,0 +1,23 @@ + + + stock.valuation.layer.tree + stock.valuation.layer + + + + + + + + + + stock.valuation.layer.form + stock.valuation.layer + + + + + + + + From 86aef2f986597c1c60ff42f1410d7a9b0cc1ed18 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 9 Aug 2024 07:57:20 +0000 Subject: [PATCH 02/17] [IMP] stock_valuation_fifo_lot: adjust view --- .../views/stock_valuation_layer_views.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml index 1901131c..9242b2ae 100644 --- a/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml +++ b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml @@ -1,12 +1,29 @@ + + stock.valuation.layer.search + stock.valuation.layer + + + + + + + + stock.valuation.layer.tree stock.valuation.layer + + + + + + From d4945b96ee1aef98bc0db0f87600cbb83f81e9df Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 13 Sep 2024 09:56:10 +0000 Subject: [PATCH 03/17] reflect oca changes --- stock_valuation_fifo_lot/README.rst | 13 +- stock_valuation_fifo_lot/__manifest__.py | 6 +- .../i18n/stock_valuation_fifo_lot.pot | 39 ++++ stock_valuation_fifo_lot/models/__init__.py | 2 + stock_valuation_fifo_lot/models/product.py | 167 ++++++++---------- .../models/res_company.py | 12 ++ .../models/res_config_settings.py | 14 ++ stock_valuation_fifo_lot/models/stock_move.py | 23 +-- stock_valuation_fifo_lot/readme/CONFIGURE.rst | 2 + .../readme/CONTRIBUTORS.rst | 5 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 32 ++-- stock_valuation_fifo_lot/tests/__init__.py | 1 + .../tests/test_stock_valuation_fifo_lot.py | 143 +++++++++++++++ .../views/res_config_settings_views.xml | 32 ++++ .../views/stock_valuation_layer_views.xml | 17 +- 16 files changed, 384 insertions(+), 124 deletions(-) create mode 100644 stock_valuation_fifo_lot/i18n/stock_valuation_fifo_lot.pot create mode 100644 stock_valuation_fifo_lot/models/res_company.py create mode 100644 stock_valuation_fifo_lot/models/res_config_settings.py create mode 100644 stock_valuation_fifo_lot/readme/CONFIGURE.rst create mode 100644 stock_valuation_fifo_lot/static/description/icon.png create mode 100644 stock_valuation_fifo_lot/tests/__init__.py create mode 100644 stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py create mode 100644 stock_valuation_fifo_lot/views/res_config_settings_views.xml diff --git a/stock_valuation_fifo_lot/README.rst b/stock_valuation_fifo_lot/README.rst index b8809519..d4757d4a 100644 --- a/stock_valuation_fifo_lot/README.rst +++ b/stock_valuation_fifo_lot/README.rst @@ -7,7 +7,7 @@ Stock Valuation Fifo Lot !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:c2a4a8addfa492b1a12ab3bd17b857715a3f32304bd2839b9e69ced26533f74a + !! source digest: sha256:877af52a350ab6a61b6c128c4fbcffe909e4a8274c8ec064390dc9b1c36d9253 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png @@ -40,6 +40,12 @@ This module is used to calculate FIFO cost by lot. .. contents:: :local: +Configuration +============= + +If necessary, update the 'Use FIFO cost by lot' setting under Inventory > Configuration > Settings to use the lot cost instead of the standard _get_price() behavior when there is no relation to a purchase order in the stock move. +(enabled by default). + Bug Tracker =========== @@ -67,6 +73,11 @@ Contributors * Saran Limpajitkutaporn * Pimolnat Suntian +* `Quartile `__: + + * Aung Ko Ko Lin + + Maintainers ~~~~~~~~~~~ diff --git a/stock_valuation_fifo_lot/__manifest__.py b/stock_valuation_fifo_lot/__manifest__.py index 586b5f48..8562607b 100644 --- a/stock_valuation_fifo_lot/__manifest__.py +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -9,11 +9,9 @@ "license": "AGPL-3", "author": "Ecosoft, Odoo Community Association (OCA)", "website": "https://github.com/OCA/stock-logistics-workflow", - "depends": [ - "stock_account", - "stock_no_negative", - ], + "depends": ["stock_account", "stock_no_negative"], "data": [ + "views/res_config_settings_views.xml", "views/stock_valuation_layer_views.xml", ], "installable": True, diff --git a/stock_valuation_fifo_lot/i18n/stock_valuation_fifo_lot.pot b/stock_valuation_fifo_lot/i18n/stock_valuation_fifo_lot.pot new file mode 100644 index 00000000..6d0a6e01 --- /dev/null +++ b/stock_valuation_fifo_lot/i18n/stock_valuation_fifo_lot.pot @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_valuation_fifo_lot +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.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: stock_valuation_fifo_lot +#: model:ir.model.fields,field_description:stock_valuation_fifo_lot.field_stock_valuation_layer__lot_ids +msgid "Lots/Serial Numbers" +msgstr "" + +#. module: stock_valuation_fifo_lot +#: model:ir.model,name:stock_valuation_fifo_lot.model_product_product +msgid "Product" +msgstr "" + +#. module: stock_valuation_fifo_lot +#: model:ir.model,name:stock_valuation_fifo_lot.model_stock_landed_cost +msgid "Stock Landed Cost" +msgstr "" + +#. module: stock_valuation_fifo_lot +#: model:ir.model,name:stock_valuation_fifo_lot.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: stock_valuation_fifo_lot +#: model:ir.model,name:stock_valuation_fifo_lot.model_stock_valuation_layer +msgid "Stock Valuation Layer" +msgstr "" diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py index c5490433..f797b4f1 100644 --- a/stock_valuation_fifo_lot/models/__init__.py +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -1,5 +1,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) from . import product +from . import res_company +from . import res_config_settings from . import stock_move from . import stock_valuation_layer diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index 6d652d5d..411d95c5 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -12,25 +12,14 @@ def _sort_by_all_candidates(self, all_candidates, sort_by): """Hook function for other sort by""" return all_candidates - def _get_all_candidates(self, company, sort_by=None): - all_candidates = ( - self.env["stock.valuation.layer"] - .sudo() - .search( - [ - ("product_id", "=", self.id), - ("remaining_qty", ">", 0), - ("company_id", "=", company.id), - ] - ) - ) + def _get_fifo_candidates(self, company): + all_candidates = super()._get_fifo_candidates(company) + sort_by = self.env.context.get("sort_by") if sort_by == "lot_create_date": def sorting_key(candidate): - if len(candidate.lot_ids) > 1: + if candidate.lot_ids: return min(candidate.lot_ids.mapped("create_date")) - elif candidate.lot_ids: - return candidate.lot_ids[0].create_date else: return candidate.create_date @@ -43,80 +32,80 @@ def _run_fifo(self, quantity, company): self.ensure_one() move_id = self._context.get("used_in_move_id") if self.tracking == "none" or not move_id: - vals = super()._run_fifo(quantity, company) - else: - move = self.env["stock.move"].browse(move_id) - move_lines = move._get_out_move_lines() - tmp_value = 0 - tmp_remaining_qty = 0 - for move_line in move_lines: - # Find back incoming stock valuation layers - # (called candidates here) to value `quantity`. - qty_to_take_on_candidates = move_line.product_uom_id._compute_quantity( - move_line.qty_done, move.product_id.uom_id + return super()._run_fifo(quantity, company) + + move = self.env["stock.move"].browse(move_id) + move_lines = move._get_out_move_lines() + tmp_value = 0 + tmp_remaining_qty = 0 + for move_line in move_lines: + # Find back incoming stock valuation layers + # (called candidates here) to value `quantity`. + qty_to_take_on_candidates = move_line.product_uom_id._compute_quantity( + move_line.qty_done, move.product_id.uom_id + ) + # Find incoming stock valuation layers that have lot_ids on their moves + # Check with stock_move_id.lot_ids to cover the situation where the stock + # was received either before or after the installation of this module + candidates = self._get_fifo_candidates(company).filtered( + lambda l: move_line.lot_id in l.stock_move_id.lot_ids + ) + for candidate in candidates: + qty_taken_on_candidate = min( + qty_to_take_on_candidates, candidate.remaining_qty + ) + + candidate_unit_cost = ( + candidate.remaining_value / candidate.remaining_qty ) - candidates = self._get_all_candidates(company).filtered( - lambda l: move_line.lot_id in l.lot_ids + value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost + value_taken_on_candidate = candidate.currency_id.round( + value_taken_on_candidate ) - for candidate in candidates: - qty_taken_on_candidate = min( - qty_to_take_on_candidates, candidate.remaining_qty - ) - - candidate_unit_cost = ( - candidate.remaining_value / candidate.remaining_qty - ) - value_taken_on_candidate = ( - qty_taken_on_candidate * candidate_unit_cost - ) - value_taken_on_candidate = candidate.currency_id.round( - value_taken_on_candidate - ) - new_remaining_value = ( - candidate.remaining_value - value_taken_on_candidate - ) - - candidate_vals = { - "remaining_qty": candidate.remaining_qty - - qty_taken_on_candidate, - "remaining_value": new_remaining_value, - } - - candidate.write(candidate_vals) - - qty_to_take_on_candidates -= qty_taken_on_candidate - tmp_value += value_taken_on_candidate - - if float_is_zero( - qty_to_take_on_candidates, - precision_rounding=self.uom_id.rounding, - ): - break - - if candidates and qty_to_take_on_candidates > 0: - tmp_value += abs(candidate.unit_cost * -qty_to_take_on_candidates) - tmp_remaining_qty += qty_to_take_on_candidates - - # Calculate standard price (Sorted by lot created date) - all_candidates = self._get_all_candidates( - company, sort_by="lot_create_date" - ) - new_standard_price = 0.0 - if all_candidates: - new_standard_price = all_candidates[0].unit_cost - elif candidates: - new_standard_price = candidate.unit_cost - - # Update standard price - if new_standard_price and self.cost_method == "fifo": - self.sudo().with_company(company.id).with_context( - disable_auto_svl=True - ).standard_price = new_standard_price - - # Value - vals = { - "remaining_qty": -tmp_remaining_qty, - "value": -tmp_value, - "unit_cost": tmp_value / (quantity + tmp_remaining_qty), - } + new_remaining_value = ( + candidate.remaining_value - value_taken_on_candidate + ) + + candidate_vals = { + "remaining_qty": candidate.remaining_qty - qty_taken_on_candidate, + "remaining_value": new_remaining_value, + } + + candidate.write(candidate_vals) + + qty_to_take_on_candidates -= qty_taken_on_candidate + tmp_value += value_taken_on_candidate + + if float_is_zero( + qty_to_take_on_candidates, + precision_rounding=self.uom_id.rounding, + ): + break + + if candidates and qty_to_take_on_candidates > 0: + tmp_value += abs(candidate.unit_cost * -qty_to_take_on_candidates) + tmp_remaining_qty += qty_to_take_on_candidates + + # Calculate standard price (Sorted by lot created date) + all_candidates = self.with_context( + sort_by="lot_create_date" + )._get_fifo_candidates(company) + new_standard_price = 0.0 + if all_candidates: + new_standard_price = all_candidates[0].unit_cost + elif candidates: + new_standard_price = candidate.unit_cost + + # Update standard price + if new_standard_price and self.cost_method == "fifo": + self.sudo().with_company(company.id).with_context( + disable_auto_svl=True + ).standard_price = new_standard_price + + # Value + vals = { + "remaining_qty": -tmp_remaining_qty, + "value": -tmp_value, + "unit_cost": tmp_value / (quantity + tmp_remaining_qty), + } return vals diff --git a/stock_valuation_fifo_lot/models/res_company.py b/stock_valuation_fifo_lot/models/res_company.py new file mode 100644 index 00000000..6445a538 --- /dev/null +++ b/stock_valuation_fifo_lot/models/res_company.py @@ -0,0 +1,12 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + use_lot_get_price_unit_fifo = fields.Boolean( + default=True, help="Use the FIFO price unit by lot when there is no PO." + ) diff --git a/stock_valuation_fifo_lot/models/res_config_settings.py b/stock_valuation_fifo_lot/models/res_config_settings.py new file mode 100644 index 00000000..c5115315 --- /dev/null +++ b/stock_valuation_fifo_lot/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + use_lot_get_price_unit_fifo = fields.Boolean( + related="company_id.use_lot_get_price_unit_fifo", + readonly=False, + help="Use the FIFO price unit by lot when there is no PO.", + ) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 11690bfd..bea3ff17 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -49,9 +49,9 @@ def _create_in_svl(self, forced_quantity=None): move.product_id.cost_method == "fifo" and move.product_id.tracking != "none" ): - all_candidates = move.product_id._get_all_candidates( - move.company_id, sort_by="lot_create_date" - ) + all_candidates = move.product_id.with_context( + sort_by="lot_create_date" + )._get_fifo_candidates(move.company_id) if all_candidates: move.product_id.sudo().with_company( move.company_id.id @@ -68,13 +68,15 @@ def _get_price_unit(self): No PO, Get price unit from lot price """ self.ensure_one() - price_unit = super()._get_price_unit() + if not self.company_id.use_lot_get_price_unit_fifo: + return super()._get_price_unit() if ( - not self.purchase_line_id + hasattr(self, "purchase_line_id") + and not self.purchase_line_id and self.product_id.cost_method == "fifo" and len(self.lot_ids) == 1 ): - candidates = ( + candidate = ( self.env["stock.valuation.layer"] .sudo() .search( @@ -85,13 +87,12 @@ def _get_price_unit(self): "in", self.lot_ids.ids, ), - ("quantity", ">", 0), - ("value", ">", 0), + ("remaining_qty", ">", 0), ("company_id", "=", self.company_id.id), ], limit=1, ) ) - if candidates: - price_unit = candidates[0].unit_cost - return price_unit + if candidate: + return candidate.remaining_value / candidate.remaining_qty + return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/readme/CONFIGURE.rst b/stock_valuation_fifo_lot/readme/CONFIGURE.rst new file mode 100644 index 00000000..61b74d16 --- /dev/null +++ b/stock_valuation_fifo_lot/readme/CONFIGURE.rst @@ -0,0 +1,2 @@ +If necessary, update the 'Use FIFO cost by lot' setting under Inventory > Configuration > Settings to use the lot cost instead of the standard _get_price() behavior when there is no relation to a purchase order in the stock move. +(enabled by default). diff --git a/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst index eef9b340..e2e916aa 100644 --- a/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst +++ b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst @@ -3,3 +3,8 @@ * Tharathip Chaweewongphan * Saran Limpajitkutaporn * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + \ No newline at end of file diff --git a/stock_valuation_fifo_lot/static/description/icon.png b/stock_valuation_fifo_lot/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/stock_valuation_fifo_lot/static/description/index.html b/stock_valuation_fifo_lot/static/description/index.html index cd386759..b0972660 100644 --- a/stock_valuation_fifo_lot/static/description/index.html +++ b/stock_valuation_fifo_lot/static/description/index.html @@ -366,7 +366,7 @@

Stock Valuation Fifo Lot

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:c2a4a8addfa492b1a12ab3bd17b857715a3f32304bd2839b9e69ced26533f74a +!! source digest: sha256:877af52a350ab6a61b6c128c4fbcffe909e4a8274c8ec064390dc9b1c36d9253 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Alpha License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

This module is used to calculate FIFO cost by lot.

@@ -379,17 +379,23 @@

Stock Valuation Fifo Lot

Table of contents

+
+

Configuration

+

If necessary, update the ‘Use FIFO cost by lot’ setting under Inventory > Configuration > Settings to use the lot cost instead of the standard _get_price() behavior when there is no relation to a purchase order in the stock move. +(enabled by default).

+
-

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 @@ -397,15 +403,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Ecosoft
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose diff --git a/stock_valuation_fifo_lot/tests/__init__.py b/stock_valuation_fifo_lot/tests/__init__.py new file mode 100644 index 00000000..3aefeb55 --- /dev/null +++ b/stock_valuation_fifo_lot/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_valuation_fifo_lot diff --git a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py new file mode 100644 index 00000000..2e5d6991 --- /dev/null +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -0,0 +1,143 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo.tests.common import Form, TransactionCase + + +class TestStockAccountFifoReturnOrigin(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + product_category = cls.env["product.category"].create( + { + "name": "Test Category", + "property_cost_method": "fifo", + "property_valuation": "real_time", + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "type": "product", + "categ_id": product_category.id, + "tracking": "lot", + } + ) + cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") + cls.customer_location = cls.env.ref("stock.stock_location_customers") + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.picking_type_in = cls.env.ref("stock.picking_type_in") + cls.picking_type_out = cls.env.ref("stock.picking_type_out") + + def create_picking(self, location, location_dest, picking_type): + return self.env["stock.picking"].create( + { + "location_id": location.id, + "location_dest_id": location_dest.id, + "picking_type_id": picking_type.id, + } + ) + + def create_stock_move(self, picking, product, price=0.0): + move = self.env["stock.move"].create( + { + "name": "Move", + "product_id": product.id, + "location_id": picking.location_id.id, + "location_dest_id": picking.location_dest_id.id, + "product_uom": product.uom_id.id, + "product_uom_qty": 5.0, + "picking_id": picking.id, + } + ) + if price: + move.write({"price_unit": price}) + return move + + def create_stock_move_line(self, move, picking, lot_name=False): + move_line = self.env["stock.move.line"].create( + { + "move_id": move.id, + "picking_id": picking.id, + "product_id": move.product_id.id, + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + "product_uom_id": move.product_uom.id, + "qty_done": move.product_uom_qty, + "lot_name": lot_name, + } + ) + return move_line + + def test_stock_valuation_fifo_lot(self): + receipt_picking_1 = self.create_picking( + self.supplier_location, self.stock_location, self.picking_type_in + ) + move = self.create_stock_move(receipt_picking_1, self.product, 100) + self.create_stock_move_line(move, receipt_picking_1, "11111") + + receipt_picking_1.action_confirm() + receipt_picking_1.action_assign() + receipt_picking_1._action_done() + self.assertEqual(move.stock_valuation_layer_ids.value, 500) + + receipt_picking_2 = self.create_picking( + self.supplier_location, self.stock_location, self.picking_type_in + ) + move = self.create_stock_move(receipt_picking_2, self.product, 200) + self.create_stock_move_line(move, receipt_picking_2, "22222") + + receipt_picking_2.action_confirm() + receipt_picking_2.action_assign() + receipt_picking_2._action_done() + self.assertEqual(move.stock_valuation_layer_ids.value, 1000) + self.assertEqual(self.product.standard_price, 100) + + delivery_picking1 = self.create_picking( + self.stock_location, self.customer_location, self.picking_type_out + ) + move = self.create_stock_move(delivery_picking1, self.product) + move_line = self.create_stock_move_line(move, delivery_picking1) + lot_id = self.env["stock.lot"].search( + [("name", "=", "22222"), ("product_id", "=", self.product.id)] + ) + move_line.write({"lot_id": lot_id}) + + delivery_picking1.action_confirm() + delivery_picking1.action_assign() + delivery_picking1._action_done() + self.assertEqual(abs(move.stock_valuation_layer_ids.value), 1000) + + # Test return delivery + receipt_picking_3 = self.create_picking( + self.supplier_location, self.stock_location, self.picking_type_in + ) + move = self.create_stock_move(receipt_picking_3, self.product, 300) + self.create_stock_move_line(move, receipt_picking_3, "33333") + + receipt_picking_3.action_confirm() + receipt_picking_3.action_assign() + receipt_picking_3._action_done() + self.assertEqual(move.stock_valuation_layer_ids.value, 1500) + + return_picking_wizard_form = Form( + self.env["stock.return.picking"].with_context( + active_ids=receipt_picking_3.ids, + active_id=receipt_picking_3.id, + active_model="stock.picking", + ) + ) + return_picking_wizard = return_picking_wizard_form.save() + return_picking_wizard.product_return_moves.write({"quantity": 5}) + return_picking_wizard_action = return_picking_wizard.create_returns() + return_picking = self.env["stock.picking"].browse( + return_picking_wizard_action["res_id"] + ) + return_move = return_picking.move_ids + return_move.move_line_ids.qty_done = 5 + return_picking.button_validate() + self.assertEqual(abs(return_move.stock_valuation_layer_ids.value), 1500) diff --git a/stock_valuation_fifo_lot/views/res_config_settings_views.xml b/stock_valuation_fifo_lot/views/res_config_settings_views.xml new file mode 100644 index 00000000..975f288e --- /dev/null +++ b/stock_valuation_fifo_lot/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + res.config.settings.form + res.config.settings + + + +

+
+ +
+
+
+
+ + + + diff --git a/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml index 9242b2ae..853cdeed 100644 --- a/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml +++ b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml @@ -1,12 +1,19 @@ - + stock.valuation.layer.search stock.valuation.layer - + + + + @@ -15,15 +22,9 @@ stock.valuation.layer - - - - - - From 84a2277d7de0c8dac94ba5e47d79f5608b5564cc Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 13 Sep 2024 09:58:13 +0000 Subject: [PATCH 04/17] relect oca changes --- .../stock_valuation_fifo_lot_mrp_landed_cost | 1 + .../setup.py | 6 + .../README.rst | 98 ++++ .../__init__.py | 1 + .../__manifest__.py | 15 + .../models/__init__.py | 1 + .../models/stock_landed_cost.py | 17 + .../readme/CONTRIBUTORS.rst | 10 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 437 ++++++++++++++++++ 10 files changed, 587 insertions(+) create mode 120000 setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost create mode 100644 setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/README.rst create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/__init__.py create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst create mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html diff --git a/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost b/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost new file mode 120000 index 00000000..d3d287ed --- /dev/null +++ b/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost @@ -0,0 +1 @@ +../../../../stock_valuation_fifo_lot_mrp_landed_cost \ No newline at end of file diff --git a/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py b/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/README.rst b/stock_valuation_fifo_lot_mrp_landed_cost/README.rst new file mode 100644 index 00000000..54504e94 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/README.rst @@ -0,0 +1,98 @@ +======================================== +Stock Valuation Fifo Lot MRP Landed Cost +======================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:21ea12a0400da8515046cc3fa647d54482ff123046d5f5e8ae0e90b89058f0b9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |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%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_valuation_fifo_lot_mrp_landed_cost + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_valuation_fifo_lot_mrp_landed_cost + :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/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**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 +~~~~~~~ + +* Ecosoft + +Contributors +~~~~~~~~~~~~ + +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + + +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-newtratip| image:: https://github.com/newtratip.png?size=40px + :target: https://github.com/newtratip + :alt: newtratip + +Current `maintainer `__: + +|maintainer-newtratip| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py b/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py b/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py new file mode 100644 index 00000000..dac94be7 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + "name": "Stock Valuation Fifo Lot MRP Landed Cost", + "version": "16.0.1.0.0", + "category": "Warehouse Management", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "Ecosoft, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "depends": ["stock_valuation_fifo_lot", "mrp_landed_costs"], + "installable": True, + "maintainers": ["newtratip"], +} diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py b/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py new file mode 100644 index 00000000..e9907b68 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py @@ -0,0 +1 @@ +from . import stock_landed_cost diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py b/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py new file mode 100644 index 00000000..98ac48b2 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py @@ -0,0 +1,17 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models + + +class StockLandedCost(models.Model): + _inherit = "stock.landed.cost" + + def button_validate(self): + "Update Lots/SN on stock.valuation.layer line" + res = super().button_validate() + for rec in self.stock_valuation_layer_ids: + lot_ids = self.mrp_production_ids.mapped("lot_producing_id") + if lot_ids: + rec.write({"lot_ids": lot_ids}) + return res diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst b/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..e2e916aa --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + \ No newline at end of file diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst new file mode 100644 index 00000000..b5e969bd --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs. diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html b/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html new file mode 100644 index 00000000..08f45c70 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html @@ -0,0 +1,437 @@ + + + + + +Stock Valuation Fifo Lot MRP Landed Cost + + + +
+

Stock Valuation Fifo Lot MRP Landed Cost

+ + +

Alpha License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

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

+
    +
  • Ecosoft
  • +
+
+
+

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:

+

newtratip

+

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

+

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

+
+
+
+ + From 3f0c5915719be16e0f6170559180e87cf8ec25fd Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 13 Sep 2024 10:07:53 +0000 Subject: [PATCH 05/17] [RMV] stock_valuation_fifo_lot_mrp_landed_cost --- .../stock_valuation_fifo_lot_mrp_landed_cost | 1 - .../setup.py | 6 - .../README.rst | 98 ---- .../__init__.py | 1 - .../__manifest__.py | 15 - .../models/__init__.py | 1 - .../models/stock_landed_cost.py | 17 - .../readme/CONTRIBUTORS.rst | 10 - .../readme/DESCRIPTION.rst | 1 - .../static/description/index.html | 437 ------------------ 10 files changed, 587 deletions(-) delete mode 120000 setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost delete mode 100644 setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/README.rst delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/__init__.py delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst delete mode 100644 stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html diff --git a/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost b/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost deleted file mode 120000 index d3d287ed..00000000 --- a/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost +++ /dev/null @@ -1 +0,0 @@ -../../../../stock_valuation_fifo_lot_mrp_landed_cost \ No newline at end of file diff --git a/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py b/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py deleted file mode 100644 index 28c57bb6..00000000 --- a/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -import setuptools - -setuptools.setup( - setup_requires=['setuptools-odoo'], - odoo_addon=True, -) diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/README.rst b/stock_valuation_fifo_lot_mrp_landed_cost/README.rst deleted file mode 100644 index 54504e94..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/README.rst +++ /dev/null @@ -1,98 +0,0 @@ -======================================== -Stock Valuation Fifo Lot MRP Landed Cost -======================================== - -.. - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! This file is generated by oca-gen-addon-readme !! - !! changes will be overwritten. !! - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:21ea12a0400da8515046cc3fa647d54482ff123046d5f5e8ae0e90b89058f0b9 - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png - :target: https://odoo-community.org/page/development-status - :alt: Alpha -.. |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%2Fstock--logistics--workflow-lightgray.png?logo=github - :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_valuation_fifo_lot_mrp_landed_cost - :alt: OCA/stock-logistics-workflow -.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_valuation_fifo_lot_mrp_landed_cost - :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/stock-logistics-workflow&target_branch=16.0 - :alt: Try me on Runboat - -|badge1| |badge2| |badge3| |badge4| |badge5| - -This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs. - -.. IMPORTANT:: - This is an alpha version, the data model and design can change at any time without warning. - Only for development or testing purpose, do not use in production. - `More details on development status `_ - -**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 -~~~~~~~ - -* Ecosoft - -Contributors -~~~~~~~~~~~~ - -* `Ecosoft `__: - - * Tharathip Chaweewongphan - * Saran Limpajitkutaporn - * Pimolnat Suntian - -* `Quartile `__: - - * Aung Ko Ko Lin - - -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-newtratip| image:: https://github.com/newtratip.png?size=40px - :target: https://github.com/newtratip - :alt: newtratip - -Current `maintainer `__: - -|maintainer-newtratip| - -This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. - -You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py b/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py deleted file mode 100644 index 0650744f..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import models diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py b/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py deleted file mode 100644 index dac94be7..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) - -{ - "name": "Stock Valuation Fifo Lot MRP Landed Cost", - "version": "16.0.1.0.0", - "category": "Warehouse Management", - "development_status": "Alpha", - "license": "AGPL-3", - "author": "Ecosoft, Odoo Community Association (OCA)", - "website": "https://github.com/OCA/stock-logistics-workflow", - "depends": ["stock_valuation_fifo_lot", "mrp_landed_costs"], - "installable": True, - "maintainers": ["newtratip"], -} diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py b/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py deleted file mode 100644 index e9907b68..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import stock_landed_cost diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py b/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py deleted file mode 100644 index 98ac48b2..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) - -from odoo import models - - -class StockLandedCost(models.Model): - _inherit = "stock.landed.cost" - - def button_validate(self): - "Update Lots/SN on stock.valuation.layer line" - res = super().button_validate() - for rec in self.stock_valuation_layer_ids: - lot_ids = self.mrp_production_ids.mapped("lot_producing_id") - if lot_ids: - rec.write({"lot_ids": lot_ids}) - return res diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst b/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst deleted file mode 100644 index e2e916aa..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst +++ /dev/null @@ -1,10 +0,0 @@ -* `Ecosoft `__: - - * Tharathip Chaweewongphan - * Saran Limpajitkutaporn - * Pimolnat Suntian - -* `Quartile `__: - - * Aung Ko Ko Lin - \ No newline at end of file diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst deleted file mode 100644 index b5e969bd..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst +++ /dev/null @@ -1 +0,0 @@ -This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs. diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html b/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html deleted file mode 100644 index 08f45c70..00000000 --- a/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html +++ /dev/null @@ -1,437 +0,0 @@ - - - - - -Stock Valuation Fifo Lot MRP Landed Cost - - - -
-

Stock Valuation Fifo Lot MRP Landed Cost

- - -

Alpha License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

-

This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs.

-
-

Important

-

This is an alpha version, the data model and design can change at any time without warning. -Only for development or testing purpose, do not use in production. -More details on development status

-
-

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

-
    -
  • Ecosoft
  • -
-
-
-

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:

-

newtratip

-

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

-

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

-
-
-
- - From 9d5d4d2809023b8d85006c94d21f2028b4e1e86b Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Sat, 14 Sep 2024 15:55:02 +0000 Subject: [PATCH 06/17] awesome improvements --- stock_valuation_fifo_lot/__manifest__.py | 4 +- stock_valuation_fifo_lot/models/__init__.py | 1 + stock_valuation_fifo_lot/models/product.py | 129 +++++++----------- stock_valuation_fifo_lot/models/stock_move.py | 95 +++++-------- .../models/stock_move_line.py | 41 ++++++ .../models/stock_valuation_layer.py | 6 + .../tests/test_stock_valuation_fifo_lot.py | 3 - .../views/stock_move_line_views.xml | 30 ++++ 8 files changed, 166 insertions(+), 143 deletions(-) create mode 100644 stock_valuation_fifo_lot/models/stock_move_line.py create mode 100644 stock_valuation_fifo_lot/views/stock_move_line_views.xml diff --git a/stock_valuation_fifo_lot/__manifest__.py b/stock_valuation_fifo_lot/__manifest__.py index 8562607b..e149a62c 100644 --- a/stock_valuation_fifo_lot/__manifest__.py +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -1,4 +1,5 @@ # Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) { @@ -7,11 +8,12 @@ "category": "Warehouse Management", "development_status": "Alpha", "license": "AGPL-3", - "author": "Ecosoft, Odoo Community Association (OCA)", + "author": "Ecosoft, Quartile, Odoo Community Association (OCA)", "website": "https://github.com/OCA/stock-logistics-workflow", "depends": ["stock_account", "stock_no_negative"], "data": [ "views/res_config_settings_views.xml", + "views/stock_move_line_views.xml", "views/stock_valuation_layer_views.xml", ], "installable": True, diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py index f797b4f1..6586f880 100644 --- a/stock_valuation_fifo_lot/models/__init__.py +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -4,4 +4,5 @@ from . import res_company from . import res_config_settings from . import stock_move +from . import stock_move_line from . import stock_valuation_layer diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index 411d95c5..bb001f4c 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -1,111 +1,82 @@ # Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from collections import defaultdict + from odoo import models +from odoo.osv import expression from odoo.tools import float_is_zero class ProductProduct(models.Model): _inherit = "product.product" + def _get_fifo_candidates_domain(self, company): + res = super()._get_fifo_candidates_domain(company) + fifo_lot = self.env.context.get("fifo_lot") + if not fifo_lot: + return res + return expression.AND([res, [("lot_ids", "in", fifo_lot.ids)]]) + def _sort_by_all_candidates(self, all_candidates, sort_by): """Hook function for other sort by""" return all_candidates def _get_fifo_candidates(self, company): all_candidates = super()._get_fifo_candidates(company) + fifo_lot = self.env.context.get("fifo_lot") + if fifo_lot: + for svl in all_candidates: + if not svl._get_unconsumed_in_move_line(fifo_lot): + all_candidates -= svl sort_by = self.env.context.get("sort_by") if sort_by == "lot_create_date": def sorting_key(candidate): if candidate.lot_ids: return min(candidate.lot_ids.mapped("create_date")) - else: - return candidate.create_date + return candidate.create_date all_candidates = all_candidates.sorted(key=sorting_key) elif sort_by is not None: all_candidates = self._sort_by_all_candidates(all_candidates, sort_by) return all_candidates - def _run_fifo(self, quantity, company): - self.ensure_one() - move_id = self._context.get("used_in_move_id") - if self.tracking == "none" or not move_id: - return super()._run_fifo(quantity, company) - - move = self.env["stock.move"].browse(move_id) - move_lines = move._get_out_move_lines() - tmp_value = 0 - tmp_remaining_qty = 0 - for move_line in move_lines: - # Find back incoming stock valuation layers - # (called candidates here) to value `quantity`. - qty_to_take_on_candidates = move_line.product_uom_id._compute_quantity( - move_line.qty_done, move.product_id.uom_id + # Depends on https://github.com/odoo/odoo/pull/180245 + def _get_qty_taken_on_candidate(self, qty_to_take_on_candidates, candidate): + fifo_lot = self.env.context.get("fifo_lot") + if fifo_lot: + candidate_move_line = candidate._get_unconsumed_in_move_line(fifo_lot) + qty_to_take_on_candidates = min( + qty_to_take_on_candidates, candidate_move_line.qty_remaining ) - # Find incoming stock valuation layers that have lot_ids on their moves - # Check with stock_move_id.lot_ids to cover the situation where the stock - # was received either before or after the installation of this module - candidates = self._get_fifo_candidates(company).filtered( - lambda l: move_line.lot_id in l.stock_move_id.lot_ids + candidate_move_line.qty_consumed += qty_to_take_on_candidates + candidate_move_line.cost_consumed += qty_to_take_on_candidates * ( + candidate.remaining_value / candidate.remaining_qty ) - for candidate in candidates: - qty_taken_on_candidate = min( - qty_to_take_on_candidates, candidate.remaining_qty - ) - - candidate_unit_cost = ( - candidate.remaining_value / candidate.remaining_qty - ) - value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost - value_taken_on_candidate = candidate.currency_id.round( - value_taken_on_candidate - ) - new_remaining_value = ( - candidate.remaining_value - value_taken_on_candidate - ) - - candidate_vals = { - "remaining_qty": candidate.remaining_qty - qty_taken_on_candidate, - "remaining_value": new_remaining_value, - } - - candidate.write(candidate_vals) + return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate) - qty_to_take_on_candidates -= qty_taken_on_candidate - tmp_value += value_taken_on_candidate - - if float_is_zero( - qty_to_take_on_candidates, - precision_rounding=self.uom_id.rounding, - ): - break - - if candidates and qty_to_take_on_candidates > 0: - tmp_value += abs(candidate.unit_cost * -qty_to_take_on_candidates) - tmp_remaining_qty += qty_to_take_on_candidates - - # Calculate standard price (Sorted by lot created date) - all_candidates = self.with_context( - sort_by="lot_create_date" - )._get_fifo_candidates(company) - new_standard_price = 0.0 - if all_candidates: - new_standard_price = all_candidates[0].unit_cost - elif candidates: - new_standard_price = candidate.unit_cost - - # Update standard price - if new_standard_price and self.cost_method == "fifo": - self.sudo().with_company(company.id).with_context( - disable_auto_svl=True - ).standard_price = new_standard_price - - # Value - vals = { - "remaining_qty": -tmp_remaining_qty, - "value": -tmp_value, - "unit_cost": tmp_value / (quantity + tmp_remaining_qty), - } + def _run_fifo(self, quantity, company): + self.ensure_one() + fifo_move = self._context.get("fifo_move") + if self.tracking == "none" or not fifo_move: + return super()._run_fifo(quantity, company) + remaining_qty = quantity + vals = defaultdict(float) + correction_move_line = self.env.context.get("correction_move_line") + move_lines = correction_move_line or fifo_move._get_out_move_lines() + for ml in move_lines: + ml_qty = fifo_move.product_uom._compute_quantity(ml.qty_done, self.uom_id) + fifo_qty = min(remaining_qty, ml_qty) + self = self.with_context(fifo_lot=ml.lot_id, fifo_qty=fifo_qty) + ml_fifo_vals = super()._run_fifo(fifo_qty, company) + for key, value in ml_fifo_vals.items(): + if key in ("remaining_qty", "value"): + vals[key] += value + continue + vals[key] = value # unit_cost + remaining_qty -= fifo_qty + if float_is_zero(remaining_qty, precision_rounding=self.uom_id.rounding): + break return vals diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index bea3ff17..29fd2e51 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -1,32 +1,25 @@ # Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import models +from odoo import Command, models class StockMove(models.Model): _inherit = "stock.move" def _prepare_common_svl_vals(self): - """ - Prepare lots/serial numbers on stock valuation report - """ + """Add lots/serials to the stock valuation layer.""" self.ensure_one() res = super()._prepare_common_svl_vals() - res.update( - { - "lot_ids": [(6, 0, self.lot_ids.ids)], - } - ) + res.update({"lot_ids": [Command.set(self.lot_ids.ids)]}) return res def _create_out_svl(self, forced_quantity=None): - """ - Send context current move to _create_out_svl function - """ + """Set the move as a context for processing in _run_fifo().""" layers = self.env["stock.valuation.layer"] for move in self: - move = move.with_context(used_in_move_id=move.id) + move = move.with_context(fifo_move=move) layer = super(StockMove, move)._create_out_svl( forced_quantity=forced_quantity ) @@ -34,65 +27,47 @@ def _create_out_svl(self, forced_quantity=None): return layers def _create_in_svl(self, forced_quantity=None): - """ - 1. Check stock move - Multiple lot on the stock move is not - allowed for incoming transfer - 2. Change product standard price to first available lot price - """ + """Change product standard price to the first available lot price.""" layers = self.env["stock.valuation.layer"] for move in self: layer = super(StockMove, move)._create_in_svl( forced_quantity=forced_quantity ) - # Calculate standard price (Sorted by lot created date) - if ( - move.product_id.cost_method == "fifo" - and move.product_id.tracking != "none" - ): - all_candidates = move.product_id.with_context( - sort_by="lot_create_date" - )._get_fifo_candidates(move.company_id) - if all_candidates: - move.product_id.sudo().with_company( - move.company_id.id - ).with_context( - disable_auto_svl=True - ).standard_price = all_candidates[ - 0 - ].unit_cost + product = move.product_id + # Calculate standard price (sorted by lot created date) + if product.cost_method != "fifo" or product.tracking == "none": + continue + product = product.with_context(sort_by="lot_create_date") + candidate = product._get_fifo_candidates(move.company_id)[:1] + if not candidate: + continue + product = product.with_company(move.company_id.id) + product = product.with_context(disable_auto_svl=True) + product.sudo().standard_price = candidate.unit_cost layers |= layer return layers def _get_price_unit(self): - """ - No PO, Get price unit from lot price + """No PO (e.g. customer returns) and get the price unit from the last consumed + incoming move line for the lot. """ self.ensure_one() if not self.company_id.use_lot_get_price_unit_fifo: return super()._get_price_unit() - if ( - hasattr(self, "purchase_line_id") - and not self.purchase_line_id - and self.product_id.cost_method == "fifo" - and len(self.lot_ids) == 1 - ): - candidate = ( - self.env["stock.valuation.layer"] - .sudo() - .search( - [ - ("product_id", "=", self.product_id.id), - ( - "lot_ids", - "in", - self.lot_ids.ids, - ), - ("remaining_qty", ">", 0), - ("company_id", "=", self.company_id.id), - ], - limit=1, - ) + if hasattr(self, "purchase_line_id") and self.purchase_line_id: + return super()._get_price_unit() + if self.product_id.cost_method == "fifo" and len(self.lot_ids) == 1: + # Get the last consumed incoming move line. + move_line = self.env["stock.move.line"].search( + [ + ("product_id", "=", self.product_id.id), + ("lot_id", "=", self.lot_ids.id), + ("qty_consumed", ">", 0), + ("company_id", "=", self.company_id.id), + ], + order="id desc", + limit=1, ) - if candidate: - return candidate.remaining_value / candidate.remaining_qty + if move_line: + return move_line.cost_consumed / move_line.qty_consumed return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py new file mode 100644 index 00000000..0c571280 --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -0,0 +1,41 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import api, fields, models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + qty_consumed = fields.Float( + help="Quantity that has gone out of the inventory for valued incoming moves " + "for FIFO products with a lot/serial.", + ) + qty_remaining = fields.Float( + compute="_compute_qty_remaining", + store=True, + help="Remaining quantity for valued incoming moves for FIFO products with a " + "lot/serial.", + ) + company_currency_id = fields.Many2one(related="company_id.currency_id") + cost_consumed = fields.Monetary( + currency_field="company_currency_id", + help="The value of the inventory that has been consumed for FIFO products with " + "a lot/serial.", + ) + + @api.depends("qty_done", "qty_consumed") + def _compute_qty_remaining(self): + for rec in self: + if rec.location_usage not in ( + "internal", + "transit", + ) and rec.location_dest_usage in ("internal", "transit"): + rec.qty_remaining = rec.qty_done - rec.qty_consumed + + def _create_correction_svl(self, move, diff): + # Pass the move line as a context value in case qty_done is overridden in a done + # transfer, to correctly identify which record should be processed in + # _run_fifo(). + move = move.with_context(correction_move_line=self) + return super()._create_correction_svl(move, diff) diff --git a/stock_valuation_fifo_lot/models/stock_valuation_layer.py b/stock_valuation_fifo_lot/models/stock_valuation_layer.py index 0b2c10d4..c805bfdb 100644 --- a/stock_valuation_fifo_lot/models/stock_valuation_layer.py +++ b/stock_valuation_fifo_lot/models/stock_valuation_layer.py @@ -11,3 +11,9 @@ class StockValuationLayer(models.Model): comodel_name="stock.lot", string="Lots/Serial Numbers", ) + + def _get_unconsumed_in_move_line(self, lot): + self.ensure_one() + return self.stock_move_id.move_line_ids.filtered( + lambda x: x.lot_id == lot and x.qty_remaining + ) diff --git a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py index 2e5d6991..d0b32828 100644 --- a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -1,6 +1,3 @@ -# Copyright 2024 Quartile (https://www.quartile.co) -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) - # Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). diff --git a/stock_valuation_fifo_lot/views/stock_move_line_views.xml b/stock_valuation_fifo_lot/views/stock_move_line_views.xml new file mode 100644 index 00000000..f97a94b3 --- /dev/null +++ b/stock_valuation_fifo_lot/views/stock_move_line_views.xml @@ -0,0 +1,30 @@ + + + stock.move.line.tree + stock.move.line + + + + + + + + + + + + stock.move.line.search + stock.move.line + + + + + + + + + From fa44d0dec57d543957677c9368719ac5642e1205 Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Mon, 16 Sep 2024 09:38:00 +0000 Subject: [PATCH 07/17] add a means of resolving conflicts, update readme --- stock_valuation_fifo_lot/__init__.py | 1 + stock_valuation_fifo_lot/__manifest__.py | 2 + stock_valuation_fifo_lot/hooks.py | 34 +++++++++++ stock_valuation_fifo_lot/models/__init__.py | 1 + stock_valuation_fifo_lot/models/product.py | 32 +++++++--- .../models/res_company.py | 6 +- .../models/res_config_settings.py | 7 ++- stock_valuation_fifo_lot/models/stock_lot.py | 32 ++++++++++ stock_valuation_fifo_lot/models/stock_move.py | 10 ++-- .../models/stock_move_line.py | 47 ++++++++++++--- .../models/stock_valuation_layer.py | 2 +- stock_valuation_fifo_lot/readme/CONFIGURE.rst | 7 ++- .../readme/CONTRIBUTORS.rst | 2 +- .../readme/DESCRIPTION.rst | 51 +++++++++++++++- stock_valuation_fifo_lot/readme/USAGE.rst | 11 ++++ .../views/res_config_settings_views.xml | 10 ++-- .../views/stock_move_line_views.xml | 59 +++++++++++++++++-- .../views/stock_package_level_views.xml | 17 ++++++ 18 files changed, 291 insertions(+), 40 deletions(-) create mode 100644 stock_valuation_fifo_lot/hooks.py create mode 100644 stock_valuation_fifo_lot/models/stock_lot.py create mode 100644 stock_valuation_fifo_lot/readme/USAGE.rst create mode 100644 stock_valuation_fifo_lot/views/stock_package_level_views.xml diff --git a/stock_valuation_fifo_lot/__init__.py b/stock_valuation_fifo_lot/__init__.py index 8ebc8a7c..0dcdc5e3 100644 --- a/stock_valuation_fifo_lot/__init__.py +++ b/stock_valuation_fifo_lot/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) from . import models +from .hooks import post_init_hook diff --git a/stock_valuation_fifo_lot/__manifest__.py b/stock_valuation_fifo_lot/__manifest__.py index e149a62c..99c5d8b8 100644 --- a/stock_valuation_fifo_lot/__manifest__.py +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -14,8 +14,10 @@ "data": [ "views/res_config_settings_views.xml", "views/stock_move_line_views.xml", + "views/stock_package_level_views.xml", "views/stock_valuation_layer_views.xml", ], "installable": True, + "post_init_hook": "post_init_hook", "maintainers": ["newtratip"], } diff --git a/stock_valuation_fifo_lot/hooks.py b/stock_valuation_fifo_lot/hooks.py new file mode 100644 index 00000000..2317c98f --- /dev/null +++ b/stock_valuation_fifo_lot/hooks.py @@ -0,0 +1,34 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import SUPERUSER_ID, api +from odoo.tools import float_is_zero + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + + svls = env["stock.valuation.layer"].search([("stock_move_id", "!=", False)]) + for svl in svls: + svl.lot_ids = svl.stock_move_id.lot_ids + if not svl.lot_ids: + continue + if svl.quantity <= 0: # Skip outgoing ones + continue + if svl.product_id.with_company(svl.company_id.id).cost_method != "fifo": + continue + svl_consumed_qty = svl_consumed_qty_bal = svl.quantity - svl.remaining_qty + if not svl_consumed_qty: + continue + svl_total_value = svl.value + sum(svl.stock_valuation_layer_ids.mapped("value")) + svl_consumed_value = svl_total_value - svl.remaining_value + product_uom = svl.product_id.uom_id + for ml in svl.stock_move_id.move_line_ids.sorted("id"): + ml_uom = ml.product_uom_id + ml_qty = ml_uom._compute_quantity(ml.qty_done, product_uom) + qty_to_allocate = min(svl_consumed_qty_bal, ml_qty) + ml.qty_consumed += product_uom._compute_quantity(qty_to_allocate, ml_uom) + svl_consumed_qty_bal -= qty_to_allocate + ml.value_consumed += svl_consumed_value * qty_to_allocate / svl_consumed_qty + if float_is_zero(svl_consumed_qty_bal, precision_rounding=ml_uom.rounding): + break diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py index 6586f880..49f67d5e 100644 --- a/stock_valuation_fifo_lot/models/__init__.py +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -3,6 +3,7 @@ from . import product from . import res_company from . import res_config_settings +from . import stock_lot from . import stock_move from . import stock_move_line from . import stock_valuation_layer diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index bb001f4c..abae8f30 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -4,7 +4,8 @@ from collections import defaultdict -from odoo import models +from odoo import _, models +from odoo.exceptions import UserError from odoo.osv import expression from odoo.tools import float_is_zero @@ -30,6 +31,15 @@ def _get_fifo_candidates(self, company): for svl in all_candidates: if not svl._get_unconsumed_in_move_line(fifo_lot): all_candidates -= svl + if not all_candidates: + raise UserError( + _( + "There is no remaining balance for FIFO valuation for the " + "lot/serial %s. Please select a Force FIFO Lot/Serial in the " + "detailed operation line." + ) + % fifo_lot.display_name + ) sort_by = self.env.context.get("sort_by") if sort_by == "lot_create_date": @@ -47,12 +57,19 @@ def sorting_key(candidate): def _get_qty_taken_on_candidate(self, qty_to_take_on_candidates, candidate): fifo_lot = self.env.context.get("fifo_lot") if fifo_lot: - candidate_move_line = candidate._get_unconsumed_in_move_line(fifo_lot) - qty_to_take_on_candidates = min( - qty_to_take_on_candidates, candidate_move_line.qty_remaining + candidate_ml = candidate._get_unconsumed_in_move_line(fifo_lot) + ml_uom = candidate_ml.product_uom_id + ml_qty_remaining = ml_uom._compute_quantity( + candidate_ml.qty_remaining, candidate_ml.product_id.uom_id ) - candidate_move_line.qty_consumed += qty_to_take_on_candidates - candidate_move_line.cost_consumed += qty_to_take_on_candidates * ( + qty_to_take_on_candidates = min(qty_to_take_on_candidates, ml_qty_remaining) + ml_qty_to_take_on_candidates = ( + candidate_ml.product_id.uom_id._compute_quantity( + qty_to_take_on_candidates, ml_uom + ) + ) + candidate_ml.qty_consumed += ml_qty_to_take_on_candidates + candidate_ml.value_consumed += ml_qty_to_take_on_candidates * ( candidate.remaining_value / candidate.remaining_qty ) return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate) @@ -67,9 +84,10 @@ def _run_fifo(self, quantity, company): correction_move_line = self.env.context.get("correction_move_line") move_lines = correction_move_line or fifo_move._get_out_move_lines() for ml in move_lines: + fifo_lot = ml.force_fifo_lot_id or ml.lot_id ml_qty = fifo_move.product_uom._compute_quantity(ml.qty_done, self.uom_id) fifo_qty = min(remaining_qty, ml_qty) - self = self.with_context(fifo_lot=ml.lot_id, fifo_qty=fifo_qty) + self = self.with_context(fifo_lot=fifo_lot, fifo_qty=fifo_qty) ml_fifo_vals = super()._run_fifo(fifo_qty, company) for key, value in ml_fifo_vals.items(): if key in ("remaining_qty", "value"): diff --git a/stock_valuation_fifo_lot/models/res_company.py b/stock_valuation_fifo_lot/models/res_company.py index 6445a538..6152a3c0 100644 --- a/stock_valuation_fifo_lot/models/res_company.py +++ b/stock_valuation_fifo_lot/models/res_company.py @@ -7,6 +7,8 @@ class ResCompany(models.Model): _inherit = "res.company" - use_lot_get_price_unit_fifo = fields.Boolean( - default=True, help="Use the FIFO price unit by lot when there is no PO." + use_lot_cost_for_new_stock = fields.Boolean( + "Use Last Lot/Serial Cost for New Stock", + default=True, + help="Use the lot/serial cost for FIFO products for non-purchase receipts.", ) diff --git a/stock_valuation_fifo_lot/models/res_config_settings.py b/stock_valuation_fifo_lot/models/res_config_settings.py index c5115315..d6a1484e 100644 --- a/stock_valuation_fifo_lot/models/res_config_settings.py +++ b/stock_valuation_fifo_lot/models/res_config_settings.py @@ -7,8 +7,9 @@ class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" - use_lot_get_price_unit_fifo = fields.Boolean( - related="company_id.use_lot_get_price_unit_fifo", + use_lot_cost_for_new_stock = fields.Boolean( + "Use Last Lot/Serial Cost for New Stock", + related="company_id.use_lot_cost_for_new_stock", readonly=False, - help="Use the FIFO price unit by lot when there is no PO.", + help="Use the lot/serial cost for FIFO products for non-purchase receipts.", ) diff --git a/stock_valuation_fifo_lot/models/stock_lot.py b/stock_valuation_fifo_lot/models/stock_lot.py new file mode 100644 index 00000000..fd58f2ce --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_lot.py @@ -0,0 +1,32 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import api, fields, models + + +class StockLot(models.Model): + _inherit = "stock.lot" + + is_force_fifo_candidate = fields.Boolean( + compute="_compute_is_force_fifo_candidate", + store=True, + help="Technical field to indicate that the lot has no on-hand quantity but has " + "a remaining value in FIFO valuation terms.", + ) + + @api.depends( + "quant_ids.quantity", "product_id.stock_move_ids.move_line_ids.qty_remaining" + ) + def _compute_is_force_fifo_candidate(self): + for lot in self: + if lot.product_id.cost_method != "fifo": + continue + if not self.env["stock.move.line"].search( + [("lot_id", "=", lot.id), ("qty_remaining", ">", 0)] + ): + continue + lot.is_force_fifo_candidate = not bool( + lot.quant_ids.filtered( + lambda x: x.location_id.usage == "internal" and x.quantity + ) + ) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 29fd2e51..7c631366 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -20,17 +20,16 @@ def _create_out_svl(self, forced_quantity=None): layers = self.env["stock.valuation.layer"] for move in self: move = move.with_context(fifo_move=move) - layer = super(StockMove, move)._create_out_svl( + layers |= super(StockMove, move)._create_out_svl( forced_quantity=forced_quantity ) - layers |= layer return layers def _create_in_svl(self, forced_quantity=None): """Change product standard price to the first available lot price.""" layers = self.env["stock.valuation.layer"] for move in self: - layer = super(StockMove, move)._create_in_svl( + layers |= super(StockMove, move)._create_in_svl( forced_quantity=forced_quantity ) product = move.product_id @@ -44,7 +43,6 @@ def _create_in_svl(self, forced_quantity=None): product = product.with_company(move.company_id.id) product = product.with_context(disable_auto_svl=True) product.sudo().standard_price = candidate.unit_cost - layers |= layer return layers def _get_price_unit(self): @@ -52,7 +50,7 @@ def _get_price_unit(self): incoming move line for the lot. """ self.ensure_one() - if not self.company_id.use_lot_get_price_unit_fifo: + if not self.company_id.use_lot_cost_for_new_stock: return super()._get_price_unit() if hasattr(self, "purchase_line_id") and self.purchase_line_id: return super()._get_price_unit() @@ -69,5 +67,5 @@ def _get_price_unit(self): limit=1, ) if move_line: - return move_line.cost_consumed / move_line.qty_consumed + return move_line.value_consumed / move_line.qty_consumed return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 0c571280..f7725ccb 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -12,26 +12,59 @@ class StockMoveLine(models.Model): "for FIFO products with a lot/serial.", ) qty_remaining = fields.Float( - compute="_compute_qty_remaining", + compute="_compute_remaining_value", store=True, help="Remaining quantity for valued incoming moves for FIFO products with a " "lot/serial.", ) company_currency_id = fields.Many2one(related="company_id.currency_id") - cost_consumed = fields.Monetary( + value_consumed = fields.Monetary( currency_field="company_currency_id", help="The value of the inventory that has been consumed for FIFO products with " "a lot/serial.", ) + value_remaining = fields.Monetary( + compute="_compute_remaining_value", + store=True, + currency_field="company_currency_id", + ) + force_fifo_lot_id = fields.Many2one( + "stock.lot", + "Force FIFO Lot/Serial", + help="Specify a lot/serial to be consumed (in FIFO costing terms) for the " + "outgoing move line, in case the selected lot has already gone out of stock " + "(in FIFO costing terms).", + ) - @api.depends("qty_done", "qty_consumed") - def _compute_qty_remaining(self): + @api.depends( + "qty_done", "qty_consumed", "move_id.stock_valuation_layer_ids.remaining_value" + ) + def _compute_remaining_value(self): for rec in self: - if rec.location_usage not in ( + if ( + rec.product_id.with_company(rec.company_id.id).cost_method != "fifo" + or not rec.lot_id + ): + continue + if rec.location_usage in ( "internal", "transit", - ) and rec.location_dest_usage in ("internal", "transit"): - rec.qty_remaining = rec.qty_done - rec.qty_consumed + ) or rec.location_dest_usage not in ("internal", "transit"): + continue + rec.qty_remaining = rec.qty_done - rec.qty_consumed + layers = rec.move_id.stock_valuation_layer_ids + remaining_qty = rec.product_uom_id._compute_quantity( + sum(layers.mapped("remaining_qty")), rec.product_id.uom_id + ) + if not remaining_qty: + rec.qty_remaining = 0 + rec.value_remaining = 0 + continue + rec.value_remaining = ( + sum(layers.mapped("remaining_value")) + * rec.qty_remaining + / remaining_qty + ) def _create_correction_svl(self, move, diff): # Pass the move line as a context value in case qty_done is overridden in a done diff --git a/stock_valuation_fifo_lot/models/stock_valuation_layer.py b/stock_valuation_fifo_lot/models/stock_valuation_layer.py index c805bfdb..2d1d53f7 100644 --- a/stock_valuation_fifo_lot/models/stock_valuation_layer.py +++ b/stock_valuation_fifo_lot/models/stock_valuation_layer.py @@ -9,7 +9,7 @@ class StockValuationLayer(models.Model): lot_ids = fields.Many2many( comodel_name="stock.lot", - string="Lots/Serial Numbers", + string="Lots/Serials", ) def _get_unconsumed_in_move_line(self, lot): diff --git a/stock_valuation_fifo_lot/readme/CONFIGURE.rst b/stock_valuation_fifo_lot/readme/CONFIGURE.rst index 61b74d16..6d102b3c 100644 --- a/stock_valuation_fifo_lot/readme/CONFIGURE.rst +++ b/stock_valuation_fifo_lot/readme/CONFIGURE.rst @@ -1,2 +1,5 @@ -If necessary, update the 'Use FIFO cost by lot' setting under Inventory > Configuration > Settings to use the lot cost instead of the standard _get_price() behavior when there is no relation to a purchase order in the stock move. -(enabled by default). +Disable the 'Use Last Lot/Serial Cost for New Stock' setting under *Inventory > +Configuration > Settings*, which is enabled by default, to use the standard +`_get_price()` behavior instead of the lot cost, for receipts of specific lots/serials +with no link to a purchase order (i.e. customer returns and positive inventory +adjustments). diff --git a/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst index e2e916aa..c57efe9a 100644 --- a/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst +++ b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst @@ -7,4 +7,4 @@ * `Quartile `__: * Aung Ko Ko Lin - \ No newline at end of file + * Yoshi Tashiro diff --git a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst index e1787438..0a692220 100644 --- a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst +++ b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst @@ -1 +1,50 @@ -This module is used to calculate FIFO cost by lot. +This module changes the scope of FIFO cost calculation to specific lots/serials (as +opposed to products), effectively achieving Specific Identification costing method. + +Example: Lot-Level Costing +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Purchase: + + - Lot A: 100 units at $10 each. + - Lot B: 100 units at $12 each. + +- Sale: + + - 50 units from Lot B. + +- COGS Calculation: + + - 50 units * $12 = $600 assigned to COGS. + +- Ending Inventory: + + - Lot A: 100 units at $10 each. + - Lot B: 50 units at $12 each. + +Main UI Changes +~~~~~~~~~~~~~~~ + +- Stock Valuation Layer + + - Adds the following field: + + - 'Lots/Serials': Taken from related stock moves. + +- Stock Move Line + + - Adds the following fields: + + - 'Qty Consumed' [*]_: Consumed quantity by outgoing moves. + - 'Value Consumed' [*]_: Consumed value by outgoing moves. + - 'Qty Remaining' [*]_: Remaining quantity (the total by product should match that + of the inventory valuation). + - 'Value Remaining' [*]_: Remaining value (the total by product should match that + of the inventory valuation). + - 'Force FIFO Lot/Serial': Used when you are stuck by not being able to find a FIFO + balance for the lot in an outgoing move line. + + .. [*] Updated only for valued incoming moves of the products with FIFO costing method. + The values here represent the theoretical figures in terms of FIFO costing, + meaning that they may differ from the actual stock situation especially for + those updated at the installation of this module. diff --git a/stock_valuation_fifo_lot/readme/USAGE.rst b/stock_valuation_fifo_lot/readme/USAGE.rst new file mode 100644 index 00000000..1cccb72f --- /dev/null +++ b/stock_valuation_fifo_lot/readme/USAGE.rst @@ -0,0 +1,11 @@ +Process an outgoing move with a lot/serial for a product of FIFO costing method, and the +costs are calculated based on the lot/serial. + +You will get a user error in case the lot/serial of your choice (in an outgoing move) +does not have a FIFO balance (i.e. there is no remaining quantity for the incoming move +lines linked to the candidate SVL; this is expected to happen for lots/serials created +before the installation of this module, unless your actual inventory operations have +been strictly FIFO). In such situations, you should select a "rogue" lot/serial (one +that still exists in terms of FIFO costing, but not in reality, due to the inconsistency +carried over from the past) in the 'Force FIFO Lot/Serial' field so that this lot/serial +is used for FIFO costing instead. diff --git a/stock_valuation_fifo_lot/views/res_config_settings_views.xml b/stock_valuation_fifo_lot/views/res_config_settings_views.xml index 975f288e..8d0ab84b 100644 --- a/stock_valuation_fifo_lot/views/res_config_settings_views.xml +++ b/stock_valuation_fifo_lot/views/res_config_settings_views.xml @@ -11,18 +11,18 @@ >
- +
diff --git a/stock_valuation_fifo_lot/views/stock_move_line_views.xml b/stock_valuation_fifo_lot/views/stock_move_line_views.xml index f97a94b3..f59507bb 100644 --- a/stock_valuation_fifo_lot/views/stock_move_line_views.xml +++ b/stock_valuation_fifo_lot/views/stock_move_line_views.xml @@ -4,12 +4,40 @@ stock.move.line - - + + + + - - - + + + + + @@ -27,4 +55,25 @@ + + stock.move.line.operations.tree + stock.move.line + + + + + + +
diff --git a/stock_valuation_fifo_lot/views/stock_package_level_views.xml b/stock_valuation_fifo_lot/views/stock_package_level_views.xml new file mode 100644 index 00000000..3324adb1 --- /dev/null +++ b/stock_valuation_fifo_lot/views/stock_package_level_views.xml @@ -0,0 +1,17 @@ + + + Package Level + stock.package_level + + + + + + + + From 647c3c0702f28266349036b3645402481a06bb18 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Fri, 20 Sep 2024 07:59:21 +0000 Subject: [PATCH 08/17] adj, add tests --- stock_valuation_fifo_lot/models/stock_move.py | 7 +- .../models/stock_move_line.py | 15 + .../tests/test_stock_valuation_fifo_lot.py | 419 ++++++++++++++---- 3 files changed, 364 insertions(+), 77 deletions(-) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 7c631366..216c5b24 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -60,12 +60,17 @@ def _get_price_unit(self): [ ("product_id", "=", self.product_id.id), ("lot_id", "=", self.lot_ids.id), + "|", ("qty_consumed", ">", 0), + ("qty_remaining", ">", 0), ("company_id", "=", self.company_id.id), ], order="id desc", limit=1, ) if move_line: - return move_line.value_consumed / move_line.qty_consumed + if move_line.qty_consumed: + return move_line.value_consumed / move_line.qty_consumed + else: + return move_line.value_remaining / move_line.qty_remaining return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index f7725ccb..4ef0e97d 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -50,6 +50,21 @@ def _compute_remaining_value(self): "internal", "transit", ) or rec.location_dest_usage not in ("internal", "transit"): + layers = rec.move_id.stock_valuation_layer_ids + remaining_qty_layers = layers.filtered(lambda l: l.remaining_qty > 0) + if not remaining_qty_layers: + rec.qty_remaining = 0 + rec.value_remaining = 0 + continue + rec.qty_remaining = rec.product_uom_id._compute_quantity( + sum(remaining_qty_layers.mapped("remaining_qty")), + rec.product_id.uom_id, + ) + rec.value_remaining = ( + sum(remaining_qty_layers.mapped("remaining_value")) + * sum(remaining_qty_layers.mapped("remaining_qty")) + / rec.qty_remaining + ) continue rec.qty_remaining = rec.qty_done - rec.qty_consumed layers = rec.move_id.stock_valuation_layer_ids diff --git a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py index d0b32828..7c69f666 100644 --- a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -1,7 +1,6 @@ # Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - from odoo.tests.common import Form, TransactionCase @@ -30,111 +29,379 @@ def setUpClass(cls): cls.picking_type_in = cls.env.ref("stock.picking_type_in") cls.picking_type_out = cls.env.ref("stock.picking_type_out") - def create_picking(self, location, location_dest, picking_type): - return self.env["stock.picking"].create( + def create_picking( + self, + location, + location_dest, + picking_type, + lot_numbers, + price_unit=0.0, + is_receipt=True, + ): + picking = self.env["stock.picking"].create( { "location_id": location.id, "location_dest_id": location_dest.id, "picking_type_id": picking_type.id, } ) - - def create_stock_move(self, picking, product, price=0.0): move = self.env["stock.move"].create( { - "name": "Move", - "product_id": product.id, + "name": "Test", + "product_id": self.product.id, "location_id": picking.location_id.id, "location_dest_id": picking.location_dest_id.id, - "product_uom": product.uom_id.id, + "product_uom": self.product.uom_id.id, "product_uom_qty": 5.0, "picking_id": picking.id, } ) - if price: - move.write({"price_unit": price}) - return move + if price_unit: + move.write({"price_unit": price_unit}) - def create_stock_move_line(self, move, picking, lot_name=False): - move_line = self.env["stock.move.line"].create( - { - "move_id": move.id, - "picking_id": picking.id, - "product_id": move.product_id.id, - "location_id": move.location_id.id, - "location_dest_id": move.location_dest_id.id, - "product_uom_id": move.product_uom.id, - "qty_done": move.product_uom_qty, - "lot_name": lot_name, - } + for lot in lot_numbers: + move_line = self.env["stock.move.line"].create( + { + "move_id": move.id, + "picking_id": picking.id, + "product_id": self.product.id, + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + "product_uom_id": move.product_uom.id, + "qty_done": move.product_uom_qty, + } + ) + if is_receipt: + move_line.lot_name = lot + else: + lot = self.env["stock.lot"].search( + [("product_id", "=", self.product.id), ("name", "=", lot)], limit=1 + ) + move_line.lot_id = lot.id + picking.action_confirm() + picking.action_assign() + picking._action_done() + return picking, move + + def transfer_return(self, original_picking, return_qty): + """Handles product return for a given picking""" + return_picking_wizard_form = Form( + self.env["stock.return.picking"].with_context( + active_ids=original_picking.ids, + active_id=original_picking.id, + active_model="stock.picking", + ) + ) + return_picking_wizard = return_picking_wizard_form.save() + return_picking_wizard.product_return_moves.write({"quantity": return_qty}) + return_picking_wizard_action = return_picking_wizard.create_returns() + return_picking = self.env["stock.picking"].browse( + return_picking_wizard_action["res_id"] ) - return move_line + return_move = return_picking.move_ids + return_move.move_line_ids.qty_done = return_qty + return_picking.button_validate() + return return_move def test_stock_valuation_fifo_lot(self): - receipt_picking_1 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for the first receipt should be 500.0", ) - move = self.create_stock_move(receipt_picking_1, self.product, 100) - self.create_stock_move_line(move, receipt_picking_1, "11111") - receipt_picking_1.action_confirm() - receipt_picking_1.action_assign() - receipt_picking_1._action_done() - self.assertEqual(move.stock_valuation_layer_ids.value, 500) + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for the second receipt should be 1000.0", + ) + self.assertEqual( + self.product.standard_price, + 100.0, + "Standard price should be set to 100.0 after first receipt", + ) + + # Create delivery for lot 002 + _, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out.stock_valuation_layer_ids.value), + 1000.0, + "Stock valuation for the delivery should be 1000.0", + ) - receipt_picking_2 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + # Test return receipt + receipt_picking_3, move = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["003"], + 300.0, + ) + self.assertEqual( + move.stock_valuation_layer_ids.value, + 1500.0, + "Stock valuation for the third receipt should be 1500.0", ) - move = self.create_stock_move(receipt_picking_2, self.product, 200) - self.create_stock_move_line(move, receipt_picking_2, "22222") - receipt_picking_2.action_confirm() - receipt_picking_2.action_assign() - receipt_picking_2._action_done() - self.assertEqual(move.stock_valuation_layer_ids.value, 1000) - self.assertEqual(self.product.standard_price, 100) + return_move = self.transfer_return(receipt_picking_3, 5.0) + self.assertEqual( + abs(return_move.stock_valuation_layer_ids.value), + 1500.0, + "Stock valuation after return should be 1500.0", + ) - delivery_picking1 = self.create_picking( - self.stock_location, self.customer_location, self.picking_type_out + def test_receive_deliver_return_lot(self): + receipt_picking, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001", "002", "003"], + 100.0, ) - move = self.create_stock_move(delivery_picking1, self.product) - move_line = self.create_stock_move_line(move, delivery_picking1) - lot_id = self.env["stock.lot"].search( - [("name", "=", "22222"), ("product_id", "=", self.product.id)] + self.assertEqual(len(receipt_picking.move_line_ids), 3) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 1500.0, + "Stock valuation for multiple receipts should be 1500.0", ) - move_line.write({"lot_id": lot_id}) + self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 15.0) - delivery_picking1.action_confirm() - delivery_picking1.action_assign() - delivery_picking1._action_done() - self.assertEqual(abs(move.stock_valuation_layer_ids.value), 1000) + # Deliver lot 002 + delivery_picking, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for delivery of lot 002 should be 500.0", + ) + self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 10.0) - # Test return delivery - receipt_picking_3 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + # Return lot 002 + move_in_2 = self.transfer_return(delivery_picking, 5.0) + self.assertEqual( + move_in_2.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for returned lot 002 should be 500.0", ) - move = self.create_stock_move(receipt_picking_3, self.product, 300) - self.create_stock_move_line(move, receipt_picking_3, "33333") + self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 5.0) - receipt_picking_3.action_confirm() - receipt_picking_3.action_assign() - receipt_picking_3._action_done() - self.assertEqual(move.stock_valuation_layer_ids.value, 1500) + # Deliver lot 002 again + _, move_out_2 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out_2.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for second delivery of lot 002 should be 500.0", + ) + self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 0.0) - return_picking_wizard_form = Form( - self.env["stock.return.picking"].with_context( - active_ids=receipt_picking_3.ids, - active_id=receipt_picking_3.id, - active_model="stock.picking", - ) + def test_cost_tracking_by_lot(self): + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, ) - return_picking_wizard = return_picking_wizard_form.save() - return_picking_wizard.product_return_moves.write({"quantity": 5}) - return_picking_wizard_action = return_picking_wizard.create_returns() - return_picking = self.env["stock.picking"].browse( - return_picking_wizard_action["res_id"] + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for lot 001 should be 500.0", + ) + + _, move_in_2 = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + self.assertEqual( + move_in_2.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for lot 002 should be 1000.0", + ) + + # Deliver lot 001 + self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) + + # Deliver lot 002 + delivery_picking_2, _ = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + + # Return lot 002 + move_in = self.transfer_return(delivery_picking_2, 5.0) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for returned lot 002 should be 1000.0", + ) + + def test_change_qty_done_in_done_move_line(self): + receipt_picking, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 500.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 2500.0, + "Stock valuation for the first receipt should be 2500.0", + ) + # Change qty_done of the move line (increase) + move_line = receipt_picking.move_line_ids[0] + move_line.qty_done += 2.0 + self.assertEqual( + move_line.qty_done, + 7.0, + "The qty_done of the incoming move line should be increased to 7.0", + ) + self.assertEqual( + sum(move_in.stock_valuation_layer_ids.mapped("value")), + 3500.0, + "Stock valuation should reflect the increased qty_done", + ) + + # Change qty_done of the move line (decrease) + move_line.qty_done -= 1.0 + self.assertEqual( + move_line.qty_done, + 6.0, + "The qty_done of the incoming move line should be decreased to 6.0", + ) + self.assertEqual( + sum(move_in.stock_valuation_layer_ids.mapped("value")), + 3000.0, + "Stock valuation should reflect the decreased qty_done", + ) + + delivery_picking, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) + self.assertEqual( + abs(sum(move_out.stock_valuation_layer_ids.mapped("value"))), + 2500.0, + "Stock valuation for the outgoing should be 2500.0", + ) + + # Change qty_done of the move line (increase) + move_line = delivery_picking.move_line_ids[0] + move_line.qty_done -= 2.0 + self.assertEqual( + move_line.qty_done, + 3.0, + "The qty_done of the outgoing move line should be decreased to 3.0", + ) + self.assertEqual( + abs(sum(move_out.stock_valuation_layer_ids.mapped("value"))), + 1500.0, + "Stock valuation should reflect the decreased qty_done", + ) + + # Change qty_done of the move line (decrease) + move_line.qty_done += 1.0 + self.assertEqual( + move_line.qty_done, + 4.0, + "The qty_done of the outgoing move line should be increased to 4.0", + ) + self.assertEqual( + abs(sum(move_out.stock_valuation_layer_ids.mapped("value"))), + 2000.0, + "Stock valuation should reflect the increased qty_done", + ) + + def test_inventory_adjustment_after_multiple_receipts(self): + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for lot 0001 should be 500.0", + ) + + _, move_in_2 = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + self.assertEqual( + move_in_2.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for lot 0002 should be 1000.0", + ) + lot = self.env["stock.lot"].search( + [("name", "=", "002"), ("product_id", "=", self.product.id)], limit=1 + ) + inventory_quant = self.env["stock.quant"].search( + [ + ("location_id", "=", self.stock_location.id), + ("product_id", "=", self.product.id), + ("lot_id", "=", lot.id), + ] + ) + inventory_quant.inventory_quantity = 10.0 + inventory_quant.action_apply_inventory() + move = self.env["stock.move"].search( + [("product_id", "=", self.product.id), ("is_inventory", "=", True)], + limit=1, + ) + self.assertEqual( + move.stock_valuation_layer_ids.value, + 1000.0, + "Stock valuation for lot 002 should be 1000.0", ) - return_move = return_picking.move_ids - return_move.move_line_ids.qty_done = 5 - return_picking.button_validate() - self.assertEqual(abs(return_move.stock_valuation_layer_ids.value), 1500) From 88e3356c24cccdd780a99550858e4a995339000b Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Sat, 21 Sep 2024 05:32:37 +0000 Subject: [PATCH 09/17] adj --- stock_valuation_fifo_lot/models/product.py | 2 +- stock_valuation_fifo_lot/models/stock_move.py | 5 ++-- .../models/stock_move_line.py | 27 ++++++++++--------- .../readme/DESCRIPTION.rst | 7 +++-- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index abae8f30..ab60aec3 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -85,7 +85,7 @@ def _run_fifo(self, quantity, company): move_lines = correction_move_line or fifo_move._get_out_move_lines() for ml in move_lines: fifo_lot = ml.force_fifo_lot_id or ml.lot_id - ml_qty = fifo_move.product_uom._compute_quantity(ml.qty_done, self.uom_id) + ml_qty = ml.product_uom_id._compute_quantity(ml.qty_done, self.uom_id) fifo_qty = min(remaining_qty, ml_qty) self = self.with_context(fifo_lot=fifo_lot, fifo_qty=fifo_qty) ml_fifo_vals = super()._run_fifo(fifo_qty, company) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 216c5b24..4dadf265 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -55,7 +55,7 @@ def _get_price_unit(self): if hasattr(self, "purchase_line_id") and self.purchase_line_id: return super()._get_price_unit() if self.product_id.cost_method == "fifo" and len(self.lot_ids) == 1: - # Get the last consumed incoming move line. + # Get the most recent incoming move line for the lot. move_line = self.env["stock.move.line"].search( [ ("product_id", "=", self.product_id.id), @@ -71,6 +71,5 @@ def _get_price_unit(self): if move_line: if move_line.qty_consumed: return move_line.value_consumed / move_line.qty_consumed - else: - return move_line.value_remaining / move_line.qty_remaining + return move_line.value_remaining / move_line.qty_remaining return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 4ef0e97d..0b32cfd8 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -50,26 +50,29 @@ def _compute_remaining_value(self): "internal", "transit", ) or rec.location_dest_usage not in ("internal", "transit"): - layers = rec.move_id.stock_valuation_layer_ids - remaining_qty_layers = layers.filtered(lambda l: l.remaining_qty > 0) - if not remaining_qty_layers: - rec.qty_remaining = 0 - rec.value_remaining = 0 + # Specifically for the case where qty_done of the outgoing + # stock move line with done state is reduced, which creates + # a positive stock valuation layer for the outgoing move. + layers = rec.move_id.stock_valuation_layer_ids.filtered( + lambda l: l.remaining_qty > 0 + ) + if not layers: + rec.qty_remaining = 0.0 + rec.value_remaining = 0.0 continue - rec.qty_remaining = rec.product_uom_id._compute_quantity( - sum(remaining_qty_layers.mapped("remaining_qty")), - rec.product_id.uom_id, + rec.qty_remaining = rec.product_id.uom_id._compute_quantity( + sum(layers.mapped("remaining_qty")), rec.product_uom_id ) rec.value_remaining = ( - sum(remaining_qty_layers.mapped("remaining_value")) - * sum(remaining_qty_layers.mapped("remaining_qty")) + sum(layers.mapped("remaining_value")) + * sum(layers.mapped("remaining_qty")) / rec.qty_remaining ) continue rec.qty_remaining = rec.qty_done - rec.qty_consumed layers = rec.move_id.stock_valuation_layer_ids - remaining_qty = rec.product_uom_id._compute_quantity( - sum(layers.mapped("remaining_qty")), rec.product_id.uom_id + remaining_qty = rec.product_id.uom_id._compute_quantity( + sum(layers.mapped("remaining_qty")), rec.product_uom_id ) if not remaining_qty: rec.qty_remaining = 0 diff --git a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst index 0a692220..863cd871 100644 --- a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst +++ b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst @@ -44,7 +44,10 @@ Main UI Changes - 'Force FIFO Lot/Serial': Used when you are stuck by not being able to find a FIFO balance for the lot in an outgoing move line. - .. [*] Updated only for valued incoming moves of the products with FIFO costing method. + .. [*] Updated only for valued incoming moves and outgoing moves where the qty_done has been + reduced in the completed state for products with FIFO costing method. For these outgoing moves, + the system generates positive stock valuation layers with remaining_qty and remaining_value, + which need to be reflected in the related move line. The values here represent the theoretical figures in terms of FIFO costing, meaning that they may differ from the actual stock situation especially for - those updated at the installation of this module. + those updated at the installation of this module. \ No newline at end of file From afbb94d70913f9f7ce0aceac9ccf45df960efef8 Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Sat, 21 Sep 2024 10:21:47 +0000 Subject: [PATCH 10/17] add qty_base to sml, refactor code --- stock_valuation_fifo_lot/hooks.py | 26 +++++++--- stock_valuation_fifo_lot/models/product.py | 19 +++---- stock_valuation_fifo_lot/models/stock_move.py | 17 +++++-- .../models/stock_move_line.py | 50 +++++++------------ .../views/stock_move_line_views.xml | 6 +++ 5 files changed, 62 insertions(+), 56 deletions(-) diff --git a/stock_valuation_fifo_lot/hooks.py b/stock_valuation_fifo_lot/hooks.py index 2317c98f..693d6a91 100644 --- a/stock_valuation_fifo_lot/hooks.py +++ b/stock_valuation_fifo_lot/hooks.py @@ -13,22 +13,32 @@ def post_init_hook(cr, registry): svl.lot_ids = svl.stock_move_id.lot_ids if not svl.lot_ids: continue - if svl.quantity <= 0: # Skip outgoing ones + if svl.quantity <= 0: # Skip outgoing svls continue - if svl.product_id.with_company(svl.company_id.id).cost_method != "fifo": + if not svl.product_id._is_fifo(): continue + product_uom = svl.product_id.uom_id + if svl.stock_move_id._is_out(): + # The case where outgoing done qty is reduced + # Let the first move line represent for such adjustments. + ml = svl.stock_move_id.move_line_ids[0] + ml.qty_base += svl.quantity + else: + for ml in svl.stock_move_id.move_line_ids: + ml.qty_base = ml.product_uom_id._compute_quantity( + ml.qty_done, product_uom + ) svl_consumed_qty = svl_consumed_qty_bal = svl.quantity - svl.remaining_qty if not svl_consumed_qty: continue svl_total_value = svl.value + sum(svl.stock_valuation_layer_ids.mapped("value")) svl_consumed_value = svl_total_value - svl.remaining_value - product_uom = svl.product_id.uom_id for ml in svl.stock_move_id.move_line_ids.sorted("id"): - ml_uom = ml.product_uom_id - ml_qty = ml_uom._compute_quantity(ml.qty_done, product_uom) - qty_to_allocate = min(svl_consumed_qty_bal, ml_qty) - ml.qty_consumed += product_uom._compute_quantity(qty_to_allocate, ml_uom) + qty_to_allocate = min(svl_consumed_qty_bal, ml.qty_base) + ml.qty_consumed += qty_to_allocate svl_consumed_qty_bal -= qty_to_allocate ml.value_consumed += svl_consumed_value * qty_to_allocate / svl_consumed_qty - if float_is_zero(svl_consumed_qty_bal, precision_rounding=ml_uom.rounding): + if float_is_zero( + svl_consumed_qty_bal, precision_rounding=product_uom.rounding + ): break diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index ab60aec3..0044d66e 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -13,6 +13,10 @@ class ProductProduct(models.Model): _inherit = "product.product" + def _is_fifo(self): + self.ensure_one() + return self.with_company(self.company_id.id).cost_method == "fifo" + def _get_fifo_candidates_domain(self, company): res = super()._get_fifo_candidates_domain(company) fifo_lot = self.env.context.get("fifo_lot") @@ -58,18 +62,11 @@ def _get_qty_taken_on_candidate(self, qty_to_take_on_candidates, candidate): fifo_lot = self.env.context.get("fifo_lot") if fifo_lot: candidate_ml = candidate._get_unconsumed_in_move_line(fifo_lot) - ml_uom = candidate_ml.product_uom_id - ml_qty_remaining = ml_uom._compute_quantity( - candidate_ml.qty_remaining, candidate_ml.product_id.uom_id - ) - qty_to_take_on_candidates = min(qty_to_take_on_candidates, ml_qty_remaining) - ml_qty_to_take_on_candidates = ( - candidate_ml.product_id.uom_id._compute_quantity( - qty_to_take_on_candidates, ml_uom - ) + qty_to_take_on_candidates = min( + qty_to_take_on_candidates, candidate_ml.qty_remaining ) - candidate_ml.qty_consumed += ml_qty_to_take_on_candidates - candidate_ml.value_consumed += ml_qty_to_take_on_candidates * ( + candidate_ml.qty_consumed += qty_to_take_on_candidates + candidate_ml.value_consumed += qty_to_take_on_candidates * ( candidate.remaining_value / candidate.remaining_qty ) return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 4dadf265..85e5ae76 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -26,16 +26,23 @@ def _create_out_svl(self, forced_quantity=None): return layers def _create_in_svl(self, forced_quantity=None): - """Change product standard price to the first available lot price.""" + correction_ml = self.env.context.get("correction_move_line") + if forced_quantity and correction_ml: + correction_ml.qty_base += forced_quantity + return super()._create_in_svl(forced_quantity=forced_quantity) layers = self.env["stock.valuation.layer"] for move in self: - layers |= super(StockMove, move)._create_in_svl( + layer = super(StockMove, move)._create_in_svl( forced_quantity=forced_quantity ) - product = move.product_id + layers |= layer # Calculate standard price (sorted by lot created date) - if product.cost_method != "fifo" or product.tracking == "none": + product = move.product_id + if not product._is_fifo() or product.tracking == "none": continue + for ml in layer.stock_move_id.move_line_ids: + ml.qty_base = ml.qty_done + # Change product standard price to the first available lot price. product = product.with_context(sort_by="lot_create_date") candidate = product._get_fifo_candidates(move.company_id)[:1] if not candidate: @@ -54,7 +61,7 @@ def _get_price_unit(self): return super()._get_price_unit() if hasattr(self, "purchase_line_id") and self.purchase_line_id: return super()._get_price_unit() - if self.product_id.cost_method == "fifo" and len(self.lot_ids) == 1: + if self.product_id._is_fifo() and len(self.lot_ids) == 1: # Get the most recent incoming move line for the lot. move_line = self.env["stock.move.line"].search( [ diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 0b32cfd8..816cfd98 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -7,6 +7,10 @@ class StockMoveLine(models.Model): _inherit = "stock.move.line" + qty_base = fields.Float( + help="Base quantity for FIFO allocation; this should be equal to the summary " + "of the quantity of the relevant incoming stock valuation layers.", + ) qty_consumed = fields.Float( help="Quantity that has gone out of the inventory for valued incoming moves " "for FIFO products with a lot/serial.", @@ -37,43 +41,15 @@ class StockMoveLine(models.Model): ) @api.depends( - "qty_done", "qty_consumed", "move_id.stock_valuation_layer_ids.remaining_value" + "qty_base", "qty_consumed", "move_id.stock_valuation_layer_ids.remaining_value" ) def _compute_remaining_value(self): for rec in self: - if ( - rec.product_id.with_company(rec.company_id.id).cost_method != "fifo" - or not rec.lot_id - ): - continue - if rec.location_usage in ( - "internal", - "transit", - ) or rec.location_dest_usage not in ("internal", "transit"): - # Specifically for the case where qty_done of the outgoing - # stock move line with done state is reduced, which creates - # a positive stock valuation layer for the outgoing move. - layers = rec.move_id.stock_valuation_layer_ids.filtered( - lambda l: l.remaining_qty > 0 - ) - if not layers: - rec.qty_remaining = 0.0 - rec.value_remaining = 0.0 - continue - rec.qty_remaining = rec.product_id.uom_id._compute_quantity( - sum(layers.mapped("remaining_qty")), rec.product_uom_id - ) - rec.value_remaining = ( - sum(layers.mapped("remaining_value")) - * sum(layers.mapped("remaining_qty")) - / rec.qty_remaining - ) + if not rec.product_id._is_fifo() or not rec.lot_id: continue - rec.qty_remaining = rec.qty_done - rec.qty_consumed + rec.qty_remaining = rec.qty_base - rec.qty_consumed layers = rec.move_id.stock_valuation_layer_ids - remaining_qty = rec.product_id.uom_id._compute_quantity( - sum(layers.mapped("remaining_qty")), rec.product_uom_id - ) + remaining_qty = sum(layers.mapped("remaining_qty")) if not remaining_qty: rec.qty_remaining = 0 rec.value_remaining = 0 @@ -84,6 +60,16 @@ def _compute_remaining_value(self): / remaining_qty ) + def _action_done(self): + res = super()._action_done() + for ml in self.exists(): + if not ml.product_id._is_fifo(): + continue + ml.qty_base = ml.product_uom_id._compute_quantity( + ml.qty_done, ml.product_id.uom_id + ) + return res + def _create_correction_svl(self, move, diff): # Pass the move line as a context value in case qty_done is overridden in a done # transfer, to correctly identify which record should be processed in diff --git a/stock_valuation_fifo_lot/views/stock_move_line_views.xml b/stock_valuation_fifo_lot/views/stock_move_line_views.xml index f59507bb..c3cb4e82 100644 --- a/stock_valuation_fifo_lot/views/stock_move_line_views.xml +++ b/stock_valuation_fifo_lot/views/stock_move_line_views.xml @@ -13,6 +13,12 @@ + Date: Sat, 21 Sep 2024 15:54:37 +0000 Subject: [PATCH 11/17] fixup! --- stock_valuation_fifo_lot/models/product.py | 14 ++++-- stock_valuation_fifo_lot/models/stock_move.py | 49 ++++++++++++++----- .../models/stock_move_line.py | 14 ++---- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index 0044d66e..29436938 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -78,12 +78,18 @@ def _run_fifo(self, quantity, company): return super()._run_fifo(quantity, company) remaining_qty = quantity vals = defaultdict(float) - correction_move_line = self.env.context.get("correction_move_line") - move_lines = correction_move_line or fifo_move._get_out_move_lines() + correction_ml = self.env.context.get("correction_move_line") + move_lines = correction_ml or fifo_move._get_out_move_lines() + moved_qty = 0 for ml in move_lines: fifo_lot = ml.force_fifo_lot_id or ml.lot_id - ml_qty = ml.product_uom_id._compute_quantity(ml.qty_done, self.uom_id) - fifo_qty = min(remaining_qty, ml_qty) + if correction_ml: + moved_qty = quantity + else: + moved_qty = ml.product_uom_id._compute_quantity( + ml.qty_done, self.uom_id + ) + fifo_qty = min(remaining_qty, moved_qty) self = self.with_context(fifo_lot=fifo_lot, fifo_qty=fifo_qty) ml_fifo_vals = super()._run_fifo(fifo_qty, company) for key, value in ml_fifo_vals.items(): diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 85e5ae76..a6470d13 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -8,11 +8,30 @@ class StockMove(models.Model): _inherit = "stock.move" + def _action_done(self, cancel_backorder=False): + res = super()._action_done(cancel_backorder=cancel_backorder) + for move in res: + if not move.product_id._is_fifo(): + continue + if not move._is_in(): + continue + for ml in move._get_in_move_lines(): + ml.qty_base = ml.product_uom_id._compute_quantity( + ml.qty_done, ml.product_id.uom_id + ) + return res + + def _get_move_lots(self): + self.ensure_one() + correction_ml = self.env.context.get("correction_move_line") + return correction_ml.lot_id if correction_ml else self.lot_ids + def _prepare_common_svl_vals(self): """Add lots/serials to the stock valuation layer.""" self.ensure_one() res = super()._prepare_common_svl_vals() - res.update({"lot_ids": [Command.set(self.lot_ids.ids)]}) + lots = self._get_move_lots() + res.update({"lot_ids": [Command.set(lots.ids)]}) return res def _create_out_svl(self, forced_quantity=None): @@ -57,26 +76,34 @@ def _get_price_unit(self): incoming move line for the lot. """ self.ensure_one() - if not self.company_id.use_lot_cost_for_new_stock: + if ( + not self.company_id.use_lot_cost_for_new_stock + or not self.product_id._is_fifo() + ): return super()._get_price_unit() if hasattr(self, "purchase_line_id") and self.purchase_line_id: return super()._get_price_unit() - if self.product_id._is_fifo() and len(self.lot_ids) == 1: - # Get the most recent incoming move line for the lot. - move_line = self.env["stock.move.line"].search( + lots = self._get_move_lots() + if not len(lots) == 1: + return super()._get_price_unit() + # Get the most recent incoming move line for the lot. + move_line = ( + self.env["stock.move.line"] + .search( [ ("product_id", "=", self.product_id.id), - ("lot_id", "=", self.lot_ids.id), + ("lot_id", "=", lots.id), "|", ("qty_consumed", ">", 0), ("qty_remaining", ">", 0), ("company_id", "=", self.company_id.id), ], order="id desc", - limit=1, ) - if move_line: - if move_line.qty_consumed: - return move_line.value_consumed / move_line.qty_consumed - return move_line.value_remaining / move_line.qty_remaining + .filtered(lambda x: x.move_id._is_in())[:1] + ) + if move_line: + if move_line.qty_consumed: + return move_line.value_consumed / move_line.qty_consumed + return move_line.value_remaining / move_line.qty_remaining return super()._get_price_unit() diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 816cfd98..e55b4e7f 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -48,7 +48,9 @@ def _compute_remaining_value(self): if not rec.product_id._is_fifo() or not rec.lot_id: continue rec.qty_remaining = rec.qty_base - rec.qty_consumed - layers = rec.move_id.stock_valuation_layer_ids + layers = rec.move_id.stock_valuation_layer_ids.filtered( + lambda x: x.lot_ids in [rec.lot_id] + ) remaining_qty = sum(layers.mapped("remaining_qty")) if not remaining_qty: rec.qty_remaining = 0 @@ -60,16 +62,6 @@ def _compute_remaining_value(self): / remaining_qty ) - def _action_done(self): - res = super()._action_done() - for ml in self.exists(): - if not ml.product_id._is_fifo(): - continue - ml.qty_base = ml.product_uom_id._compute_quantity( - ml.qty_done, ml.product_id.uom_id - ) - return res - def _create_correction_svl(self, move, diff): # Pass the move line as a context value in case qty_done is overridden in a done # transfer, to correctly identify which record should be processed in From c227feb70cf862e6394512c274f7d2f8278fc79a Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Sun, 22 Sep 2024 01:03:02 +0000 Subject: [PATCH 12/17] fixup! --- stock_valuation_fifo_lot/models/stock_move.py | 1 + stock_valuation_fifo_lot/models/stock_move_line.py | 6 +++++- .../tests/test_stock_valuation_fifo_lot.py | 7 ++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index a6470d13..d2330f38 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -19,6 +19,7 @@ def _action_done(self, cancel_backorder=False): ml.qty_base = ml.product_uom_id._compute_quantity( ml.qty_done, ml.product_id.uom_id ) + # ml._compute_remaining_value() return res def _get_move_lots(self): diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index e55b4e7f..2a770d98 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -41,7 +41,11 @@ class StockMoveLine(models.Model): ) @api.depends( - "qty_base", "qty_consumed", "move_id.stock_valuation_layer_ids.remaining_value" + "lot_id", + "qty_base", + "qty_consumed", + "move_id.stock_valuation_layer_ids", + "move_id.stock_valuation_layer_ids.remaining_value", ) def _compute_remaining_value(self): for rec in self: diff --git a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py index 7c69f666..8ff1b4e4 100644 --- a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -4,7 +4,7 @@ from odoo.tests.common import Form, TransactionCase -class TestStockAccountFifoReturnOrigin(TransactionCase): +class TestStockValuationFifoLot(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -45,6 +45,7 @@ def create_picking( "picking_type_id": picking_type.id, } ) + move_line_qty = 5.0 move = self.env["stock.move"].create( { "name": "Test", @@ -52,7 +53,7 @@ def create_picking( "location_id": picking.location_id.id, "location_dest_id": picking.location_dest_id.id, "product_uom": self.product.uom_id.id, - "product_uom_qty": 5.0, + "product_uom_qty": move_line_qty * len(lot_numbers), "picking_id": picking.id, } ) @@ -68,7 +69,7 @@ def create_picking( "location_id": move.location_id.id, "location_dest_id": move.location_dest_id.id, "product_uom_id": move.product_uom.id, - "qty_done": move.product_uom_qty, + "qty_done": move_line_qty, } ) if is_receipt: From 21c8a7e6c7787cd61b81092aeb7a91be168e3622 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Sun, 22 Sep 2024 03:33:02 +0000 Subject: [PATCH 13/17] fix --- stock_valuation_fifo_lot/models/stock_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 2a770d98..35b1c597 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -53,7 +53,7 @@ def _compute_remaining_value(self): continue rec.qty_remaining = rec.qty_base - rec.qty_consumed layers = rec.move_id.stock_valuation_layer_ids.filtered( - lambda x: x.lot_ids in [rec.lot_id] + lambda x: rec.lot_id in x.lot_ids ) remaining_qty = sum(layers.mapped("remaining_qty")) if not remaining_qty: From d6f910cd162474f79bd4ceb35e27cc6e69c3fc7d Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Sun, 22 Sep 2024 12:16:54 +0000 Subject: [PATCH 14/17] fix post_init_hook, prevent negative valuation --- stock_valuation_fifo_lot/hooks.py | 57 +++++++++---------- stock_valuation_fifo_lot/models/stock_move.py | 20 +++++-- .../models/stock_move_line.py | 29 +++++----- .../readme/DESCRIPTION.rst | 19 ++++--- 4 files changed, 69 insertions(+), 56 deletions(-) diff --git a/stock_valuation_fifo_lot/hooks.py b/stock_valuation_fifo_lot/hooks.py index 693d6a91..fb20d3dc 100644 --- a/stock_valuation_fifo_lot/hooks.py +++ b/stock_valuation_fifo_lot/hooks.py @@ -8,37 +8,32 @@ def post_init_hook(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) - svls = env["stock.valuation.layer"].search([("stock_move_id", "!=", False)]) - for svl in svls: - svl.lot_ids = svl.stock_move_id.lot_ids - if not svl.lot_ids: + moves = env["stock.move"].search([("stock_valuation_layer_ids", "!=", False)]) + for move in moves: + if not move.product_id._is_fifo() or not move.lot_ids: continue - if svl.quantity <= 0: # Skip outgoing svls + svls = move.stock_valuation_layer_ids + svls.lot_ids = move.lot_ids + if move._is_out(): + remaining_qty = sum(svls.mapped("remaining_qty")) + if remaining_qty: + # The case where outgoing done qty is reduced + # Let the first move line take such adjustments. + move.move_line_ids[0].qty_base = remaining_qty continue - if not svl.product_id._is_fifo(): - continue - product_uom = svl.product_id.uom_id - if svl.stock_move_id._is_out(): - # The case where outgoing done qty is reduced - # Let the first move line represent for such adjustments. - ml = svl.stock_move_id.move_line_ids[0] - ml.qty_base += svl.quantity - else: - for ml in svl.stock_move_id.move_line_ids: - ml.qty_base = ml.product_uom_id._compute_quantity( - ml.qty_done, product_uom - ) - svl_consumed_qty = svl_consumed_qty_bal = svl.quantity - svl.remaining_qty - if not svl_consumed_qty: - continue - svl_total_value = svl.value + sum(svl.stock_valuation_layer_ids.mapped("value")) - svl_consumed_value = svl_total_value - svl.remaining_value - for ml in svl.stock_move_id.move_line_ids.sorted("id"): - qty_to_allocate = min(svl_consumed_qty_bal, ml.qty_base) + consumed_qty = consumed_qty_bal = sum(svls.mapped("quantity")) - sum( + svls.mapped("remaining_qty") + ) + total_value = sum(svls.mapped("value")) + sum( + svls.stock_valuation_layer_ids.mapped("value") + ) + consumed_value = total_value - sum(svls.mapped("remaining_value")) + product_uom = move.product_id.uom_id + for ml in move.move_line_ids.sorted("id"): + ml.qty_base = ml.product_uom_id._compute_quantity(ml.qty_done, product_uom) + if float_is_zero(consumed_qty_bal, precision_rounding=product_uom.rounding): + continue + qty_to_allocate = min(consumed_qty_bal, ml.qty_base) ml.qty_consumed += qty_to_allocate - svl_consumed_qty_bal -= qty_to_allocate - ml.value_consumed += svl_consumed_value * qty_to_allocate / svl_consumed_qty - if float_is_zero( - svl_consumed_qty_bal, precision_rounding=product_uom.rounding - ): - break + consumed_qty_bal -= qty_to_allocate + ml.value_consumed += consumed_value * qty_to_allocate / consumed_qty diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index d2330f38..04d0b0d5 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -2,7 +2,8 @@ # Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import Command, models +from odoo import Command, _, models +from odoo.exceptions import UserError class StockMove(models.Model): @@ -19,7 +20,6 @@ def _action_done(self, cancel_backorder=False): ml.qty_base = ml.product_uom_id._compute_quantity( ml.qty_done, ml.product_id.uom_id ) - # ml._compute_remaining_value() return res def _get_move_lots(self): @@ -36,13 +36,25 @@ def _prepare_common_svl_vals(self): return res def _create_out_svl(self, forced_quantity=None): - """Set the move as a context for processing in _run_fifo().""" layers = self.env["stock.valuation.layer"] for move in self: + # Set the move as a context for processing in _run_fifo(). move = move.with_context(fifo_move=move) - layers |= super(StockMove, move)._create_out_svl( + layer = super(StockMove, move)._create_out_svl( forced_quantity=forced_quantity ) + product = move.product_id + # To prevent unknown creation of negative inventory. + if ( + product._is_fifo() + and product.tracking != "none" + and layer.remaining_qty < 0 + ): + raise UserError( + _("Negative inventory is not allowed for product %s.") + % product.display_name + ) + layers |= layer return layers def _create_in_svl(self, forced_quantity=None): diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 35b1c597..6cf981af 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -8,29 +8,33 @@ class StockMoveLine(models.Model): _inherit = "stock.move.line" qty_base = fields.Float( - help="Base quantity for FIFO allocation; this should be equal to the summary " - "of the quantity of the relevant incoming stock valuation layers.", + help="Base quantity for FIFO allocation for FIFO valued products with a " + "lot/serial; represents the total quantity of the moves with incoming " + "valuation for the move line. In product UoM.", ) qty_consumed = fields.Float( - help="Quantity that has gone out of the inventory for valued incoming moves " - "for FIFO products with a lot/serial.", - ) - qty_remaining = fields.Float( - compute="_compute_remaining_value", - store=True, - help="Remaining quantity for valued incoming moves for FIFO products with a " - "lot/serial.", + help="Consumed quantity by outgoing valuation for FIFO valued products with " + "a lot/serial. In product UoM.", ) company_currency_id = fields.Many2one(related="company_id.currency_id") value_consumed = fields.Monetary( currency_field="company_currency_id", - help="The value of the inventory that has been consumed for FIFO products with " - "a lot/serial.", + help="Consumed value by outgoing valuation for FIFO valued products with a " + "lot/serial", + ) + qty_remaining = fields.Float( + compute="_compute_remaining_value", + store=True, + help="Remaining quantity for FIFO valued products with a lot/serial (the " + "total by product should match that of the inventory valuation). In product " + "UoM.", ) value_remaining = fields.Monetary( compute="_compute_remaining_value", store=True, currency_field="company_currency_id", + help="Remaining value for FIFO valued products with a lot/serial (the total " + "by product should match that of the inventory valuation)", ) force_fifo_lot_id = fields.Many2one( "stock.lot", @@ -44,7 +48,6 @@ class StockMoveLine(models.Model): "lot_id", "qty_base", "qty_consumed", - "move_id.stock_valuation_layer_ids", "move_id.stock_valuation_layer_ids.remaining_value", ) def _compute_remaining_value(self): diff --git a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst index 863cd871..a4bc0cbd 100644 --- a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst +++ b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst @@ -35,19 +35,22 @@ Main UI Changes - Adds the following fields: - - 'Qty Consumed' [*]_: Consumed quantity by outgoing moves. - - 'Value Consumed' [*]_: Consumed value by outgoing moves. + - 'Qty Base' [*]_: Base quantity for FIFO allocation; represents the total quantity + of the moves with incoming valuation for the move line. In product UoM. + - 'Qty Consumed' [*]_: Consumed quantity by outgoing valuation. In product UoM. + - 'Value Consumed' [*]_: Consumed value by outgoing valuation. - 'Qty Remaining' [*]_: Remaining quantity (the total by product should match that - of the inventory valuation). + of the inventory valuation). In product UoM. - 'Value Remaining' [*]_: Remaining value (the total by product should match that of the inventory valuation). - 'Force FIFO Lot/Serial': Used when you are stuck by not being able to find a FIFO balance for the lot in an outgoing move line. - .. [*] Updated only for valued incoming moves and outgoing moves where the qty_done has been - reduced in the completed state for products with FIFO costing method. For these outgoing moves, - the system generates positive stock valuation layers with remaining_qty and remaining_value, - which need to be reflected in the related move line. + .. [*] Updated only for products with FIFO costing method only, for valued incoming + moves, and outgoing moves where the qty_done has been reduced in the completed + state. + For these outgoing moves, the system generates positive stock valuation layers + with a remaining balance, which need to be reflected in the related move line. The values here represent the theoretical figures in terms of FIFO costing, meaning that they may differ from the actual stock situation especially for - those updated at the installation of this module. \ No newline at end of file + those updated at the installation of this module. From 94d03a6e9593acd6c858fc55fe3437ce44b470a9 Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Sun, 22 Sep 2024 13:18:15 +0000 Subject: [PATCH 15/17] remove _is_fifo() --- stock_valuation_fifo_lot/hooks.py | 5 ++++- stock_valuation_fifo_lot/models/product.py | 4 ---- stock_valuation_fifo_lot/models/stock_move.py | 8 ++++---- stock_valuation_fifo_lot/models/stock_move_line.py | 5 ++++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/stock_valuation_fifo_lot/hooks.py b/stock_valuation_fifo_lot/hooks.py index fb20d3dc..262a9741 100644 --- a/stock_valuation_fifo_lot/hooks.py +++ b/stock_valuation_fifo_lot/hooks.py @@ -10,7 +10,10 @@ def post_init_hook(cr, registry): moves = env["stock.move"].search([("stock_valuation_layer_ids", "!=", False)]) for move in moves: - if not move.product_id._is_fifo() or not move.lot_ids: + if ( + move.product_id.with_company(move.company_id).cost_method != "fifo" + or not move.lot_ids + ): continue svls = move.stock_valuation_layer_ids svls.lot_ids = move.lot_ids diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index 29436938..e12aa0f5 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -13,10 +13,6 @@ class ProductProduct(models.Model): _inherit = "product.product" - def _is_fifo(self): - self.ensure_one() - return self.with_company(self.company_id.id).cost_method == "fifo" - def _get_fifo_candidates_domain(self, company): res = super()._get_fifo_candidates_domain(company) fifo_lot = self.env.context.get("fifo_lot") diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index 04d0b0d5..c86e070e 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -12,7 +12,7 @@ class StockMove(models.Model): def _action_done(self, cancel_backorder=False): res = super()._action_done(cancel_backorder=cancel_backorder) for move in res: - if not move.product_id._is_fifo(): + if move.product_id.cost_method != "fifo": continue if not move._is_in(): continue @@ -46,7 +46,7 @@ def _create_out_svl(self, forced_quantity=None): product = move.product_id # To prevent unknown creation of negative inventory. if ( - product._is_fifo() + product.cost_method == "fifo" and product.tracking != "none" and layer.remaining_qty < 0 ): @@ -70,7 +70,7 @@ def _create_in_svl(self, forced_quantity=None): layers |= layer # Calculate standard price (sorted by lot created date) product = move.product_id - if not product._is_fifo() or product.tracking == "none": + if product.cost_method != "fifo" or product.tracking == "none": continue for ml in layer.stock_move_id.move_line_ids: ml.qty_base = ml.qty_done @@ -91,7 +91,7 @@ def _get_price_unit(self): self.ensure_one() if ( not self.company_id.use_lot_cost_for_new_stock - or not self.product_id._is_fifo() + or self.product_id.cost_method != "fifo" ): return super()._get_price_unit() if hasattr(self, "purchase_line_id") and self.purchase_line_id: diff --git a/stock_valuation_fifo_lot/models/stock_move_line.py b/stock_valuation_fifo_lot/models/stock_move_line.py index 6cf981af..d5ab2bde 100644 --- a/stock_valuation_fifo_lot/models/stock_move_line.py +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -52,7 +52,10 @@ class StockMoveLine(models.Model): ) def _compute_remaining_value(self): for rec in self: - if not rec.product_id._is_fifo() or not rec.lot_id: + if ( + rec.product_id.with_company(rec.company_id).cost_method != "fifo" + or not rec.lot_id + ): continue rec.qty_remaining = rec.qty_base - rec.qty_consumed layers = rec.move_id.stock_valuation_layer_ids.filtered( From 7f11c130b40bdb8cdfb109d2019f9925b5df5cab Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 23 Sep 2024 09:42:13 +0000 Subject: [PATCH 16/17] upd tests --- .../tests/test_stock_valuation_fifo_lot.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py index 8ff1b4e4..21f8efbe 100644 --- a/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -1,6 +1,7 @@ # Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.exceptions import UserError from odoo.tests.common import Form, TransactionCase @@ -37,6 +38,7 @@ def create_picking( lot_numbers, price_unit=0.0, is_receipt=True, + force_lot_name=None, ): picking = self.env["stock.picking"].create( { @@ -79,6 +81,15 @@ def create_picking( [("product_id", "=", self.product.id), ("name", "=", lot)], limit=1 ) move_line.lot_id = lot.id + if force_lot_name: + force_lot = self.env["stock.lot"].search( + [ + ("product_id", "=", self.product.id), + ("name", "=", force_lot_name), + ], + limit=1, + ) + move_line.force_fifo_lot_id = force_lot.id picking.action_confirm() picking.action_assign() picking._action_done() @@ -406,3 +417,58 @@ def test_inventory_adjustment_after_multiple_receipts(self): 1000.0, "Stock valuation for lot 002 should be 1000.0", ) + + def test_force_fifo_lot_id(self): + _, move_in = self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001", "002"], + 100.0, + ) + # Deliver lot 002 + _, move_out_002 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out_002.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for the delivery of lot 002 should be 500.0", + ) + move_line_lot_001 = move_in.move_line_ids.filtered( + lambda ml: ml.lot_name == "001" + ) + move_line_lot_001.qty_remaining = 0.0 + move_line_lot_001.qty_consumed = 5.0 + move_line_lot_002 = move_in.move_line_ids.filtered( + lambda ml: ml.lot_name == "002" + ) + move_line_lot_002.qty_remaining = 5.0 + move_line_lot_002.qty_consumed = 0.0 + + # Create delivery for lot 001 + with self.assertRaises(UserError): + _, _ = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) + _, move_out_001 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + force_lot_name="002", + ) + self.assertEqual( + abs(move_out_001.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for the delivery of lot 001 should be 500.0", + ) From 4140a0425829c0864af40b0d091b44599e924e8d Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Wed, 25 Sep 2024 09:25:56 +0000 Subject: [PATCH 17/17] remove dependency --- stock_valuation_fifo_lot/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_valuation_fifo_lot/__manifest__.py b/stock_valuation_fifo_lot/__manifest__.py index 99c5d8b8..0942d571 100644 --- a/stock_valuation_fifo_lot/__manifest__.py +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -10,7 +10,7 @@ "license": "AGPL-3", "author": "Ecosoft, Quartile, Odoo Community Association (OCA)", "website": "https://github.com/OCA/stock-logistics-workflow", - "depends": ["stock_account", "stock_no_negative"], + "depends": ["stock_account"], "data": [ "views/res_config_settings_views.xml", "views/stock_move_line_views.xml",