[IMP] stock_account: add button to create manual stock valuation layers
Add a way to create a munual stock valuation layer, create a stock valuation layers implies : - Change the stardard price with the new valuation by product unit in case of AVCO. - Create a manual stock valuation layer with the added value. - Update the remaining value of current valuation layer. - If the Inventory Valuation of the product category is automated, create related account move. task-2123752
This commit is contained in:
@@ -32,6 +32,7 @@ Dashboard / Reports for Warehouse Management includes:
|
||||
'views/product_views.xml',
|
||||
'views/stock_quant_views.xml',
|
||||
'views/stock_valuation_layer_views.xml',
|
||||
'wizard/stock_valuation_layer_revaluation_views.xml'
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
|
||||
@@ -97,6 +97,7 @@ class ProductProduct(models.Model):
|
||||
quantity_svl = fields.Float(compute='_compute_value_svl')
|
||||
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'product_id')
|
||||
valuation = fields.Selection(related="categ_id.property_valuation", readonly=True)
|
||||
cost_method = fields.Selection(related="categ_id.property_cost_method", readonly=True)
|
||||
|
||||
def write(self, vals):
|
||||
if 'standard_price' in vals and not self.env.context.get('disable_auto_svl'):
|
||||
@@ -126,6 +127,22 @@ class ProductProduct(models.Model):
|
||||
remaining.value_svl = 0
|
||||
remaining.quantity_svl = 0
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Actions
|
||||
# -------------------------------------------------------------------------
|
||||
def action_revaluation(self):
|
||||
self.ensure_one()
|
||||
ctx = dict(self._context, default_product_id=self.id, default_company_id=self.env.company.id)
|
||||
return {
|
||||
'name': _("Product Revaluation"),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.valuation.layer.revaluation',
|
||||
'view_id': self.env.ref('stock_account.stock_valuation_layer_revaluation_form_view').id,
|
||||
'type': 'ir.actions.act_window',
|
||||
'context': ctx,
|
||||
'target': 'new'
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# SVL creation helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -6,3 +6,4 @@ access_stock_picking_invoicing_payments,stock.picking,stock.model_stock_picking,
|
||||
access_stock_move_invoicing_payments_readonly,stock.move,model_stock_move,account.group_account_readonly,1,0,0,0
|
||||
access_stock_move_invoicing_payments,stock.move,model_stock_move,account.group_account_invoice,1,1,1,0
|
||||
access_stock_valuation_layer,access_stock_valuation_layer,model_stock_valuation_layer,stock.group_stock_manager,1,1,1,0
|
||||
access_stock_valuation_layer_revaluation,access_stock_valuation_layer_revaluation,model_stock_valuation_layer_revaluation,stock.group_stock_manager,1,1,1,0
|
||||
|
@@ -1,3 +1,4 @@
|
||||
from . import test_anglo_saxon_valuation_reconciliation_common
|
||||
from . import test_stockvaluation
|
||||
from . import test_stockvaluationlayer
|
||||
from . import test_stock_valuation_layer_revaluation
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import Form
|
||||
from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
|
||||
from odoo.addons.stock_account.tests.test_stockvaluationlayer import TestStockValuationCommon
|
||||
|
||||
|
||||
class TestStockValuationLayerRevaluation(TestStockValuationCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestStockValuationLayerRevaluation, cls).setUpClass()
|
||||
cls.stock_input_account, cls.stock_output_account, cls.stock_valuation_account, cls.expense_account, cls.stock_journal = _create_accounting_data(cls.env)
|
||||
cls.product1.write({
|
||||
'property_account_expense_id': cls.expense_account.id,
|
||||
})
|
||||
cls.product1.categ_id.write({
|
||||
'property_stock_account_input_categ_id': cls.stock_input_account.id,
|
||||
'property_stock_account_output_categ_id': cls.stock_output_account.id,
|
||||
'property_stock_valuation_account_id': cls.stock_valuation_account.id,
|
||||
'property_stock_journal': cls.stock_journal.id,
|
||||
})
|
||||
|
||||
cls.product1.categ_id.property_valuation = 'real_time'
|
||||
|
||||
def test_stock_valuation_layer_revaluation_avco(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
context = {
|
||||
'default_product_id': self.product1.id,
|
||||
'default_company_id': self.env.company.id,
|
||||
'default_added_value': 0.0
|
||||
}
|
||||
# Quantity of product1 is zero, raise
|
||||
with self.assertRaises(UserError):
|
||||
Form(self.env['stock.valuation.layer.revaluation'].with_context(context)).save()
|
||||
|
||||
self._make_in_move(self.product1, 10, unit_cost=2)
|
||||
self._make_in_move(self.product1, 10, unit_cost=4)
|
||||
|
||||
self.assertEqual(self.product1.standard_price, 3)
|
||||
self.assertEqual(self.product1.quantity_svl, 20)
|
||||
|
||||
old_layers = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc")
|
||||
|
||||
self.assertEqual(len(old_layers), 2)
|
||||
self.assertEqual(old_layers[0].remaining_value, 40)
|
||||
|
||||
revaluation_wizard = Form(self.env['stock.valuation.layer.revaluation'].with_context(context))
|
||||
revaluation_wizard.added_value = 20
|
||||
revaluation_wizard.account_id = self.stock_valuation_account
|
||||
revaluation_wizard.save().action_validate_revaluation()
|
||||
|
||||
# Check standard price change
|
||||
self.assertEqual(self.product1.standard_price, 4)
|
||||
self.assertEqual(self.product1.quantity_svl, 20)
|
||||
|
||||
# Check the creation of stock.valuation.layer
|
||||
new_layer = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc", limit=1)
|
||||
self.assertEqual(new_layer.value, 20)
|
||||
|
||||
# Check the remaing value of current layers
|
||||
self.assertEqual(old_layers[0].remaining_value, 50)
|
||||
self.assertEqual(sum(slv.remaining_value for slv in old_layers), 80)
|
||||
|
||||
# Check account move
|
||||
self.assertTrue(bool(new_layer.account_move_id))
|
||||
self.assertEqual(len(new_layer.account_move_id.line_ids), 2)
|
||||
|
||||
self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("debit")), 20)
|
||||
self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("credit")), 20)
|
||||
|
||||
credit_lines = [l for l in new_layer.account_move_id.line_ids if l.credit > 0]
|
||||
self.assertEqual(len(credit_lines), 1)
|
||||
self.assertEqual(credit_lines[0].account_id.id, self.stock_valuation_account.id)
|
||||
|
||||
def test_stock_valuation_layer_revaluation_avco_rounding(self):
|
||||
self.product1.categ_id.property_cost_method = 'average'
|
||||
context = {
|
||||
'default_product_id': self.product1.id,
|
||||
'default_company_id': self.env.company.id,
|
||||
'default_added_value': 0.0
|
||||
}
|
||||
# Quantity of product1 is zero, raise
|
||||
with self.assertRaises(UserError):
|
||||
Form(self.env['stock.valuation.layer.revaluation'].with_context(context)).save()
|
||||
|
||||
self._make_in_move(self.product1, 1, unit_cost=1)
|
||||
self._make_in_move(self.product1, 1, unit_cost=1)
|
||||
self._make_in_move(self.product1, 1, unit_cost=1)
|
||||
|
||||
self.assertEqual(self.product1.standard_price, 1)
|
||||
self.assertEqual(self.product1.quantity_svl, 3)
|
||||
|
||||
old_layers = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc")
|
||||
|
||||
self.assertEqual(len(old_layers), 3)
|
||||
self.assertEqual(old_layers[0].remaining_value, 1)
|
||||
|
||||
revaluation_wizard = Form(self.env['stock.valuation.layer.revaluation'].with_context(context))
|
||||
revaluation_wizard.added_value = 1
|
||||
revaluation_wizard.account_id = self.stock_valuation_account
|
||||
revaluation_wizard.save().action_validate_revaluation()
|
||||
|
||||
# Check standard price change
|
||||
self.assertEqual(self.product1.standard_price, 1.33)
|
||||
self.assertEqual(self.product1.quantity_svl, 3)
|
||||
|
||||
# Check the creation of stock.valuation.layer
|
||||
new_layer = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc", limit=1)
|
||||
self.assertEqual(new_layer.value, 1)
|
||||
|
||||
# Check the remaing value of current layers
|
||||
self.assertEqual(sum(slv.remaining_value for slv in old_layers), 4)
|
||||
self.assertTrue(1.34 in old_layers.mapped("remaining_value"))
|
||||
|
||||
# Check account move
|
||||
self.assertTrue(bool(new_layer.account_move_id))
|
||||
self.assertEqual(len(new_layer.account_move_id.line_ids), 2)
|
||||
|
||||
self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("debit")), 1)
|
||||
self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("credit")), 1)
|
||||
|
||||
credit_lines = [l for l in new_layer.account_move_id.line_ids if l.credit > 0]
|
||||
self.assertEqual(len(credit_lines), 1)
|
||||
self.assertEqual(credit_lines[0].account_id.id, self.stock_valuation_account.id)
|
||||
|
||||
def test_stock_valuation_layer_revaluation_fifo(self):
|
||||
self.product1.categ_id.property_cost_method = 'fifo'
|
||||
context = {
|
||||
'default_product_id': self.product1.id,
|
||||
'default_company_id': self.env.company.id,
|
||||
'default_added_value': 0.0
|
||||
}
|
||||
# Quantity of product1 is zero, raise
|
||||
with self.assertRaises(UserError):
|
||||
Form(self.env['stock.valuation.layer.revaluation'].with_context(context)).save()
|
||||
|
||||
self._make_in_move(self.product1, 10, unit_cost=2)
|
||||
self._make_in_move(self.product1, 10, unit_cost=4)
|
||||
|
||||
self.assertEqual(self.product1.standard_price, 2)
|
||||
self.assertEqual(self.product1.quantity_svl, 20)
|
||||
|
||||
old_layers = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc")
|
||||
|
||||
self.assertEqual(len(old_layers), 2)
|
||||
self.assertEqual(old_layers[0].remaining_value, 40)
|
||||
|
||||
revaluation_wizard = Form(self.env['stock.valuation.layer.revaluation'].with_context(context))
|
||||
revaluation_wizard.added_value = 20
|
||||
revaluation_wizard.account_id = self.stock_valuation_account
|
||||
revaluation_wizard.save().action_validate_revaluation()
|
||||
|
||||
self.assertEqual(self.product1.standard_price, 2)
|
||||
|
||||
# Check the creation of stock.valuation.layer
|
||||
new_layer = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc", limit=1)
|
||||
self.assertEqual(new_layer.value, 20)
|
||||
|
||||
# Check the remaing value of current layers
|
||||
self.assertEqual(old_layers[0].remaining_value, 50)
|
||||
self.assertEqual(sum(slv.remaining_value for slv in old_layers), 80)
|
||||
|
||||
# Check account move
|
||||
self.assertTrue(bool(new_layer.account_move_id))
|
||||
self.assertTrue(len(new_layer.account_move_id.line_ids), 2)
|
||||
|
||||
self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("debit")), 20)
|
||||
self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("credit")), 20)
|
||||
|
||||
credit_lines = [l for l in new_layer.account_move_id.line_ids if l.credit > 0]
|
||||
self.assertEqual(len(credit_lines), 1)
|
||||
@@ -50,6 +50,11 @@
|
||||
<field name="currency_id" invisible="1" />
|
||||
<field name="value" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<groupby name="product_id">
|
||||
<field name="cost_method" invisible="1"/>
|
||||
<field name="quantity_svl" invisible="1"/>
|
||||
<button name="action_revaluation" icon="fa-plus" title="Add Manual Valuation" type="object" attrs="{'invisible':['|', ('cost_method', '=', 'standard'), ('quantity_svl', '<=', 0)]}" />
|
||||
</groupby>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import stock_quantity_history
|
||||
from . import stock_picking_return
|
||||
from . import stock_valuation_layer_revaluation
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
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)
|
||||
if res.get('product_id'):
|
||||
product = self.env['product.product'].browse(res['product_id'])
|
||||
accounts = product.product_tmpl_id.get_product_accounts()
|
||||
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:
|
||||
res['account_journal_id'] = accounts['stock_journal'].id
|
||||
return res
|
||||
|
||||
company_id = fields.Many2one('res.company', "Company", readonly=True, required=True)
|
||||
currency_id = fields.Many2one('res.currency', "Currency", related='company_id.currency_id', required=True)
|
||||
|
||||
product_id = fields.Many2one('product.product', "Related product", required=True, 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", related="product_id.value_svl")
|
||||
current_quantity_svl = fields.Float("Current Quantity", related="product_id.quantity_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", required=True, check_company=True)
|
||||
account_id = fields.Many2one('account.account', "Counterpart Account", domain=[('deprecated', '=', False)], required=True, 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
|
||||
|
||||
def action_validate_revaluation(self):
|
||||
""" Revaluate the stock for `self.product_id` in `self.company_id`.
|
||||
|
||||
- Change the stardard 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 the remaining_value of layers still in stock (with a remaining quantity)
|
||||
- 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)
|
||||
|
||||
remaining_svls = self.env['stock.valuation.layer'].search([
|
||||
('product_id', '=', product_id.id),
|
||||
('remaining_qty', '>', 0),
|
||||
('company_id', '=', self.company_id.id),
|
||||
])
|
||||
|
||||
# Create a manual stock valuation layer
|
||||
revaluation_svl_vals = {
|
||||
'company_id': self.company_id.id,
|
||||
'product_id': product_id.id,
|
||||
'description': _("Manual Stock Valuation: %s.%s") % (
|
||||
self.reason or _("No Reason Given"),
|
||||
product_id.categ_id.property_cost_method == 'average' and (_(" Product cost updated from %s to %s.") % (product_id.standard_price, product_id.standard_price + self.added_value / self.current_quantity_svl)) or ''),
|
||||
'value': self.added_value,
|
||||
'quantity': 0,
|
||||
}
|
||||
|
||||
remaining_qty = sum(remaining_svls.mapped('remaining_qty'))
|
||||
remaining_value = self.added_value
|
||||
remaining_value_unit_cost = self.currency_id.round(remaining_value / remaining_qty)
|
||||
for svl in remaining_svls:
|
||||
if float_is_zero(svl.remaining_qty - remaining_qty, precision_rounding=self.product_id.uom_id.rounding):
|
||||
svl.remaining_value += remaining_value
|
||||
else:
|
||||
taken_remaining_value = remaining_value_unit_cost * svl.remaining_qty
|
||||
svl.remaining_value += taken_remaining_value
|
||||
remaining_value -= taken_remaining_value
|
||||
remaining_qty -= svl.remaining_qty
|
||||
|
||||
revaluation_svl = self.env['stock.valuation.layer'].create(revaluation_svl_vals)
|
||||
|
||||
# Update the stardard price in case of AVCO
|
||||
if product_id.categ_id.property_cost_method == 'average':
|
||||
product_id.with_context(disable_auto_svl=True).standard_price += self.added_value / self.current_quantity_svl
|
||||
|
||||
# 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_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, [revaluation_svl.id])],
|
||||
'date': self.date or fields.Date.today(),
|
||||
'move_type': 'entry',
|
||||
'line_ids': [(0, 0, {
|
||||
'name': _('%s changed stock valuation from %s to %s of %s') % (
|
||||
self.env.user.name, self.current_value_svl, self.current_value_svl + self.added_value, product_id.display_name),
|
||||
'account_id': debit_account_id,
|
||||
'debit': abs(self.added_value),
|
||||
'credit': 0,
|
||||
'product_id': product_id.id,
|
||||
}), (0, 0, {
|
||||
'name': _('%s changed stock valuation from %s to %s of %s') % (
|
||||
self.env.user.name, self.current_value_svl, self.current_value_svl + self.added_value, product_id.display_name),
|
||||
'account_id': credit_account_id,
|
||||
'debit': 0,
|
||||
'credit': abs(self.added_value),
|
||||
'product_id': product_id.id,
|
||||
})],
|
||||
}
|
||||
account_move = self.env['account.move'].create(move_vals)
|
||||
account_move.post()
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="stock_valuation_layer_revaluation_form_view" model="ir.ui.view">
|
||||
<field name="name">stock.valuation.layer.revaluation.form</field>
|
||||
<field name="model">stock.valuation.layer.revaluation</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Product Revaluation">
|
||||
<sheet>
|
||||
<group>
|
||||
<label for="current_value_svl" string="Current Value"/>
|
||||
<div class="o_row">
|
||||
<span><field name="current_value_svl" widget="monetary"/> for <field name="current_quantity_svl"/> <field name="product_uom_name" class="mx-1"/> </span>
|
||||
</div>
|
||||
<label for="added_value" string="Added Value"/>
|
||||
<div class="o_row">
|
||||
<span><field name="added_value" class="oe_inline"/> = <field name="new_value"/> (<field name="new_value_by_qty"/> by <field name="product_uom_name" class="mx-1"/>)
|
||||
<small class="mx-2 font-italic">Use a negative added value to record a decrease in the product value</small></span>
|
||||
</div>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="property_valuation" invisible="1"/>
|
||||
<group>
|
||||
<field name="reason"/>
|
||||
<field name="account_journal_id" attrs="{'invisible':[('property_valuation', '!=', 'real_time')]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="account_id" attrs="{'invisible':[('property_valuation', '!=', 'real_time')]}"/>
|
||||
<field name="date" attrs="{'invisible':[('property_valuation', '!=', 'real_time')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_validate_revaluation" string="Revalue" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user