diff --git a/models/stock_inventory_backdate.py b/models/stock_inventory_backdate.py
index d575a6d..ffc4868 100755
--- a/models/stock_inventory_backdate.py
+++ b/models/stock_inventory_backdate.py
@@ -150,52 +150,7 @@ class StockInventoryBackdate(models.Model):
return qty_in - qty_out
- def _get_historical_cost(self, product, date, company_id):
- """
- Calculate historical unit cost at a specific date.
- - Standard Price: Returns current standard price.
- - AVCO/FIFO: Calculates historical weighted average.
- """
- cost_method = product.categ_id.property_cost_method
-
- if cost_method == 'standard':
- return product.standard_price
- # Optimized Key: Use SQL for aggregation instead of loading all objects
- sql = """
- SELECT SUM(quantity), SUM(value)
- FROM stock_valuation_layer
- WHERE product_id = %s
- AND create_date <= %s
- AND company_id = %s
- """
- self.env.cr.execute(sql, (product.id, date, company_id.id))
- result = self.env.cr.fetchone()
-
- quantity = result[0] or 0.0
- value = result[1] or 0.0
-
- if quantity != 0:
- return value / quantity
-
- # Fallback: Try to find the last valid unit_cost from an incoming layer
- # This helps if current stock is 0 but we want the 'last known cost'
- last_in_sql = """
- SELECT value, quantity
- FROM stock_valuation_layer
- WHERE product_id = %s
- AND create_date <= %s
- AND company_id = %s
- AND quantity > 0
- ORDER BY create_date DESC, id DESC
- LIMIT 1
- """
- self.env.cr.execute(last_in_sql, (product.id, date, company_id.id))
- last_in = self.env.cr.fetchone()
- if last_in and last_in[1] != 0:
- return last_in[0] / last_in[1]
-
- return product.standard_price
def action_validate(self):
"""Validate and create backdated stock moves"""
@@ -268,11 +223,7 @@ class StockInventoryBackdateLine(models.Model):
default=0.0,
help="Positive value adds stock, negative value removes stock."
)
- unit_cost = fields.Float(
- string='Unit Cost',
- digits='Product Price',
- help="Custom unit cost for this backdated adjustment. If left 0, it may use standard price."
- )
+
product_uom_id = fields.Many2one(
'uom.uom',
string='Unit of Measure',
@@ -317,17 +268,7 @@ class StockInventoryBackdateLine(models.Model):
if not self.counted_qty and not self.difference_qty:
self.counted_qty = self.theoretical_qty
self.difference_qty = 0.0
-
- # Calculate historical unit cost
- if self.inventory_id.backdate_datetime:
- limit_date = self.inventory_id.backdate_datetime
- self.unit_cost = self.inventory_id._get_historical_cost(
- self.product_id,
- limit_date,
- self.inventory_id.company_id
- )
- else:
- self.unit_cost = self.product_id.standard_price
+
def _create_stock_move(self):
"""Create backdated stock move for this line"""
@@ -453,76 +394,11 @@ class StockInventoryBackdateLine(models.Model):
_logger.info(f"Found stock valuation layer for move {move.id}: {svl}")
if svl:
- new_val_layer_vals = {}
- # Update Date
- new_val_layer_vals['create_date'] = backdate
-
- # --- COST ADJUSTMENT LOGIC ---
- # If unit_cost is provided, we override the value on the layer
- # and potentially update the account move
- if self.unit_cost and self.unit_cost > 0:
- original_value = svl.value
- new_value = self.unit_cost * qty
- if self.difference_qty < 0:
- new_value = -abs(new_value) # Ensure negative if qty is negative
-
- if abs(new_value - original_value) > 0.01:
- _logger.info(f"Overriding SVL Value from {original_value} to {new_value} (Unit Cost: {self.unit_cost})")
-
- # Update SVL via SQL to avoid constraint issues or re-triggering logic
- # Also need to update remaining_value if applicable?
- # For incoming (positive diff), remaining_value should match value.
- # For outgoing, usually remaining_value is 0 on the layer itself (it consumes others).
-
- update_sql = "UPDATE stock_valuation_layer SET create_date = %s, value = %s, unit_cost = %s"
- params = [backdate, new_value, self.unit_cost]
-
- if self.difference_qty > 0:
- # Incoming: update remaining_value too
- update_sql += ", remaining_value = %s"
- params.append(new_value)
-
- update_sql += " WHERE id = %s"
- params.append(svl.id)
-
- self.env.cr.execute(update_sql, tuple(params))
-
- # Check Account Move and update amount
- if move.account_move_ids:
- for am in move.account_move_ids:
- # Update lines
- # We need to find which line is Debit/Credit and scale them?
- # Or just assuming 2 lines, simpler to match the total?
- # Let's check amounts.
-
- # Usually Inventory Adjustment creates:
- # Dr Stock, Cr Inventory Adjustment (for Gain)
- # Dr Inventory Adjustment, Cr Stock (for Loss)
-
- # We just need to replace the absolute amount on all lines that matched the old absolute amount?
- # Risky if multiple lines.
-
- # Better: Iterate lines. If line amount matches old abs(value), update to new abs(value).
- for line in am.line_ids:
- if abs(line.debit - abs(original_value)) < 0.01:
- self.env.cr.execute("UPDATE account_move_line SET debit = %s WHERE id = %s", (abs(new_value), line.id))
- if abs(line.credit - abs(original_value)) < 0.01:
- self.env.cr.execute("UPDATE account_move_line SET credit = %s WHERE id = %s", (abs(new_value), line.id))
-
- _logger.info(f"Updated account_move {am.id} amounts to match new value {new_value}")
-
- else:
- # just update date
- self.env.cr.execute(
- "UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s",
- (backdate, svl.id)
- )
- else:
- # No custom cost, just update date
- self.env.cr.execute(
- "UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s",
- (backdate, svl.id)
- )
+ # just update date
+ self.env.cr.execute(
+ "UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s",
+ (backdate, svl.id)
+ )
_logger.info(f"Updated stock_valuation_layer for move {move.id}")
else:
@@ -550,169 +426,4 @@ class StockInventoryBackdateLine(models.Model):
# Invalidate cache
self.env.cache.invalidate()
- # Ghost Value Fix: Propagate consumption to subsequent sales
- # Note: We pass the backdate because move.date might still be cached as 'now' until full reload
- self._propagate_consumption(move, backdate)
-
return move
-
- def _propagate_consumption(self, move, backdate_datetime):
- """
- Ghost Value Fix:
- Simulate 'FIFO' consumption for the newly backdated layer against subsequent outgoing moves.
- If we retroactively add stock to Jan 1, but we sold stock on Jan 2,
- that sale should have consumed this (cheaper/older) stock depending on FIFO/AVCO.
-
- This logic creates 'Correction SVLs' to expense the value of the backdated layer
- that corresponds to quantities subsequently sold.
- """
- if move.product_id.categ_id.property_cost_method not in ['average', 'fifo']:
- return
-
- # 1. Identify the new Layer
- new_svl = self.env['stock.valuation.layer'].search([
- ('stock_move_id', '=', move.id)
- ], limit=1)
-
- if not new_svl:
- return
-
- # Check if we have quantity to propagate
- # We generally only propagate INCOMING adjustments (Positive Qty) that have remaining qty
- # But for robustness (and user request), we ensure float comparison
- if float_compare(new_svl.quantity, 0, precision_rounding=move.product_uom.rounding) <= 0:
- return
-
- if float_compare(new_svl.remaining_qty, 0, precision_rounding=move.product_uom.rounding) <= 0:
- return
-
- # 2. Find Subsequent Outgoing Moves (The "Missed" Sales)
- # We look for outgoing SVLs (qty < 0) created AFTER the backdate
- outgoing_svls = self.env['stock.valuation.layer'].search([
- ('product_id', '=', move.product_id.id),
- ('company_id', '=', move.company_id.id),
- ('quantity', '<', 0), # Outgoing
- ('create_date', '>', backdate_datetime),
- ], order='create_date asc, id asc')
-
- if not outgoing_svls:
- return
-
- # 3. Consumption Loop
- qty_to_consume_total = 0.0
-
- for out_layer in outgoing_svls:
- if new_svl.remaining_qty <= 0:
- break
-
- # How much can this sale consume from our new layer?
- # It can consume whole sale qty, limited by what we have in the new layer.
- sale_qty = abs(out_layer.quantity)
-
- # Logic: In strict FIFO, this sale `out_layer` MIGHT have already consumed
- # some OLDER layer if it wasn't empty.
- # But the whole point of "Ghost Value" is that we assume the user wanted this Backdated Stock
- # to be available "Back Then".
- # So effectively, we are injecting this layer into the past.
- # Ideally, we should re-run the WHOLE fifo queue. That's too risky/complex.
- # Approximate Logic: "Any subsequent sale is a candidate to consume this 'unexpected' old stock".
-
- consume_qty = min(new_svl.remaining_qty, sale_qty)
-
- if consume_qty <= 0:
- continue
-
- # Calculate Value to Expense
- # We expense based on OUR layer's unit cost (since we are consuming OUR layer)
- unit_val = new_svl.value / new_svl.quantity if new_svl.quantity else 0
- expense_value = consume_qty * unit_val
-
- # Rounding
- expense_value = move.company_id.currency_id.round(expense_value)
-
- if float_is_zero(expense_value, precision_rounding=move.company_id.currency_id.rounding):
- continue
-
- # --- ACTION: REDUCE OUR LAYER ---
- # We update SQL directly to avoid ORM side effects / recomputes
- # Reduce remaining_qty and remaining_value
- # Note: We need to fetch latest state of new_svl inside loop if we modify it?
- # Yes, new_svl.remaining_qty is simple float in memory, we update it manually here to track loop.
-
- new_svl.remaining_qty -= consume_qty
- new_svl.remaining_value -= expense_value
-
- # Commit this reduction to DB immediately so it sticks
- self.env.cr.execute(
- "UPDATE stock_valuation_layer SET remaining_qty = remaining_qty - %s, remaining_value = remaining_value - %s WHERE id = %s",
- (consume_qty, expense_value, new_svl.id)
- )
-
- # --- ACTION: CREATE Expense Entry (Correction) ---
- # Credit Asset (we just reduced remaining_value, effectively saying "It's gone")
- # Debit COGS (Expense it)
-
- stock_val_acc = move.product_id.categ_id.property_stock_valuation_account_id.id
- cogs_acc = move.product_id.categ_id.property_stock_account_output_categ_id.id
-
- if not stock_val_acc or not cogs_acc:
- continue
-
- move_lines = [
- (0, 0, {
- 'name': _('Backdate Correction for %s') % out_layer.stock_move_id.name,
- 'account_id': cogs_acc,
- 'debit': expense_value,
- 'credit': 0,
- 'product_id': move.product_id.id,
- }),
- (0, 0, {
- 'name': _('Backdate Correction for %s') % out_layer.stock_move_id.name,
- 'account_id': stock_val_acc,
- 'debit': 0,
- 'credit': expense_value,
- 'product_id': move.product_id.id,
- }),
- ]
-
- am_vals = {
- 'ref': f"{move.name} - Consumed by {out_layer.stock_move_id.name}",
- 'date': out_layer.create_date.date(), # Match the SALE date
- 'journal_id': move.account_move_ids[0].journal_id.id if move.account_move_ids else False,
- # Use same journal as original move adjustment? Or Stock Journal?
- # Generally Stock Journal.
- 'line_ids': move_lines,
- 'move_type': 'entry',
- 'company_id': move.company_id.id,
- }
- # Fallback journal if not found
- if not am_vals['journal_id']:
- am_vals['journal_id'] = move.product_id.categ_id.property_stock_journal.id
-
- am = self.env['account.move'].create(am_vals)
- am.action_post()
-
- # Create Correction SVL
- # Value is negative (Reducing Asset)
- self.env['stock.valuation.layer'].create({
- 'product_id': move.product_id.id,
- 'value': -expense_value,
- 'quantity': 0,
- 'unit_cost': 0,
- 'remaining_qty': 0,
- 'stock_move_id': out_layer.stock_move_id.id, # Link to sale move
- 'company_id': move.company_id.id,
- 'description': _('Backdate Correction (from %s)') % move.name,
- 'account_move_id': am.id,
- })
- # Backdate this correction SVL to match sale date
- # We don't have the ID easily here as create returns record but separate from SQL.
- # But standard create sets create_date=Now.
- # We want it to look like it happened AT SALE TIME.
- # We can find it via account_move_id
- self.env.cr.execute(
- "UPDATE stock_valuation_layer SET create_date = %s WHERE account_move_id = %s",
- (out_layer.create_date, am.id)
- )
-
- _logger.info(f"Propagated consumption: Consumed {consume_qty} from Backdate Layer for Sale {out_layer.stock_move_id.name}")
diff --git a/views/stock_inventory_backdate_views.xml b/views/stock_inventory_backdate_views.xml
index 6559620..cf84f67 100755
--- a/views/stock_inventory_backdate_views.xml
+++ b/views/stock_inventory_backdate_views.xml
@@ -65,7 +65,7 @@
decoration-warning="theoretical_qty < 0"
force_save="1"/>
-
+