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 @@ +
+