diff options
| author | Simon Lejeune <sle@openerp.com> | 2019-05-22 09:40:17 +0000 |
|---|---|---|
| committer | Simon Lejeune <sle@openerp.com> | 2019-06-10 20:43:00 +0000 |
| commit | 68d8e49f2fd03bdd42c9b50fde64f706af9d3c8b (patch) | |
| tree | 27a226f9205eaf09ab78db821c3688d0fef162bd | |
| parent | 1e5624ba680fa98dc17d3edd888b70405334ddca (diff) | |
[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
11 files changed, 153 insertions, 17 deletions
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 @@ <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> 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): + 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? + # 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 - - 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, + 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 @@ <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> diff --git a/addons/stock_landed_costs/views/stock_valuation_layer_views.xml b/addons/stock_landed_costs/views/stock_valuation_layer_views.xml new file mode 100644 index 00000000000..e7eea866c40 --- /dev/null +++ b/addons/stock_landed_costs/views/stock_valuation_layer_views.xml @@ -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> + |
