From 8ce741dee16f25956439a036a42e4a5ab2825bcd Mon Sep 17 00:00:00 2001 From: ps-tubtim Date: Wed, 9 Aug 2023 10:19:57 +0700 Subject: [PATCH 01/18] [15.0][ADD] stock_valuation_fifo_lot --- stock_valuation_fifo_lot/README.rst | 90 ++++ 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 | 114 +++++ 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 | 432 ++++++++++++++++++ .../views/stock_valuation_layer_views.xml | 23 + 11 files changed, 804 insertions(+) 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/stock_valuation_fifo_lot/README.rst b/stock_valuation_fifo_lot/README.rst new file mode 100644 index 000000000000..777ce91195f3 --- /dev/null +++ b/stock_valuation_fifo_lot/README.rst @@ -0,0 +1,90 @@ +======================== +Stock Valuation Fifo Lot +======================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/15.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-15-0/stock-logistics-workflow-15-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/webui/builds.html?repo=OCA/stock-logistics-workflow&target_branch=15.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 smashing 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 000000000000..8ebc8a7c5e74 --- /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 000000000000..d163ccd690a0 --- /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": "15.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 000000000000..c5490433909a --- /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 000000000000..1391630e1e4e --- /dev/null +++ b/stock_valuation_fifo_lot/models/product.py @@ -0,0 +1,114 @@ +# 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": + all_candidates = all_candidates.sorted( + lambda l: min(l.lot_ids.mapped("create_date")) + ) + 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 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" + ) + if all_candidates: + new_standard_price = all_candidates[0].unit_cost + else: + 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 000000000000..11690bfd2134 --- /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 000000000000..12333b873764 --- /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.production.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 000000000000..eef9b340fbba --- /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 000000000000..e17874381dea --- /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 000000000000..5d2e6f904ccc --- /dev/null +++ b/stock_valuation_fifo_lot/static/description/index.html @@ -0,0 +1,432 @@ + + + + + + +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 smashing 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 000000000000..1901131c87ce --- /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 ef09b7f8e826a50546bb92e3399900b52c98cb86 Mon Sep 17 00:00:00 2001 From: Saran440 Date: Sun, 27 Aug 2023 23:07:56 +0700 Subject: [PATCH 02/18] [FIX] sorted by create_date when case no lot_ids or there is 1 date --- stock_valuation_fifo_lot/models/product.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index 1391630e1e4e..36a13d68807f 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -25,9 +25,16 @@ def _get_all_candidates(self, company, sort_by=None): ) ) if sort_by == "lot_create_date": - all_candidates = all_candidates.sorted( - lambda l: min(l.lot_ids.mapped("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 From 09061f261a9bafd8004f9e7375442241d7283e14 Mon Sep 17 00:00:00 2001 From: Saran440 Date: Thu, 7 Sep 2023 14:52:05 +0700 Subject: [PATCH 03/18] [FIX] error when not loop --- stock_valuation_fifo_lot/models/product.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stock_valuation_fifo_lot/models/product.py b/stock_valuation_fifo_lot/models/product.py index 36a13d68807f..6d652d5d0f4c 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -93,7 +93,7 @@ def _run_fifo(self, quantity, company): ): break - if qty_to_take_on_candidates > 0: + 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 @@ -101,9 +101,10 @@ def _run_fifo(self, quantity, company): 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 - else: + elif candidates: new_standard_price = candidate.unit_cost # Update standard price From 875fd6a432b94c5a0f9e2d1517f64f7f8eec9de1 Mon Sep 17 00:00:00 2001 From: TheerayutEncoder Date: Wed, 27 Sep 2023 18:41:26 +0700 Subject: [PATCH 04/18] [15.0][UPD] stock_valuation_fifo_lot --- stock_valuation_fifo_lot/__manifest__.py | 5 +---- stock_valuation_fifo_lot/models/__init__.py | 1 + .../models/stock_landed_cost.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 stock_valuation_fifo_lot/models/stock_landed_cost.py diff --git a/stock_valuation_fifo_lot/__manifest__.py b/stock_valuation_fifo_lot/__manifest__.py index d163ccd690a0..d8efdd1ae315 100644 --- a/stock_valuation_fifo_lot/__manifest__.py +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -9,10 +9,7 @@ "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", "stock_landed_costs"], "data": [ "views/stock_valuation_layer_views.xml", ], diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py index c5490433909a..12b33ac1da04 100644 --- a/stock_valuation_fifo_lot/models/__init__.py +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -3,3 +3,4 @@ from . import product from . import stock_move from . import stock_valuation_layer +from . import stock_landed_cost diff --git a/stock_valuation_fifo_lot/models/stock_landed_cost.py b/stock_valuation_fifo_lot/models/stock_landed_cost.py new file mode 100644 index 000000000000..98ac48b22a5c --- /dev/null +++ b/stock_valuation_fifo_lot/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 From 7ac2908421d7a4ee529396003769af3731b82bc5 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Sat, 9 Mar 2024 13:56:18 +0000 Subject: [PATCH 05/18] [UPD] Update stock_valuation_fifo_lot.pot --- .../i18n/stock_valuation_fifo_lot.pot | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 stock_valuation_fifo_lot/i18n/stock_valuation_fifo_lot.pot 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 000000000000..6d0a6e018e26 --- /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 "" From 6aab71cf8b9a918267fa564fc7c739ed96e03cf6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sat, 9 Mar 2024 14:01:11 +0000 Subject: [PATCH 06/18] [BOT] post-merge updates --- stock_valuation_fifo_lot/README.rst | 11 ++++-- .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 35 +++++++++--------- 3 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 stock_valuation_fifo_lot/static/description/icon.png diff --git a/stock_valuation_fifo_lot/README.rst b/stock_valuation_fifo_lot/README.rst index 777ce91195f3..f6b6b6dab0c8 100644 --- a/stock_valuation_fifo_lot/README.rst +++ b/stock_valuation_fifo_lot/README.rst @@ -2,10 +2,13 @@ Stock Valuation Fifo Lot ======================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:877af52a350ab6a61b6c128c4fbcffe909e4a8274c8ec064390dc9b1c36d9253 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png :target: https://odoo-community.org/page/development-status @@ -20,10 +23,10 @@ Stock Valuation Fifo Lot :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-15-0/stock-logistics-workflow-15-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/webui/builds.html?repo=OCA/stock-logistics-workflow&target_branch=15.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&target_branch=15.0 :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module is used to calculate FIFO cost by lot. @@ -42,7 +45,7 @@ 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 smashing it by providing a detailed and welcomed +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. 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 5d2e6f904ccc..75dbf55d69ab 100644 --- a/stock_valuation_fifo_lot/static/description/index.html +++ b/stock_valuation_fifo_lot/static/description/index.html @@ -1,20 +1,19 @@ - - + Stock Valuation Fifo Lot + + +
+

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 e9d74f8c6bc89ec57f6ceaece7d274b0dcfbf3ad Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Wed, 4 Sep 2024 03:57:21 +0000 Subject: [PATCH 14/18] [IMP] stock_valuation_fifo_lot: add lot_ids in search view --- .../views/stock_valuation_layer_views.xml | 18 ++++++++++++++++++ 1 file changed, 18 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 1901131c87ce..853cdeed2a31 100644 --- a/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml +++ b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml @@ -1,4 +1,22 @@ + + stock.valuation.layer.search + stock.valuation.layer + + + + + + + + + + + stock.valuation.layer.tree stock.valuation.layer From a7a187cdfaf0337190f53f69f158564832350153 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Thu, 5 Sep 2024 09:13:32 +0000 Subject: [PATCH 15/18] [IMP] stock_valuation_fifo_lot: improve tests --- .../tests/test_stock_valuation_fifo_lot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 39ac7a371eaf..2e5d6991870f 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 @@ -58,10 +58,11 @@ def create_stock_move(self, picking, product, price=0.0): move.write({"price_unit": price}) return move - def create_stock_move_line(self, move, lot_name=False): + 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, @@ -77,7 +78,7 @@ def test_stock_valuation_fifo_lot(self): 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, "11111") + self.create_stock_move_line(move, receipt_picking_1, "11111") receipt_picking_1.action_confirm() receipt_picking_1.action_assign() @@ -88,7 +89,7 @@ def test_stock_valuation_fifo_lot(self): 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, "22222") + self.create_stock_move_line(move, receipt_picking_2, "22222") receipt_picking_2.action_confirm() receipt_picking_2.action_assign() @@ -100,7 +101,7 @@ def test_stock_valuation_fifo_lot(self): 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) + move_line = self.create_stock_move_line(move, delivery_picking1) lot_id = self.env["stock.lot"].search( [("name", "=", "22222"), ("product_id", "=", self.product.id)] ) @@ -116,7 +117,7 @@ def test_stock_valuation_fifo_lot(self): 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, "33333") + self.create_stock_move_line(move, receipt_picking_3, "33333") receipt_picking_3.action_confirm() receipt_picking_3.action_assign() From a6a38452084f920a975c6b78ff011a711971c7c0 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 23 Sep 2024 09:48:10 +0000 Subject: [PATCH 16/18] [IMP] stock_valuation_fifo_lot - Avoid completely overriding _run_fifo and use the standard behavior. - Update the product's standard price to the first available lot price. - Improve _get_price_unit to retrieve the value from the most recent incoming move line for the lot for the No PO (e.g. customer returns) stock moves. - Add quantity and value-related fields in stock_move_line to cover all cases including multiple lots exist in an SVL record, and it is unclear which lot's remaining quantity is being delivered(Eg. Receive serials 001, 002 and 003 for an incoming move. Deliver 002, return 002 and deliver 002 again.) - Add force_fifo_lot_id in stock_move_line to handle cases where a delivery needs to be made for an existing lot created before the installation of this module (e.g., lots 001 and 002 were received, lot 002 was delivered before the module's installation, and lot 001 is delivered after the module is installed). - Add a post_init_hook to update the lot_ids in SVL for existing records and to update the field values in existing stock_move_line records. - Remove stock_no_negative from dependency and check negative inventory is created when SVL is created for delivery. --- stock_valuation_fifo_lot/README.rst | 82 ++- stock_valuation_fifo_lot/__init__.py | 1 + stock_valuation_fifo_lot/__manifest__.py | 8 +- stock_valuation_fifo_lot/hooks.py | 42 ++ stock_valuation_fifo_lot/models/__init__.py | 2 + stock_valuation_fifo_lot/models/product.py | 146 +++--- .../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 | 144 ++--- .../models/stock_move_line.py | 80 +++ .../models/stock_valuation_layer.py | 8 +- stock_valuation_fifo_lot/readme/CONFIGURE.rst | 7 +- .../readme/CONTRIBUTORS.rst | 2 +- .../readme/DESCRIPTION.rst | 57 +- stock_valuation_fifo_lot/readme/USAGE.rst | 11 + .../static/description/index.html | 115 +++- .../tests/test_stock_valuation_fifo_lot.py | 491 +++++++++++++++--- .../views/res_config_settings_views.xml | 10 +- .../views/stock_move_line_views.xml | 85 +++ .../views/stock_package_level_views.xml | 17 + 21 files changed, 1096 insertions(+), 257 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/models/stock_move_line.py create mode 100644 stock_valuation_fifo_lot/readme/USAGE.rst create mode 100644 stock_valuation_fifo_lot/views/stock_move_line_views.xml create mode 100644 stock_valuation_fifo_lot/views/stock_package_level_views.xml diff --git a/stock_valuation_fifo_lot/README.rst b/stock_valuation_fifo_lot/README.rst index d4757d4aa6a2..b4ebf39cda8c 100644 --- a/stock_valuation_fifo_lot/README.rst +++ b/stock_valuation_fifo_lot/README.rst @@ -28,7 +28,62 @@ Stock Valuation Fifo Lot |badge1| |badge2| |badge3| |badge4| |badge5| -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 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). 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 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. .. IMPORTANT:: This is an alpha version, the data model and design can change at any time without warning. @@ -43,8 +98,26 @@ This module is used to calculate FIFO cost by lot. 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). +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). + +Usage +===== + +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. Bug Tracker =========== @@ -63,6 +136,7 @@ Authors ~~~~~~~ * Ecosoft +* Quartile Contributors ~~~~~~~~~~~~ @@ -76,7 +150,7 @@ Contributors * `Quartile `__: * Aung Ko Ko Lin - + * Yoshi Tashiro Maintainers ~~~~~~~~~~~ diff --git a/stock_valuation_fifo_lot/__init__.py b/stock_valuation_fifo_lot/__init__.py index 8ebc8a7c5e74..0dcdc5e36371 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 8562607be8a1..0942d571c3e4 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,13 +8,16 @@ "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"], + "depends": ["stock_account"], "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 000000000000..262a9741f162 --- /dev/null +++ b/stock_valuation_fifo_lot/hooks.py @@ -0,0 +1,42 @@ +# 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, {}) + + moves = env["stock.move"].search([("stock_valuation_layer_ids", "!=", False)]) + for move in moves: + 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 + 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 + 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 + consumed_qty_bal -= qty_to_allocate + ml.value_consumed += consumed_value * qty_to_allocate / consumed_qty diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py index f797b4f19e5c..49f67d5ebd4f 100644 --- a/stock_valuation_fifo_lot/models/__init__.py +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -3,5 +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 411d95c5e435..e12aa0f53043 100644 --- a/stock_valuation_fifo_lot/models/product.py +++ b/stock_valuation_fifo_lot/models/product.py @@ -1,111 +1,99 @@ # 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 collections import defaultdict + +from odoo import _, models +from odoo.exceptions import UserError +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 + 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": 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_ml = candidate._get_unconsumed_in_move_line(fifo_lot) + qty_to_take_on_candidates = min( + qty_to_take_on_candidates, candidate_ml.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_ml.qty_consumed += qty_to_take_on_candidates + candidate_ml.value_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 - ) + return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate) - 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 + 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_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 + if correction_ml: + moved_qty = quantity + else: + moved_qty = ml.product_uom_id._compute_quantity( + ml.qty_done, self.uom_id ) - - 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), - } + 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(): + 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/res_company.py b/stock_valuation_fifo_lot/models/res_company.py index 6445a538ad37..6152a3c0ac22 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 c51153157270..d6a1484ef4aa 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 000000000000..fd58f2ce3696 --- /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 bea3ff17080d..c86e070e4d89 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -1,98 +1,122 @@ # 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 +from odoo.exceptions import UserError 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 move.product_id.cost_method != "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): - """ - 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)], - } - ) + lots = self._get_move_lots() + res.update({"lot_ids": [Command.set(lots.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) + # Set the move as a context for processing in _run_fifo(). + move = move.with_context(fifo_move=move) layer = super(StockMove, move)._create_out_svl( forced_quantity=forced_quantity ) + product = move.product_id + # To prevent unknown creation of negative inventory. + if ( + product.cost_method == "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): - """ - 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 - """ + 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: 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 layers |= layer + # Calculate standard price (sorted by lot created date) + product = move.product_id + 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 + # 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: + continue + product = product.with_company(move.company_id.id) + product = product.with_context(disable_auto_svl=True) + product.sudo().standard_price = candidate.unit_cost 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 + not self.company_id.use_lot_cost_for_new_stock + or self.product_id.cost_method != "fifo" ): - 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, - ) + return super()._get_price_unit() + if hasattr(self, "purchase_line_id") and self.purchase_line_id: + return super()._get_price_unit() + 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", "=", lots.id), + "|", + ("qty_consumed", ">", 0), + ("qty_remaining", ">", 0), + ("company_id", "=", self.company_id.id), + ], + order="id desc", ) - if candidate: - return candidate.remaining_value / candidate.remaining_qty + .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 new file mode 100644 index 000000000000..d5ab2bde61e6 --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_move_line.py @@ -0,0 +1,80 @@ +# 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_base = fields.Float( + 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="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="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", + "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( + "lot_id", + "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).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( + lambda x: rec.lot_id in x.lot_ids + ) + remaining_qty = sum(layers.mapped("remaining_qty")) + 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 + # 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 0b2c10d4a0d7..2d1d53f79e97 100644 --- a/stock_valuation_fifo_lot/models/stock_valuation_layer.py +++ b/stock_valuation_fifo_lot/models/stock_valuation_layer.py @@ -9,5 +9,11 @@ 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): + 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/readme/CONFIGURE.rst b/stock_valuation_fifo_lot/readme/CONFIGURE.rst index 61b74d16aa32..6d102b3cbc9a 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 e2e916aa7529..c57efe9a8fcb 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 e17874381dea..5fab771c239a 100644 --- a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst +++ b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst @@ -1 +1,56 @@ -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 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). 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 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. diff --git a/stock_valuation_fifo_lot/readme/USAGE.rst b/stock_valuation_fifo_lot/readme/USAGE.rst new file mode 100644 index 000000000000..1cccb72fc651 --- /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/static/description/index.html b/stock_valuation_fifo_lot/static/description/index.html index b097266076f2..5d5f1a238547 100644 --- a/stock_valuation_fifo_lot/static/description/index.html +++ b/stock_valuation_fifo_lot/static/description/index.html @@ -369,7 +369,73 @@

Stock Valuation Fifo Lot

!! 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.

+

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 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). 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 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.
+

Important

This is an alpha version, the data model and design can change at any time without warning. @@ -380,22 +446,34 @@

Stock Valuation Fifo Lot

-

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).

+

Configuration

+

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).

+
+
+

Usage

+

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.

-

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

Bug Tracker

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

+
-

Authors

+

Authors

  • Ecosoft
  • +
  • Quartile
-

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 @@ -438,6 +520,5 @@

Maintainers

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

- 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 2e5d6991870f..21f8efbe4b48 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,14 +1,11 @@ -# 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.exceptions import UserError from odoo.tests.common import Form, TransactionCase -class TestStockAccountFifoReturnOrigin(TransactionCase): +class TestStockValuationFifoLot(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -33,111 +30,445 @@ 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, + force_lot_name=None, + ): + 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_line_qty = 5.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_qty": 5.0, + "product_uom": self.product.uom_id.id, + "product_uom_qty": move_line_qty * len(lot_numbers), "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_line_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 + 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() + 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 move_line + 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 = 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_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", + ) + + # 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", + ) + + 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", ) - 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) + 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, + ) + 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", + ) + self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 15.0) - receipt_picking_2 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + # 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", ) - move = self.create_stock_move(receipt_picking_2, self.product, 200) - self.create_stock_move_line(move, receipt_picking_2, "22222") + self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 10.0) - 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 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", + ) + self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 5.0) - delivery_picking1 = self.create_picking( - self.stock_location, self.customer_location, self.picking_type_out + # Deliver lot 002 again + _, move_out_2 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, ) - 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( + abs(move_out_2.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for second delivery of lot 002 should be 500.0", ) - move_line.write({"lot_id": lot_id}) + self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 0.0) - delivery_picking1.action_confirm() - delivery_picking1.action_assign() - delivery_picking1._action_done() - self.assertEqual(abs(move.stock_valuation_layer_ids.value), 1000) + 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, + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.value, + 500.0, + "Stock valuation for lot 001 should be 500.0", + ) - # Test return delivery - receipt_picking_3 = self.create_picking( - self.supplier_location, self.stock_location, self.picking_type_in + _, 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", ) - 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) + # Deliver lot 001 + self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) - 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", + # 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", + ) + + 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", ) - 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( + abs(move_out_001.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for the delivery of lot 001 should be 500.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) 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 975f288e884c..8d0ab84b7be6 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 new file mode 100644 index 000000000000..c3cb4e82ada2 --- /dev/null +++ b/stock_valuation_fifo_lot/views/stock_move_line_views.xml @@ -0,0 +1,85 @@ + + + stock.move.line.tree + stock.move.line + + + + + + + + + + + + + + + + + stock.move.line.search + stock.move.line + + + + + + + + + + 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 000000000000..3324adb1a07a --- /dev/null +++ b/stock_valuation_fifo_lot/views/stock_package_level_views.xml @@ -0,0 +1,17 @@ + + + Package Level + stock.package_level + + + + + + + + From 77bcfcea7fe96e4a3f65bb90080cc0ce96b608f6 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Thu, 26 Sep 2024 08:23:34 +0000 Subject: [PATCH 17/18] [IMP] stock_valuation_fifo_lot: improve test cases and adjustment --- stock_valuation_fifo_lot/models/stock_move.py | 6 +- stock_valuation_fifo_lot/tests/__init__.py | 1 + stock_valuation_fifo_lot/tests/common.py | 114 +++++++ .../tests/test_stock_valuation_fifo_lot.py | 290 +++--------------- 4 files changed, 165 insertions(+), 246 deletions(-) create mode 100644 stock_valuation_fifo_lot/tests/common.py diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index c86e070e4d89..be72db0d6934 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -68,19 +68,21 @@ def _create_in_svl(self, forced_quantity=None): forced_quantity=forced_quantity ) layers |= layer - # Calculate standard price (sorted by lot created date) product = move.product_id 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 - # Change product standard price to the first available lot price. + # Update product standard price to the first available + # lot price (by sorting by lot create date). 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) + # `candidate.unit_cost` is not totally accurate in 16.0 because of + # https://github.com/odoo/odoo/issues/171464 product.sudo().standard_price = candidate.unit_cost return layers diff --git a/stock_valuation_fifo_lot/tests/__init__.py b/stock_valuation_fifo_lot/tests/__init__.py index 3aefeb5578d9..d3158fe7f5c7 100644 --- a/stock_valuation_fifo_lot/tests/__init__.py +++ b/stock_valuation_fifo_lot/tests/__init__.py @@ -1 +1,2 @@ +from . import common from . import test_stock_valuation_fifo_lot diff --git a/stock_valuation_fifo_lot/tests/common.py b/stock_valuation_fifo_lot/tests/common.py new file mode 100644 index 000000000000..17633e7b2ddf --- /dev/null +++ b/stock_valuation_fifo_lot/tests/common.py @@ -0,0 +1,114 @@ +# 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 TestStockValuationFifoCommon(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, + lot_numbers, + price_unit=0.0, + is_receipt=True, + force_lot_name=None, + ): + picking = self.env["stock.picking"].create( + { + "location_id": location.id, + "location_dest_id": location_dest.id, + "picking_type_id": picking_type.id, + } + ) + move_line_qty = 5.0 + move = self.env["stock.move"].create( + { + "name": "Test", + "product_id": self.product.id, + "location_id": picking.location_id.id, + "location_dest_id": picking.location_dest_id.id, + "product_uom": self.product.uom_id.id, + "product_uom_qty": move_line_qty * len(lot_numbers), + "picking_id": picking.id, + } + ) + if price_unit: + move.write({"price_unit": price_unit}) + + 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_line_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 + 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() + return picking, move + + def transfer_return(self, original_picking, return_qty): + 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 = return_picking.move_ids + return_move.move_line_ids.qty_done = return_qty + return_picking.button_validate() + return return_move 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 21f8efbe4b48..071769adadd2 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 @@ -2,187 +2,18 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.exceptions import UserError -from odoo.tests.common import Form, TransactionCase +from odoo.addons.stock_valuation_fifo_lot.tests.common import ( + TestStockValuationFifoCommon, +) -class TestStockValuationFifoLot(TransactionCase): + +class TestStockValuationFifoLot(TestStockValuationFifoCommon): @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, - lot_numbers, - price_unit=0.0, - is_receipt=True, - force_lot_name=None, - ): - picking = self.env["stock.picking"].create( - { - "location_id": location.id, - "location_dest_id": location_dest.id, - "picking_type_id": picking_type.id, - } - ) - move_line_qty = 5.0 - move = self.env["stock.move"].create( - { - "name": "Test", - "product_id": self.product.id, - "location_id": picking.location_id.id, - "location_dest_id": picking.location_dest_id.id, - "product_uom": self.product.uom_id.id, - "product_uom_qty": move_line_qty * len(lot_numbers), - "picking_id": picking.id, - } - ) - if price_unit: - move.write({"price_unit": price_unit}) - - 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_line_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 - 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() - 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 = 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): - _, 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_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", - ) - - # 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", - ) - 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", - ) - - def test_receive_deliver_return_lot(self): + def test_receive_deliver_return_deliver_lot(self): receipt_picking, move_in = self.create_picking( self.supplier_location, self.stock_location, @@ -192,13 +23,16 @@ def test_receive_deliver_return_lot(self): ) self.assertEqual(len(receipt_picking.move_line_ids), 3) self.assertEqual( - move_in.stock_valuation_layer_ids.value, + move_in.stock_valuation_layer_ids.remaining_value, 1500.0, - "Stock valuation for multiple receipts should be 1500.0", + "Remaining value for receipt should be 1500.0", + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.remaining_qty, + 15.0, + "Remaining quantity for receipt should be 15.0", ) - self.assertEqual(move_in.stock_valuation_layer_ids.remaining_qty, 15.0) - # Deliver lot 002 delivery_picking, move_out = self.create_picking( self.stock_location, self.customer_location, @@ -211,18 +45,20 @@ def test_receive_deliver_return_lot(self): 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) + self.assertEqual( + move_in.stock_valuation_layer_ids.remaining_qty, + 10.0, + "Remaining quantity for first incoming receipt should be 10.0", + ) - # Return lot 002 - move_in_2 = self.transfer_return(delivery_picking, 5.0) + return_move = self.transfer_return(delivery_picking, 5.0) self.assertEqual( - move_in_2.stock_valuation_layer_ids.value, + return_move.stock_valuation_layer_ids.remaining_value, 500.0, - "Stock valuation for returned lot 002 should be 500.0", + "Remaining value for returned lot 002 should be 500.0", ) - self.assertEqual(move_in_2.stock_valuation_layer_ids.remaining_qty, 5.0) + self.assertEqual(return_move.stock_valuation_layer_ids.remaining_qty, 5.0) - # Deliver lot 002 again _, move_out_2 = self.create_picking( self.stock_location, self.customer_location, @@ -235,59 +71,45 @@ def test_receive_deliver_return_lot(self): 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) + self.assertEqual( + return_move.stock_valuation_layer_ids.remaining_qty, + 0.0, + "The remaining qauntity of returned lot 002 should be 0.00", + ) - def test_cost_tracking_by_lot(self): - _, move_in = self.create_picking( + def test_delivery_use_incoming_price(self): + 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 001 should be 500.0", - ) - - _, move_in_2 = self.create_picking( + 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( + 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), + 1000.0, + "Stock valuation for delivery of lot 002 should be 1000.0", + ) - # Return lot 002 - move_in = self.transfer_return(delivery_picking_2, 5.0) + return_move = self.transfer_return(delivery_picking, 5.0) self.assertEqual( - move_in.stock_valuation_layer_ids.value, + return_move.stock_valuation_layer_ids.remaining_value, 1000.0, - "Stock valuation for returned lot 002 should be 1000.0", + "Remaining value for returned lot 002 should be 1000.0", ) def test_change_qty_done_in_done_move_line(self): @@ -299,11 +121,11 @@ def test_change_qty_done_in_done_move_line(self): 500.0, ) self.assertEqual( - move_in.stock_valuation_layer_ids.value, + move_in.stock_valuation_layer_ids.remaining_value, 2500.0, - "Stock valuation for the first receipt should be 2500.0", + "Remaining value 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( @@ -317,7 +139,6 @@ def test_change_qty_done_in_done_move_line(self): "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, @@ -343,7 +164,6 @@ def test_change_qty_done_in_done_move_line(self): "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( @@ -357,7 +177,6 @@ def test_change_qty_done_in_done_move_line(self): "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, @@ -371,31 +190,20 @@ def test_change_qty_done_in_done_move_line(self): ) def test_inventory_adjustment_after_multiple_receipts(self): - _, move_in = self.create_picking( + 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.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 ) @@ -415,7 +223,7 @@ def test_inventory_adjustment_after_multiple_receipts(self): self.assertEqual( move.stock_valuation_layer_ids.value, 1000.0, - "Stock valuation for lot 002 should be 1000.0", + "Stock valuation for lot 002 should be 1000.0 for positive quantity 5.", ) def test_force_fifo_lot_id(self): @@ -426,19 +234,13 @@ def test_force_fifo_lot_id(self): ["001", "002"], 100.0, ) - # Deliver lot 002 - _, move_out_002 = self.create_picking( + 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" ) From 4d1f25d9221affdde7296bb1961e032a63f9c2dd Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Tue, 5 Nov 2024 04:07:08 +0000 Subject: [PATCH 18/18] [IMP] stock_valuation_fifo_lot: remove updating standard price --- stock_valuation_fifo_lot/models/stock_move.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/stock_valuation_fifo_lot/models/stock_move.py b/stock_valuation_fifo_lot/models/stock_move.py index be72db0d6934..94d777b6258e 100644 --- a/stock_valuation_fifo_lot/models/stock_move.py +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -73,17 +73,6 @@ def _create_in_svl(self, forced_quantity=None): continue for ml in layer.stock_move_id.move_line_ids: ml.qty_base = ml.qty_done - # Update product standard price to the first available - # lot price (by sorting by lot create date). - 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) - # `candidate.unit_cost` is not totally accurate in 16.0 because of - # https://github.com/odoo/odoo/issues/171464 - product.sudo().standard_price = candidate.unit_cost return layers def _get_price_unit(self):