add calculation costing per unit at date
This commit is contained in:
parent
d48e5ebd76
commit
ffd0db1ecd
@ -149,6 +149,53 @@ class StockInventoryBackdate(models.Model):
|
|||||||
|
|
||||||
return qty_in - qty_out
|
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):
|
def action_validate(self):
|
||||||
"""Validate and create backdated stock moves"""
|
"""Validate and create backdated stock moves"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
@ -220,6 +267,11 @@ class StockInventoryBackdateLine(models.Model):
|
|||||||
default=0.0,
|
default=0.0,
|
||||||
help="Positive value adds stock, negative value removes stock."
|
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(
|
product_uom_id = fields.Many2one(
|
||||||
'uom.uom',
|
'uom.uom',
|
||||||
string='Unit of Measure',
|
string='Unit of Measure',
|
||||||
@ -264,6 +316,17 @@ class StockInventoryBackdateLine(models.Model):
|
|||||||
if not self.counted_qty and not self.difference_qty:
|
if not self.counted_qty and not self.difference_qty:
|
||||||
self.counted_qty = self.theoretical_qty
|
self.counted_qty = self.theoretical_qty
|
||||||
self.difference_qty = 0.0
|
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):
|
def _create_stock_move(self):
|
||||||
"""Create backdated stock move for this line"""
|
"""Create backdated stock move for this line"""
|
||||||
@ -384,19 +447,87 @@ class StockInventoryBackdateLine(models.Model):
|
|||||||
|
|
||||||
# Update stock valuation layer
|
# Update stock valuation layer
|
||||||
# Check if valuation layer exists
|
# Check if valuation layer exists
|
||||||
svl_count = self.env['stock.valuation.layer'].search_count([('stock_move_id', '=', move.id)])
|
svl = self.env['stock.valuation.layer'].search([('stock_move_id', '=', move.id)], limit=1)
|
||||||
_logger.info(f"Found {svl_count} stock valuation layers for move {move.id}")
|
# svl_count = self.env['stock.valuation.layer'].search_count([('stock_move_id', '=', move.id)])
|
||||||
|
_logger.info(f"Found stock valuation layer for move {move.id}: {svl}")
|
||||||
|
|
||||||
if svl_count > 0:
|
if svl:
|
||||||
self.env.cr.execute(
|
new_val_layer_vals = {}
|
||||||
"UPDATE stock_valuation_layer SET create_date = %s WHERE stock_move_id = %s",
|
# Update Date
|
||||||
(backdate, move.id)
|
new_val_layer_vals['create_date'] = backdate
|
||||||
)
|
|
||||||
_logger.info(f"Updated stock_valuation_layer for move {move.id}, rows affected: {self.env.cr.rowcount}")
|
# --- 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info(f"Updated stock_valuation_layer for move {move.id}")
|
||||||
else:
|
else:
|
||||||
_logger.warning(f"No stock valuation layer found for move {move.id}. Product may not use real-time valuation or cost is zero.")
|
_logger.warning(f"No stock valuation layer found for move {move.id}. Product may not use real-time valuation or cost is zero.")
|
||||||
|
|
||||||
# Update account moves if they exist
|
# Update account moves dates if they exist (Run unconditionally as date fix is always needed)
|
||||||
# Refresh move to get account_move_ids
|
# Refresh move to get account_move_ids
|
||||||
move = self.env['stock.move'].browse(move.id)
|
move = self.env['stock.move'].browse(move.id)
|
||||||
if move.account_move_ids:
|
if move.account_move_ids:
|
||||||
|
|||||||
@ -65,6 +65,7 @@
|
|||||||
decoration-warning="theoretical_qty < 0"
|
decoration-warning="theoretical_qty < 0"
|
||||||
force_save="1"/>
|
force_save="1"/>
|
||||||
<field name="counted_qty"/>
|
<field name="counted_qty"/>
|
||||||
|
<field name="unit_cost" optional="show" required="1"/>
|
||||||
<field name="difference_qty" sum="Total Adjustment"/>
|
<field name="difference_qty" sum="Total Adjustment"/>
|
||||||
<field name="product_uom_id" string="UoM" groups="uom.group_uom"/>
|
<field name="product_uom_id" string="UoM" groups="uom.group_uom"/>
|
||||||
<field name="has_negative_theoretical" column_invisible="1"/>
|
<field name="has_negative_theoretical" column_invisible="1"/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user