diff --git a/setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot b/setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot new file mode 120000 index 000000000000..bf4290934245 --- /dev/null +++ b/setup/stock_valuation_fifo_lot/odoo/addons/stock_valuation_fifo_lot @@ -0,0 +1 @@ +../../../../stock_valuation_fifo_lot \ No newline at end of file diff --git a/setup/stock_valuation_fifo_lot/setup.py b/setup/stock_valuation_fifo_lot/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_valuation_fifo_lot/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost b/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost new file mode 120000 index 000000000000..d3d287eddbd6 --- /dev/null +++ b/setup/stock_valuation_fifo_lot_mrp_landed_cost/odoo/addons/stock_valuation_fifo_lot_mrp_landed_cost @@ -0,0 +1 @@ +../../../../stock_valuation_fifo_lot_mrp_landed_cost \ No newline at end of file diff --git a/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py b/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_valuation_fifo_lot_mrp_landed_cost/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_valuation_fifo_lot/README.rst b/stock_valuation_fifo_lot/README.rst new file mode 100644 index 000000000000..b4ebf39cda8c --- /dev/null +++ b/stock_valuation_fifo_lot/README.rst @@ -0,0 +1,178 @@ +======================== +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 + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_valuation_fifo_lot + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_valuation_fifo_lot + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module 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. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +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 +=========== + +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 +* Quartile + +Contributors +~~~~~~~~~~~~ + +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + * Yoshi Tashiro + +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..0dcdc5e36371 --- /dev/null +++ b/stock_valuation_fifo_lot/__init__.py @@ -0,0 +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 new file mode 100644 index 000000000000..0942d571c3e4 --- /dev/null +++ b/stock_valuation_fifo_lot/__manifest__.py @@ -0,0 +1,23 @@ +# 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) + +{ + "name": "Stock Valuation Fifo Lot", + "version": "16.0.1.0.0", + "category": "Warehouse Management", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "Ecosoft, Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "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/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 "" diff --git a/stock_valuation_fifo_lot/models/__init__.py b/stock_valuation_fifo_lot/models/__init__.py new file mode 100644 index 000000000000..49f67d5ebd4f --- /dev/null +++ b/stock_valuation_fifo_lot/models/__init__.py @@ -0,0 +1,9 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import product +from . import res_company +from . import res_config_settings +from . import stock_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 new file mode 100644 index 000000000000..e12aa0f53043 --- /dev/null +++ b/stock_valuation_fifo_lot/models/product.py @@ -0,0 +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 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")) + 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 + + # 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 + ) + candidate_ml.qty_consumed += qty_to_take_on_candidates + candidate_ml.value_consumed += qty_to_take_on_candidates * ( + candidate.remaining_value / candidate.remaining_qty + ) + return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate) + + 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 + ) + 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 new file mode 100644 index 000000000000..6152a3c0ac22 --- /dev/null +++ b/stock_valuation_fifo_lot/models/res_company.py @@ -0,0 +1,14 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + 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 new file mode 100644 index 000000000000..d6a1484ef4aa --- /dev/null +++ b/stock_valuation_fifo_lot/models/res_config_settings.py @@ -0,0 +1,15 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + use_lot_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 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 new file mode 100644 index 000000000000..be72db0d6934 --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_move.py @@ -0,0 +1,124 @@ +# 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 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): + """Add lots/serials to the stock valuation layer.""" + self.ensure_one() + res = super()._prepare_common_svl_vals() + lots = self._get_move_lots() + res.update({"lot_ids": [Command.set(lots.ids)]}) + return res + + def _create_out_svl(self, forced_quantity=None): + layers = self.env["stock.valuation.layer"] + for move in self: + # Set the move as a context for processing in _run_fifo(). + move = move.with_context(fifo_move=move) + 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): + 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 + ) + layers |= layer + 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 + # 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): + """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_cost_for_new_stock + or self.product_id.cost_method != "fifo" + ): + 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", + ) + .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 new file mode 100644 index 000000000000..2d1d53f79e97 --- /dev/null +++ b/stock_valuation_fifo_lot/models/stock_valuation_layer.py @@ -0,0 +1,19 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import fields, models + + +class StockValuationLayer(models.Model): + _inherit = "stock.valuation.layer" + + lot_ids = fields.Many2many( + comodel_name="stock.lot", + string="Lots/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 new file mode 100644 index 000000000000..6d102b3cbc9a --- /dev/null +++ b/stock_valuation_fifo_lot/readme/CONFIGURE.rst @@ -0,0 +1,5 @@ +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 new file mode 100644 index 000000000000..c57efe9a8fcb --- /dev/null +++ b/stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + * Yoshi Tashiro diff --git a/stock_valuation_fifo_lot/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..5fab771c239a --- /dev/null +++ b/stock_valuation_fifo_lot/readme/DESCRIPTION.rst @@ -0,0 +1,56 @@ +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/icon.png b/stock_valuation_fifo_lot/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/stock_valuation_fifo_lot/static/description/icon.png differ 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..5d5f1a238547 --- /dev/null +++ b/stock_valuation_fifo_lot/static/description/index.html @@ -0,0 +1,524 @@ + + + + + +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 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. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

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

+

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.

+
+ +
+
+

Authors

+
    +
  • Ecosoft
  • +
  • Quartile
  • +
+
+
+

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/tests/__init__.py b/stock_valuation_fifo_lot/tests/__init__.py new file mode 100644 index 000000000000..d3158fe7f5c7 --- /dev/null +++ b/stock_valuation_fifo_lot/tests/__init__.py @@ -0,0 +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 new file mode 100644 index 000000000000..071769adadd2 --- /dev/null +++ b/stock_valuation_fifo_lot/tests/test_stock_valuation_fifo_lot.py @@ -0,0 +1,276 @@ +# 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.addons.stock_valuation_fifo_lot.tests.common import ( + TestStockValuationFifoCommon, +) + + +class TestStockValuationFifoLot(TestStockValuationFifoCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_receive_deliver_return_deliver_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.remaining_value, + 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", + ) + + delivery_picking, move_out = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for delivery of lot 002 should be 500.0", + ) + self.assertEqual( + move_in.stock_valuation_layer_ids.remaining_qty, + 10.0, + "Remaining quantity for first incoming receipt should be 10.0", + ) + + return_move = self.transfer_return(delivery_picking, 5.0) + self.assertEqual( + return_move.stock_valuation_layer_ids.remaining_value, + 500.0, + "Remaining value for returned lot 002 should be 500.0", + ) + self.assertEqual(return_move.stock_valuation_layer_ids.remaining_qty, 5.0) + + _, move_out_2 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + self.assertEqual( + abs(move_out_2.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for second delivery of lot 002 should be 500.0", + ) + self.assertEqual( + return_move.stock_valuation_layer_ids.remaining_qty, + 0.0, + "The remaining qauntity of returned lot 002 should be 0.00", + ) + + def test_delivery_use_incoming_price(self): + self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, + ) + self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.0, + ) + 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_move = self.transfer_return(delivery_picking, 5.0) + self.assertEqual( + return_move.stock_valuation_layer_ids.remaining_value, + 1000.0, + "Remaining value 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.remaining_value, + 2500.0, + "Remaining value for the first receipt should be 2500.0", + ) + + 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", + ) + + 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", + ) + + 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", + ) + + 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): + self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["001"], + 100.0, + ) + self.create_picking( + self.supplier_location, + self.stock_location, + self.picking_type_in, + ["002"], + 200.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 for positive quantity 5.", + ) + + 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, + ) + self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["002"], + is_receipt=False, + ) + move_line_lot_001 = move_in.move_line_ids.filtered( + lambda ml: ml.lot_name == "001" + ) + move_line_lot_001.qty_remaining = 0.0 + move_line_lot_001.qty_consumed = 5.0 + move_line_lot_002 = move_in.move_line_ids.filtered( + lambda ml: ml.lot_name == "002" + ) + move_line_lot_002.qty_remaining = 5.0 + move_line_lot_002.qty_consumed = 0.0 + + # Create delivery for lot 001 + with self.assertRaises(UserError): + _, _ = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + ) + _, move_out_001 = self.create_picking( + self.stock_location, + self.customer_location, + self.picking_type_out, + ["001"], + is_receipt=False, + force_lot_name="002", + ) + self.assertEqual( + abs(move_out_001.stock_valuation_layer_ids.value), + 500.0, + "Stock valuation for the delivery of lot 001 should be 500.0", + ) diff --git a/stock_valuation_fifo_lot/views/res_config_settings_views.xml b/stock_valuation_fifo_lot/views/res_config_settings_views.xml new file mode 100644 index 000000000000..8d0ab84b7be6 --- /dev/null +++ b/stock_valuation_fifo_lot/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + res.config.settings.form + res.config.settings + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/stock_valuation_fifo_lot/views/stock_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 + + + + + + + + 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..853cdeed2a31 --- /dev/null +++ b/stock_valuation_fifo_lot/views/stock_valuation_layer_views.xml @@ -0,0 +1,41 @@ + + + stock.valuation.layer.search + stock.valuation.layer + + + + + + + + + + + + + stock.valuation.layer.tree + stock.valuation.layer + + + + + + + + + + stock.valuation.layer.form + stock.valuation.layer + + + + + + + + diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/README.rst b/stock_valuation_fifo_lot_mrp_landed_cost/README.rst new file mode 100644 index 000000000000..54504e940f3f --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/README.rst @@ -0,0 +1,98 @@ +======================================== +Stock Valuation Fifo Lot MRP Landed Cost +======================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:21ea12a0400da8515046cc3fa647d54482ff123046d5f5e8ae0e90b89058f0b9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/stock_valuation_fifo_lot_mrp_landed_cost + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-stock_valuation_fifo_lot_mrp_landed_cost + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Ecosoft + +Contributors +~~~~~~~~~~~~ + +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-newtratip| image:: https://github.com/newtratip.png?size=40px + :target: https://github.com/newtratip + :alt: newtratip + +Current `maintainer `__: + +|maintainer-newtratip| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py b/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py b/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py new file mode 100644 index 000000000000..dac94be7711f --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + "name": "Stock Valuation Fifo Lot MRP Landed Cost", + "version": "16.0.1.0.0", + "category": "Warehouse Management", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "Ecosoft, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "depends": ["stock_valuation_fifo_lot", "mrp_landed_costs"], + "installable": True, + "maintainers": ["newtratip"], +} diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py b/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py new file mode 100644 index 000000000000..e9907b68d094 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/models/__init__.py @@ -0,0 +1 @@ +from . import stock_landed_cost diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py b/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py new file mode 100644 index 000000000000..98ac48b22a5c --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/models/stock_landed_cost.py @@ -0,0 +1,17 @@ +# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models + + +class StockLandedCost(models.Model): + _inherit = "stock.landed.cost" + + def button_validate(self): + "Update Lots/SN on stock.valuation.layer line" + res = super().button_validate() + for rec in self.stock_valuation_layer_ids: + lot_ids = self.mrp_production_ids.mapped("lot_producing_id") + if lot_ids: + rec.write({"lot_ids": lot_ids}) + return res diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst b/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..e2e916aa7529 --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/readme/CONTRIBUTORS.rst @@ -0,0 +1,10 @@ +* `Ecosoft `__: + + * Tharathip Chaweewongphan + * Saran Limpajitkutaporn + * Pimolnat Suntian + +* `Quartile `__: + + * Aung Ko Ko Lin + \ No newline at end of file diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst b/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..b5e969bd4a7a --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module propagates the lot_producing_id of mrp_production_ids to the SVL when adding the landed cost for these MOs. diff --git a/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html b/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html new file mode 100644 index 000000000000..08f45c70c26c --- /dev/null +++ b/stock_valuation_fifo_lot_mrp_landed_cost/static/description/index.html @@ -0,0 +1,437 @@ + + + + + +Stock Valuation Fifo Lot MRP Landed Cost + + + +
+

Stock Valuation Fifo Lot MRP Landed Cost

+ + +

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

+

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

+
+

Important

+

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

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

newtratip

+

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

+

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

+
+
+
+ +