diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/stock_landed_costs/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/stock_landed_costs/tests')
6 files changed, 1273 insertions, 0 deletions
diff --git a/addons/stock_landed_costs/tests/__init__.py b/addons/stock_landed_costs/tests/__init__.py new file mode 100644 index 00000000..178d046e --- /dev/null +++ b/addons/stock_landed_costs/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +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/common.py b/addons/stock_landed_costs/tests/common.py new file mode 100644 index 00000000..3ab4984a --- /dev/null +++ b/addons/stock_landed_costs/tests/common.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon + + +class TestStockLandedCostsCommon(ValuationReconciliationTestCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + # Objects + cls.Product = cls.env['product.product'] + cls.Picking = cls.env['stock.picking'] + cls.Move = cls.env['stock.move'] + cls.LandedCost = cls.env['stock.landed.cost'] + cls.CostLine = cls.env['stock.landed.cost.lines'] + # References + cls.warehouse = cls.company_data['default_warehouse'] + cls.supplier_id = cls.env['res.partner'].create({'name': 'My Test Supplier'}).id + cls.customer_id = cls.env['res.partner'].create({'name': 'My Test Customer'}).id + cls.supplier_location_id = cls.env.ref('stock.stock_location_suppliers').id + cls.customer_location_id = cls.env.ref('stock.stock_location_customers').id + cls.categ_all = cls.stock_account_product_categ + cls.expenses_journal = cls.company_data['default_journal_purchase'] + cls.stock_journal = cls.env['account.journal'].create({ + 'name': 'Stock Journal', + 'code': 'STJTEST', + 'type': 'general', + }) + # Create product refrigerator & oven + cls.product_refrigerator = cls.Product.create({ + 'name': 'Refrigerator', + 'type': 'product', + 'standard_price': 1.0, + 'weight': 10, + 'volume': 1, + 'categ_id': cls.categ_all.id}) + cls.product_oven = cls.Product.create({ + 'name': 'Microwave Oven', + 'type': 'product', + 'standard_price': 1.0, + 'weight': 20, + 'volume': 1.5, + 'categ_id': cls.categ_all.id}) + # Create service type product 1.Labour 2.Brokerage 3.Transportation 4.Packaging + cls.landed_cost = cls.Product.create({'name': 'Landed Cost', 'type': 'service'}) + cls.brokerage_quantity = cls.Product.create({'name': 'Brokerage Cost', 'type': 'service'}) + cls.transportation_weight = cls.Product.create({'name': 'Transportation Cost', 'type': 'service'}) + cls.packaging_volume = cls.Product.create({'name': 'Packaging Cost', 'type': 'service'}) diff --git a/addons/stock_landed_costs/tests/test_stock_landed_costs.py b/addons/stock_landed_costs/tests/test_stock_landed_costs.py new file mode 100644 index 00000000..9e86cdf6 --- /dev/null +++ b/addons/stock_landed_costs/tests/test_stock_landed_costs.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.stock_landed_costs.tests.common import TestStockLandedCostsCommon +from odoo.exceptions import ValidationError +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestStockLandedCosts(TestStockLandedCostsCommon): + + def test_stock_landed_costs(self): + # In order to test the landed costs feature of stock, + # I create a landed cost, confirm it and check its account move created + + # I create 2 products with different volume and gross weight and configure + # them for real_time valuation and fifo costing method + product_landed_cost_1 = self.env['product.product'].create({ + 'name': "LC product 1", + 'weight': 10, + 'volume': 1, + 'categ_id': self.stock_account_product_categ.id, + 'type': 'product', + }) + + product_landed_cost_2 = self.env['product.product'].create({ + 'name': "LC product 2", + 'weight': 20, + 'volume': 1.5, + 'categ_id': self.stock_account_product_categ.id, + 'type': 'product', + }) + + self.assertEqual(product_landed_cost_1.value_svl, 0) + self.assertEqual(product_landed_cost_1.quantity_svl, 0) + self.assertEqual(product_landed_cost_2.value_svl, 0) + self.assertEqual(product_landed_cost_2.quantity_svl, 0) + + picking_default_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) + + # I create 2 picking moving those products + vals = dict(picking_default_vals, **{ + 'name': 'LC_pick_1', + 'picking_type_id': self.warehouse.out_type_id.id, + 'move_lines': [(0, 0, { + 'product_id': product_landed_cost_1.id, + 'product_uom_qty': 5, + 'product_uom': self.ref('uom.product_uom_unit'), + 'location_id': self.warehouse.lot_stock_id.id, + 'location_dest_id': self.ref('stock.stock_location_customers'), + })], + }) + picking_landed_cost_1 = self.env['stock.picking'].new(vals) + picking_landed_cost_1.onchange_picking_type() + picking_landed_cost_1.move_lines.onchange_product_id() + picking_landed_cost_1.move_lines.name = 'move 1' + vals = picking_landed_cost_1._convert_to_write(picking_landed_cost_1._cache) + picking_landed_cost_1 = self.env['stock.picking'].create(vals) + + # Confirm and assign picking + self.env.company.anglo_saxon_accounting = True + picking_landed_cost_1.action_confirm() + picking_landed_cost_1.action_assign() + picking_landed_cost_1.move_lines.quantity_done = 5 + picking_landed_cost_1.button_validate() + + vals = dict(picking_default_vals, **{ + 'name': 'LC_pick_2', + 'picking_type_id': self.warehouse.out_type_id.id, + 'move_lines': [(0, 0, { + 'product_id': product_landed_cost_2.id, + 'product_uom_qty': 10, + 'product_uom': self.ref('uom.product_uom_unit'), + 'location_id': self.warehouse.lot_stock_id.id, + 'location_dest_id': self.ref('stock.stock_location_customers'), + })], + }) + picking_landed_cost_2 = self.env['stock.picking'].new(vals) + picking_landed_cost_2.onchange_picking_type() + picking_landed_cost_2.move_lines.onchange_product_id() + picking_landed_cost_2.move_lines.name = 'move 2' + vals = picking_landed_cost_2._convert_to_write(picking_landed_cost_2._cache) + picking_landed_cost_2 = self.env['stock.picking'].create(vals) + + # Confirm and assign picking + picking_landed_cost_2.action_confirm() + picking_landed_cost_2.action_assign() + picking_landed_cost_2.move_lines.quantity_done = 10 + picking_landed_cost_2.button_validate() + + self.assertEqual(product_landed_cost_1.value_svl, 0) + self.assertEqual(product_landed_cost_1.quantity_svl, -5) + self.assertEqual(product_landed_cost_2.value_svl, 0) + self.assertEqual(product_landed_cost_2.quantity_svl, -10) + + # I create a landed cost for those 2 pickings + default_vals = self.env['stock.landed.cost'].default_get(list(self.env['stock.landed.cost'].fields_get())) + virtual_home_staging = self.env['product.product'].create({ + 'name': 'Virtual Home Staging', + 'categ_id': self.stock_account_product_categ.id, + }) + default_vals.update({ + 'picking_ids': [picking_landed_cost_1.id, picking_landed_cost_2.id], + 'account_journal_id': self.expenses_journal, + 'cost_lines': [ + (0, 0, {'product_id': virtual_home_staging.id}), + (0, 0, {'product_id': virtual_home_staging.id}), + (0, 0, {'product_id': virtual_home_staging.id}), + (0, 0, {'product_id': virtual_home_staging.id})], + 'valuation_adjustment_lines': [], + }) + cost_lines_values = { + 'name': ['equal split', 'split by quantity', 'split by weight', 'split by volume'], + 'split_method': ['equal', 'by_quantity', 'by_weight', 'by_volume'], + 'price_unit': [10, 150, 250, 20], + } + stock_landed_cost_1 = self.env['stock.landed.cost'].new(default_vals) + for index, cost_line in enumerate(stock_landed_cost_1.cost_lines): + cost_line.onchange_product_id() + cost_line.name = cost_lines_values['name'][index] + cost_line.split_method = cost_lines_values['split_method'][index] + cost_line.price_unit = cost_lines_values['price_unit'][index] + vals = stock_landed_cost_1._convert_to_write(stock_landed_cost_1._cache) + stock_landed_cost_1 = self.env['stock.landed.cost'].create(vals) + + # I compute the landed cost using Compute button + stock_landed_cost_1.compute_landed_cost() + + # I check the valuation adjustment lines + for valuation in stock_landed_cost_1.valuation_adjustment_lines: + if valuation.cost_line_id.name == 'equal split': + self.assertEqual(valuation.additional_landed_cost, 5) + elif valuation.cost_line_id.name == 'split by quantity' and valuation.move_id.name == "move 1": + self.assertEqual(valuation.additional_landed_cost, 50) + elif valuation.cost_line_id.name == 'split by quantity' and valuation.move_id.name == "move 2": + self.assertEqual(valuation.additional_landed_cost, 100) + elif valuation.cost_line_id.name == 'split by weight' and valuation.move_id.name == "move 1": + self.assertEqual(valuation.additional_landed_cost, 50) + elif valuation.cost_line_id.name == 'split by weight' and valuation.move_id.name == "move 2": + self.assertEqual(valuation.additional_landed_cost, 200) + elif valuation.cost_line_id.name == 'split by volume' and valuation.move_id.name == "move 1": + self.assertEqual(valuation.additional_landed_cost, 5) + elif valuation.cost_line_id.name == 'split by volume' and valuation.move_id.name == "move 2": + self.assertEqual(valuation.additional_landed_cost, 15) + else: + raise ValidationError('unrecognized valuation adjustment line') + + # I confirm the landed cost + stock_landed_cost_1.button_validate() + + # I check that the landed cost is now "Closed" and that it has an accounting entry + self.assertEqual(stock_landed_cost_1.state, "done") + self.assertTrue(stock_landed_cost_1.account_move_id) + self.assertEqual(len(stock_landed_cost_1.account_move_id.line_ids), 48) + + lc_value = sum(stock_landed_cost_1.account_move_id.line_ids.filtered(lambda aml: aml.account_id.name.startswith('Expenses')).mapped('debit')) + product_value = abs(product_landed_cost_1.value_svl) + abs(product_landed_cost_2.value_svl) + self.assertEqual(lc_value, product_value) + + self.assertEqual(len(picking_landed_cost_1.move_lines.stock_valuation_layer_ids), 5) + self.assertEqual(len(picking_landed_cost_2.move_lines.stock_valuation_layer_ids), 5) diff --git a/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py b/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py new file mode 100644 index 00000000..f18ec214 --- /dev/null +++ b/addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import unittest +from odoo.addons.stock_landed_costs.tests.common import TestStockLandedCostsCommon +from odoo.addons.stock_landed_costs.tests.test_stockvaluationlayer import TestStockValuationLCCommon +from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data + +from odoo.tests import tagged, Form + + +@tagged('post_install', '-at_install') +class TestLandedCosts(TestStockLandedCostsCommon): + + def setUp(self): + super(TestLandedCosts, self).setUp() + # Create picking incoming shipment + self.picking_in = self.Picking.create({ + 'partner_id': self.supplier_id, + 'picking_type_id': self.warehouse.in_type_id.id, + 'location_id': self.supplier_location_id, + 'location_dest_id': self.warehouse.lot_stock_id.id}) + self.Move.create({ + 'name': self.product_refrigerator.name, + 'product_id': self.product_refrigerator.id, + 'product_uom_qty': 5, + 'product_uom': self.product_refrigerator.uom_id.id, + 'picking_id': self.picking_in.id, + 'location_id': self.supplier_location_id, + 'location_dest_id': self.warehouse.lot_stock_id.id}) + self.Move.create({ + 'name': self.product_oven.name, + 'product_id': self.product_oven.id, + 'product_uom_qty': 10, + 'product_uom': self.product_oven.uom_id.id, + 'picking_id': self.picking_in.id, + 'location_id': self.supplier_location_id, + 'location_dest_id': self.warehouse.lot_stock_id.id}) + # Create picking outgoing shipment + self.picking_out = self.Picking.create({ + 'partner_id': self.customer_id, + 'picking_type_id': self.warehouse.out_type_id.id, + 'location_id': self.warehouse.lot_stock_id.id, + 'location_dest_id': self.customer_location_id}) + self.Move.create({ + 'name': self.product_refrigerator.name, + 'product_id': self.product_refrigerator.id, + 'product_uom_qty': 2, + 'product_uom': self.product_refrigerator.uom_id.id, + 'picking_id': self.picking_out.id, + 'location_id': self.warehouse.lot_stock_id.id, + 'location_dest_id': self.customer_location_id}) + + def test_00_landed_costs_on_incoming_shipment(self): + """ Test landed cost on incoming shipment """ + # + # (A) Purchase product + + # Services Quantity Weight Volume + # ----------------------------------------------------- + # 1. Refrigerator 5 10 1 + # 2. Oven 10 20 1.5 + + # (B) Add some costs on purchase + + # Services Amount Split Method + # ------------------------------------------- + # 1.labour 10 By Equal + # 2.brokerage 150 By Quantity + # 3.transportation 250 By Weight + # 4.packaging 20 By Volume + + # Process incoming shipment + income_ship = self._process_incoming_shipment() + # Create landed costs + stock_landed_cost = self._create_landed_costs({ + 'equal_price_unit': 10, + 'quantity_price_unit': 150, + 'weight_price_unit': 250, + 'volume_price_unit': 20}, income_ship) + # Compute landed costs + stock_landed_cost.compute_landed_cost() + + valid_vals = { + 'equal': 5.0, + 'by_quantity_refrigerator': 50.0, + 'by_quantity_oven': 100.0, + 'by_weight_refrigerator': 50.0, + 'by_weight_oven': 200, + 'by_volume_refrigerator': 5.0, + 'by_volume_oven': 15.0} + + # Check valuation adjustment line recognized or not + self._validate_additional_landed_cost_lines(stock_landed_cost, valid_vals) + # Validate the landed cost. + stock_landed_cost.button_validate() + self.assertTrue(stock_landed_cost.account_move_id, 'Landed costs should be available account move lines') + account_entry = self.env['account.move.line'].read_group( + [('move_id', '=', stock_landed_cost.account_move_id.id)], ['debit', 'credit', 'move_id'], ['move_id'])[0] + self.assertEqual(account_entry['debit'], account_entry['credit'], 'Debit and credit are not equal') + self.assertEqual(account_entry['debit'], 430.0, 'Wrong Account Entry') + + def test_01_negative_landed_costs_on_incoming_shipment(self): + """ Test negative landed cost on incoming shipment """ + # + # (A) Purchase Product + + # Services Quantity Weight Volume + # ----------------------------------------------------- + # 1. Refrigerator 5 10 1 + # 2. Oven 10 20 1.5 + + # (B) Sale refrigerator's part of the quantity + + # (C) Add some costs on purchase + + # Services Amount Split Method + # ------------------------------------------- + # 1.labour 10 By Equal + # 2.brokerage 150 By Quantity + # 3.transportation 250 By Weight + # 4.packaging 20 By Volume + + # (D) Decrease cost that already added on purchase + # (apply negative entry) + + # Services Amount Split Method + # ------------------------------------------- + # 1.labour -5 By Equal + # 2.brokerage -50 By Quantity + # 3.transportation -50 By Weight + # 4.packaging -5 By Volume + + # Process incoming shipment + income_ship = self._process_incoming_shipment() + # Refrigerator outgoing shipment. + self._process_outgoing_shipment() + # Apply landed cost for incoming shipment. + stock_landed_cost = self._create_landed_costs({ + 'equal_price_unit': 10, + 'quantity_price_unit': 150, + 'weight_price_unit': 250, + 'volume_price_unit': 20}, income_ship) + # Compute landed costs + stock_landed_cost.compute_landed_cost() + valid_vals = { + 'equal': 5.0, + 'by_quantity_refrigerator': 50.0, + 'by_quantity_oven': 100.0, + 'by_weight_refrigerator': 50.0, + 'by_weight_oven': 200.0, + 'by_volume_refrigerator': 5.0, + 'by_volume_oven': 15.0} + # Check valuation adjustment line recognized or not + self._validate_additional_landed_cost_lines(stock_landed_cost, valid_vals) + # Validate the landed cost. + stock_landed_cost.button_validate() + self.assertTrue(stock_landed_cost.account_move_id, 'Landed costs should be available account move lines') + # Create negative landed cost for previously incoming shipment. + stock_negative_landed_cost = self._create_landed_costs({ + 'equal_price_unit': -5, + 'quantity_price_unit': -50, + 'weight_price_unit': -50, + 'volume_price_unit': -5}, income_ship) + # Compute negative landed costs + stock_negative_landed_cost.compute_landed_cost() + valid_vals = { + 'equal': -2.5, + 'by_quantity_refrigerator': -16.67, + 'by_quantity_oven': -33.33, + 'by_weight_refrigerator': -10.00, + 'by_weight_oven': -40.00, + 'by_volume_refrigerator': -1.25, + 'by_volume_oven': -3.75} + # Check valuation adjustment line recognized or not + self._validate_additional_landed_cost_lines(stock_negative_landed_cost, valid_vals) + # Validate the landed cost. + stock_negative_landed_cost.button_validate() + self.assertEqual(stock_negative_landed_cost.state, 'done', 'Negative landed costs should be in done state') + self.assertTrue(stock_negative_landed_cost.account_move_id, 'Landed costs should be available account move lines') + account_entry = self.env['account.move.line'].read_group( + [('move_id', '=', stock_negative_landed_cost.account_move_id.id)], ['debit', 'credit', 'move_id'], ['move_id'])[0] + self.assertEqual(account_entry['debit'], account_entry['credit'], 'Debit and credit are not equal') + move_lines = [ + {'name': 'split by volume - Microwave Oven', 'debit': 3.75, 'credit': 0.0}, + {'name': 'split by volume - Microwave Oven', 'debit': 0.0, 'credit': 3.75}, + {'name': 'split by weight - Microwave Oven', 'debit': 40.0, 'credit': 0.0}, + {'name': 'split by weight - Microwave Oven', 'debit': 0.0, 'credit': 40.0}, + {'name': 'split by quantity - Microwave Oven', 'debit': 33.33, 'credit': 0.0}, + {'name': 'split by quantity - Microwave Oven', 'debit': 0.0, 'credit': 33.33}, + {'name': 'equal split - Microwave Oven', 'debit': 2.5, 'credit': 0.0}, + {'name': 'equal split - Microwave Oven', 'debit': 0.0, 'credit': 2.5}, + {'name': 'split by volume - Refrigerator: 2.0 already out', 'debit': 0.5, 'credit': 0.0}, + {'name': 'split by volume - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 0.5}, + {'name': 'split by weight - Refrigerator: 2.0 already out', 'debit': 4.0, 'credit': 0.0}, + {'name': 'split by weight - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 4.0}, + {'name': 'split by weight - Refrigerator', 'debit': 0.0, 'credit': 10.0}, + {'name': 'split by weight - Refrigerator', 'debit': 10.0, 'credit': 0.0}, + {'name': 'split by volume - Refrigerator', 'debit': 0.0, 'credit': 1.25}, + {'name': 'split by volume - Refrigerator', 'debit': 1.25, 'credit': 0.0}, + {'name': 'split by quantity - Refrigerator: 2.0 already out', 'debit': 6.67, 'credit': 0.0}, + {'name': 'split by quantity - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 6.67}, + {'name': 'split by quantity - Refrigerator', 'debit': 16.67, 'credit': 0.0}, + {'name': 'split by quantity - Refrigerator', 'debit': 0.0, 'credit': 16.67}, + {'name': 'equal split - Refrigerator: 2.0 already out', 'debit': 1.0, 'credit': 0.0}, + {'name': 'equal split - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 1.0}, + {'name': 'equal split - Refrigerator', 'debit': 2.5, 'credit': 0.0}, + {'name': 'equal split - Refrigerator', 'debit': 0.0, 'credit': 2.5} + ] + if stock_negative_landed_cost.account_move_id.company_id.anglo_saxon_accounting: + move_lines += [ + {'name': 'split by volume - Refrigerator: 2.0 already out', 'debit': 0.5, 'credit': 0.0}, + {'name': 'split by volume - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 0.5}, + {'name': 'split by weight - Refrigerator: 2.0 already out', 'debit': 4.0, 'credit': 0.0}, + {'name': 'split by weight - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 4.0}, + {'name': 'split by quantity - Refrigerator: 2.0 already out', 'debit': 6.67, 'credit': 0.0}, + {'name': 'split by quantity - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 6.67}, + {'name': 'equal split - Refrigerator: 2.0 already out', 'debit': 1.0, 'credit': 0.0}, + {'name': 'equal split - Refrigerator: 2.0 already out', 'debit': 0.0, 'credit': 1.0}, + ] + self.assertRecordValues( + sorted(stock_negative_landed_cost.account_move_id.line_ids, key=lambda d: (d['name'], d['debit'])), + sorted(move_lines, key=lambda d: (d['name'], d['debit'])), + ) + + def _process_incoming_shipment(self): + """ Two product incoming shipment. """ + # Confirm incoming shipment. + self.picking_in.action_confirm() + # Transfer incoming shipment + res_dict = self.picking_in.button_validate() + wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict.get('context'))).save() + wizard.process() + return self.picking_in + + def _process_outgoing_shipment(self): + """ One product Outgoing shipment. """ + # Confirm outgoing shipment. + self.picking_out.action_confirm() + # Product assign to outgoing shipments + self.picking_out.action_assign() + # Transfer picking. + + res_dict = self.picking_out.button_validate() + wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save() + wizard.process() + + def _create_landed_costs(self, value, picking_in): + return self.LandedCost.create(dict( + picking_ids=[(6, 0, [picking_in.id])], + account_journal_id=self.expenses_journal.id, + cost_lines=[ + (0, 0, { + 'name': 'equal split', + 'split_method': 'equal', + 'price_unit': value['equal_price_unit'], + 'product_id': self.landed_cost.id}), + (0, 0, { + 'name': 'split by quantity', + 'split_method': 'by_quantity', + 'price_unit': value['quantity_price_unit'], + 'product_id': self.brokerage_quantity.id}), + (0, 0, { + 'name': 'split by weight', + 'split_method': 'by_weight', + 'price_unit': value['weight_price_unit'], + 'product_id': self.transportation_weight.id}), + (0, 0, { + 'name': 'split by volume', + 'split_method': 'by_volume', + 'price_unit': value['volume_price_unit'], + 'product_id': self.packaging_volume.id}) + ], + )) + + def _validate_additional_landed_cost_lines(self, stock_landed_cost, valid_vals): + for valuation in stock_landed_cost.valuation_adjustment_lines: + add_cost = valuation.additional_landed_cost + split_method = valuation.cost_line_id.split_method + product = valuation.move_id.product_id + if split_method == 'equal': + self.assertEqual(add_cost, valid_vals['equal'], self._error_message(valid_vals['equal'], add_cost)) + elif split_method == 'by_quantity' and product == self.product_refrigerator: + self.assertEqual(add_cost, valid_vals['by_quantity_refrigerator'], self._error_message(valid_vals['by_quantity_refrigerator'], add_cost)) + elif split_method == 'by_quantity' and product == self.product_oven: + self.assertEqual(add_cost, valid_vals['by_quantity_oven'], self._error_message(valid_vals['by_quantity_oven'], add_cost)) + elif split_method == 'by_weight' and product == self.product_refrigerator: + self.assertEqual(add_cost, valid_vals['by_weight_refrigerator'], self._error_message(valid_vals['by_weight_refrigerator'], add_cost)) + elif split_method == 'by_weight' and product == self.product_oven: + self.assertEqual(add_cost, valid_vals['by_weight_oven'], self._error_message(valid_vals['by_weight_oven'], add_cost)) + elif split_method == 'by_volume' and product == self.product_refrigerator: + self.assertEqual(add_cost, valid_vals['by_volume_refrigerator'], self._error_message(valid_vals['by_volume_refrigerator'], add_cost)) + elif split_method == 'by_volume' and product == self.product_oven: + self.assertEqual(add_cost, valid_vals['by_volume_oven'], self._error_message(valid_vals['by_volume_oven'], add_cost)) + + def _error_message(self, actucal_cost, computed_cost): + return 'Additional Landed Cost should be %s instead of %s' % (actucal_cost, computed_cost) + + +@tagged('post_install', '-at_install') +class TestLandedCostsWithPurchaseAndInv(TestStockValuationLCCommon): + def test_invoice_after_lc(self): + self.env.company.anglo_saxon_accounting = True + self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo' + self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time' + self.price_diff_account = self.env['account.account'].create({ + 'name': 'price diff account', + 'code': 'price diff account', + 'user_type_id': self.env.ref('account.data_account_type_current_assets').id, + }) + self.product1.property_account_creditor_price_difference = self.price_diff_account + + # Create PO + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.env['res.partner'].create({'name': 'vendor'}) + with po_form.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 1 + po_line.price_unit = 455.0 + order = po_form.save() + order.button_confirm() + + # Receive the goods + receipt = order.picking_ids[0] + receipt.move_lines.quantity_done = 1 + receipt.button_validate() + + # Check SVL and AML + svl = self.env['stock.valuation.layer'].search([('stock_move_id', '=', receipt.move_lines.id)]) + self.assertAlmostEqual(svl.value, 455) + aml = self.env['account.move.line'].search([('account_id', '=', self.company_data['default_account_stock_valuation'].id)]) + self.assertAlmostEqual(aml.debit, 455) + + # Create and validate LC + lc = self.env['stock.landed.cost'].create(dict( + picking_ids=[(6, 0, [receipt.id])], + account_journal_id=self.stock_journal.id, + cost_lines=[ + (0, 0, { + 'name': 'equal split', + 'split_method': 'equal', + 'price_unit': 99, + 'product_id': self.productlc1.id, + }), + ], + )) + lc.compute_landed_cost() + lc.button_validate() + + # Check LC, SVL and AML + self.assertAlmostEqual(lc.valuation_adjustment_lines.final_cost, 554) + svl = self.env['stock.valuation.layer'].search([('stock_move_id', '=', receipt.move_lines.id)], order='id desc', limit=1) + self.assertAlmostEqual(svl.value, 99) + aml = self.env['account.move.line'].search([('account_id', '=', self.company_data['default_account_stock_valuation'].id)], order='id desc', limit=1) + self.assertAlmostEqual(aml.debit, 99) + + # Create an invoice with the same price + move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) + move_form.invoice_date = move_form.date + move_form.partner_id = order.partner_id + move_form.purchase_id = order + move = move_form.save() + move.action_post() + + # Check nothing was posted in the price difference account + price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id), ('move_id', '=', move.id)]) + self.assertEqual(len(price_diff_aml), 0, "No line should have been generated in the price difference account.") diff --git a/addons/stock_landed_costs/tests/test_stock_landed_costs_rounding.py b/addons/stock_landed_costs/tests/test_stock_landed_costs_rounding.py new file mode 100644 index 00000000..392d3739 --- /dev/null +++ b/addons/stock_landed_costs/tests/test_stock_landed_costs_rounding.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.stock_landed_costs.tests.common import TestStockLandedCostsCommon +from odoo.tests import tagged + + +@tagged('post_install', '-at_install') +class TestStockLandedCostsRounding(TestStockLandedCostsCommon): + + def test_stock_landed_costs_rounding(self): + # In order to test the rounding in landed costs feature of stock, I create 2 landed cost + + # Define undivisible units + product_uom_unit_round_1 = self.env.ref('uom.product_uom_unit') + product_uom_unit_round_1.write({ + 'name': 'Undivisible Units', + 'rounding': 1.0, + }) + + # I create 2 products with different cost prices and configure them for real_time + # valuation and real price costing method + product_landed_cost_3 = self.env['product.product'].create({ + 'name': "LC product 3", + 'uom_id': product_uom_unit_round_1.id, + }) + product_landed_cost_3.product_tmpl_id.categ_id.property_cost_method = 'fifo' + product_landed_cost_3.product_tmpl_id.categ_id.property_stock_account_input_categ_id = self.company_data['default_account_expense'] + product_landed_cost_3.product_tmpl_id.categ_id.property_stock_account_output_categ_id = self.company_data['default_account_revenue'] + + product_landed_cost_4 = self.env['product.product'].create({ + 'name': "LC product 4", + 'uom_id': product_uom_unit_round_1.id, + }) + product_landed_cost_4.product_tmpl_id.categ_id.property_cost_method = 'fifo' + product_landed_cost_4.product_tmpl_id.categ_id.property_valuation = 'real_time' + product_landed_cost_4.product_tmpl_id.categ_id.property_stock_account_input_categ_id = self.company_data['default_account_expense'] + product_landed_cost_4.product_tmpl_id.categ_id.property_stock_account_output_categ_id = self.company_data['default_account_revenue'] + + picking_default_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get())) + + # I create 2 pickings moving those products + vals = dict(picking_default_vals, **{ + 'name': 'LC_pick_3', + 'picking_type_id': self.warehouse.in_type_id.id, + 'move_lines': [(0, 0, { + 'product_id': product_landed_cost_3.id, + 'product_uom_qty': 13, + 'product_uom': product_uom_unit_round_1.id, + 'location_id': self.ref('stock.stock_location_customers'), + 'location_dest_id': self.warehouse.lot_stock_id.id, + })], + }) + picking_landed_cost_3 = self.env['stock.picking'].new(vals) + picking_landed_cost_3.onchange_picking_type() + picking_landed_cost_3.move_lines.onchange_product_id() + picking_landed_cost_3.move_lines.name = 'move 3' + vals = picking_landed_cost_3._convert_to_write(picking_landed_cost_3._cache) + picking_landed_cost_3 = self.env['stock.picking'].create(vals) + + vals = dict(picking_default_vals, **{ + 'name': 'LC_pick_4', + 'picking_type_id': self.warehouse.in_type_id.id, + 'move_lines': [(0, 0, { + 'product_id': product_landed_cost_4.id, + 'product_uom_qty': 1, + 'product_uom': self.ref('uom.product_uom_dozen'), + 'location_id': self.ref('stock.stock_location_customers'), + 'location_dest_id': self.warehouse.lot_stock_id.id, + 'price_unit': 17.00 / 12.00, + })], + }) + picking_landed_cost_4 = self.env['stock.picking'].new(vals) + picking_landed_cost_4.onchange_picking_type() + picking_landed_cost_4.move_lines.onchange_product_id() + picking_landed_cost_4.move_lines.name = 'move 4' + vals = picking_landed_cost_4._convert_to_write(picking_landed_cost_4._cache) + picking_landed_cost_4 = self.env['stock.picking'].create(vals) + + # We perform all the tests for LC_pick_3 + # I receive picking LC_pick_3, and check how many quants are created + picking_landed_cost_3.move_lines.price_unit = 1.0 + picking_landed_cost_3.action_confirm() + picking_landed_cost_3.action_assign() + picking_landed_cost_3._action_done() + + virtual_interior_design = self.env['product.product'].create({'name': 'Virtual Interior Design'}) + + # I create a landed cost for picking 3 + default_vals = self.env['stock.landed.cost'].default_get(list(self.env['stock.landed.cost'].fields_get())) + default_vals.update({ + 'picking_ids': [picking_landed_cost_3.id], + 'account_journal_id': self.expenses_journal, + 'cost_lines': [(0, 0, {'product_id': virtual_interior_design.id})], + 'valuation_adjustment_lines': [], + }) + stock_landed_cost_2 = self.env['stock.landed.cost'].new(default_vals) + stock_landed_cost_2.cost_lines.onchange_product_id() + stock_landed_cost_2.cost_lines.name = 'equal split' + stock_landed_cost_2.cost_lines.split_method = 'equal' + stock_landed_cost_2.cost_lines.price_unit = 15 + vals = stock_landed_cost_2._convert_to_write(stock_landed_cost_2._cache) + stock_landed_cost_2 = self.env['stock.landed.cost'].create(vals) + + # I compute the landed cost using Compute button + stock_landed_cost_2.compute_landed_cost() + + # I check the valuation adjustment lines + for valuation in stock_landed_cost_2.valuation_adjustment_lines: + self.assertEqual(valuation.additional_landed_cost, 15) + + # I confirm the landed cost + stock_landed_cost_2.button_validate() + + # I check that the landed cost is now "Closed" and that it has an accounting entry + self.assertEqual(stock_landed_cost_2.state, 'done') + self.assertTrue(stock_landed_cost_2.account_move_id) + + # We perform all the tests for LC_pick_4 + # I receive picking LC_pick_4, and check how many quants are created + picking_landed_cost_4.move_lines.price_unit = 17.0/12.0 + picking_landed_cost_4.action_confirm() + picking_landed_cost_4.action_assign() + picking_landed_cost_4._action_done() + + # I create a landed cost for picking 4 + default_vals = self.env['stock.landed.cost'].default_get(list(self.env['stock.landed.cost'].fields_get())) + default_vals.update({ + 'picking_ids': [picking_landed_cost_4.id], + 'account_journal_id': self.expenses_journal, + 'cost_lines': [(0, 0, {'product_id': virtual_interior_design.id})], + 'valuation_adjustment_lines': [], + }) + stock_landed_cost_3 = self.env['stock.landed.cost'].new(default_vals) + stock_landed_cost_3.cost_lines.onchange_product_id() + stock_landed_cost_3.cost_lines.name = 'equal split' + stock_landed_cost_3.cost_lines.split_method = 'equal' + stock_landed_cost_3.cost_lines.price_unit = 11 + vals = stock_landed_cost_3._convert_to_write(stock_landed_cost_3._cache) + stock_landed_cost_3 = self.env['stock.landed.cost'].create(vals) + + # I compute the landed cost using Compute button + stock_landed_cost_3.compute_landed_cost() + + # I check the valuation adjustment lines + for valuation in stock_landed_cost_3.valuation_adjustment_lines: + self.assertEqual(valuation.additional_landed_cost, 11) + + # I confirm the landed cost + stock_landed_cost_3.button_validate() + + # I check that the landed cost is now "Closed" and that it has an accounting entry + self.assertEqual(stock_landed_cost_3.state, 'done') + self.assertTrue(stock_landed_cost_3.account_move_id) 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 00000000..30ea438f --- /dev/null +++ b/addons/stock_landed_costs/tests/test_stockvaluationlayer.py @@ -0,0 +1,536 @@ +# -*- 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_landed_costs.tests.common import TestStockLandedCostsCommon + + +class TestStockValuationLCCommon(TestStockLandedCostsCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.product1 = cls.env['product.product'].create({ + 'name': 'product1', + 'type': 'product', + 'categ_id': cls.stock_account_product_categ.id, + }) + cls.productlc1 = cls.env['product.product'].create({ + 'name': 'product1', + 'type': 'service', + 'categ_id': cls.stock_account_product_categ.id, + }) + + def setUp(self): + super().setUp() + self.days = 0 + + def _get_stock_input_move_lines(self): + return self.env['account.move.line'].search([ + ('account_id', '=', self.company_data['default_account_stock_in'].id), + ], order='id') + + def _get_stock_output_move_lines(self): + return self.env['account.move.line'].search([ + ('account_id', '=', self.company_data['default_account_stock_out'].id), + ], order='id') + + def _get_stock_valuation_move_lines(self): + return self.env['account.move.line'].search([ + ('account_id', '=', self.company_data['default_account_stock_valuation'].id), + ], order='id') + + def _get_payable_move_lines(self): + return self.env['account.move.line'].search([ + ('account_id', '=', self.company_data['default_account_payable'].id), + ], order='id') + + def _get_expense_move_lines(self): + return self.env['account.move.line'].search([ + ('account_id', '=', self.company_data['default_account_expense'].id), + ], order='id') + + def _make_lc(self, move, amount): + picking = move.picking_id + lc = Form(self.env['stock.landed.cost']) + lc.account_journal_id = self.stock_journal + 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 + + def _make_in_move(self, product, quantity, unit_cost=None, create_picking=False): + """ Helper to create and validate a receipt move. + """ + unit_cost = unit_cost or product.standard_price + in_move = self.env['stock.move'].create({ + 'name': 'in %s units @ %s per unit' % (str(quantity), str(unit_cost)), + 'product_id': product.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id, + 'product_uom': self.env.ref('uom.product_uom_unit').id, + 'product_uom_qty': quantity, + 'price_unit': unit_cost, + 'picking_type_id': self.company_data['default_warehouse'].in_type_id.id, + }) + + if create_picking: + picking = self.env['stock.picking'].create({ + 'picking_type_id': in_move.picking_type_id.id, + 'location_id': in_move.location_id.id, + 'location_dest_id': in_move.location_dest_id.id, + }) + in_move.write({'picking_id': picking.id}) + + in_move._action_confirm() + in_move._action_assign() + in_move.move_line_ids.qty_done = quantity + in_move._action_done() + + self.days += 1 + return in_move.with_context(svl=True) + + def _make_out_move(self, product, quantity, force_assign=None, create_picking=False): + """ Helper to create and validate a delivery move. + """ + out_move = self.env['stock.move'].create({ + 'name': 'out %s units' % str(quantity), + 'product_id': product.id, + 'location_id': self.company_data['default_warehouse'].lot_stock_id.id, + 'location_dest_id': self.env.ref('stock.stock_location_customers').id, + 'product_uom': self.env.ref('uom.product_uom_unit').id, + 'product_uom_qty': quantity, + 'picking_type_id': self.company_data['default_warehouse'].out_type_id.id, + }) + + if create_picking: + picking = self.env['stock.picking'].create({ + 'picking_type_id': out_move.picking_type_id.id, + 'location_id': out_move.location_id.id, + 'location_dest_id': out_move.location_dest_id.id, + }) + out_move.write({'picking_id': picking.id}) + + out_move._action_confirm() + out_move._action_assign() + if force_assign: + self.env['stock.move.line'].create({ + 'move_id': out_move.id, + 'product_id': out_move.product_id.id, + 'product_uom_id': out_move.product_uom.id, + 'location_id': out_move.location_id.id, + 'location_dest_id': out_move.location_dest_id.id, + }) + out_move.move_line_ids.qty_done = quantity + out_move._action_done() + + self.days += 1 + return out_move.with_context(svl=True) + + +@tagged('-at_install', 'post_install') +class TestStockValuationLCFIFO(TestStockValuationLCCommon): + 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) + self.assertEqual(self.product1.standard_price, 20) + + 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) + + def test_fifo_to_standard_1(self): + move1 = self._make_in_move(self.product1, 10, unit_cost=10) + move2 = self._make_in_move(self.product1, 10, unit_cost=15) + move3 = self._make_out_move(self.product1, 5) + lc = self._make_lc(move1, 100) + self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard' + + out_svl = self.product1.stock_valuation_layer_ids.sorted()[-2] + in_svl = self.product1.stock_valuation_layer_ids.sorted()[-1] + + self.assertEqual(out_svl.value, -250) + self.assertEqual(in_svl.value, 225) + + def test_rounding_1(self): + """3@100, out 1, out 1, out 1""" + move1 = self._make_in_move(self.product1, 3, unit_cost=20, create_picking=True) + lc = self._make_lc(move1, 40) + move2 = self._make_out_move(self.product1, 1) + move3 = self._make_out_move(self.product1, 1) + move4 = self._make_out_move(self.product1, 1) + + self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), [60.0, 40.0, -33.33, -33.34, -33.33]) + self.assertEqual(self.product1.value_svl, 0) + self.assertEqual(self.product1.quantity_svl, 0) + + def test_rounding_2(self): + """3@98, out 1, out 1, out 1""" + move1 = self._make_in_move(self.product1, 3, unit_cost=20, create_picking=True) + lc = self._make_lc(move1, 38) + move2 = self._make_out_move(self.product1, 1) + move3 = self._make_out_move(self.product1, 1) + move4 = self._make_out_move(self.product1, 1) + + self.assertEqual(move2.stock_valuation_layer_ids.value, -32.67) + self.assertEqual(move3.stock_valuation_layer_ids.value, -32.67) + self.assertAlmostEqual(move4.stock_valuation_layer_ids.value, -32.66, delta=0.01) # self.env.company.currency_id.round(-32.66) -> -32.660000000000004 + self.assertEqual(self.product1.value_svl, 0) + self.assertEqual(self.product1.quantity_svl, 0) + + def test_rounding_3(self): + """3@4.85, out 1, out 1, out 1""" + move1 = self._make_in_move(self.product1, 3, unit_cost=1, create_picking=True) + lc = self._make_lc(move1, 1.85) + move2 = self._make_out_move(self.product1, 1) + move3 = self._make_out_move(self.product1, 1) + move4 = self._make_out_move(self.product1, 1) + + self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), [3.0, 1.85, -1.62, -1.62, -1.61]) + self.assertEqual(self.product1.value_svl, 0) + self.assertEqual(self.product1.quantity_svl, 0) + + def test_in_and_out_1(self): + move1 = self._make_in_move(self.product1, 10, unit_cost=100, create_picking=True) + self.assertEqual(move1.stock_valuation_layer_ids[0].remaining_value, 1000) + lc1 = self._make_lc(move1, 100) + self.assertEqual(move1.stock_valuation_layer_ids[0].remaining_value, 1100) + lc2 = self._make_lc(move1, 50) + self.assertEqual(move1.stock_valuation_layer_ids[0].remaining_value, 1150) + self.assertEqual(self.product1.value_svl, 1150) + self.assertEqual(self.product1.quantity_svl, 10) + move2 = self._make_out_move(self.product1, 1) + self.assertEqual(move2.stock_valuation_layer_ids.value, -115) + + +@tagged('-at_install', 'post_install') +class TestStockValuationLCAVCO(TestStockValuationLCCommon): + def setUp(self): + super(TestStockValuationLCAVCO, self).setUp() + self.product1.product_tmpl_id.categ_id.property_cost_method = 'average' + 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) + + 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(len(self.product1.stock_valuation_layer_ids), 2) + 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, 375) + self.assertEqual(self.product1.quantity_svl, 19) + + +@tagged('-at_install', 'post_install') +class TestStockValuationLCFIFOVB(TestStockValuationLCCommon): + @classmethod + def setUpClass(cls): + super(TestStockValuationLCFIFOVB, cls).setUpClass() + cls.vendor1 = cls.env['res.partner'].create({'name': 'vendor1'}) + cls.vendor1.property_account_payable_id = cls.company_data['default_account_payable'] + cls.vendor2 = cls.env['res.partner'].create({'name': 'vendor2'}) + cls.vendor2.property_account_payable_id = cls.company_data['default_account_payable'] + cls.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo' + cls.product1.product_tmpl_id.categ_id.property_valuation = 'real_time' + + def test_vendor_bill_flow_anglo_saxon_1(self): + """In anglo saxon accounting, receive 10@10 and invoice. Then invoice 1@50 as a landed costs + and create a linked landed costs record. + """ + self.env.company.anglo_saxon_accounting = True + + # Create an RFQ for self.product1, 10@10 + rfq = Form(self.env['purchase.order']) + rfq.partner_id = self.vendor1 + + with rfq.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 10 + po_line.price_unit = 10 + po_line.taxes_id.clear() + + rfq = rfq.save() + rfq.button_confirm() + + # Process the receipt + receipt = rfq.picking_ids + wiz = receipt.button_validate() + wiz = Form(self.env['stock.immediate.transfer'].with_context(wiz['context'])).save().process() + self.assertEqual(rfq.order_line.qty_received, 10) + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 0) + self.assertEqual(input_aml.credit, 100) + valuation_aml = self._get_stock_valuation_move_lines()[-1] + self.assertEqual(valuation_aml.debit, 100) + self.assertEqual(valuation_aml.credit, 0) + + # Create a vendor bill for the RFQ + action = rfq.action_create_invoice() + vb = self.env['account.move'].browse(action['res_id']) + vb.invoice_date = vb.date + vb.action_post() + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 100) + self.assertEqual(input_aml.credit, 0) + payable_aml = self._get_payable_move_lines()[-1] + self.assertEqual(payable_aml.debit, 0) + self.assertEqual(payable_aml.credit, 100) + + # Create a vendor bill for a landed cost product, post it and validate a landed cost + # linked to this vendor bill. LC; 1@50 + lcvb = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) + lcvb.invoice_date = lcvb.date + lcvb.partner_id = self.vendor2 + with lcvb.invoice_line_ids.new() as inv_line: + inv_line.product_id = self.productlc1 + inv_line.price_unit = 50 + inv_line.is_landed_costs_line = True + with lcvb.invoice_line_ids.edit(0) as inv_line: + inv_line.tax_ids.clear() + lcvb = lcvb.save() + lcvb.action_post() + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 50) + self.assertEqual(input_aml.credit, 0) + payable_aml = self._get_payable_move_lines()[-1] + self.assertEqual(payable_aml.debit, 0) + self.assertEqual(payable_aml.credit, 50) + + action = lcvb.button_create_landed_costs() + lc = Form(self.env[action['res_model']].browse(action['res_id'])) + lc.picking_ids.add(receipt) + lc = lc.save() + lc.button_validate() + + self.assertEqual(lc.cost_lines.price_unit, 50) + self.assertEqual(lc.cost_lines.product_id, self.productlc1) + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 0) + self.assertEqual(input_aml.credit, 50) + valuation_aml = self._get_stock_valuation_move_lines()[-1] + self.assertEqual(valuation_aml.debit, 50) + self.assertEqual(valuation_aml.credit, 0) + + # Check reconciliation of input aml of lc + lc_input_aml = lc.account_move_id.line_ids.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_in']) + self.assertTrue(len(lc_input_aml.full_reconcile_id), 1) + + self.assertEqual(self.product1.quantity_svl, 10) + self.assertEqual(self.product1.value_svl, 150) + + def test_vendor_bill_flow_anglo_saxon_2(self): + """In anglo saxon accounting, receive 10@10 and invoice with the addition of 1@50 as a + landed costs and create a linked landed costs record. + """ + self.env.company.anglo_saxon_accounting = True + + # Create an RFQ for self.product1, 10@10 + rfq = Form(self.env['purchase.order']) + rfq.partner_id = self.vendor1 + + with rfq.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 10 + po_line.price_unit = 10 + po_line.taxes_id.clear() + + rfq = rfq.save() + rfq.button_confirm() + + # Process the receipt + receipt = rfq.picking_ids + wiz = receipt.button_validate() + wiz = Form(self.env['stock.immediate.transfer'].with_context(wiz['context'])).save() + wiz.process() + self.assertEqual(rfq.order_line.qty_received, 10) + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 0) + self.assertEqual(input_aml.credit, 100) + valuation_aml = self._get_stock_valuation_move_lines()[-1] + self.assertEqual(valuation_aml.debit, 100) + self.assertEqual(valuation_aml.credit, 0) + + # Create a vendor bill for the RFQ and add to it the landed cost + vb = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) + vb.partner_id = self.vendor1 + vb.invoice_date = vb.date + with vb.invoice_line_ids.new() as inv_line: + inv_line.product_id = self.productlc1 + inv_line.price_unit = 50 + inv_line.is_landed_costs_line = True + vb = vb.save() + vb.action_post() + + action = vb.button_create_landed_costs() + lc = Form(self.env[action['res_model']].browse(action['res_id'])) + lc.picking_ids.add(receipt) + lc = lc.save() + lc.button_validate() + + # Check reconciliation of input aml of lc + lc_input_aml = lc.account_move_id.line_ids.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_in']) + self.assertTrue(len(lc_input_aml.full_reconcile_id), 1) + + def test_vendor_bill_flow_continental_1(self): + """In continental accounting, receive 10@10 and invoice. Then invoice 1@50 as a landed costs + and create a linked landed costs record. + """ + self.env.company.anglo_saxon_accounting = False + + # Create an RFQ for self.product1, 10@10 + rfq = Form(self.env['purchase.order']) + rfq.partner_id = self.vendor1 + + with rfq.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 10 + po_line.price_unit = 10 + po_line.taxes_id.clear() + + rfq = rfq.save() + rfq.button_confirm() + + # Process the receipt + receipt = rfq.picking_ids + wiz = receipt.button_validate() + wiz = Form(self.env['stock.immediate.transfer'].with_context(wiz['context'])).save().process() + self.assertEqual(rfq.order_line.qty_received, 10) + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 0) + self.assertEqual(input_aml.credit, 100) + valuation_aml = self._get_stock_valuation_move_lines()[-1] + self.assertEqual(valuation_aml.debit, 100) + self.assertEqual(valuation_aml.credit, 0) + + # Create a vebdor bill for the RFQ + action = rfq.action_create_invoice() + vb = self.env['account.move'].browse(action['res_id']) + vb.invoice_date = vb.date + vb.action_post() + + expense_aml = self._get_expense_move_lines()[-1] + self.assertEqual(expense_aml.debit, 100) + self.assertEqual(expense_aml.credit, 0) + + payable_aml = self._get_payable_move_lines()[-1] + self.assertEqual(payable_aml.debit, 0) + self.assertEqual(payable_aml.credit, 100) + + # Create a vendor bill for a landed cost product, post it and validate a landed cost + # linked to this vendor bill. LC; 1@50 + lcvb = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) + lcvb.partner_id = self.vendor2 + lcvb.invoice_date = lcvb.date + with lcvb.invoice_line_ids.new() as inv_line: + inv_line.product_id = self.productlc1 + inv_line.price_unit = 50 + inv_line.is_landed_costs_line = True + with lcvb.invoice_line_ids.edit(0) as inv_line: + inv_line.tax_ids.clear() + lcvb = lcvb.save() + lcvb.action_post() + + expense_aml = self._get_expense_move_lines()[-1] + self.assertEqual(expense_aml.debit, 50) + self.assertEqual(expense_aml.credit, 0) + payable_aml = self._get_payable_move_lines()[-1] + self.assertEqual(payable_aml.debit, 0) + self.assertEqual(payable_aml.credit, 50) + + action = lcvb.button_create_landed_costs() + lc = Form(self.env[action['res_model']].browse(action['res_id'])) + lc.picking_ids.add(receipt) + lc = lc.save() + lc.button_validate() + + self.assertEqual(lc.cost_lines.price_unit, 50) + self.assertEqual(lc.cost_lines.product_id, self.productlc1) + + input_aml = self._get_stock_input_move_lines()[-1] + self.assertEqual(input_aml.debit, 0) + self.assertEqual(input_aml.credit, 50) + valuation_aml = self._get_stock_valuation_move_lines()[-1] + self.assertEqual(valuation_aml.debit, 50) + self.assertEqual(valuation_aml.credit, 0) + + self.assertEqual(self.product1.quantity_svl, 10) + self.assertEqual(self.product1.value_svl, 150) |
