Skip to content

Commit

Permalink
add a means of resolving conflicts, update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
yostashiro committed Sep 16, 2024
1 parent 9d5d4d2 commit fa44d0d
Show file tree
Hide file tree
Showing 18 changed files with 291 additions and 40 deletions.
1 change: 1 addition & 0 deletions stock_valuation_fifo_lot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)

from . import models
from .hooks import post_init_hook
2 changes: 2 additions & 0 deletions stock_valuation_fifo_lot/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
"data": [
"views/res_config_settings_views.xml",
"views/stock_move_line_views.xml",
"views/stock_package_level_views.xml",
"views/stock_valuation_layer_views.xml",
],
"installable": True,
"post_init_hook": "post_init_hook",
"maintainers": ["newtratip"],
}
34 changes: 34 additions & 0 deletions stock_valuation_fifo_lot/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2024 Quartile (https://www.quartile.co)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)

from odoo import SUPERUSER_ID, api
from odoo.tools import float_is_zero


def post_init_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})

svls = env["stock.valuation.layer"].search([("stock_move_id", "!=", False)])
for svl in svls:
svl.lot_ids = svl.stock_move_id.lot_ids
if not svl.lot_ids:
continue
if svl.quantity <= 0: # Skip outgoing ones
continue
if svl.product_id.with_company(svl.company_id.id).cost_method != "fifo":
continue
svl_consumed_qty = svl_consumed_qty_bal = svl.quantity - svl.remaining_qty
if not svl_consumed_qty:
continue
svl_total_value = svl.value + sum(svl.stock_valuation_layer_ids.mapped("value"))
svl_consumed_value = svl_total_value - svl.remaining_value
product_uom = svl.product_id.uom_id
for ml in svl.stock_move_id.move_line_ids.sorted("id"):
ml_uom = ml.product_uom_id
ml_qty = ml_uom._compute_quantity(ml.qty_done, product_uom)
qty_to_allocate = min(svl_consumed_qty_bal, ml_qty)
ml.qty_consumed += product_uom._compute_quantity(qty_to_allocate, ml_uom)
svl_consumed_qty_bal -= qty_to_allocate
ml.value_consumed += svl_consumed_value * qty_to_allocate / svl_consumed_qty
if float_is_zero(svl_consumed_qty_bal, precision_rounding=ml_uom.rounding):
break
1 change: 1 addition & 0 deletions stock_valuation_fifo_lot/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from . import product
from . import res_company
from . import res_config_settings
from . import stock_lot
from . import stock_move
from . import stock_move_line
from . import stock_valuation_layer
32 changes: 25 additions & 7 deletions stock_valuation_fifo_lot/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from collections import defaultdict

from odoo import models
from odoo import _, models
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import float_is_zero

Expand All @@ -30,6 +31,15 @@ def _get_fifo_candidates(self, company):
for svl in all_candidates:
if not svl._get_unconsumed_in_move_line(fifo_lot):
all_candidates -= svl
if not all_candidates:
raise UserError(
_(
"There is no remaining balance for FIFO valuation for the "
"lot/serial %s. Please select a Force FIFO Lot/Serial in the "
"detailed operation line."
)
% fifo_lot.display_name
)
sort_by = self.env.context.get("sort_by")
if sort_by == "lot_create_date":

Expand All @@ -47,12 +57,19 @@ def sorting_key(candidate):
def _get_qty_taken_on_candidate(self, qty_to_take_on_candidates, candidate):
fifo_lot = self.env.context.get("fifo_lot")
if fifo_lot:
candidate_move_line = candidate._get_unconsumed_in_move_line(fifo_lot)
qty_to_take_on_candidates = min(
qty_to_take_on_candidates, candidate_move_line.qty_remaining
candidate_ml = candidate._get_unconsumed_in_move_line(fifo_lot)
ml_uom = candidate_ml.product_uom_id
ml_qty_remaining = ml_uom._compute_quantity(
candidate_ml.qty_remaining, candidate_ml.product_id.uom_id
)
candidate_move_line.qty_consumed += qty_to_take_on_candidates
candidate_move_line.cost_consumed += qty_to_take_on_candidates * (
qty_to_take_on_candidates = min(qty_to_take_on_candidates, ml_qty_remaining)
ml_qty_to_take_on_candidates = (
candidate_ml.product_id.uom_id._compute_quantity(
qty_to_take_on_candidates, ml_uom
)
)
candidate_ml.qty_consumed += ml_qty_to_take_on_candidates
candidate_ml.value_consumed += ml_qty_to_take_on_candidates * (
candidate.remaining_value / candidate.remaining_qty
)
return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate)
Expand All @@ -67,9 +84,10 @@ def _run_fifo(self, quantity, company):
correction_move_line = self.env.context.get("correction_move_line")
move_lines = correction_move_line or fifo_move._get_out_move_lines()
for ml in move_lines:
fifo_lot = ml.force_fifo_lot_id or ml.lot_id
ml_qty = fifo_move.product_uom._compute_quantity(ml.qty_done, self.uom_id)
fifo_qty = min(remaining_qty, ml_qty)
self = self.with_context(fifo_lot=ml.lot_id, fifo_qty=fifo_qty)
self = self.with_context(fifo_lot=fifo_lot, fifo_qty=fifo_qty)
ml_fifo_vals = super()._run_fifo(fifo_qty, company)
for key, value in ml_fifo_vals.items():
if key in ("remaining_qty", "value"):
Expand Down
6 changes: 4 additions & 2 deletions stock_valuation_fifo_lot/models/res_company.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
class ResCompany(models.Model):
_inherit = "res.company"

use_lot_get_price_unit_fifo = fields.Boolean(
default=True, help="Use the FIFO price unit by lot when there is no PO."
use_lot_cost_for_new_stock = fields.Boolean(
"Use Last Lot/Serial Cost for New Stock",
default=True,
help="Use the lot/serial cost for FIFO products for non-purchase receipts.",
)
7 changes: 4 additions & 3 deletions stock_valuation_fifo_lot/models/res_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

use_lot_get_price_unit_fifo = fields.Boolean(
related="company_id.use_lot_get_price_unit_fifo",
use_lot_cost_for_new_stock = fields.Boolean(
"Use Last Lot/Serial Cost for New Stock",
related="company_id.use_lot_cost_for_new_stock",
readonly=False,
help="Use the FIFO price unit by lot when there is no PO.",
help="Use the lot/serial cost for FIFO products for non-purchase receipts.",
)
32 changes: 32 additions & 0 deletions stock_valuation_fifo_lot/models/stock_lot.py
Original file line number Diff line number Diff line change
@@ -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
)
)
10 changes: 4 additions & 6 deletions stock_valuation_fifo_lot/models/stock_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,16 @@ def _create_out_svl(self, forced_quantity=None):
layers = self.env["stock.valuation.layer"]
for move in self:
move = move.with_context(fifo_move=move)
layer = super(StockMove, move)._create_out_svl(
layers |= super(StockMove, move)._create_out_svl(
forced_quantity=forced_quantity
)
layers |= layer
return layers

def _create_in_svl(self, forced_quantity=None):
"""Change product standard price to the first available lot price."""
layers = self.env["stock.valuation.layer"]
for move in self:
layer = super(StockMove, move)._create_in_svl(
layers |= super(StockMove, move)._create_in_svl(
forced_quantity=forced_quantity
)
product = move.product_id
Expand All @@ -44,15 +43,14 @@ def _create_in_svl(self, forced_quantity=None):
product = product.with_company(move.company_id.id)
product = product.with_context(disable_auto_svl=True)
product.sudo().standard_price = candidate.unit_cost
layers |= layer
return layers

def _get_price_unit(self):
"""No PO (e.g. customer returns) and get the price unit from the last consumed
incoming move line for the lot.
"""
self.ensure_one()
if not self.company_id.use_lot_get_price_unit_fifo:
if not self.company_id.use_lot_cost_for_new_stock:
return super()._get_price_unit()
if hasattr(self, "purchase_line_id") and self.purchase_line_id:
return super()._get_price_unit()
Expand All @@ -69,5 +67,5 @@ def _get_price_unit(self):
limit=1,
)
if move_line:
return move_line.cost_consumed / move_line.qty_consumed
return move_line.value_consumed / move_line.qty_consumed
return super()._get_price_unit()
47 changes: 40 additions & 7 deletions stock_valuation_fifo_lot/models/stock_move_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,59 @@ class StockMoveLine(models.Model):
"for FIFO products with a lot/serial.",
)
qty_remaining = fields.Float(
compute="_compute_qty_remaining",
compute="_compute_remaining_value",
store=True,
help="Remaining quantity for valued incoming moves for FIFO products with a "
"lot/serial.",
)
company_currency_id = fields.Many2one(related="company_id.currency_id")
cost_consumed = fields.Monetary(
value_consumed = fields.Monetary(
currency_field="company_currency_id",
help="The value of the inventory that has been consumed for FIFO products with "
"a lot/serial.",
)
value_remaining = fields.Monetary(
compute="_compute_remaining_value",
store=True,
currency_field="company_currency_id",
)
force_fifo_lot_id = fields.Many2one(
"stock.lot",
"Force FIFO Lot/Serial",
help="Specify a lot/serial to be consumed (in FIFO costing terms) for the "
"outgoing move line, in case the selected lot has already gone out of stock "
"(in FIFO costing terms).",
)

@api.depends("qty_done", "qty_consumed")
def _compute_qty_remaining(self):
@api.depends(
"qty_done", "qty_consumed", "move_id.stock_valuation_layer_ids.remaining_value"
)
def _compute_remaining_value(self):
for rec in self:
if rec.location_usage not in (
if (
rec.product_id.with_company(rec.company_id.id).cost_method != "fifo"
or not rec.lot_id
):
continue
if rec.location_usage in (
"internal",
"transit",
) and rec.location_dest_usage in ("internal", "transit"):
rec.qty_remaining = rec.qty_done - rec.qty_consumed
) or rec.location_dest_usage not in ("internal", "transit"):
continue
rec.qty_remaining = rec.qty_done - rec.qty_consumed
layers = rec.move_id.stock_valuation_layer_ids
remaining_qty = rec.product_uom_id._compute_quantity(
sum(layers.mapped("remaining_qty")), rec.product_id.uom_id
)
if not remaining_qty:
rec.qty_remaining = 0
rec.value_remaining = 0
continue
rec.value_remaining = (
sum(layers.mapped("remaining_value"))
* rec.qty_remaining
/ remaining_qty
)

def _create_correction_svl(self, move, diff):
# Pass the move line as a context value in case qty_done is overridden in a done
Expand Down
2 changes: 1 addition & 1 deletion stock_valuation_fifo_lot/models/stock_valuation_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class StockValuationLayer(models.Model):

lot_ids = fields.Many2many(
comodel_name="stock.lot",
string="Lots/Serial Numbers",
string="Lots/Serials",
)

def _get_unconsumed_in_move_line(self, lot):
Expand Down
7 changes: 5 additions & 2 deletions stock_valuation_fifo_lot/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
If necessary, update the 'Use FIFO cost by lot' setting under Inventory > Configuration > Settings to use the lot cost instead of the standard _get_price() behavior when there is no relation to a purchase order in the stock move.
(enabled by default).
Disable the 'Use Last Lot/Serial Cost for New Stock' setting under *Inventory >
Configuration > Settings*, which is enabled by default, to use the standard
`_get_price()` behavior instead of the lot cost, for receipts of specific lots/serials
with no link to a purchase order (i.e. customer returns and positive inventory
adjustments).
2 changes: 1 addition & 1 deletion stock_valuation_fifo_lot/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
* `Quartile <https://www.quartile.co>`__:

* Aung Ko Ko Lin

* Yoshi Tashiro
51 changes: 50 additions & 1 deletion stock_valuation_fifo_lot/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1 +1,50 @@
This module is used to calculate FIFO cost by lot.
This module changes the scope of FIFO cost calculation to specific lots/serials (as
opposed to products), effectively achieving Specific Identification costing method.

Example: Lot-Level Costing
~~~~~~~~~~~~~~~~~~~~~~~~~~

- Purchase:

- Lot A: 100 units at $10 each.
- Lot B: 100 units at $12 each.

- Sale:

- 50 units from Lot B.

- COGS Calculation:

- 50 units * $12 = $600 assigned to COGS.

- Ending Inventory:

- Lot A: 100 units at $10 each.
- Lot B: 50 units at $12 each.

Main UI Changes
~~~~~~~~~~~~~~~

- Stock Valuation Layer

- Adds the following field:

- 'Lots/Serials': Taken from related stock moves.

- Stock Move Line

- Adds the following fields:

- 'Qty Consumed' [*]_: Consumed quantity by outgoing moves.
- 'Value Consumed' [*]_: Consumed value by outgoing moves.
- 'Qty Remaining' [*]_: Remaining quantity (the total by product should match that
of the inventory valuation).
- 'Value Remaining' [*]_: Remaining value (the total by product should match that
of the inventory valuation).
- 'Force FIFO Lot/Serial': Used when you are stuck by not being able to find a FIFO
balance for the lot in an outgoing move line.

.. [*] Updated only for valued incoming moves of the products with FIFO costing method.
The values here represent the theoretical figures in terms of FIFO costing,
meaning that they may differ from the actual stock situation especially for
those updated at the installation of this module.
11 changes: 11 additions & 0 deletions stock_valuation_fifo_lot/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit fa44d0d

Please sign in to comment.