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) 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