add propagation method to future stock movement valuation

This commit is contained in:
Suherdy Yacob 2026-01-08 17:26:37 +07:00
parent 479513d880
commit 2ba8d857a2
2 changed files with 150 additions and 22 deletions

View File

@ -119,40 +119,168 @@ class StockInventoryRevaluation(models.Model):
# Create Stock Valuation Layer # Create Stock Valuation Layer
self._create_valuation_layer(move) self._create_valuation_layer(move)
# AVCO/FIFO Logic: Update Standard Price and Distribute Value to Layers # 1. Common Logic: Calculate Unit Adjustment & Update Standard Price
# This fixes the issue where revaluation creates a layer but doesn't update the moving average cost # This applies to Standard Price, AVCO, and FIFO
if self.product_id.categ_id.property_cost_method in ['average', 'fifo'] and self.quantity > 0: if self.quantity > 0:
# 1. Update Standard Price unit_adjust = self.extra_cost / self.quantity
# We use disable_auto_svl to prevent Odoo from creating an extra SVL for this price change new_std_price = self.product_id.standard_price + unit_adjust
new_std_price = self.product_id.standard_price + (self.extra_cost / self.quantity)
# Update the price on the product
# For Standard Price: This sets the new fixed cost.
# For AVCO/FIFO: This updates the current running average.
self.product_id.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price}) self.product_id.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price})
# 2. Distribute Value to Remaining Layers (Crucial for correct COGS later) # 2. AVCO/FIFO Specific Logic: Distribute Value to Layers & Propagate to Sales
# Standard Price does not use layers for costing, so we skip this part for it.
if self.product_id.categ_id.property_cost_method in ['average', 'fifo'] and self.quantity > 0:
# Distribute to Remaining Stock (The "Survivor" Layers)
# Track how much we distributed
total_distributed = 0.0
remaining_svls = self.env['stock.valuation.layer'].search([ remaining_svls = self.env['stock.valuation.layer'].search([
('product_id', '=', self.product_id.id), ('product_id', '=', self.product_id.id),
('remaining_qty', '>', 0), ('remaining_qty', '>', 0),
('company_id', '=', self.company_id.id), ('company_id', '=', self.company_id.id),
('create_date', '<=', self.date),
]) ])
remaining_svls = remaining_svls.filtered(lambda l: l.create_date <= self.date)
if remaining_svls: if remaining_svls:
remaining_qty_total = sum(remaining_svls.mapped('remaining_qty'))
if remaining_qty_total > 0:
remaining_value_to_distribute = self.extra_cost
remaining_value_unit_cost = remaining_value_to_distribute / remaining_qty_total
for layer in remaining_svls: for layer in remaining_svls:
if float_compare(layer.remaining_qty, remaining_qty_total, precision_rounding=self.product_id.uom_id.rounding) >= 0: adjustment_amount = layer.remaining_qty * unit_adjust
taken_remaining_value = remaining_value_to_distribute adjustment_amount = self.currency_id.round(adjustment_amount)
if not float_is_zero(adjustment_amount, precision_rounding=self.currency_id.rounding):
layer.sudo().write({
'remaining_value': layer.remaining_value + adjustment_amount,
# 'value': layer.value + adjustment_amount # Leaving value untouched to preserve historic record
})
total_distributed += adjustment_amount
# 3. Distribute to Sold Stock (The "Forward Propagation")
# Only distribute what's left! This ensures we don't "correct" sales of NEW stock.
remaining_value_to_expense = self.extra_cost - total_distributed
remaining_value_to_expense = self.currency_id.round(remaining_value_to_expense)
if float_compare(remaining_value_to_expense, 0, precision_rounding=self.currency_id.rounding) > 0:
# Find outgoing moves (sales) that happened AFTER revaluation date
outgoing_svls = self.env['stock.valuation.layer'].search([
('product_id', '=', self.product_id.id),
('company_id', '=', self.company_id.id),
('quantity', '<', 0), # Outgoing
('create_date', '>', self.date), # After revaluation
], order='create_date asc, id asc') # Chronological
if outgoing_svls:
for out_layer in outgoing_svls:
if float_compare(remaining_value_to_expense, 0, precision_rounding=self.currency_id.rounding) <= 0:
break # Limit reached
# How much correction does this move "deserve"?
qty_sold = abs(out_layer.quantity)
theoretical_correction = qty_sold * unit_adjust
theoretical_correction = self.currency_id.round(theoretical_correction)
# We can only give what we have left
actual_correction = min(theoretical_correction, remaining_value_to_expense) # If positive adjustment
if unit_adjust < 0: # If negative adjustment?
# If unit_adjust is negative, everything is negative.
# extra_cost is neg, total_dist is neg, remaining_to_exp is neg.
# abs(actual) = min(abs(theo), abs(rem))
# implementation detail: let's handle signs properly?
# Simplify: assume always positive for logic "Cap", but math works.
# Wait, min() with negatives works differently.
# -100 vs -50. min is -100. We want "Closest to zero".
pass
# Handle Sign-Agnostic Capping
# We want to reduce the magnitude of remaining_to_expense towards zero.
# If remaining is +100, we reduce by positive amounts.
# If remaining is -100, we reduce by negative amounts.
if remaining_value_to_expense > 0:
actual_correction = min(theoretical_correction, remaining_value_to_expense)
else: else:
taken_remaining_value = remaining_value_unit_cost * layer.remaining_qty # theoretical is likely negative too because unit_adjust is negative
actual_correction = max(theoretical_correction, remaining_value_to_expense)
# Rounding if float_is_zero(actual_correction, precision_rounding=self.currency_id.rounding):
taken_remaining_value = self.currency_id.round(taken_remaining_value) continue
layer.sudo().write({'remaining_value': layer.remaining_value + taken_remaining_value}) # Create Correction SVL
stock_val_acc = self.product_id.categ_id.property_stock_valuation_account_id.id
cogs_acc = self.product_id.categ_id.property_stock_account_output_categ_id.id
remaining_value_to_distribute -= taken_remaining_value if not stock_val_acc or not cogs_acc:
remaining_qty_total -= layer.remaining_qty continue
# Accounting Entries
# If Positive Adjustment (Value Added):
# Dr COGS -> Increased Cost
# Cr Stock Asset -> Decreased Asset (Since we put it all in Asset initially)
# Wait, initially we Debited Asset +100.
# Now we say "50 of that was sold".
# So we Credit Asset -50, Debit COGS +50.
# Correct.
# If Negative Adjustment (Value Removed):
# Initially Credited Asset -100.
# "50 of that removal belongs to sales".
# So we Debit Asset +50, Credit COGS -50 (Reduce Cost).
# The math: actual_correction is -50.
# Using same logic: Debit COGS with -50? (Credit 50).
# Credit Asset with -50? (Debit 50).
# Logic holds purely with signs.
move_lines = [
(0, 0, {
'name': _('Revaluation Correction for %s') % out_layer.stock_move_id.name,
'account_id': cogs_acc,
'debit': actual_correction if actual_correction > 0 else 0,
'credit': -actual_correction if actual_correction < 0 else 0,
'product_id': self.product_id.id,
}),
(0, 0, {
'name': _('Revaluation Correction for %s') % out_layer.stock_move_id.name,
'account_id': stock_val_acc,
'debit': -actual_correction if actual_correction < 0 else 0,
'credit': actual_correction if actual_correction > 0 else 0,
'product_id': self.product_id.id,
}),
]
am_vals = {
'ref': f"{self.name} - Corr - {out_layer.stock_move_id.name}",
'date': out_layer.create_date.date(),
'journal_id': self.account_journal_id.id,
'line_ids': move_lines,
'move_type': 'entry',
'company_id': self.company_id.id,
}
am = self.env['account.move'].create(am_vals)
am.action_post()
# Correction SVL
# Value should be negative of correction to reduce Asset
# If correction is +50 (add to COGS), SVL value is -50 (remove from Asset).
svl_value = -actual_correction
new_svl = self.env['stock.valuation.layer'].create({
'product_id': self.product_id.id,
'value': svl_value,
'quantity': 0,
'unit_cost': 0,
'remaining_qty': 0,
'stock_move_id': out_layer.stock_move_id.id,
'company_id': self.company_id.id,
'description': _('Revaluation Correction (from %s)') % self.name,
'account_move_id': am.id,
})
self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s',
(out_layer.create_date, new_svl.id))
remaining_value_to_expense -= actual_correction
self.state = 'done' self.state = 'done'