[REF] stock_landed_costs: adapt to valuation layers
We need to increment the value of another layer. We don't want to write on this layer in place as we used to do for multiple reasons: - valuation at date: before the application of an lc, the value should not change - if the layer was consumed, we would need to adapt the consumers and if one day we would have needed to understand what happened in the fifo stack it would be hell So we allow to create a valuation layer linked to another valuation layer through a simple M2O O2M. At the time it is only used on the landed costs to add value and no quantity so the code expects it would be the case and it is not enforced. We adapt `_run_fifo` to use the full value. We also add a stat button on the landed cost to access the valuation layers, like what is done for the picking and manufacturing orders. task-1875873
This commit is contained in:
@@ -204,7 +204,11 @@ class ProductProduct(models.Model):
|
||||
new_standard_price = candidate.unit_cost
|
||||
qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate.remaining_qty)
|
||||
|
||||
value_taken_on_candidate = qty_taken_on_candidate * candidate.unit_cost
|
||||
candidate_unit_cost = candidate.unit_cost
|
||||
if candidate.stock_valuation_layer_ids:
|
||||
candidate_unit_cost += sum(candidate.stock_valuation_layer_ids.mapped('value')) / candidate.remaining_qty
|
||||
|
||||
value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost
|
||||
candidate_vals = {
|
||||
'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate,
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ class StockValuationLayer(models.Model):
|
||||
value = fields.Monetary('Total Value', readonly=True)
|
||||
remaining_qty = fields.Float(digits=0, readonly=True)
|
||||
description = fields.Char('Description', readonly=True)
|
||||
stock_valuation_layer_id = fields.Many2one('stock.valuation.layer', 'Linked Stock Valuation Layer', readonly=True)
|
||||
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_valuation_layer_id')
|
||||
stock_move_id = fields.Many2one('stock.move', 'Stock Move', readonly=True)
|
||||
account_move_id = fields.Many2one('account.move', 'Journal Entry', readonly=True)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<field name="description" />
|
||||
<field name="account_move_id" attrs="{'invisible': [('account_move_id', '=', False)]}" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="stock_valuation_layer_id" attrs="{'invisible': [('stock_valuation_layer_id', '=', False)]}" />
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
@@ -21,6 +21,7 @@ This module allows you to easily add extra costs on pickings and decide the spli
|
||||
'data/stock_landed_cost_data.xml',
|
||||
'views/product_views.xml',
|
||||
'views/stock_landed_cost_views.xml',
|
||||
'views/stock_valuation_layer_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
|
||||
from . import product
|
||||
from . import stock_landed_cost
|
||||
from . import stock_valuation_layer
|
||||
|
||||
|
||||
@@ -9,12 +9,6 @@ from odoo.addons.stock_landed_costs.models import product
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
landed_cost_value = fields.Float('Landed Cost')
|
||||
|
||||
|
||||
class LandedCost(models.Model):
|
||||
_name = 'stock.landed.cost'
|
||||
_description = 'Stock Landed Cost'
|
||||
@@ -53,6 +47,7 @@ class LandedCost(models.Model):
|
||||
required=True, states={'done': [('readonly', True)]})
|
||||
company_id = fields.Many2one('res.company', string="Company",
|
||||
related='account_journal_id.company_id', readonly=False)
|
||||
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_landed_cost_id')
|
||||
|
||||
@api.one
|
||||
@api.depends('cost_lines.price_unit')
|
||||
@@ -101,25 +96,33 @@ class LandedCost(models.Model):
|
||||
'line_ids': [],
|
||||
}
|
||||
for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
|
||||
# Prorate the value at what's still in stock
|
||||
cost_to_add = (line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
|
||||
remaining_qty = sum(line.move_id.stock_valuation_layer_ids.mapped('remaining_qty'))
|
||||
linked_layer = line.move_id.stock_valuation_layer_ids[-1] # Maybe the LC layer should be linked to multiple IN layer?
|
||||
|
||||
new_landed_cost_value = line.move_id.landed_cost_value + line.additional_landed_cost
|
||||
line.move_id.write({
|
||||
'landed_cost_value': new_landed_cost_value,
|
||||
'value': line.move_id.value + line.additional_landed_cost,
|
||||
'remaining_value': line.move_id.remaining_value + cost_to_add,
|
||||
'price_unit': (line.move_id.value + line.additional_landed_cost) / line.move_id.product_qty,
|
||||
# Prorate the value at what's still in stock
|
||||
cost_to_add = (remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
|
||||
valuation_layer = self.env['stock.valuation.layer'].create({
|
||||
'value': cost_to_add,
|
||||
'unit_cost': 0,
|
||||
'quantity': 0,
|
||||
'remaining_qty': 0,
|
||||
'stock_valuation_layer_id': linked_layer.id,
|
||||
'description': cost.name,
|
||||
'stock_move_id': line.move_id.id,
|
||||
'product_id': line.move_id.product_id.id,
|
||||
'stock_landed_cost_id': self.id,
|
||||
'company_id': self.company_id.id,
|
||||
})
|
||||
# `remaining_qty` is negative if the move is out and delivered proudcts that were not
|
||||
# in stock.
|
||||
qty_out = 0
|
||||
if line.move_id._is_in():
|
||||
qty_out = line.move_id.product_qty - line.move_id.remaining_qty
|
||||
qty_out = line.move_id.product_qty - remaining_qty
|
||||
elif line.move_id._is_out():
|
||||
qty_out = line.move_id.product_qty
|
||||
move_vals['line_ids'] += line._create_accounting_entries(move, qty_out)
|
||||
|
||||
move_vals['stock_valuation_layer_ids'] = [(6, None, [valuation_layer.id])]
|
||||
move = move.create(move_vals)
|
||||
cost.write({'state': 'done', 'account_move_id': move.id})
|
||||
move.post()
|
||||
@@ -153,7 +156,7 @@ class LandedCost(models.Model):
|
||||
'product_id': move.product_id.id,
|
||||
'move_id': move.id,
|
||||
'quantity': move.product_qty,
|
||||
'former_cost': move.value,
|
||||
'former_cost': sum(move.stock_valuation_layer_ids.mapped('value')),
|
||||
'weight': move.product_id.weight * move.product_qty,
|
||||
'volume': move.product_id.volume * move.product_qty
|
||||
}
|
||||
@@ -227,6 +230,12 @@ class LandedCost(models.Model):
|
||||
AdjustementLines.browse(key).write({'additional_landed_cost': value})
|
||||
return True
|
||||
|
||||
def action_view_stock_valuation_layers(self):
|
||||
self.ensure_one()
|
||||
domain = [('id', 'in', self.stock_valuation_layer_ids.ids)]
|
||||
action = self.env.ref('stock_account.stock_valuation_layer_action').read()[0]
|
||||
return dict(action, domain=domain)
|
||||
|
||||
|
||||
class LandedCostLine(models.Model):
|
||||
_name = 'stock.landed.cost.lines'
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockValuationLayer(models.Model):
|
||||
"""Stock Valuation Layer"""
|
||||
|
||||
_inherit = 'stock.valuation.layer'
|
||||
|
||||
stock_landed_cost_id = fields.Many2one('stock.landed.cost', 'Landed Cost')
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
from . import test_stock_landed_costs
|
||||
from . import test_stock_landed_costs_purchase
|
||||
from . import test_stock_landed_costs_rounding
|
||||
from . import test_stockvaluationlayer
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
""" Implementation of "INVENTORY VALUATION TESTS (With valuation layers)" spreadsheet. """
|
||||
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.addons.stock_account.tests.test_stockvaluationlayer import TestStockValuationCommon
|
||||
|
||||
|
||||
class TestStockValuationLC(TestStockValuationCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestStockValuationLC, cls).setUpClass()
|
||||
cls.productlc1 = cls.env['product.product'].create({
|
||||
'name': 'product1',
|
||||
'type': 'service',
|
||||
'landed_cost_ok': True,
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def _make_lc(self, move, amount):
|
||||
picking = move.picking_id
|
||||
lc = Form(self.env['stock.landed.cost'])
|
||||
lc.account_journal_id = self.env['account.journal'].search([('name', 'ilike', '%misc%')], limit=1)
|
||||
lc.picking_ids.add(move.picking_id)
|
||||
with lc.cost_lines.new() as cost_line:
|
||||
cost_line.product_id = self.productlc1
|
||||
cost_line.price_unit = amount
|
||||
lc = lc.save()
|
||||
lc.compute_landed_cost()
|
||||
lc.button_validate()
|
||||
return lc
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestStockValuationLCFIFO(TestStockValuationLC):
|
||||
def setUp(self):
|
||||
super(TestStockValuationLCFIFO, self).setUp()
|
||||
self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
|
||||
self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
|
||||
|
||||
def test_normal_1(self):
|
||||
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
|
||||
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
||||
lc = self._make_lc(move1, 100)
|
||||
move3 = self._make_out_move(self.product1, 1)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 380)
|
||||
self.assertEqual(self.product1.quantity_svl, 19)
|
||||
|
||||
def test_negative_1(self):
|
||||
self.product1.standard_price = 10
|
||||
move1 = self._make_out_move(self.product1, 2, force_assign=True)
|
||||
move2 = self._make_in_move(self.product1, 10, unit_cost=15, create_picking=True)
|
||||
lc = self._make_lc(move2, 100)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 200)
|
||||
self.assertEqual(self.product1.quantity_svl, 8)
|
||||
|
||||
def test_alreadyout_1(self):
|
||||
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
|
||||
move2 = self._make_out_move(self.product1, 10)
|
||||
lc = self._make_lc(move1, 100)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 0)
|
||||
self.assertEqual(self.product1.quantity_svl, 0)
|
||||
|
||||
def test_alreadyout_2(self):
|
||||
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
|
||||
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
|
||||
move2 = self._make_out_move(self.product1, 1)
|
||||
lc = self._make_lc(move1, 100)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 380)
|
||||
self.assertEqual(self.product1.quantity_svl, 19)
|
||||
|
||||
def test_alreadyout_3(self):
|
||||
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
|
||||
move2 = self._make_out_move(self.product1, 10)
|
||||
move1.move_line_ids.qty_done = 15
|
||||
lc = self._make_lc(move1, 60)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 70)
|
||||
self.assertEqual(self.product1.quantity_svl, 5)
|
||||
@@ -13,6 +13,12 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button string="Valuation" type="object"
|
||||
name="action_view_stock_valuation_layers"
|
||||
class="oe_stat_button" icon="fa-dollar" groups="stock.group_stock_manager"
|
||||
attrs="{'invisible': [('state', 'not in', ['done'])]}" />
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<odoo>
|
||||
<record id="stock_valuation_layer_form_inherited" model="ir.ui.view">
|
||||
<field name="name">stock.valuation.layer.form.inherited</field>
|
||||
<field name="model">stock.valuation.layer</field>
|
||||
<field name="inherit_id" ref="stock_account.stock_valuation_layer_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='stock_move_id']" position="after">
|
||||
<field name="stock_landed_cost_id" attrs="{'invisible': [('stock_landed_cost_id', '=', False)]}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user