Skip to content

Commit

Permalink
[16.0][ADD] stock_valuation_specific_identification
Browse files Browse the repository at this point in the history
New module to add Specific Identification Inventory Valuation Method.
  • Loading branch information
matt454357 committed Aug 14, 2024
1 parent ee521a2 commit 7fcb654
Show file tree
Hide file tree
Showing 32 changed files with 1,987 additions and 0 deletions.
202 changes: 202 additions & 0 deletions stock_valuation_specific_identification/README.rst
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.
4 changes: 4 additions & 0 deletions stock_valuation_specific_identification/__init__.py
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
25 changes: 25 additions & 0 deletions stock_valuation_specific_identification/__manifest__.py
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",
],
}
8 changes: 8 additions & 0 deletions stock_valuation_specific_identification/models/__init__.py
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 stock_valuation_specific_identification/models/product.py
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 stock_valuation_specific_identification/models/product_category.py
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")
Loading

0 comments on commit 7fcb654

Please sign in to comment.