add support for manufactured product also
This commit is contained in:
parent
16d9d42ad9
commit
693f80e02b
@ -9,7 +9,7 @@
|
|||||||
redistributing the cost to COGS and Inventory.
|
redistributing the cost to COGS and Inventory.
|
||||||
""",
|
""",
|
||||||
'author': 'Suherdy Yacob',
|
'author': 'Suherdy Yacob',
|
||||||
'depends': ['stock_account', 'account'],
|
'depends': ['stock_account', 'account', 'mrp'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/ir_sequence_data.xml',
|
'data/ir_sequence_data.xml',
|
||||||
|
|||||||
@ -103,9 +103,12 @@ class StockReadjustValuation(models.Model):
|
|||||||
# Re-run initial stock just in case dates changed
|
# Re-run initial stock just in case dates changed
|
||||||
self._compute_initial_stock()
|
self._compute_initial_stock()
|
||||||
|
|
||||||
|
# 1. Reset & Load Base Vendor Data
|
||||||
|
# We separate Vendor (Immutable) from Mfg (Mutable)
|
||||||
|
vendor_data = {} # {product_id: (qty, value)}
|
||||||
|
|
||||||
for line in self.line_ids:
|
for line in self.line_ids:
|
||||||
# 2. Purchases (Start <= Date <= End)
|
# Vendor Incoming Layers
|
||||||
# Find incoming layers
|
|
||||||
incoming_layers = self.env['stock.valuation.layer'].search([
|
incoming_layers = self.env['stock.valuation.layer'].search([
|
||||||
('product_id', '=', line.product_id.id),
|
('product_id', '=', line.product_id.id),
|
||||||
('create_date', '>=', self.date_start),
|
('create_date', '>=', self.date_start),
|
||||||
@ -114,19 +117,131 @@ class StockReadjustValuation(models.Model):
|
|||||||
('stock_move_id.location_id.usage', '=', 'supplier'), # From Vendor
|
('stock_move_id.location_id.usage', '=', 'supplier'), # From Vendor
|
||||||
('company_id', '=', self.company_id.id)
|
('company_id', '=', self.company_id.id)
|
||||||
])
|
])
|
||||||
|
v_qty = sum(incoming_layers.mapped('quantity'))
|
||||||
|
v_val = sum(incoming_layers.mapped('value'))
|
||||||
|
vendor_data[line.product_id.id] = (v_qty, v_val)
|
||||||
|
|
||||||
line.purchase_qty = sum(incoming_layers.mapped('quantity'))
|
# Initialize with Vendor Data only first
|
||||||
line.purchase_value = sum(incoming_layers.mapped('value'))
|
line.purchase_qty = v_qty
|
||||||
|
line.purchase_value = v_val
|
||||||
# 3. Calculate Weighted Average
|
|
||||||
total_qty = line.qty_counted + line.purchase_qty
|
|
||||||
total_value = line.target_initial_value + line.purchase_value
|
|
||||||
|
|
||||||
|
# Initial Average calc
|
||||||
|
total_qty = line.qty_counted + v_qty
|
||||||
|
total_value = line.target_initial_value + v_val
|
||||||
if not float_is_zero(total_qty, precision_rounding=line.product_id.uom_id.rounding):
|
if not float_is_zero(total_qty, precision_rounding=line.product_id.uom_id.rounding):
|
||||||
line.new_average_cost = total_value / total_qty
|
line.new_average_cost = total_value / total_qty
|
||||||
else:
|
else:
|
||||||
line.new_average_cost = 0.0
|
line.new_average_cost = line.product_id.standard_price
|
||||||
|
|
||||||
|
# 2. Iterative Cost Rollup (for Manufacturing)
|
||||||
|
# We loop to allow lower-level component cost changes to propagate to higher-level FGs
|
||||||
|
# 3 iterations should be sufficient for most chains in a retrospective batch
|
||||||
|
for _ in range(3):
|
||||||
|
# Create a localized cost map for fast lookup
|
||||||
|
# {product_id: new_cost}
|
||||||
|
cost_map = {l.product_id.id: l.new_average_cost for l in self.line_ids}
|
||||||
|
|
||||||
|
changes_made = False
|
||||||
|
|
||||||
|
for line in self.line_ids:
|
||||||
|
# Find Incoming Mfg Layers (Production -> Stock)
|
||||||
|
mfg_layers = self.env['stock.valuation.layer'].search([
|
||||||
|
('product_id', '=', line.product_id.id),
|
||||||
|
('create_date', '>=', self.date_start),
|
||||||
|
('create_date', '<=', self.date_end),
|
||||||
|
('quantity', '>', 0),
|
||||||
|
('stock_move_id.location_id.usage', '=', 'production'), # From Production
|
||||||
|
('company_id', '=', self.company_id.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
if not mfg_layers:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate New Value for these MOs
|
||||||
|
# We need to look at the MOs linked to these moves
|
||||||
|
moves = mfg_layers.mapped('stock_move_id')
|
||||||
|
production_ids = moves.mapped('production_id')
|
||||||
|
|
||||||
|
total_mfg_qty = 0
|
||||||
|
total_mfg_value = 0
|
||||||
|
|
||||||
|
for production in production_ids:
|
||||||
|
# Calculate Cost of this specific MO
|
||||||
|
# Cost = Components + Operations
|
||||||
|
|
||||||
|
# 1. Components
|
||||||
|
comp_cost = 0
|
||||||
|
for raw_move in production.move_raw_ids.filtered(lambda m: m.state == 'done'):
|
||||||
|
# Use NEW cost if available, else Standard Price
|
||||||
|
p_id = raw_move.product_id.id
|
||||||
|
price = cost_map.get(p_id, raw_move.product_id.standard_price)
|
||||||
|
comp_cost += raw_move.product_uom_qty * price
|
||||||
|
|
||||||
|
# 2. Operations (Workcenters) - Assume standard logic didn't change (immutable for now)
|
||||||
|
# We can fetch the 'value' from the original valuation layer?
|
||||||
|
# No, we must recalculate or look at existing cost share.
|
||||||
|
# Simplified: We assume Operation Cost is preserved from original recording?
|
||||||
|
# Or simpler: We don't touch Op Cost, we only readjust Material Cost.
|
||||||
|
# But how to get "Original Op Cost" easily?
|
||||||
|
# We can diff the Original Layer Value vs Original Component Value?
|
||||||
|
# That's complicated.
|
||||||
|
# Alternative: We just re-sum everything?
|
||||||
|
# Let's check if we can get operation cost.
|
||||||
|
# Usually stored in analytic lines or just captured in the layer?
|
||||||
|
# For V17, stock.valuation.layer has the total value.
|
||||||
|
# If we simply recalculate material, we might lose overheads if we don't include them.
|
||||||
|
# SAFE BET:
|
||||||
|
# Get Original Unit Cost of MO = Layer Value / Layer Qty.
|
||||||
|
# Get Original Material Cost.
|
||||||
|
# Difference = Overhead.
|
||||||
|
# New Cost = New Material + Overhead.
|
||||||
|
|
||||||
|
# Let's try to preserve Overhead.
|
||||||
|
|
||||||
|
# Original Layer for this MO (Output)
|
||||||
|
orig_output_layers = self.env['stock.valuation.layer'].search([('stock_move_id.production_id', '=', production.id), ('quantity', '>', 0)])
|
||||||
|
orig_val = sum(orig_output_layers.mapped('value'))
|
||||||
|
orig_qty = sum(orig_output_layers.mapped('quantity'))
|
||||||
|
|
||||||
|
if orig_qty == 0: continue
|
||||||
|
|
||||||
|
# Original Material Cost (from Moves)
|
||||||
|
# We sum the ABSOLUTE value of raw moves
|
||||||
|
orig_raw_val = sum([abs(m.value) for m in production.move_raw_ids if m.state == 'done'])
|
||||||
|
|
||||||
|
overhead_val = orig_val - orig_raw_val
|
||||||
|
|
||||||
|
# New Material Cost
|
||||||
|
# Calculated above as `comp_cost`
|
||||||
|
|
||||||
|
new_mo_total_value = comp_cost + overhead_val
|
||||||
|
|
||||||
|
# Add to totals
|
||||||
|
total_mfg_qty += orig_qty
|
||||||
|
total_mfg_value += new_mo_total_value
|
||||||
|
|
||||||
|
# Update Line
|
||||||
|
v_qty, v_val = vendor_data.get(line.product_id.id, (0, 0))
|
||||||
|
|
||||||
|
line.purchase_qty = v_qty + total_mfg_qty
|
||||||
|
line.purchase_value = v_val + total_mfg_value
|
||||||
|
|
||||||
|
# Recalculate Average
|
||||||
|
total_qty = line.qty_counted + line.purchase_qty
|
||||||
|
total_value = line.target_initial_value + line.purchase_value
|
||||||
|
|
||||||
|
prev_avg = line.new_average_cost
|
||||||
|
if not float_is_zero(total_qty, precision_rounding=line.product_id.uom_id.rounding):
|
||||||
|
line.new_average_cost = total_value / total_qty
|
||||||
|
else:
|
||||||
|
line.new_average_cost = 0.0
|
||||||
|
|
||||||
|
if abs(line.new_average_cost - prev_avg) > 0.01:
|
||||||
|
changes_made = True
|
||||||
|
|
||||||
|
if not changes_made:
|
||||||
|
break
|
||||||
|
|
||||||
self.state = 'calculated'
|
self.state = 'calculated'
|
||||||
|
|
||||||
def action_reset_to_draft(self):
|
def action_reset_to_draft(self):
|
||||||
@ -200,6 +315,161 @@ class StockReadjustValuation(models.Model):
|
|||||||
if not float_is_zero(diff, precision_digits=2):
|
if not float_is_zero(diff, precision_digits=2):
|
||||||
self._create_correction_from_move(move, diff)
|
self._create_correction_from_move(move, diff)
|
||||||
|
|
||||||
|
# Global Cost Map (Product ID -> New Cost)
|
||||||
|
# This map contains the new_average_cost for all products in the batch.
|
||||||
|
cost_map = {l.product_id.id: l.new_average_cost for l in self.line_ids}
|
||||||
|
|
||||||
|
for line in self.line_ids:
|
||||||
|
# 4. Readjust Manufacturing Output (Incoming MOs)
|
||||||
|
# We need to correct the value of the Finished Good entering stock
|
||||||
|
# based on the new cost of components.
|
||||||
|
|
||||||
|
mfg_layers = self.env['stock.valuation.layer'].search([
|
||||||
|
('product_id', '=', line.product_id.id),
|
||||||
|
('create_date', '>=', self.date_start),
|
||||||
|
('create_date', '<=', self.date_end),
|
||||||
|
('quantity', '>', 0), # Incoming
|
||||||
|
('stock_move_id.location_id.usage', '=', 'production'), # From Production
|
||||||
|
('company_id', '=', self.company_id.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
mfg_moves = mfg_layers.mapped('stock_move_id')
|
||||||
|
|
||||||
|
for move in mfg_moves:
|
||||||
|
production = move.production_id
|
||||||
|
if not production: continue
|
||||||
|
|
||||||
|
# Re-Calculate MO Cost (Components New Cost + Overhead)
|
||||||
|
comp_cost = 0
|
||||||
|
for raw_move in production.move_raw_ids.filtered(lambda m: m.state == 'done'):
|
||||||
|
p_id = raw_move.product_id.id
|
||||||
|
# Use NEW cost from cost_map if available, else Standard Price
|
||||||
|
price = cost_map.get(p_id, raw_move.product_id.standard_price)
|
||||||
|
comp_cost += raw_move.product_uom_qty * price
|
||||||
|
|
||||||
|
# Overhead Preservation
|
||||||
|
# Get total original output value for the entire MO
|
||||||
|
all_output_layers_for_mo = self.env['stock.valuation.layer'].search([('stock_move_id.production_id', '=', production.id)])
|
||||||
|
all_output_val_for_mo = sum(all_output_layers_for_mo.mapped('value'))
|
||||||
|
|
||||||
|
# Original Material Cost (from Raw Moves for the entire MO)
|
||||||
|
orig_raw_val_for_mo = sum([abs(m.value) for m in production.move_raw_ids if m.state == 'done'])
|
||||||
|
|
||||||
|
overhead_total = all_output_val_for_mo - orig_raw_val_for_mo
|
||||||
|
|
||||||
|
# New Total Cost for the entire MO
|
||||||
|
new_total_cost_for_mo = comp_cost + overhead_total
|
||||||
|
|
||||||
|
# Target Value for THIS specific move (proportion of total MO cost)
|
||||||
|
target_value = 0.0
|
||||||
|
if production.qty_produced > 0:
|
||||||
|
ratio = move.product_uom_qty / production.qty_produced
|
||||||
|
target_value = new_total_cost_for_mo * ratio
|
||||||
|
|
||||||
|
# Current Value of THIS move (sum of all layers for this move)
|
||||||
|
current_val = sum(self.env['stock.valuation.layer'].search([('stock_move_id', '=', move.id)]).mapped('value'))
|
||||||
|
|
||||||
|
diff = target_value - current_val
|
||||||
|
|
||||||
|
if not float_is_zero(diff, precision_digits=2):
|
||||||
|
# Create Correction
|
||||||
|
# Original Move: Dr Stock, Cr WIP
|
||||||
|
# Find accounts from original move's account_move_ids
|
||||||
|
|
||||||
|
# We need to find the account move linked to the original stock move
|
||||||
|
original_am = move.account_move_ids.filtered(lambda am: am.state == 'posted')
|
||||||
|
if not original_am:
|
||||||
|
# Fallback: try to find an SVL for this move that has an account move
|
||||||
|
original_svl_with_am = self.env['stock.valuation.layer'].search([
|
||||||
|
('stock_move_id', '=', move.id),
|
||||||
|
('account_move_id', '!=', False)
|
||||||
|
], limit=1)
|
||||||
|
if original_svl_with_am:
|
||||||
|
original_am = original_svl_with_am.account_move_id
|
||||||
|
|
||||||
|
if not original_am:
|
||||||
|
_logger.warning("Could not find original account move for stock move %s to determine accounts for MO adjustment.", move.name)
|
||||||
|
continue # Skip if can't find accounts
|
||||||
|
|
||||||
|
am_lines = original_am.line_ids
|
||||||
|
|
||||||
|
# Find Stock Account (usually debit for incoming)
|
||||||
|
stock_acc_id = line.product_id.categ_id.property_stock_valuation_account_id.id
|
||||||
|
stock_line = am_lines.filtered(lambda l: l.account_id.id == stock_acc_id)
|
||||||
|
|
||||||
|
# Find WIP Account (usually credit for incoming)
|
||||||
|
wip_acc_id = False
|
||||||
|
# Try to find the credit line that is not the stock account
|
||||||
|
wip_line = am_lines.filtered(lambda l: l.credit > 0 and l.account_id.id != stock_acc_id)
|
||||||
|
if wip_line:
|
||||||
|
wip_acc_id = wip_line[0].account_id.id
|
||||||
|
else:
|
||||||
|
# Fallback: try to find the stock production location's valuation_out_account_id
|
||||||
|
prod_location = self.env['stock.location'].search([('usage', '=', 'production')], limit=1)
|
||||||
|
if prod_location and prod_location.valuation_out_account_id:
|
||||||
|
wip_acc_id = prod_location.valuation_out_account_id.id
|
||||||
|
|
||||||
|
if not (stock_acc_id and wip_acc_id):
|
||||||
|
_logger.warning("Could not determine stock or WIP accounts for MO adjustment for move %s.", move.name)
|
||||||
|
continue # Skip if accounts not found
|
||||||
|
|
||||||
|
# Construct Account Move
|
||||||
|
# If diff > 0 (Value Up): Dr Stock, Cr WIP
|
||||||
|
debit_acc = stock_acc_id
|
||||||
|
credit_acc = wip_acc_id
|
||||||
|
amt = diff
|
||||||
|
|
||||||
|
if diff < 0:
|
||||||
|
# If diff < 0 (Value Down): Dr WIP, Cr Stock
|
||||||
|
debit_acc = wip_acc_id
|
||||||
|
credit_acc = stock_acc_id
|
||||||
|
amt = abs(diff)
|
||||||
|
|
||||||
|
description = _("Readjustment MO: %s") % production.name
|
||||||
|
|
||||||
|
move_vals = {
|
||||||
|
'journal_id': self.journal_id.id,
|
||||||
|
'date': self.date_end.date(),
|
||||||
|
'ref': f"{self.name} - {description}",
|
||||||
|
'move_type': 'entry',
|
||||||
|
'line_ids': [
|
||||||
|
(0, 0, {
|
||||||
|
'name': description,
|
||||||
|
'account_id': debit_acc,
|
||||||
|
'debit': amt,
|
||||||
|
'credit': 0,
|
||||||
|
'product_id': line.product_id.id,
|
||||||
|
}),
|
||||||
|
(0, 0, {
|
||||||
|
'name': description,
|
||||||
|
'account_id': credit_acc,
|
||||||
|
'debit': 0,
|
||||||
|
'credit': amt,
|
||||||
|
'product_id': line.product_id.id,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
ac_move = self.env['account.move'].create(move_vals)
|
||||||
|
ac_move.action_post()
|
||||||
|
self.account_move_ids = [(4, ac_move.id)]
|
||||||
|
|
||||||
|
# Create SVL
|
||||||
|
svl_vals = {
|
||||||
|
'company_id': self.company_id.id,
|
||||||
|
'product_id': line.product_id.id,
|
||||||
|
'description': description,
|
||||||
|
'stock_move_id': move.id,
|
||||||
|
'quantity': 0,
|
||||||
|
'value': diff, # Value is positive for increase, negative for decrease
|
||||||
|
'account_move_id': ac_move.id,
|
||||||
|
}
|
||||||
|
new_svl = self.env['stock.valuation.layer'].create(svl_vals)
|
||||||
|
|
||||||
|
# Force Date
|
||||||
|
self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (move.date, new_svl.id))
|
||||||
|
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (self.date_end.date(), ac_move.id))
|
||||||
|
|
||||||
self.state = 'done'
|
self.state = 'done'
|
||||||
|
|
||||||
def _create_correction_from_move(self, move, amount):
|
def _create_correction_from_move(self, move, amount):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user