summaryrefslogtreecommitdiff
path: root/addons/stock_landed_costs/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/stock_landed_costs/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/stock_landed_costs/tests')
-rw-r--r--addons/stock_landed_costs/tests/__init__.py6
-rw-r--r--addons/stock_landed_costs/tests/common.py50
-rw-r--r--addons/stock_landed_costs/tests/test_stock_landed_costs.py161
-rw-r--r--addons/stock_landed_costs/tests/test_stock_landed_costs_purchase.py366
-rw-r--r--addons/stock_landed_costs/tests/test_stock_landed_costs_rounding.py154
-rw-r--r--addons/stock_landed_costs/tests/test_stockvaluationlayer.py536
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)