stock_readjust_valuation/models/stock_readjust_valuation.py

1588 lines
69 KiB
Python

from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero
from collections import defaultdict
import logging
_logger = logging.getLogger(__name__)
class StockReadjustValuation(models.Model):
_name = 'stock.readjust.valuation'
_description = 'Stock Readjust Valuation'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default=lambda self: _('New'))
date_start = fields.Datetime(string='Start Date', required=True)
date_end = fields.Datetime(string='End Date', required=True)
journal_id = fields.Many2one('account.journal', string='Journal', required=True)
line_ids = fields.One2many('stock.readjust.valuation.line', 'readjust_id', string='Products')
state = fields.Selection([
('draft', 'Draft'),
('calculated', 'Calculated'),
('processing', 'Processing'),
('done', 'Done'),
('cancel', 'Cancelled')
], string='Status', default='draft', tracking=True)
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
account_move_ids = fields.Many2many('account.move', string='Journal Entries', readonly=True)
account_move_count = fields.Integer(compute='_compute_account_move_count', string='Journal Entries Count')
@api.depends('account_move_ids')
def _compute_account_move_count(self):
for record in self:
record.account_move_count = len(record.account_move_ids)
def action_view_journal_entries(self):
self.ensure_one()
return {
'name': _('Journal Entries'),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'view_mode': 'list,form',
'domain': [('id', 'in', self.account_move_ids.ids)],
'context': {'create': False},
}
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('stock.readjust.valuation') or _('New')
return super().create(vals_list)
def action_load_products(self):
self.ensure_one()
# Find products that have stock moves in the period or prior
# For simplicity, we can load all storable products, or those with moves.
# Let's find products with valuation layers.
domain = [('type', '=', 'product')]
products = self.env['product.product'].search(domain)
existing_products = self.line_ids.mapped('product_id')
products_to_add = products - existing_products
lines = []
for product in products_to_add:
lines.append((0, 0, {
'product_id': product.id,
}))
self.write({'line_ids': lines})
# Auto-calculate initial stock for all lines (new and old)
self._compute_initial_stock()
# Explicitly set defaults for newly loaded lines
# Only set if they are zero/empty? Or force reset?
# When loading, we assume we want the defaults.
for line in self.line_ids:
if float_is_zero(line.qty_counted, precision_rounding=line.product_id.uom_id.rounding) and float_is_zero(line.target_initial_value, precision_digits=2):
line.qty_counted = line.initial_qty
line.target_initial_value = line.initial_value
def _compute_initial_stock(self):
for record in self:
for line in record.line_ids:
# 1. Initial Stock (Before Start Date)
layers = self.env['stock.valuation.layer'].search([
('product_id', '=', line.product_id.id),
('create_date', '<', record.date_start),
('company_id', '=', record.company_id.id)
])
line.initial_qty = sum(layers.mapped('quantity'))
line.initial_value = sum(layers.mapped('value'))
line.initial_qty = sum(layers.mapped('quantity'))
line.initial_value = sum(layers.mapped('value'))
# Removed automatic reset of counted/target values to prevent overwriting user input
# This is now handled by _onchange_product_id and action_load_products explicitly.
def action_calculate(self):
self.ensure_one()
# Re-run initial stock just in case dates changed
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:
# Vendor Incoming Layers
incoming_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', '=', 'supplier'), # From Vendor
('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)
# Initialize with Vendor Data only first
line.purchase_qty = v_qty
line.purchase_value = v_val
# 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):
line.new_average_cost = total_value / total_qty
else:
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'
def action_reset_to_draft(self):
self.ensure_one()
self.state = 'draft'
def action_apply(self):
self.ensure_one()
if self.state not in ('calculated', 'processing'):
if self.state == 'done':
return
if self.state != 'calculated':
raise UserError(_("Please calculate first."))
# 0. Set to processing if not already
if self.state != 'processing':
self.state = 'processing'
self.env.cr.commit()
# Optimization: Pre-fetch data to avoid loops
product_ids = self.line_ids.mapped('product_id').ids
if not product_ids:
return
all_transactions = []
cost_map = {l.product_id.id: l.new_average_cost for l in self.line_ids}
# --- Batch Step 1: Initial Values ---
processed_initial_products = set()
existing_initial_ams = self.account_move_ids.filtered(lambda m: "Readjustment: Initial Value" in (m.ref or ""))
for am in existing_initial_ams:
for line in am.line_ids:
if line.product_id:
processed_initial_products.add(line.product_id.id)
for line in self.line_ids:
if line.product_id.id in processed_initial_products:
continue
initial_diff = line.target_initial_value - line.initial_value
if not float_is_zero(initial_diff, precision_digits=2):
self._create_correction_layer(line.product_id, initial_diff, self.date_start, _("Readjustment: Initial Value"))
line.product_id.sudo().write({'standard_price': line.new_average_cost})
# Commit after Step 1
self.env.cr.commit()
# --- Batch Step 2: Identify Moves ---
base_domain = [
('product_id', 'in', product_ids),
('create_date', '>=', self.date_start),
('create_date', '<=', self.date_end),
('company_id', '=', self.company_id.id)
]
# Outgoing: Quantity < 0
outgoing_moves = self.env['stock.valuation.layer'].search(base_domain + [('quantity', '<', 0)]).mapped('stock_move_id')
# MFG Incoming: Quantity > 0 and Location Usage = Production
mfg_moves = self.env['stock.valuation.layer'].search(base_domain + [
('quantity', '>', 0),
('stock_move_id.location_id.usage', '=', 'production')
]).mapped('stock_move_id')
all_target_moves = outgoing_moves | mfg_moves
# --- IDEMPOTENCY CHECK ---
processed_move_ids = set()
if self.account_move_ids:
related_svls = self.env['stock.valuation.layer'].search([
('account_move_id', 'in', self.account_move_ids.ids)
])
processed_move_ids = set(related_svls.mapped('stock_move_id').ids)
moves_to_process = all_target_moves.filtered(lambda m: m.id not in processed_move_ids)
if not moves_to_process:
self.state = 'done'
return
# --- Batch Step 3: Fetch Data for Moves ---
all_layers_for_moves = self.env['stock.valuation.layer'].search([
('stock_move_id', 'in', moves_to_process.ids)
])
layers_by_move = defaultdict(lambda: self.env['stock.valuation.layer'])
for l in all_layers_for_moves:
layers_by_move[l.stock_move_id.id] |= l
move_to_am_map = {}
for move in moves_to_process:
am = move.account_move_ids.filtered(lambda m: m.state == 'posted')
if not am:
svls = layers_by_move[move.id]
am = svls.mapped('account_move_id').filtered(lambda m: m.state == 'posted')
if am:
move_to_am_map[move.id] = am[0]
# --- Batch Step 4: Process Outgoing (COGS) ---
outgoing_to_process = moves_to_process.filtered(lambda m: m in outgoing_moves)
for move in outgoing_to_process:
product_id = move.product_id.id
new_cost = cost_map.get(product_id)
if new_cost is None: continue
move_layers = layers_by_move[move.id]
qty_sold = abs(sum(move_layers.mapped('quantity')))
curr_val_abs = abs(sum(move_layers.mapped('value')))
target_value = qty_sold * new_cost
diff = target_value - curr_val_abs
if abs(diff) > 0.01:
original_am = move_to_am_map.get(move.id)
txs = self._prepare_correction_vals(move, -diff, original_am)
all_transactions.extend(txs)
# --- Batch Step 5: Process Manufacturing (Incoming) ---
mfg_to_process = moves_to_process.filtered(lambda m: m in mfg_moves)
for move in mfg_to_process:
production = move.production_id
if not production: continue
# Re-Calculate MO Cost
comp_cost = 0
for raw_move in production.move_raw_ids:
if raw_move.state != 'done': continue
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
all_out_layers = self.env['stock.valuation.layer'].search([('stock_move_id.production_id', '=', production.id)])
all_output_val = sum(all_out_layers.mapped('value'))
orig_raw_val = sum([abs(m.value) for m in production.move_raw_ids if m.state == 'done'])
overhead_total = all_output_val - orig_raw_val
new_total_cost = comp_cost + overhead_total
target_val = 0.0
if production.qty_produced > 0:
ratio = move.product_uom_qty / production.qty_produced
target_val = new_total_cost * ratio
current_val = sum(layers_by_move[move.id].mapped('value'))
diff = target_val - current_val
if abs(diff) > 0.01:
original_am = move_to_am_map.get(move.id)
txs = self._prepare_correction_vals(move, diff, original_am, is_mfg=True)
all_transactions.extend(txs)
# --- Batch Step 6: Chunked Create & Post ---
if not all_transactions:
self.state = 'done'
return
chunk_size = 50
created_moves = self.env['account.move']
# We need to process 'all_transactions' in chunks
for i in range(0, len(all_transactions), chunk_size):
chunk__txs = all_transactions[i:i+chunk_size]
# 1. Create Moves
move_vals_list = [t['move_vals'] for t in chunk__txs]
chunk_moves = self.env['account.move'].create(move_vals_list)
# 2. Post Moves
chunk_moves.action_post()
created_moves += chunk_moves
# 3. Create SVLs
svl_vals_list = []
for t, am in zip(chunk__txs, chunk_moves):
if t.get('svl_vals'):
vals = t['svl_vals']
vals['account_move_id'] = am.id
svl_vals_list.append(vals)
if svl_vals_list:
chunk_svls = self.env['stock.valuation.layer'].create(svl_vals_list)
# 4b. Force Dates SVL
tx_with_svl = [t for t in chunk__txs if t.get('svl_vals')]
date_map = defaultdict(list)
for svl, t in zip(chunk_svls, tx_with_svl):
date_map[t['date_svl']].append(svl.id)
for d, ids in date_map.items():
self.env.cr.execute("UPDATE stock_valuation_layer SET create_date = %s WHERE id IN %s", (d, tuple(ids)))
# 4a. Force Dates Moves (Per Chunk)
if chunk_moves:
self.env.cr.execute("UPDATE account_move SET date = %s WHERE id IN %s", (self.date_end.date(), tuple(chunk_moves.ids)))
# Link Created Moves to this record (Important for Resume Logic)
self.write({'account_move_ids': [(4, m.id) for m in chunk_moves]})
# COMMIT PER CHUNK
self.env.cr.commit()
_logger.info("Stock Readjustment: Processed chunk %s/%s", i + len(chunk__txs), len(all_transactions))
self.state = 'done'
def _prepare_correction_vals(self, move, amount, original_am=False, is_mfg=False):
"""
Returns list of transaction dicts:
[{
'move_vals': dict,
'svl_vals': dict (optional),
'date_move': date,
'date_svl': datetime
}]
"""
results = []
product = move.product_id
# 1. Accounts
debit_acc = False
credit_acc = False
if original_am:
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)
valuation_acc_id = accounts['stock_valuation'].id
if is_mfg:
# For MFG: Dr Stock (Asset), Cr WIP (Other)
stock_line = original_am.line_ids.filtered(lambda l: l.account_id.id == valuation_acc_id)
wip_line = original_am.line_ids.filtered(lambda l: l.account_id.id != valuation_acc_id)
if stock_line and wip_line:
stock_id = stock_line[0].account_id.id
wip_id = wip_line[0].account_id.id
if amount > 0: # Increase Value
debit_acc, credit_acc = stock_id, wip_id
else:
debit_acc, credit_acc = wip_id, stock_id
else:
# For COGS: Dr Cost, Cr Stock
# If amount (SVL Val Adjust) < 0 (Cost increased): Dr Cost, Cr Stock
# If amount > 0 (Cost decreased): Dr Stock, Cr Cost
# amount passed here is "-diff" for COGS.
# diff = Target - Current.
# If Target (correct) > Current: diff > 0.
# amount = -diff (< 0).
# SVL Value = -amount = diff (> 0). Asset Increases.
# COGS Decreases?
# Wait.
# If we sold at 100 (Cost=100). Should be 110.
# SVL -100. Should be -110.
# We need key adjustment: -10.
# SVL Value Adjustment: -10.
# In action_apply logic:
# target = 110. current = 100.
# diff = 10.
# amount passed = -10.
# SVL Value = -(-10) = +10. (This increases inventory value??)
# Let's re-read action_apply logic carefully.
# target_value = 110. curr_val_abs = 100.
# diff = +10.
# amount passed = -10.
# SVL.value = -amount = -(-10) = +10.
# If we add +10 to inventory, we INCREASE stock value.
# But expected stock value for outgoing is NEGATIVE (credit).
# -100 + 10 = -90. This REDUCES the cost of goods sold.
# But we wanted cost to be 110 (Higher).
# So we need to add -10 to the layer.
# In _prepare:
# svl['value'] = -amount.
# We want svl['value'] to be -10.
# So -amount = -10 => amount = 10.
# Back to action_apply:
# I passed `self._prepare_correction_vals(move, -diff, ...)`
# If diff=10, amount=-10.
# svl['value'] = 10. WRONG.
# FIX in _prepare or action_apply:
# If I want to make stock value MORE NEGATIVE (Increase Cost),
# I need a negative adjustment.
# New Value (-110) = Old Value (-100) + Adj (-10).
# So passed 'amount' should be such that -amount = -10.
# So amount = 10.
# So I should pass `diff` (which is 10), not `-diff`.
# Wait, I passed `-diff` in my `action_apply` code.
# Line: `txs = self._prepare_correction_vals(move, -diff, original_am)`
# I should have passed `diff`.
# I CAN FIX THIS IN `_prepare_correction_vals` by flipping sign for COGS!
# Or strict adherence to args.
# Let's fix semantics here.
# 'amount' arg usually implies "Amount to Adjust".
# If I pass `diff`, then `svl['value']` will be `-diff` = `-10`. Correct.
# So `action_apply` passed `-diff`. So `svl['value']` becomes `+diff` (+10). Wrong.
# I need to invert logic in `_prepare` correction for COGS if I assume input is `-diff`?
# Or just treat `amount` as "Value Adjustment".
# Let's define: amount = The value to ADD to the SVL.
# `svl_vals['value'] = amount`.
# And `move_amount = abs(amount)`.
# Direction determined by sign.
# Existing OLD `_create_correction_from_move` had: `svl_vals['value'] = -amount`.
# And `action_apply` calculated `diff` and called it with `diff`.
# Logic: diff=10. call(10). svl=-10. Correct.
# In my NEW `action_apply`, I passed `-diff`.
# call(-10).
# If I keep `svl['value'] = -amount`:
# svl = -(-10) = 10. Wrong.
# Strategy: I will change `_prepare` to strictly use `amount` as the SVL Value Adjustment.
# `svl_vals['value'] = amount`.
# And I need to verify what I passed in `action_apply`.
# I passed `-diff` (which is -10).
# So `svl = -10`. Correct.
# So `svl_vals['value'] = amount` is the way.
pass
# Let's Implement Correct Logic (ignoring legacy -amount swap overhead if possible)
# But I need to respect what I wrote in `action_apply` which is already saved.
# I passed `-diff` (e.g. -10).
# So I want SVL Value = -10.
# So I set `value: amount`.
# Accounts Direction:
# If amount < 0 (Stock Value Down / More Negative):
# COGS logic: Value -100 -> -110. (Cost Up).
# Dr COGS, Cr Stock.
# Debit = Expense. Credit = Stock.
# move_amount = 10.
# If amount > 0 (Stock Value Up / Less Negative):
# Value -100 -> -90. (Cost Down).
# Dr Stock, Cr COGS.
if not (debit_acc and credit_acc):
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)
expense = accounts['expense'].id
stock = accounts['stock_output'].id or expense
val = accounts['stock_valuation'].id
if is_mfg:
debit_acc = val
credit_acc = stock
else:
debit_acc = stock
credit_acc = val
# Direction Swap based on Amount
# amount is the SVL Value Change.
# If amount < 0: Stock Value Decreases (Credit Stock). Dr Other.
# If amount > 0: Stock Value Increases (Debit Stock). Cr Other.
# Default assignment (Cost/WIP vs Stock):
# debit_acc = Cost/WIP
# credit_acc = Stock (Valuation)
move_amount = amount
if move_amount < 0:
# Stock Credit. Dr Cost, Cr Stock.
# Already defaults.
move_amount = abs(move_amount)
else:
# Stock Debit. Dr Stock, Cr Cost.
debit_acc, credit_acc = credit_acc, debit_acc
# POS Logic for LABEL and Reference
ref_text = f"{self.name} - Adj for {move.name}"
is_pos = False
pos_order_obj = False
pos_session_obj = False
# Reuse Logic
if move.origin:
pos_orders = self.env['pos.order'].search(['|', ('name', '=', move.origin), ('pos_reference', '=', move.origin)], limit=1)
if pos_orders:
is_pos = True
pos_order_obj = pos_orders[0]
pos_session_obj = pos_order_obj.session_id
if not is_pos:
sessions = self.env['pos.session'].search([('name', '=', move.origin)], limit=1)
if sessions:
is_pos = True
pos_session_obj = sessions[0]
if not is_pos and move.picking_id:
if 'pos_order_id' in move.picking_id._fields and move.picking_id.pos_order_id:
is_pos = True
pos_order_obj = move.picking_id.pos_order_id
pos_session_obj = pos_order_obj.session_id
if not is_pos and 'pos_session_id' in move.picking_id._fields and move.picking_id.pos_session_id:
is_pos = True
pos_session_obj = move.picking_id.pos_session_id
if not is_pos and move.picking_id and move.picking_id.origin:
sessions = self.env['pos.session'].search([('name', '=', move.picking_id.origin)], limit=1)
if sessions:
is_pos = True
pos_session_obj = sessions[0]
if not is_pos and move.picking_id:
pos_orders = self.env['pos.order'].search([('picking_ids', 'in', move.picking_id.id)], limit=1)
if pos_orders:
is_pos = True
# Weak link
pos_session_obj = pos_orders[0].session_id
if not is_pos:
candidates = [move.origin, move.reference, move.picking_id.name]
for c in candidates:
if c and ('POS' in c or 'Sesi' in c):
is_pos = True
break
if is_pos:
details = []
if pos_order_obj:
details.append(pos_order_obj.name)
if pos_session_obj:
details.append(pos_session_obj.name)
if details:
ref_text = f"{self.name} - POS Adj for {move.name} ({' - '.join(details)})"
else:
ref_text = f"{self.name} - POS Adj for {move.name}"
# Product Name in Label
label_text = f"{_('Readjustment')} - {product.display_name}"
move_vals = {
'journal_id': self.journal_id.id,
'date': self.date_end.date(),
'ref': ref_text,
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': label_text,
'account_id': debit_acc,
'debit': move_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': label_text,
'account_id': credit_acc,
'debit': 0,
'credit': move_amount,
'product_id': product.id,
}),
]
}
svl_vals = {
'company_id': self.company_id.id,
'product_id': product.id,
'description': label_text,
'stock_move_id': move.id,
'quantity': 0,
'value': amount, # Direct usage of amount
'account_move_id': False,
}
results.append({
'move_vals': move_vals,
'svl_vals': svl_vals,
'date_move': self.date_end.date(),
'date_svl': move.date
})
# Secondary POS Logic
if is_pos:
final_expense_acc = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)['expense'].id
# debit_acc above was the Interim/Cost account if amount < 0.
# If amount < 0 (Cost Up), we debited Cost.
# We want to transfer that Cost to Final Expense.
# Dr Final, Cr Interim.
# If amount > 0 (Cost Down), we Credited Cost.
# We want to reduce Final Expense.
# Dr Interim, Cr Final.
# The 'debit_acc' calculated above might be Stock if amount > 0.
# We need the Interim Account specifically.
# Interim is the "Cost/Expense" account in the pair, regardless of direction.
# In my logic block:
# If amount < 0: debit_acc = StockOut/Expense(Interim), credit_acc = Stock.
# If amount > 0: debit_acc = Stock, credit_acc = Interim.
interim_acc = debit_acc if amount < 0 else credit_acc
if interim_acc != final_expense_acc:
# Create Secondary Move
# Direction:
# If amount < 0 (Cost Up): Transfer Cost Increase to Final. Dr Final, Cr Interim.
sec_debit = final_expense_acc
sec_credit = interim_acc
if amount > 0:
# Cost Down. Transfer Cost Decrease. Cr Final, Dr Interim.
sec_debit = interim_acc
sec_credit = final_expense_acc
move_vals_2 = {
'journal_id': self.journal_id.id,
'date': self.date_end.date(),
'ref': ref_text,
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': label_text,
'account_id': sec_debit,
'debit': move_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': label_text,
'account_id': sec_credit,
'debit': 0,
'credit': move_amount,
'product_id': product.id,
}),
]
}
results.append({
'move_vals': move_vals_2,
'svl_vals': False, # No SVL
'date_move': self.date_end.date(),
'date_svl': False
})
return results
def action_apply_IGNORED(self):
self.ensure_one()
if self.state != 'calculated':
raise UserError(_("Please calculate first."))
for line in self.line_ids:
# 1. Readjust Initial Stock Value (if changed)
initial_diff = line.target_initial_value - line.initial_value
if not float_is_zero(initial_diff, precision_digits=2):
# Create a layer to adjust the starting value
# We simply create a value-only layer.
# Value = initial_diff.
self._create_correction_layer(line.product_id, initial_diff, self.date_start, _("Readjustment: Initial Value"))
new_cost = line.new_average_cost
# 2. Update Product Cost
# We bypass the costing method check and force the update
line.product_id.sudo().write({'standard_price': new_cost})
# 3. Readjust COGS (Outgoing Moves)
# Find outgoing moves in the period
outgoing_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), # Outgoing
('company_id', '=', self.company_id.id)
])
# Idempotent Logic: Group by Stock Move to strictly fix the net value
moves = outgoing_layers.mapped('stock_move_id')
for move in moves:
# Sum ALL layers for this move (Original + Landed Costs + Previous Corrections)
# We need to find all layers linked to this move.
all_layers_for_move = self.env['stock.valuation.layer'].search([
('stock_move_id', '=', move.id),
('product_id', '=', line.product_id.id)
])
# Calculate NET state
total_qty = sum(all_layers_for_move.mapped('quantity')) # Should be negative (e.g. -5)
current_value = sum(all_layers_for_move.mapped('value')) # Should be negative (e.g. -5000)
# Math using positive numbers for clarity
qty_sold = abs(total_qty)
curr_val_abs = abs(current_value)
target_value = qty_sold * new_cost
diff = target_value - curr_val_abs
# If diff > 0: means Cost SHOULD be higher (e.g. 5500 vs 5000).
# Since stored value is negative (-5000), we need to make it MORE negative (-5500).
# So we must ADD -500 (which is -diff).
# If diff < 0: means Cost SHOULD be lower (e.g. 4500 vs 5000).
# Stored -5000, want -4500.
# Must ADD +500 (which is -diff).
# So in all cases, adjustment value = -diff.
# Only create correction if diff implies a change
if not float_is_zero(diff, precision_digits=2):
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'
def _create_correction_from_move(self, move, amount):
# Wrapper to use move context
# SVL Value = -amount
svl_vals = {
'company_id': self.company_id.id,
'product_id': move.product_id.id,
'description': _('Readjustment: %s') % self.name,
'stock_move_id': move.id,
'quantity': 0,
'value': -amount,
'account_move_id': False,
}
new_svl = self.env['stock.valuation.layer'].create(svl_vals)
def _create_correction_from_move(self, move, amount):
# Wrapper to use move context
# SVL Value = -amount
svl_vals = {
'company_id': self.company_id.id,
'product_id': move.product_id.id,
'description': _('Readjustment: %s') % self.name,
'stock_move_id': move.id,
'quantity': 0,
'value': -amount,
'account_move_id': False,
}
new_svl = self.env['stock.valuation.layer'].create(svl_vals)
# Accounting
product = move.product_id
# Default Accounts
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)
expense_acc = accounts['expense']
stock_out_acc = accounts['stock_output'] or expense_acc
valuation_acc = accounts['stock_valuation']
debit_acc = stock_out_acc.id
credit_acc = valuation_acc.id
# Attempt to find ACTUAL accounts used in the original move
# We look for an SVL linked to this move that has an account move
original_svls = self.env['stock.valuation.layer'].search([
('stock_move_id', '=', move.id),
('account_move_id', '!=', False)
], limit=1)
if original_svls:
am = original_svls.account_move_id
# Analyze lines to find Asset vs Expense
# We assume the move involves the Product.
# Typical Sale: Dr COGS, Cr Asset.
# Asset Account should match the Category's Valuation Account.
asset_acc_id = valuation_acc.id
found_expense_id = False
for line in am.line_ids:
if line.account_id.id == asset_acc_id:
continue # This is the asset account
# If it's not the asset account, and it has the product (optional check), it's likely the COGS
# In POS, it might be granular.
if line.account_id.id != asset_acc_id:
found_expense_id = line.account_id.id
break
if found_expense_id:
debit_acc = found_expense_id
# credit_acc stays as valuation_acc (Asset)
if not (debit_acc and credit_acc):
return
interim_account_id = debit_acc
# Enhanced POS Detection
is_pos = False
pos_order_obj = False
pos_session_obj = False
# 1. Check Origin for Exact Match (Order or Session)
if move.origin:
# Try finding Order
pos_orders = self.env['pos.order'].search(['|', ('name', '=', move.origin), ('pos_reference', '=', move.origin)], limit=1)
if pos_orders:
is_pos = True
pos_order_obj = pos_orders[0]
pos_session_obj = pos_order_obj.session_id
# Try finding Session (if not Order)
if not is_pos:
sessions = self.env['pos.session'].search([('name', '=', move.origin)], limit=1)
if sessions:
is_pos = True
pos_session_obj = sessions[0]
# 2. Check Picking Links (Direct Fields)
if not is_pos and move.picking_id:
# Check pos_order_id
if 'pos_order_id' in move.picking_id._fields and move.picking_id.pos_order_id:
is_pos = True
pos_order_obj = move.picking_id.pos_order_id
pos_session_obj = pos_order_obj.session_id
# Check pos_session_id (if exists on picking)
if not is_pos and 'pos_session_id' in move.picking_id._fields and move.picking_id.pos_session_id:
is_pos = True
pos_session_obj = move.picking_id.pos_session_id
# 3. Check Picking Origin for Session Name
if not is_pos and move.picking_id and move.picking_id.origin:
sessions = self.env['pos.session'].search([('name', '=', move.picking_id.origin)], limit=1)
if sessions:
is_pos = True
pos_session_obj = sessions[0]
# 4. Fallback: Search via picking_ids (Last Resort)
if not is_pos and move.picking_id:
pos_orders = self.env['pos.order'].search([('picking_ids', 'in', move.picking_id.id)], limit=1)
if pos_orders:
is_pos = True
# If we found an order via consolidated picking, we can't be sure it's the right ORDER,
# but we can be reasonably sure it's the right SESSION (if grouping by session).
# We will grab the session, but NOT claim it's this specific order unless we have no other info.
pos_session_obj = pos_orders[0].session_id
# Only set pos_order_obj if we think it implies 1:1
# We'll skip setting pos_order_obj here to avoid "Arbitrary Order" confusion, just link Session.
# 5. String Detection (Legacy/Weak)
if not is_pos:
candidates = [move.origin, move.reference, move.picking_id.name]
for c in candidates:
if c and ('POS' in c or 'Sesi' in c):
is_pos = True
break
move_amount = amount
if move_amount < 0:
debit_acc, credit_acc = credit_acc, debit_acc
move_amount = abs(move_amount)
ref_text = f"{self.name} - Adj for {move.name}"
if is_pos:
details = []
if pos_order_obj:
details.append(pos_order_obj.name)
if pos_session_obj:
details.append(pos_session_obj.name)
if details:
ref_text = f"{self.name} - POS Adj for {move.name} ({' - '.join(details)})"
else:
ref_text = f"{self.name} - POS Adj for {move.name}"
move_vals = {
'journal_id': self.journal_id.id,
'date': self.date_end.date(), # Use End Date for Accounting
'ref': ref_text,
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': _('Readjustment COGS'),
'account_id': debit_acc,
'debit': move_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': _('Readjustment Valuation'),
'account_id': credit_acc,
'debit': 0,
'credit': move_amount,
'product_id': product.id,
}),
]
}
account_move = self.env['account.move'].create(move_vals)
account_move.action_post()
self.account_move_ids = [(4, account_move.id)]
new_svl.account_move_id = account_move.id
# Force Date
# SVL keeps the Move Date (for valuation history)
self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (move.date, new_svl.id))
# Account Move uses End Date
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (self.date_end.date(), account_move.id))
# SECONDARY CORRECTION FOR POS (Interim -> Final Expense)
# If this was a POS order, the Stock Move only hit the Interim Account (Stock Output).
# The POS Session later moved it to the Final Expense Account.
# We must propagate the correction to the Final Expense.
if is_pos and interim_account_id != expense_acc.id:
interim_acc = interim_account_id
final_acc = expense_acc.id
# Logic:
# If Amt < 0 (Cost Decreased):
# We want to Credit Expense.
# Primary Correction Check: Dr Asset, Cr Interim.
# Secondary Correction: Dr Interim, Cr Expense.
sec_debit_acc = interim_acc
sec_credit_acc = final_acc
sec_amount = abs(amount)
if amount > 0:
# Cost Increased.
# Primary: Dr Interim, Cr Asset.
# Secondary: Dr Expense, Cr Interim.
sec_debit_acc = final_acc
sec_credit_acc = interim_acc
ref_text = f"{self.name} - POS Adj for {move.name}"
if pos_order_obj:
if pos_order_obj.session_id:
ref_text += f" ({pos_order_obj.session_id.name})"
else:
ref_text += f" ({pos_order_obj.name})"
move_vals_2 = {
'journal_id': self.journal_id.id,
'date': self.date_end.date(), # Use End Date
'ref': ref_text,
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': _('Readjustment POS Expense'),
'account_id': sec_debit_acc,
'debit': sec_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': _('Readjustment POS Expense'),
'account_id': sec_credit_acc,
'debit': 0,
'credit': sec_amount,
'product_id': product.id,
}),
]
}
sec_move = self.env['account.move'].create(move_vals_2)
sec_move.action_post()
self.account_move_ids = [(4, sec_move.id)]
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (self.date_end.date(), sec_move.id))
def _create_correction_layer(self, product, amount, date, description):
""" Create a generic correction layer (e.g. for Initial Stock) """
svl_vals = {
'company_id': self.company_id.id,
'product_id': product.id,
'description': description,
'stock_move_id': False,
'quantity': 0,
'value': amount,
'account_move_id': False,
}
new_svl = self.env['stock.valuation.layer'].create(svl_vals)
# Accounting
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)
valuation_acc = accounts['stock_valuation']
expense_acc = accounts['expense'] # Or we can use a specific revaluation account from Journal?
# Let's use the Journal's account if possible, or Fallback to Expense/Stock Output
if not (valuation_acc and expense_acc):
return
# Positive Amount = Value Increase = Dr Inventory, Cr Expense/Gain
debit_acc = valuation_acc.id
credit_acc = expense_acc.id
move_amount = amount
if move_amount < 0:
# Value Decrease = Dr Expense/Loss, Cr Inventory
debit_acc, credit_acc = credit_acc, debit_acc
move_amount = abs(move_amount)
move_vals = {
'journal_id': self.journal_id.id,
'date': date.date(),
'ref': f"{self.name} - {description}",
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': description,
'account_id': debit_acc,
'debit': move_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': description,
'account_id': credit_acc,
'debit': 0,
'credit': move_amount,
'product_id': product.id,
}),
]
}
move = self.env['account.move'].create(move_vals)
move.action_post()
self.account_move_ids = [(4, move.id)]
new_svl.account_move_id = move.id
# Force Date
self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (date, new_svl.id))
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (date.date(), move.id))
# SECONDARY CORRECTION FOR POS (Interim -> Final Expense)
# If this was a POS order, the Stock Move only hit the Interim Account (Stock Output).
# The POS Session later moved it to the Final Expense Account.
# We must propagate the correction to the Final Expense.
if move.picking_id.pos_order_id and stock_out_acc.id != expense_acc.id:
interim_acc = stock_out_acc.id
final_acc = expense_acc.id
# Logic:
# If Amt < 0 (Cost Decreased):
# We want to Credit Expense.
# Primary Correction Check: Dr Asset, Cr Interim.
# Secondary Correction: Dr Interim, Cr Expense.
sec_debit_acc = interim_acc
sec_credit_acc = final_acc
sec_amount = abs(amount)
if amount > 0:
# Cost Increased.
# Primary: Dr Interim, Cr Asset.
# Secondary: Dr Expense, Cr Interim.
sec_debit_acc = final_acc
sec_credit_acc = interim_acc
move_vals_2 = {
'journal_id': self.journal_id.id,
'date': move.date.date(),
'ref': f"{self.name} - POS Adj for {move.name}",
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': _('Readjustment POS Expense'),
'account_id': sec_debit_acc,
'debit': sec_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': _('Readjustment POS Expense'),
'account_id': sec_credit_acc,
'debit': 0,
'credit': sec_amount,
'product_id': product.id,
}),
]
}
sec_move = self.env['account.move'].create(move_vals_2)
sec_move.action_post()
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (move.date.date(), sec_move.id))
def _create_correction_layer(self, product, amount, date, description):
""" Create a generic correction layer (e.g. for Initial Stock) """
svl_vals = {
'company_id': self.company_id.id,
'product_id': product.id,
'description': description,
'stock_move_id': False,
'quantity': 0,
'value': amount,
'account_move_id': False,
}
new_svl = self.env['stock.valuation.layer'].create(svl_vals)
# Accounting
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)
valuation_acc = accounts['stock_valuation']
expense_acc = accounts['expense'] # Or we can use a specific revaluation account from Journal?
# Let's use the Journal's account if possible, or Fallback to Expense/Stock Output
if not (valuation_acc and expense_acc):
return
# Positive Amount = Value Increase = Dr Inventory, Cr Expense/Gain
debit_acc = valuation_acc.id
credit_acc = expense_acc.id
move_amount = amount
if move_amount < 0:
# Value Decrease = Dr Expense/Loss, Cr Inventory
debit_acc, credit_acc = credit_acc, debit_acc
move_amount = abs(move_amount)
move_vals = {
'journal_id': self.journal_id.id,
'date': date.date(),
'ref': f"{self.name} - {description}",
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': description,
'account_id': debit_acc,
'debit': move_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': description,
'account_id': credit_acc,
'debit': 0,
'credit': move_amount,
'product_id': product.id,
}),
]
}
move = self.env['account.move'].create(move_vals)
move.action_post()
new_svl.account_move_id = move.id
# Force Date
self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (date, new_svl.id))
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (date.date(), move.id))
def _create_correction(self, layer, amount):
""" Create SVL and AM correction for the given layer """
# Only adjust if amount is non-zero
# SVL
svl_vals = {
'company_id': self.company_id.id,
'product_id': layer.product_id.id,
'description': _('Readjustment: %s') % self.name,
'stock_move_id': layer.stock_move_id.id,
'quantity': 0,
'value': -amount, # Adjustment to the ASSET value on the layer?
# Wait. Outgoing layer has negative value (e.g. -100).
# If correct cost is higher (e.g. 110), value should be -110.
# So we need to add -10.
# 'amount' coming in is (Correct - Current).
# (110 - 100) = 10 (Positive magnitude).
# Since layer.value is negative, we want to make it MORE negative.
# So we should add -amount to the asset.
'account_move_id': False, # Linked below
}
# If amount is positive (Cost increased):
# We need to Credit Asset, Debit COGS.
# Layer Value change: Negative.
pass
# Actually let's reuse logic from revaluation.
# If Cost Increased: We under-expensed. Need to expense more.
# Dr COGS, Cr Asset.
# Asset Value Change = -Amount.
# If Cost Decreased: We over-expensed. Need to return to asset.
# Dr Asset, Cr COGS.
# Asset Value Change = +Amount.
# My 'diff' calculation: Correct (big) - Current (small) = Positive.
# Means Correct Cost > Current cost.
# So we need to reduce Asset (Cr) and increase COGS (Dr).
# So SVL value should be -diff.
svl_vals['value'] = -amount
new_svl = self.env['stock.valuation.layer'].create(svl_vals)
# Accounting Entry
account_moves = layer.account_move_id
if not account_moves:
return
# We find the original accounts used.
# Or look up standard accounts.
product = layer.product_id
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False)
expense_acc = accounts['expense']
stock_out_acc = accounts['stock_output'] or expense_acc
valuation_acc = accounts['stock_valuation']
if not (stock_out_acc and valuation_acc):
return # Skip if no accounts
# Dr COGS (stock_out), Cr Valuation
debit_acc = stock_out_acc.id
credit_acc = valuation_acc.id
move_amount = amount
if move_amount < 0:
# Cost Decreased. Reverse. Dr Valuation, Cr COGS.
debit_acc, credit_acc = credit_acc, debit_acc
move_amount = abs(move_amount)
move_vals = {
'journal_id': self.journal_id.id,
'date': layer.create_date.date(), # Backdate
'ref': f"{self.name} - Adj for {layer.stock_move_id.name}",
'move_type': 'entry',
'line_ids': [
(0, 0, {
'name': _('Readjustment COGS'),
'account_id': debit_acc,
'debit': move_amount,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': _('Readjustment Valuation'),
'account_id': credit_acc,
'debit': 0,
'credit': move_amount,
'product_id': product.id,
}),
]
}
move = self.env['account.move'].create(move_vals)
move.action_post()
new_svl.account_move_id = move.id
# Force Date
self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (layer.create_date, new_svl.id))
self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (layer.create_date.date(), move.id))
class StockReadjustValuationLine(models.Model):
_name = 'stock.readjust.valuation.line'
_description = 'Line for Readjustment'
readjust_id = fields.Many2one('stock.readjust.valuation', required=True, ondelete='cascade')
product_id = fields.Many2one('product.product', required=True)
state = fields.Selection(related='readjust_id.state')
initial_qty = fields.Float(string='Qty at Start', readonly=True)
qty_counted = fields.Float(string='Qty Counted')
initial_value = fields.Float(string='Valuation at Start', readonly=True)
target_initial_value = fields.Float(string='Target Valuation')
purchase_qty = fields.Float(string='Purchase Qty', readonly=True)
purchase_value = fields.Float(string='Purchase Value', readonly=True)
new_average_cost = fields.Float(string='New Average Cost', readonly=True)
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id and self.readjust_id.date_start:
# Calculate Initial Stock
layers = self.env['stock.valuation.layer'].search([
('product_id', '=', self.product_id.id),
('create_date', '<', self.readjust_id.date_start),
('company_id', '=', self.readjust_id.company_id.id)
])
self.initial_qty = sum(layers.mapped('quantity'))
self.initial_value = sum(layers.mapped('value'))
self.qty_counted = self.initial_qty
self.target_initial_value = self.initial_value