stock_inventory_revaluation/models/stock_inventory_revaluation.py

200 lines
8.9 KiB
Python
Executable File

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