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': 'tree,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._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._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: # Check for Scrap / Inventory Loss if move.location_dest_id.usage == 'inventory' and (move.scrapped or move.location_dest_id.scrap_location): if move.location_dest_id.valuation_in_account_id: stock = move.location_dest_id.valuation_in_account_id.id 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