-
-
Notifications
You must be signed in to change notification settings - Fork 654
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Avoid completely overriding _run_fifo and use the standard behavior. - Update the product's standard price to the first available lot price. - Improve _get_price_unit to retrieve the value from the most recent incoming move line for the lot for the No PO (e.g. customer returns) stock moves. - Add quantity and value-related fields in stock_move_line to cover all cases including multiple lots exist in an SVL record, and it is unclear which lot's remaining quantity is being delivered(Eg. Receive serials 001, 002 and 003 for an incoming move. Deliver 002, return 002 and deliver 002 again.) - Add force_fifo_lot_id in stock_move_line to handle cases where a delivery needs to be made for an existing lot created before the installation of this module (e.g., lots 001 and 002 were received, lot 002 was delivered before the module's installation, and lot 001 is delivered after the module is installed). - Add a post_init_hook to update the lot_ids in SVL for existing records and to update the field values in existing stock_move_line records. - Remove stock_no_negative from dependency and check negative inventory is created when SVL is created for delivery.
- Loading branch information
1 parent
a7a187c
commit bdc4591
Showing
21 changed files
with
1,096 additions
and
257 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
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 |
---|---|---|
@@ -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 |
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
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,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 |
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
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 |
---|---|---|
@@ -1,111 +1,99 @@ | ||
# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th) | ||
# Copyright 2024 Quartile (https://www.quartile.co) | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) | ||
|
||
from odoo import models | ||
from collections import defaultdict | ||
|
||
from odoo import _, models | ||
from odoo.exceptions import UserError | ||
from odoo.osv import expression | ||
from odoo.tools import float_is_zero | ||
|
||
|
||
class ProductProduct(models.Model): | ||
_inherit = "product.product" | ||
|
||
def _get_fifo_candidates_domain(self, company): | ||
res = super()._get_fifo_candidates_domain(company) | ||
fifo_lot = self.env.context.get("fifo_lot") | ||
if not fifo_lot: | ||
return res | ||
return expression.AND([res, [("lot_ids", "in", fifo_lot.ids)]]) | ||
|
||
def _sort_by_all_candidates(self, all_candidates, sort_by): | ||
"""Hook function for other sort by""" | ||
return all_candidates | ||
|
||
def _get_fifo_candidates(self, company): | ||
all_candidates = super()._get_fifo_candidates(company) | ||
fifo_lot = self.env.context.get("fifo_lot") | ||
if fifo_lot: | ||
for svl in all_candidates: | ||
if not svl._get_unconsumed_in_move_line(fifo_lot): | ||
all_candidates -= svl | ||
if not all_candidates: | ||
raise UserError( | ||
_( | ||
"There is no remaining balance for FIFO valuation for the " | ||
"lot/serial %s. Please select a Force FIFO Lot/Serial in the " | ||
"detailed operation line." | ||
) | ||
% fifo_lot.display_name | ||
) | ||
sort_by = self.env.context.get("sort_by") | ||
if sort_by == "lot_create_date": | ||
|
||
def sorting_key(candidate): | ||
if candidate.lot_ids: | ||
return min(candidate.lot_ids.mapped("create_date")) | ||
else: | ||
return candidate.create_date | ||
return candidate.create_date | ||
|
||
all_candidates = all_candidates.sorted(key=sorting_key) | ||
elif sort_by is not None: | ||
all_candidates = self._sort_by_all_candidates(all_candidates, sort_by) | ||
return all_candidates | ||
|
||
def _run_fifo(self, quantity, company): | ||
self.ensure_one() | ||
move_id = self._context.get("used_in_move_id") | ||
if self.tracking == "none" or not move_id: | ||
return super()._run_fifo(quantity, company) | ||
|
||
move = self.env["stock.move"].browse(move_id) | ||
move_lines = move._get_out_move_lines() | ||
tmp_value = 0 | ||
tmp_remaining_qty = 0 | ||
for move_line in move_lines: | ||
# Find back incoming stock valuation layers | ||
# (called candidates here) to value `quantity`. | ||
qty_to_take_on_candidates = move_line.product_uom_id._compute_quantity( | ||
move_line.qty_done, move.product_id.uom_id | ||
# Depends on https://github.com/odoo/odoo/pull/180245 | ||
def _get_qty_taken_on_candidate(self, qty_to_take_on_candidates, candidate): | ||
fifo_lot = self.env.context.get("fifo_lot") | ||
if fifo_lot: | ||
candidate_ml = candidate._get_unconsumed_in_move_line(fifo_lot) | ||
qty_to_take_on_candidates = min( | ||
qty_to_take_on_candidates, candidate_ml.qty_remaining | ||
) | ||
# Find incoming stock valuation layers that have lot_ids on their moves | ||
# Check with stock_move_id.lot_ids to cover the situation where the stock | ||
# was received either before or after the installation of this module | ||
candidates = self._get_fifo_candidates(company).filtered( | ||
lambda l: move_line.lot_id in l.stock_move_id.lot_ids | ||
candidate_ml.qty_consumed += qty_to_take_on_candidates | ||
candidate_ml.value_consumed += qty_to_take_on_candidates * ( | ||
candidate.remaining_value / candidate.remaining_qty | ||
) | ||
for candidate in candidates: | ||
qty_taken_on_candidate = min( | ||
qty_to_take_on_candidates, candidate.remaining_qty | ||
) | ||
return super()._get_qty_taken_on_candidate(qty_to_take_on_candidates, candidate) | ||
|
||
candidate_unit_cost = ( | ||
candidate.remaining_value / candidate.remaining_qty | ||
) | ||
value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost | ||
value_taken_on_candidate = candidate.currency_id.round( | ||
value_taken_on_candidate | ||
) | ||
new_remaining_value = ( | ||
candidate.remaining_value - value_taken_on_candidate | ||
def _run_fifo(self, quantity, company): | ||
self.ensure_one() | ||
fifo_move = self._context.get("fifo_move") | ||
if self.tracking == "none" or not fifo_move: | ||
return super()._run_fifo(quantity, company) | ||
remaining_qty = quantity | ||
vals = defaultdict(float) | ||
correction_ml = self.env.context.get("correction_move_line") | ||
move_lines = correction_ml or fifo_move._get_out_move_lines() | ||
moved_qty = 0 | ||
for ml in move_lines: | ||
fifo_lot = ml.force_fifo_lot_id or ml.lot_id | ||
if correction_ml: | ||
moved_qty = quantity | ||
else: | ||
moved_qty = ml.product_uom_id._compute_quantity( | ||
ml.qty_done, self.uom_id | ||
) | ||
|
||
candidate_vals = { | ||
"remaining_qty": candidate.remaining_qty - qty_taken_on_candidate, | ||
"remaining_value": new_remaining_value, | ||
} | ||
|
||
candidate.write(candidate_vals) | ||
|
||
qty_to_take_on_candidates -= qty_taken_on_candidate | ||
tmp_value += value_taken_on_candidate | ||
|
||
if float_is_zero( | ||
qty_to_take_on_candidates, | ||
precision_rounding=self.uom_id.rounding, | ||
): | ||
break | ||
|
||
if candidates and qty_to_take_on_candidates > 0: | ||
tmp_value += abs(candidate.unit_cost * -qty_to_take_on_candidates) | ||
tmp_remaining_qty += qty_to_take_on_candidates | ||
|
||
# Calculate standard price (Sorted by lot created date) | ||
all_candidates = self.with_context( | ||
sort_by="lot_create_date" | ||
)._get_fifo_candidates(company) | ||
new_standard_price = 0.0 | ||
if all_candidates: | ||
new_standard_price = all_candidates[0].unit_cost | ||
elif candidates: | ||
new_standard_price = candidate.unit_cost | ||
|
||
# Update standard price | ||
if new_standard_price and self.cost_method == "fifo": | ||
self.sudo().with_company(company.id).with_context( | ||
disable_auto_svl=True | ||
).standard_price = new_standard_price | ||
|
||
# Value | ||
vals = { | ||
"remaining_qty": -tmp_remaining_qty, | ||
"value": -tmp_value, | ||
"unit_cost": tmp_value / (quantity + tmp_remaining_qty), | ||
} | ||
fifo_qty = min(remaining_qty, moved_qty) | ||
self = self.with_context(fifo_lot=fifo_lot, fifo_qty=fifo_qty) | ||
ml_fifo_vals = super()._run_fifo(fifo_qty, company) | ||
for key, value in ml_fifo_vals.items(): | ||
if key in ("remaining_qty", "value"): | ||
vals[key] += value | ||
continue | ||
vals[key] = value # unit_cost | ||
remaining_qty -= fifo_qty | ||
if float_is_zero(remaining_qty, precision_rounding=self.uom_id.rounding): | ||
break | ||
return vals |
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
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
Oops, something went wrong.