-
-
Notifications
You must be signed in to change notification settings - Fork 722
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[16.0][ADD] stock_valuation_specific_identification
New module to add Specific Identification Inventory Valuation Method.
- Loading branch information
1 parent
ee521a2
commit 7fcb654
Showing
32 changed files
with
1,987 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
=========================================== | ||
Specific Identification Inventory Valuation | ||
=========================================== | ||
|
||
.. | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! This file is generated by oca-gen-addon-readme !! | ||
!! changes will be overwritten. !! | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
!! source digest: sha256:25acecb89b0f46b489b4a953ca0f1ce7f4173c57634c44d0bd601276430d4c63 | ||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! | ||
.. |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--warehouse-lightgray.png?logo=github | ||
:target: https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_valuation_specific_identification | ||
:alt: OCA/stock-logistics-warehouse | ||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png | ||
:target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-16-0/stock-logistics-warehouse-16-0-stock_valuation_specific_identification | ||
: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-warehouse&target_branch=16.0 | ||
:alt: Try me on Runboat | ||
|
||
|badge1| |badge2| |badge3| |badge4| |badge5| | ||
|
||
The Specific Identification Inventory Valuation Method tracks value of | ||
specific items in inventory based on lot or serial number. This method | ||
is distinguished from FIFO, which groups pieces of inventory together | ||
based on when they were purchased, and how much they cost. | ||
|
||
This module adds fields to the valuation layer, so that costs can be | ||
tracked by lot/serial. This feature is associated with the product | ||
category. In order to enable Specific Identification, the user must | ||
choose the FIFO costing method for the category. Non-tracked products in | ||
the category will use the FIFO method. | ||
|
||
Note that negative stock quantity is not allowed for products with | ||
Specific Identification valuation. | ||
|
||
.. 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 <https://odoo-community.org/page/development-status>`_ | ||
|
||
**Table of contents** | ||
|
||
.. contents:: | ||
:local: | ||
|
||
Use Cases / Context | ||
=================== | ||
|
||
The Specific Identification Valuation Method requires that products be | ||
tracked by lot or serial number. This method is typically applied in | ||
companies that deal with a low volume of high-value items such as | ||
vehicles, jewelry, or custom handicrafts. It is also applied when buying | ||
and selling high-value used products or collectables. | ||
|
||
This method is appropriate in situations where the value of a specific | ||
serial number is significantly different from that of another serial | ||
number of the same product. It is also appropriate where a specific | ||
serial number may have significant changes in value over time, relative | ||
to other serial numbers of the same product. | ||
|
||
Installation | ||
============ | ||
|
||
For this module to be useful, you will need some accounting features. In | ||
the community edition, you should install the OCA account_usability | ||
module. | ||
|
||
Configuration | ||
============= | ||
|
||
To configure this module, you need to: | ||
|
||
1. Go to Inventory -> Configuration -> Product Categories | ||
2. Set Inventory Valuation to Automated | ||
3. Set Inventory Valuation Costing Method to FIFO | ||
4. Check the box labeled "Cost by Lot/Serial" | ||
|
||
Usage | ||
===== | ||
|
||
**Update Unit Price of Specific Lots/Serials** | ||
|
||
- Go to Inventory -> Reporting -> Valuation | ||
- Choose Group By -> Lot/Serial | ||
- Expand the row for a lot/serial that is configured for Specific | ||
Identification Valuation | ||
- Click the + (plus) button | ||
- Complete the Revaluation Wizard | ||
|
||
**Feature Demonstration:** | ||
|
||
In the following, we demonstrate two cases in which this module changes | ||
the way stock valuation is performed by the system. To set up these | ||
cases, we need to: | ||
|
||
1. Configure a category for Specific Identification Valuation | ||
2. Create a product, assigned to that category, with serial tracking | ||
enabled | ||
|
||
**Specific identification of purchased value:** | ||
|
||
1. Purchase one unit of the product on its own purchase order, | ||
serialized as SN01 | ||
2. Purchase a second unit of the product on a separate purchase order, | ||
with a different cost, serialized as SN02 | ||
3. Sell the second unit of the product (SN02) before selling the first | ||
(SN01) | ||
|
||
With FIFO costing method, the outbound move for the sold unit would take | ||
the value of the first unit purchased. This module changes the behavior | ||
so that the value of the outbound move will be that of the second unit | ||
(SN02). | ||
|
||
**Specific identification of value changes:** | ||
|
||
1. Manufacture two units of the product, serialized as SN03 and SN04 | ||
2. Revalue one of the serialized units | ||
3. Sell the revalued unit | ||
|
||
With FIFO costing method, the outbound move for the sold unit would be | ||
valued as the total valuation for value of the first unit purchased. | ||
This module changes the behavior so that the value of the outbound move | ||
will be that of the second unit (SN02). | ||
|
||
Known issues / Roadmap | ||
====================== | ||
|
||
**Known Issues:** | ||
|
||
- In order to have different prices for different serialized units of | ||
the same product, create a separate purchase order for each serial | ||
number | ||
- When returning a delivery of multiple serialized units of the same | ||
product, each serial number must be returned separately | ||
- Negative stock quantities are not allowed for products with specific | ||
identification valuation | ||
- Modifying completed moves with multiple lots/serials is not allowed | ||
|
||
**Future Improvements:** | ||
|
||
- Inherit and modify the wizard ``stock.valuation.layer.revaluation``, | ||
rather than creating a new wizard | ||
``stock.valuation.layer.lot.revaluation`` | ||
- Inherit and modify the product ``_run_fifo()`` method, rather than | ||
creating the lot ``_run_out_spec_ident`` method | ||
|
||
Bug Tracker | ||
=========== | ||
|
||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/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 <https://github.com/OCA/stock-logistics-warehouse/issues/new?body=module:%20stock_valuation_specific_identification%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. | ||
|
||
Do not contact contributors directly about support or help with technical issues. | ||
|
||
Credits | ||
======= | ||
|
||
Authors | ||
------- | ||
|
||
* Matt Taylor | ||
|
||
Contributors | ||
------------ | ||
|
||
- Matt Taylor [email protected] | ||
(https://github.com/asphaltzipper) | ||
|
||
Other credits | ||
------------- | ||
|
||
The development of this module has been financially supported by: | ||
|
||
- Asphalt Zipper Inc. | ||
|
||
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. | ||
|
||
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_valuation_specific_identification>`_ project on GitHub. | ||
|
||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from . import models | ||
from . import wizards |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Copyright 2024 Matt Taylor | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
{ | ||
"name": "Specific Identification Inventory Valuation", | ||
"summary": "Track value of specific items in inventory based on lot or serial number", | ||
"version": "16.0.1.0.0", | ||
# see https://odoo-community.org/page/development-status | ||
"development_status": "Alpha", | ||
"category": "stock", | ||
"website": "https://github.com/OCA/stock-logistics-warehouse", | ||
"author": "Matt Taylor, Odoo Community Association (OCA)", | ||
# see https://odoo-community.org/page/maintainer-role for a description of the maintainer role and responsibilities | ||
"license": "AGPL-3", | ||
"application": False, | ||
"installable": True, | ||
"depends": [ | ||
"stock_account", | ||
], | ||
"data": [ | ||
"views/product_category_views.xml", | ||
"views/stock_valuation_layer_views.xml", | ||
"wizards/stock_valuation_layer_lot_revaluation_views.xml", | ||
"security/ir.model.access.csv", | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from . import product | ||
from . import product_category | ||
from . import stock_lot | ||
from . import stock_move | ||
from . import stock_move_line | ||
from . import stock_valuation_layer |
113 changes: 113 additions & 0 deletions
113
stock_valuation_specific_identification/models/product.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# Copyright 2024 Matt Taylor | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import api, fields, models, _ | ||
from odoo.exceptions import UserError, ValidationError | ||
from odoo.tools import float_is_zero, float_repr, float_round, float_compare | ||
from odoo.exceptions import ValidationError | ||
from collections import defaultdict | ||
from datetime import datetime | ||
|
||
|
||
class ProductTemplate(models.Model): | ||
_inherit = 'product.template' | ||
|
||
specific_ident_cost = fields.Boolean( | ||
related="categ_id.property_specific_ident_cost", | ||
readonly=True, | ||
) | ||
|
||
|
||
class ProductProduct(models.Model): | ||
_inherit = 'product.product' | ||
|
||
specific_ident_cost = fields.Boolean( | ||
related="categ_id.property_specific_ident_cost", | ||
readonly=True, | ||
) | ||
|
||
# TODO: Handle lots/serials on change of cost method. | ||
# See the superseding write() method of product.product model in file | ||
# stock_account/models/product.py. | ||
|
||
def action_revaluation(self): | ||
self.ensure_one() | ||
if ( | ||
not self.specific_ident_cost or | ||
self.product_tmpl_id.tracking == 'none' | ||
): | ||
return super(ProductProduct, self).action_revaluation() | ||
else: | ||
raise UserError(_("This product must be revalued by lot/serial")) | ||
|
||
def _run_fifo(self, quantity, company): | ||
if ( | ||
not self.specific_ident_cost or | ||
self.product_tmpl_id.tracking == 'none' | ||
): | ||
return super(ProductProduct, self)._run_fifo(quantity, company) | ||
|
||
self.ensure_one() | ||
move = self.env.context.get('move', False) | ||
if move: | ||
rounding = move.product_uom.rounding | ||
valued_move_lines = move.move_line_ids.filtered( | ||
lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id) | ||
candidates = self.env['stock.valuation.layer'].sudo().search([ | ||
('product_id', '=', self.id), | ||
('remaining_qty', '>', 0), | ||
('company_id', '=', company.id), | ||
]) | ||
# extra lots | ||
lots = valued_move_lines.mapped('lot_id') | ||
new_standard_price = 0 | ||
tmp_value = 0 # to accumulate the value taken on the candidates | ||
qty_to_take_on_lots = {x: 0.0 for x in lots} | ||
|
||
for valued_move_line in valued_move_lines: | ||
lot = valued_move_line.lot_id | ||
qty_to_take_on_candidates = valued_move_line.product_uom_id._compute_quantity( | ||
valued_move_line.qty_done, move.product_id.uom_id) | ||
qty_to_take_on_lots[lot] += qty_to_take_on_candidates | ||
for candidate in candidates: | ||
if float_compare(candidate.remaining_qty, | ||
sum(candidate.stock_move_id.move_line_ids.mapped('remaining_qty')), | ||
precision_rounding=rounding) != 0: | ||
raise UserError(_("Line remaining quantity does not match " | ||
"move remaining quantity for move %s" % | ||
candidate.stock_move_id.name)) | ||
for candidate_line in candidate.stock_move_id.move_line_ids.filtered( | ||
lambda x: x.lot_id == lot and x.remaining_qty > 0.0): | ||
qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate_line.remaining_qty) | ||
candidate_unit_cost = candidate.remaining_value / candidate.remaining_qty | ||
new_standard_price = candidate_unit_cost | ||
qty_to_take_on_lots[lot] -= qty_taken_on_candidate | ||
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_line.write({ | ||
'remaining_qty': candidate_line.remaining_qty - qty_taken_on_candidate, | ||
}) | ||
candidate.write({ | ||
'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate, | ||
'remaining_value': new_remaining_value, | ||
}) | ||
# tmp_qty += qty_taken_on_candidate | ||
tmp_value += value_taken_on_candidate | ||
|
||
unavailable_lots = self.env['stock.lot'] | ||
for lot, qty in qty_to_take_on_lots.items(): | ||
if qty > 0.0: | ||
unavailable_lots |= lot | ||
if unavailable_lots: | ||
raise UserError(_("We can't process the move because the following " | ||
"lots/serials are not available: %s" % | ||
", ".join(unavailable_lots.mapped('name')))) | ||
|
||
if new_standard_price and move.product_id.cost_method == 'fifo': | ||
self.sudo().with_company(company.id).with_context(disable_auto_svl=True).standard_price = new_standard_price | ||
vals = { | ||
'value': -tmp_value, | ||
'unit_cost': tmp_value / quantity, | ||
} | ||
return vals |
36 changes: 36 additions & 0 deletions
36
stock_valuation_specific_identification/models/product_category.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Copyright 2024 Matt Taylor | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import api, fields, models | ||
from odoo.exceptions import ValidationError | ||
|
||
|
||
class ProductCategory(models.Model): | ||
_inherit = "product.category" | ||
|
||
# This can fall back to any property_cost_method, for products that are not tracked | ||
property_specific_ident_cost = fields.Boolean( | ||
string="Cost by Lot/Serial", | ||
default=False, | ||
company_dependent=True, | ||
help="""Specific Identification Valuation: | ||
- Tracked products are valued according to specific lot/serial numbers. | ||
- Untracked products are valued according to the FIFO Method. | ||
""", | ||
) | ||
|
||
# TODO: Consider another approach, like using a new property_cost_method | ||
# property_cost_method = fields.Selection( | ||
# selection_add=('specific', 'Specific Ident (SID/FIFO)'), | ||
# help="""Standard Price: The products are valued at their standard cost defined on the product. | ||
# Average Cost (AVCO): The products are valued at weighted average cost. | ||
# First In First Out (FIFO): The products are valued supposing those that enter the company first will also leave it first. | ||
# Specific Ident (SID/FIFO): The products are valued by specific lot/serial, or FIFO if not tracked. | ||
# """, | ||
# ) | ||
|
||
@api.constrains('property_cost_method', 'property_specific_ident_cost') | ||
def _validate_specific_ident_cost(self): | ||
if self.property_cost_method and self.property_cost_method != 'fifo': | ||
raise ValidationError("Costing by Lot/Serial requires FIFO as a " | ||
"fallback for untracked products") |
Oops, something went wrong.