change the strategy to readjust with chunking method and add resume feature

This commit is contained in:
Suherdy Yacob 2026-01-14 11:12:53 +07:00
parent 7296290bdc
commit c9b08f37b4
2 changed files with 543 additions and 2 deletions

View File

@ -1,6 +1,10 @@
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'
@ -17,6 +21,7 @@ class StockReadjustValuation(models.Model):
state = fields.Selection([
('draft', 'Draft'),
('calculated', 'Calculated'),
('processing', 'Processing'),
('done', 'Done'),
('cancel', 'Cancelled')
], string='Status', default='draft', tracking=True)
@ -249,6 +254,542 @@ class StockReadjustValuation(models.Model):
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:
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."))

View File

@ -11,8 +11,8 @@
<button name="action_load_products" string="Load Products" type="object" class="oe_highlight" invisible="state != 'draft'"/>
<button name="action_calculate" string="Calculate" type="object" class="oe_highlight" invisible="state != 'draft'"/>
<button name="action_reset_to_draft" string="Reset to Draft" type="object" invisible="state != 'calculated'"/>
<button name="action_apply" string="Apply Readjustment" type="object" class="oe_highlight" invisible="state != 'calculated'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,calculated,done"/>
<button name="action_apply" string="Apply Readjustment" type="object" class="oe_highlight" invisible="state not in ['calculated', 'processing']"/>
<field name="state" widget="statusbar" statusbar_visible="draft,calculated,processing,done"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">