diff --git a/models/stock_inventory_backdate.py b/models/stock_inventory_backdate.py index 297c78e..eccef5e 100755 --- a/models/stock_inventory_backdate.py +++ b/models/stock_inventory_backdate.py @@ -149,6 +149,53 @@ 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""" self.ensure_one() @@ -220,6 +267,11 @@ 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', @@ -264,6 +316,17 @@ 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""" @@ -384,19 +447,87 @@ class StockInventoryBackdateLine(models.Model): # Update stock valuation layer # Check if valuation layer exists - svl_count = self.env['stock.valuation.layer'].search_count([('stock_move_id', '=', move.id)]) - _logger.info(f"Found {svl_count} stock valuation layers for move {move.id}") + svl = self.env['stock.valuation.layer'].search([('stock_move_id', '=', move.id)], limit=1) + # 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: - self.env.cr.execute( - "UPDATE stock_valuation_layer SET create_date = %s WHERE stock_move_id = %s", - (backdate, move.id) - ) - _logger.info(f"Updated stock_valuation_layer for move {move.id}, rows affected: {self.env.cr.rowcount}") + 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) + ) + + _logger.info(f"Updated stock_valuation_layer for move {move.id}") else: _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 move = self.env['stock.move'].browse(move.id) if move.account_move_ids: diff --git a/views/stock_inventory_backdate_views.xml b/views/stock_inventory_backdate_views.xml index 3708ae5..6559620 100755 --- a/views/stock_inventory_backdate_views.xml +++ b/views/stock_inventory_backdate_views.xml @@ -65,6 +65,7 @@ decoration-warning="theoretical_qty < 0" force_save="1"/> +