from odoo import models, fields, api, _ import logging from odoo.exceptions import UserError from odoo.tools import float_compare, float_is_zero from datetime import timedelta class StockInventoryRevaluation(models.Model): _name = 'stock.inventory.revaluation' _description = 'Stock Inventory Revaluation' _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default=lambda self: _('New')) date = fields.Datetime(string='Date', required=True, default=fields.Datetime.now) product_id = fields.Many2one('product.product', string='Product', required=True, domain=[('type', '=', 'product')]) account_journal_id = fields.Many2one('account.journal', string='Journal', required=True) account_id = fields.Many2one('account.account', string='Account', help="Counterpart account for the revaluation") current_value = fields.Float(string='Current Value', compute='_compute_current_value', store=True) quantity = fields.Float(string='Quantity', compute='_compute_current_value', store=True) extra_cost = fields.Float(string='Extra Cost', help="Amount to add to the stock value") state = fields.Selection([ ('draft', 'Draft'), ('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) @api.depends('product_id', 'date') def _compute_current_value(self): for record in self: if record.product_id and record.date: # Calculate quantity and value at the specific date layers = self.env['stock.valuation.layer'].search([ ('product_id', '=', record.product_id.id), ('create_date', '<=', record.date), ('company_id', '=', record.company_id.id) ]) record.quantity = sum(layers.mapped('quantity')) record.current_value = sum(layers.mapped('value')) elif record.product_id: record.quantity = record.product_id.quantity_svl record.current_value = record.product_id.value_svl else: record.quantity = 0.0 record.current_value = 0.0 @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.inventory.revaluation') or _('New') return super().create(vals_list) def action_validate(self): self.ensure_one() if float_is_zero(self.extra_cost, precision_rounding=self.currency_id.rounding): raise UserError(_("The Extra Cost cannot be zero.")) # Create Accounting Entry move_vals = self._prepare_account_move_vals() move = self.env['account.move'].with_context(default_date=self.date.date()).create(move_vals) # Check and fix sequence date mismatch if move.name == '/' and not move.posted_before: move._set_next_sequence() if move.name and move.date: move_date = move.date expected_prefix = move_date.strftime('%Y/%m') # If the sequence doesn't contain the expected Year/Month (e.g. 2025/11) # We strictly enforce that 2025/11 is in the name if date is Nov 2025 if expected_prefix not in move.name: journal_id = move.journal_id.id date_start = move_date.replace(day=1) # Calculate end of month next_month = move_date.replace(day=28) + timedelta(days=4) date_end = next_month - timedelta(days=next_month.day) # correct period query last_move = self.env['account.move'].search([ ('journal_id', '=', journal_id), ('name', '!=', '/'), ('date', '>=', date_start), ('date', '<=', date_end), ('company_id', '=', move.company_id.id), ('name', 'like', f"%{expected_prefix}%") ], order='sequence_number desc', limit=1) new_seq = 1 prefix = "" if last_move and last_move.name: # Try to parse the sequence number from the end parts = last_move.name.split('/') if len(parts) >= 2 and parts[-1].isdigit(): new_seq = int(parts[-1]) + 1 prefix = "/".join(parts[:-1]) + "/" else: # Construct prefix from the current (wrong) name but replacing the date part # Assuming format PREFIX/YEAR/MONTH/SEQ parts = move.name.split('/') if len(parts) >= 3: # Attempt to reconstruct: STJ/2025/12/XXXX -> STJ/2025/11/ # We know move_date.year and move_date.month # Let's try to preserve the prefix (index 0) prefix_code = parts[0] prefix = f"{prefix_code}/{move_date.year}/{move_date.month:02d}/" if prefix: new_name = f"{prefix}{new_seq:04d}" move.write({'name': new_name}) move.action_post() # Create Stock Valuation Layer self._create_valuation_layer(move) # AVCO/FIFO Logic: Update Standard Price and Distribute Value to Layers # This fixes the issue where revaluation creates a layer but doesn't update the moving average cost if self.product_id.categ_id.property_cost_method in ['average', 'fifo'] and self.quantity > 0: # 1. Update Standard Price # We use disable_auto_svl to prevent Odoo from creating an extra SVL for this price change new_std_price = self.product_id.standard_price + (self.extra_cost / self.quantity) self.product_id.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price}) # 2. Distribute Value to Remaining Layers (Crucial for correct COGS later) remaining_svls = self.env['stock.valuation.layer'].search([ ('product_id', '=', self.product_id.id), ('remaining_qty', '>', 0), ('company_id', '=', self.company_id.id), ]) if remaining_svls: remaining_qty_total = sum(remaining_svls.mapped('remaining_qty')) if remaining_qty_total > 0: remaining_value_to_distribute = self.extra_cost remaining_value_unit_cost = remaining_value_to_distribute / remaining_qty_total for layer in remaining_svls: if float_compare(layer.remaining_qty, remaining_qty_total, precision_rounding=self.product_id.uom_id.rounding) >= 0: taken_remaining_value = remaining_value_to_distribute else: taken_remaining_value = remaining_value_unit_cost * layer.remaining_qty # Rounding taken_remaining_value = self.currency_id.round(taken_remaining_value) layer.sudo().write({'remaining_value': layer.remaining_value + taken_remaining_value}) remaining_value_to_distribute -= taken_remaining_value remaining_qty_total -= layer.remaining_qty self.state = 'done' def _prepare_account_move_vals(self): self.ensure_one() debit_account_id = self.product_id.categ_id.property_stock_valuation_account_id.id # Auto-detect counterpart account if not set credit_account_id = self.account_id.id if not credit_account_id: if self.extra_cost > 0: credit_account_id = self.product_id.categ_id.property_stock_account_input_categ_id.id else: credit_account_id = self.product_id.categ_id.property_stock_account_output_categ_id.id if not debit_account_id: raise UserError(_("Please define the Stock Valuation Account for product category: %s") % self.product_id.categ_id.name) if not credit_account_id: raise UserError(_("Please define the Stock Input/Output Account for product category: %s, or select an Account manually.") % self.product_id.categ_id.name) amount = self.extra_cost name = _('%s - Revaluation') % self.name # If amount is negative, swap accounts/logic or just let debits be negative? # Usually easier to swap or just have positive/negative balance. # Standard: Debit Stock, Credit Counterpart for increase. lines = [ (0, 0, { 'name': name, 'account_id': debit_account_id, 'debit': amount if amount > 0 else 0, 'credit': -amount if amount < 0 else 0, 'product_id': self.product_id.id, }), (0, 0, { 'name': name, 'account_id': credit_account_id, 'debit': -amount if amount < 0 else 0, 'credit': amount if amount > 0 else 0, }), ] return { 'ref': self.name, 'date': self.date.date(), # BACKDATE HERE 'journal_id': self.account_journal_id.id, 'line_ids': lines, 'move_type': 'entry', } def _create_valuation_layer(self, move): self.ensure_one() layer_vals = { 'product_id': self.product_id.id, 'value': self.extra_cost, 'unit_cost': 0, # Not adjusting unit cost directly, just total value 'quantity': 0, 'remaining_qty': 0, 'description': _('Revaluation: %s') % self.name, 'account_move_id': move.id, 'company_id': self.company_id.id, # We try to force the date if the model allows it, but stock.valuation.layer usually takes create_date. # However, for reporting, Odoo joins with account_move. } # Note: stock.valuation.layer 'create_date' is automatic. # But we can try to override it or rely on the account move date for reports. # Standard Odoo valuation reports often rely on the move date. layer = self.env['stock.valuation.layer'].create(layer_vals) # Force backdate the validation layer's create_date to match the revaluation date # This is critical for "Inventory Valuation at Date" reports. self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (self.date, layer.id)) @property def currency_id(self): return self.company_id.currency_id