[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:
Simon Lejeune
2019-06-10 20:43:00 +00:00
parent 1e5624ba68
commit 68d8e49f2f
11 changed files with 153 additions and 17 deletions
+5 -1
View File
@@ -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>