236 lines
12 KiB
Python
236 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_compare, float_is_zero, format_list
|
|
|
|
|
|
class StockValuationLayerRevaluation(models.TransientModel):
|
|
_name = 'stock.valuation.layer.revaluation'
|
|
_description = "Wizard model to reavaluate a stock inventory for a product"
|
|
_check_company_auto = True
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
res = super().default_get(default_fields)
|
|
context = self.env.context
|
|
if res.get('lot_id'):
|
|
lot = self.env['stock.lot'].browse(res['lot_id']).exists()
|
|
if lot:
|
|
res['product_id'] = lot.product_id.id
|
|
if context.get('active_model') == 'stock.valuation.layer':
|
|
# coming from action button "Adjust Valuation" in valuation layer list view
|
|
active_ids = context.get('active_ids')
|
|
layers = self.env['stock.valuation.layer'].browse(active_ids).exists()
|
|
product = layers.product_id
|
|
if len(product) > 1:
|
|
raise UserError(_("You cannot revalue multiple products at once"))
|
|
if any(float_is_zero(layer.remaining_qty, precision_rounding=product.uom_id.rounding) for layer in layers):
|
|
raise UserError(_("You cannot adjust the valuation of a layer with zero quantity"))
|
|
res['adjusted_layer_ids'] = active_ids
|
|
res['product_id'] = product.id
|
|
product = self.env['product.product'].browse(res.get('product_id'))
|
|
if 'product_id' in default_fields:
|
|
if not product:
|
|
raise UserError(_("You cannot adjust valuation without a product"))
|
|
if product.categ_id.property_cost_method == 'standard':
|
|
raise UserError(_("You cannot revalue a product with a standard cost method."))
|
|
if product.quantity_svl <= 0:
|
|
raise UserError(_("You cannot revalue a product with an empty or negative stock."))
|
|
if 'account_journal_id' not in res and 'account_journal_id' in default_fields and product.categ_id.property_valuation == 'real_time':
|
|
accounts = product.product_tmpl_id.get_product_accounts()
|
|
res['account_journal_id'] = accounts['stock_journal'].id
|
|
return res
|
|
|
|
company_id = fields.Many2one('res.company', "Company", readonly=True, required=True, default=lambda self: self.env.company)
|
|
currency_id = fields.Many2one('res.currency', "Currency", related='company_id.currency_id', required=True)
|
|
|
|
adjusted_layer_ids = fields.Many2many('stock.valuation.layer', string="Valuation Layers", help="Valuations layers being adjusted")
|
|
product_id = fields.Many2one('product.product', "Related product", required=True, check_company=True)
|
|
lot_id = fields.Many2one('stock.lot', "Related lot/serial number", check_company=True)
|
|
property_valuation = fields.Selection(related='product_id.categ_id.property_valuation')
|
|
product_uom_name = fields.Char("Unit of Measure", related='product_id.uom_id.name')
|
|
current_value_svl = fields.Float("Current Value", compute='_compute_current_value_svl')
|
|
current_quantity_svl = fields.Float("Current Quantity", compute='_compute_current_value_svl')
|
|
|
|
added_value = fields.Monetary("Added value", required=True)
|
|
new_value = fields.Monetary("New value", compute='_compute_new_value')
|
|
new_value_by_qty = fields.Monetary("New value by quantity", compute='_compute_new_value')
|
|
reason = fields.Char("Reason", help="Reason of the revaluation")
|
|
|
|
account_journal_id = fields.Many2one('account.journal', "Journal", check_company=True)
|
|
account_id = fields.Many2one('account.account', "Counterpart Account", domain=[('deprecated', '=', False)], check_company=True)
|
|
date = fields.Date("Accounting Date")
|
|
|
|
@api.depends('current_value_svl', 'current_quantity_svl', 'added_value')
|
|
def _compute_new_value(self):
|
|
for reval in self:
|
|
reval.new_value = reval.current_value_svl + reval.added_value
|
|
if not float_is_zero(reval.current_quantity_svl, precision_rounding=self.product_id.uom_id.rounding):
|
|
reval.new_value_by_qty = reval.new_value / reval.current_quantity_svl
|
|
else:
|
|
reval.new_value_by_qty = 0.0
|
|
|
|
@api.depends('product_id.quantity_svl', 'product_id.value_svl', 'adjusted_layer_ids', 'lot_id')
|
|
def _compute_current_value_svl(self):
|
|
for reval in self:
|
|
if reval.adjusted_layer_ids:
|
|
reval.current_quantity_svl = sum(reval.adjusted_layer_ids.mapped('remaining_qty'))
|
|
reval.current_value_svl = sum(reval.adjusted_layer_ids.mapped('remaining_value'))
|
|
if reval.lot_id:
|
|
reval.current_quantity_svl = reval.lot_id.quantity_svl
|
|
reval.current_value_svl = reval.lot_id.value_svl
|
|
else:
|
|
reval.current_quantity_svl = reval.product_id.quantity_svl
|
|
reval.current_value_svl = reval.product_id.value_svl
|
|
|
|
def action_validate_revaluation(self):
|
|
""" Adjust the valuation of layers `self.adjusted_layer_ids` for
|
|
`self.product_id` in `self.company_id`, or the entire stock for that
|
|
product if no layers are specified (all layers with positive remaining
|
|
quantity).
|
|
|
|
- Change the standard price with the new valuation by product unit.
|
|
- Create a manual stock valuation layer with the `added_value` of `self`.
|
|
- Distribute the `added_value` on the remaining_value of the layers
|
|
- If the Inventory Valuation of the product category is automated, create
|
|
related account move.
|
|
"""
|
|
self.ensure_one()
|
|
if self.currency_id.is_zero(self.added_value):
|
|
raise UserError(_("The added value doesn't have any impact on the stock valuation"))
|
|
|
|
product_id = self.product_id.with_company(self.company_id)
|
|
lot_id = self.lot_id.with_company(self.company_id)
|
|
|
|
remaining_domain = [
|
|
('product_id', '=', product_id.id),
|
|
('remaining_qty', '>', 0),
|
|
('company_id', '=', self.company_id.id),
|
|
]
|
|
if lot_id:
|
|
remaining_domain.append(('lot_id', '=', lot_id.id))
|
|
layers_with_qty = self.env['stock.valuation.layer'].search(remaining_domain)
|
|
adjusted_layers = self.adjusted_layer_ids or layers_with_qty
|
|
|
|
description = _("Manual Stock Valuation: %s.", self.reason or _("No Reason Given"))
|
|
# Update the stardard price in case of AVCO/FIFO
|
|
cost_method = product_id.categ_id.property_cost_method
|
|
if cost_method in ['average', 'fifo']:
|
|
previous_cost = lot_id.standard_price if lot_id else product_id.standard_price
|
|
total_product_qty = sum(layers_with_qty.mapped('remaining_qty'))
|
|
if lot_id:
|
|
lot_id.with_context(disable_auto_svl=True).standard_price += self.added_value / total_product_qty
|
|
product_id.with_context(disable_auto_svl=True).standard_price += self.added_value / product_id.quantity_svl
|
|
if self.lot_id:
|
|
description += _(
|
|
" lot/serial number cost updated from %(previous)s to %(new_cost)s.",
|
|
previous=previous_cost,
|
|
new_cost=lot_id.standard_price
|
|
)
|
|
else:
|
|
description += _(
|
|
" Product cost updated from %(previous)s to %(new_cost)s.",
|
|
previous=previous_cost,
|
|
new_cost=product_id.standard_price
|
|
)
|
|
|
|
revaluation_svl_vals = {
|
|
'company_id': self.company_id.id,
|
|
'product_id': product_id.id,
|
|
'description': description,
|
|
'value': self.added_value,
|
|
'lot_id': self.lot_id.id,
|
|
'quantity': 0,
|
|
}
|
|
|
|
qty_by_lots = defaultdict(float)
|
|
|
|
remaining_qty = sum(adjusted_layers.mapped('remaining_qty'))
|
|
remaining_value = self.added_value
|
|
remaining_value_unit_cost = self.currency_id.round(remaining_value / remaining_qty)
|
|
|
|
# adjust all layers by the unit value change per unit, except the last layer which gets
|
|
# whatever is left. This avoids rounding issues e.g. $10 on 3 products => 3.33, 3.33, 3.34
|
|
for svl in adjusted_layers:
|
|
if product_id.lot_valuated and not lot_id:
|
|
qty_by_lots[svl.lot_id.id] += svl.remaining_qty
|
|
if float_is_zero(svl.remaining_qty - remaining_qty, precision_rounding=self.product_id.uom_id.rounding):
|
|
taken_remaining_value = remaining_value
|
|
else:
|
|
taken_remaining_value = remaining_value_unit_cost * svl.remaining_qty
|
|
if float_compare(svl.remaining_value + taken_remaining_value, 0, precision_rounding=self.product_id.uom_id.rounding) < 0:
|
|
raise UserError(_('The value of a stock valuation layer cannot be negative. Landed cost could be use to correct a specific transfer.'))
|
|
|
|
svl.remaining_value += taken_remaining_value
|
|
remaining_value -= taken_remaining_value
|
|
remaining_qty -= svl.remaining_qty
|
|
|
|
previous_value_svl = self.current_value_svl
|
|
|
|
if qty_by_lots:
|
|
vals = revaluation_svl_vals.copy()
|
|
total_qty = sum(adjusted_layers.mapped('remaining_qty'))
|
|
revaluation_svl_vals = []
|
|
for lot, qty in qty_by_lots.items():
|
|
value = self.added_value * qty / total_qty
|
|
revaluation_svl_vals.append(
|
|
dict(vals, value=value, lot_id=lot)
|
|
)
|
|
|
|
revaluation_svl = self.env['stock.valuation.layer'].create(revaluation_svl_vals)
|
|
|
|
# If the Inventory Valuation of the product category is automated, create related account move.
|
|
if self.property_valuation != 'real_time':
|
|
return True
|
|
|
|
accounts = product_id.product_tmpl_id.get_product_accounts()
|
|
|
|
if self.added_value < 0:
|
|
debit_account_id = self.account_id.id
|
|
credit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id
|
|
else:
|
|
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id
|
|
credit_account_id = self.account_id.id
|
|
|
|
move_description = _('%(user)s changed stock valuation from %(previous)s to %(new_value)s - %(product)s\n%(reason)s',
|
|
user=self.env.user.name,
|
|
previous=previous_value_svl,
|
|
new_value=previous_value_svl + self.added_value,
|
|
product=product_id.display_name,
|
|
reason=description,
|
|
)
|
|
|
|
if self.adjusted_layer_ids:
|
|
adjusted_layer_descriptions = [f"{layer.reference} (id: {layer.id})" for layer in self.adjusted_layer_ids]
|
|
move_description += _("\nAffected valuation layers: %s", format_list(self.env, adjusted_layer_descriptions))
|
|
|
|
move_vals = [{
|
|
'journal_id': self.account_journal_id.id or accounts['stock_journal'].id,
|
|
'company_id': self.company_id.id,
|
|
'ref': _("Revaluation of %s", product_id.display_name),
|
|
'stock_valuation_layer_ids': [(6, None, [svl.id])],
|
|
'date': self.date or fields.Date.today(),
|
|
'move_type': 'entry',
|
|
'line_ids': [(0, 0, {
|
|
'name': move_description,
|
|
'account_id': debit_account_id,
|
|
'debit': abs(svl.value),
|
|
'credit': 0,
|
|
'product_id': svl.product_id.id,
|
|
}), (0, 0, {
|
|
'name': move_description,
|
|
'account_id': credit_account_id,
|
|
'debit': 0,
|
|
'credit': abs(svl.value),
|
|
'product_id': svl.product_id.id,
|
|
})],
|
|
} for svl in revaluation_svl]
|
|
account_move = self.env['account.move'].create(move_vals)
|
|
account_move._post()
|
|
|
|
return True
|