From 68d8e49f2fd03bdd42c9b50fde64f706af9d3c8b Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Wed, 22 May 2019 09:40:17 +0000 Subject: [PATCH] [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 --- addons/stock_account/models/product.py | 6 +- .../models/stock_valuation_layer.py | 2 + .../views/stock_valuation_layer_views.xml | 1 + addons/stock_landed_costs/__manifest__.py | 1 + addons/stock_landed_costs/models/__init__.py | 2 + .../models/stock_landed_cost.py | 41 +++++---- .../models/stock_valuation_layer.py | 13 +++ addons/stock_landed_costs/tests/__init__.py | 1 + .../tests/test_stockvaluationlayer.py | 84 +++++++++++++++++++ .../views/stock_landed_cost_views.xml | 6 ++ .../views/stock_valuation_layer_views.xml | 13 +++ 11 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 addons/stock_landed_costs/models/stock_valuation_layer.py create mode 100644 addons/stock_landed_costs/tests/test_stockvaluationlayer.py create mode 100644 addons/stock_landed_costs/views/stock_valuation_layer_views.xml diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 36f47692176..7d31503d62d 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -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, } diff --git a/addons/stock_account/models/stock_valuation_layer.py b/addons/stock_account/models/stock_valuation_layer.py index 4eb6f5c86a8..0382107380b 100644 --- a/addons/stock_account/models/stock_valuation_layer.py +++ b/addons/stock_account/models/stock_valuation_layer.py @@ -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) diff --git a/addons/stock_account/views/stock_valuation_layer_views.xml b/addons/stock_account/views/stock_valuation_layer_views.xml index bbcbc1af797..654877673db 100644 --- a/addons/stock_account/views/stock_valuation_layer_views.xml +++ b/addons/stock_account/views/stock_valuation_layer_views.xml @@ -28,6 +28,7 @@ + diff --git a/addons/stock_landed_costs/__manifest__.py b/addons/stock_landed_costs/__manifest__.py index 67e39d6810e..88e4de8dbd9 100644 --- a/addons/stock_landed_costs/__manifest__.py +++ b/addons/stock_landed_costs/__manifest__.py @@ -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, diff --git a/addons/stock_landed_costs/models/__init__.py b/addons/stock_landed_costs/models/__init__.py index 15a94628e98..8b81c037995 100644 --- a/addons/stock_landed_costs/models/__init__.py +++ b/addons/stock_landed_costs/models/__init__.py @@ -3,3 +3,5 @@ from . import product from . import stock_landed_cost +from . import stock_valuation_layer + diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index 1d513fdc746..fd524beaee0 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -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' diff --git a/addons/stock_landed_costs/models/stock_valuation_layer.py b/addons/stock_landed_costs/models/stock_valuation_layer.py new file mode 100644 index 00000000000..56e2e82bcce --- /dev/null +++ b/addons/stock_landed_costs/models/stock_valuation_layer.py @@ -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') + diff --git a/addons/stock_landed_costs/tests/__init__.py b/addons/stock_landed_costs/tests/__init__.py index 750868a9cbb..178d046e639 100644 --- a/addons/stock_landed_costs/tests/__init__.py +++ b/addons/stock_landed_costs/tests/__init__.py @@ -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 diff --git a/addons/stock_landed_costs/tests/test_stockvaluationlayer.py b/addons/stock_landed_costs/tests/test_stockvaluationlayer.py new file mode 100644 index 00000000000..d09c4611080 --- /dev/null +++ b/addons/stock_landed_costs/tests/test_stockvaluationlayer.py @@ -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) diff --git a/addons/stock_landed_costs/views/stock_landed_cost_views.xml b/addons/stock_landed_costs/views/stock_landed_cost_views.xml index cd5c9541228..a38175dd71c 100644 --- a/addons/stock_landed_costs/views/stock_landed_cost_views.xml +++ b/addons/stock_landed_costs/views/stock_landed_cost_views.xml @@ -13,6 +13,12 @@ +
+