summaryrefslogtreecommitdiff
path: root/addons/stock_account/tests
diff options
context:
space:
mode:
Diffstat (limited to 'addons/stock_account/tests')
-rw-r--r--addons/stock_account/tests/__init__.py5
-rw-r--r--addons/stock_account/tests/test_account_move.py120
-rw-r--r--addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py119
-rw-r--r--addons/stock_account/tests/test_stock_valuation_layer_revaluation.py173
-rw-r--r--addons/stock_account/tests/test_stockvaluation.py3748
-rw-r--r--addons/stock_account/tests/test_stockvaluationlayer.py900
6 files changed, 5065 insertions, 0 deletions
diff --git a/addons/stock_account/tests/__init__.py b/addons/stock_account/tests/__init__.py
new file mode 100644
index 00000000..a396f09d
--- /dev/null
+++ b/addons/stock_account/tests/__init__.py
@@ -0,0 +1,5 @@
+from . import test_account_move
+from . import test_anglo_saxon_valuation_reconciliation_common
+from . import test_stockvaluation
+from . import test_stockvaluationlayer
+from . import test_stock_valuation_layer_revaluation
diff --git a/addons/stock_account/tests/test_account_move.py b/addons/stock_account/tests/test_account_move.py
new file mode 100644
index 00000000..7b260700
--- /dev/null
+++ b/addons/stock_account/tests/test_account_move.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
+from odoo.tests.common import tagged, Form
+
+
+@tagged("post_install", "-at_install")
+class TestAccountMove(AccountTestInvoicingCommon):
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ (
+ cls.stock_input_account,
+ cls.stock_output_account,
+ cls.stock_valuation_account,
+ cls.expense_account,
+ cls.stock_journal,
+ ) = _create_accounting_data(cls.env)
+
+ cls.product_A = cls.env["product.product"].create(
+ {
+ "name": "Product A",
+ "type": "product",
+ "default_code": "prda",
+ "categ_id": cls.env.ref("product.product_category_all").id,
+ "taxes_id": [(5, 0, 0)],
+ "supplier_taxes_id": [(5, 0, 0)],
+ "lst_price": 100.0,
+ "standard_price": 10.0,
+ "property_account_income_id": cls.company_data["default_account_revenue"].id,
+ "property_account_expense_id": cls.company_data["default_account_expense"].id,
+ }
+ )
+ cls.product_A.categ_id.write(
+ {
+ "property_stock_account_input_categ_id": cls.stock_input_account.id,
+ "property_stock_account_output_categ_id": cls.stock_output_account.id,
+ "property_stock_valuation_account_id": cls.stock_valuation_account.id,
+ "property_stock_journal": cls.stock_journal.id,
+ "property_valuation": "real_time",
+ "property_cost_method": "standard",
+ }
+ )
+
+ def test_standard_perpetual_01_mc_01(self):
+ rate = self.currency_data["rates"].sorted()[0].rate
+
+ move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
+ move_form.partner_id = self.partner_a
+ move_form.currency_id = self.currency_data["currency"]
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_A
+ line_form.tax_ids.clear()
+ invoice = move_form.save()
+
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
+ self.assertEqual(len(invoice.mapped("line_ids")), 2)
+ self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 1)
+
+ invoice._post()
+
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
+ self.assertEqual(len(invoice.mapped("line_ids")), 4)
+ self.assertEqual(len(invoice.mapped("line_ids").filtered("is_anglo_saxon_line")), 2)
+ self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 2)
+
+ def test_fifo_perpetual_01_mc_01(self):
+ self.product_A.categ_id.property_cost_method = "fifo"
+ rate = self.currency_data["rates"].sorted()[0].rate
+
+ move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
+ move_form.partner_id = self.partner_a
+ move_form.currency_id = self.currency_data["currency"]
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_A
+ line_form.tax_ids.clear()
+ invoice = move_form.save()
+
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
+ self.assertEqual(len(invoice.mapped("line_ids")), 2)
+ self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 1)
+
+ invoice._post()
+
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
+ self.assertEqual(len(invoice.mapped("line_ids")), 4)
+ self.assertEqual(len(invoice.mapped("line_ids").filtered("is_anglo_saxon_line")), 2)
+ self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 2)
+
+ def test_average_perpetual_01_mc_01(self):
+ self.product_A.categ_id.property_cost_method = "average"
+ rate = self.currency_data["rates"].sorted()[0].rate
+
+ move_form = Form(self.env["account.move"].with_context(default_move_type="out_invoice"))
+ move_form.partner_id = self.partner_a
+ move_form.currency_id = self.currency_data["currency"]
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.product_id = self.product_A
+ line_form.tax_ids.clear()
+ invoice = move_form.save()
+
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
+ self.assertEqual(len(invoice.mapped("line_ids")), 2)
+ self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 1)
+
+ invoice._post()
+
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_total)
+ self.assertAlmostEqual(self.product_A.lst_price * rate, invoice.amount_residual)
+ self.assertEqual(len(invoice.mapped("line_ids")), 4)
+ self.assertEqual(len(invoice.mapped("line_ids").filtered("is_anglo_saxon_line")), 2)
+ self.assertEqual(len(invoice.mapped("line_ids.currency_id")), 2)
diff --git a/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py b/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py
new file mode 100644
index 00000000..0af2a63c
--- /dev/null
+++ b/addons/stock_account/tests/test_anglo_saxon_valuation_reconciliation_common.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.account.tests.common import AccountTestInvoicingCommon
+from odoo.tests import tagged
+from odoo import fields
+
+
+@tagged('-at_install', 'post_install')
+class ValuationReconciliationTestCommon(AccountTestInvoicingCommon):
+ """ Base class for tests checking interim accounts reconciliation works
+ in anglosaxon accounting. It sets up everything we need in the tests, and is
+ extended in both sale_stock and purchase modules to run the 'true' tests.
+ """
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.stock_account_product_categ = cls.env['product.category'].create({
+ 'name': 'Test category',
+ 'property_valuation': 'real_time',
+ 'property_cost_method': 'fifo',
+ 'property_stock_valuation_account_id': cls.company_data['default_account_stock_valuation'].id,
+ 'property_stock_account_input_categ_id': cls.company_data['default_account_stock_in'].id,
+ 'property_stock_account_output_categ_id': cls.company_data['default_account_stock_out'].id,
+ })
+
+ uom_unit = cls.env.ref('uom.product_uom_unit')
+
+ cls.test_product_order = cls.env['product.product'].create({
+ 'name': "Test product template invoiced on order",
+ 'standard_price': 42.0,
+ 'type': 'product',
+ 'categ_id': cls.stock_account_product_categ.id,
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+ cls.test_product_delivery = cls.env['product.product'].create({
+ 'name': 'Test product template invoiced on delivery',
+ 'standard_price': 42.0,
+ 'type': 'product',
+ 'categ_id': cls.stock_account_product_categ.id,
+ 'uom_id': uom_unit.id,
+ 'uom_po_id': uom_unit.id,
+ })
+
+ @classmethod
+ def setup_company_data(cls, company_name, chart_template=None, **kwargs):
+ company_data = super().setup_company_data(company_name, chart_template=chart_template, **kwargs)
+
+ # Create stock config.
+ company_data.update({
+ 'default_account_stock_in': cls.env['account.account'].create({
+ 'name': 'default_account_stock_in',
+ 'code': 'STOCKIN',
+ 'reconcile': True,
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': company_data['company'].id,
+ }),
+ 'default_account_stock_out': cls.env['account.account'].create({
+ 'name': 'default_account_stock_out',
+ 'code': 'STOCKOUT',
+ 'reconcile': True,
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': company_data['company'].id,
+ }),
+ 'default_account_stock_valuation': cls.env['account.account'].create({
+ 'name': 'default_account_stock_valuation',
+ 'code': 'STOCKVAL',
+ 'reconcile': True,
+ 'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
+ 'company_id': company_data['company'].id,
+ }),
+ 'default_warehouse': cls.env['stock.warehouse'].search(
+ [('company_id', '=', company_data['company'].id)],
+ limit=1,
+ ),
+ })
+ return company_data
+
+ def check_reconciliation(self, invoice, picking, full_reconcile=True, operation='purchase'):
+ interim_account_id = self.company_data['default_account_stock_in'].id if operation == 'purchase' else self.company_data['default_account_stock_out'].id
+ invoice_line = invoice.line_ids.filtered(lambda line: line.account_id.id == interim_account_id)
+
+ stock_moves = picking.move_lines
+
+ valuation_line = stock_moves.mapped('account_move_ids.line_ids').filtered(lambda x: x.account_id.id == interim_account_id)
+
+ if invoice.is_purchase_document() and any(l.is_anglo_saxon_line for l in invoice_line):
+ self.assertEqual(len(invoice_line), 2, "Only two line2 should have been written by invoice in stock input account")
+ self.assertTrue(valuation_line.reconciled or invoice_line[0].reconciled or invoice_line[1].reconciled, "The valuation and invoice line should have been reconciled together.")
+ else:
+ self.assertEqual(len(invoice_line), 1, "Only one line should have been written by invoice in stock input account")
+ self.assertTrue(valuation_line.reconciled or invoice_line.reconciled, "The valuation and invoice line should have been reconciled together.")
+
+ if invoice.move_type not in ('out_refund', 'in_refund'):
+ self.assertEqual(len(valuation_line), 1, "Only one line should have been written for stock valuation in stock input account")
+
+ if full_reconcile:
+ self.assertTrue(valuation_line.full_reconcile_id, "The reconciliation should be total at that point.")
+ else:
+ self.assertFalse(valuation_line.full_reconcile_id, "The reconciliation should not be total at that point.")
+
+ def _process_pickings(self, pickings, date=False, quantity=False):
+ if not date:
+ date = fields.Date.today()
+ pickings.action_confirm()
+ pickings.action_assign()
+ for picking in pickings:
+ for ml in picking.move_line_ids:
+ ml.qty_done = quantity or ml.product_qty
+ pickings._action_done()
+ self._change_pickings_date(pickings, date)
+
+ def _change_pickings_date(self, pickings, date):
+ pickings.mapped('move_lines').write({'date': date})
+ pickings.mapped('move_lines.account_move_ids').write({'name': '/', 'state': 'draft'})
+ pickings.mapped('move_lines.account_move_ids').write({'date': date})
+ pickings.move_lines.account_move_ids.action_post()
diff --git a/addons/stock_account/tests/test_stock_valuation_layer_revaluation.py b/addons/stock_account/tests/test_stock_valuation_layer_revaluation.py
new file mode 100644
index 00000000..d84f8652
--- /dev/null
+++ b/addons/stock_account/tests/test_stock_valuation_layer_revaluation.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.exceptions import UserError
+from odoo.tests import Form
+from odoo.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
+from odoo.addons.stock_account.tests.test_stockvaluationlayer import TestStockValuationCommon
+
+
+class TestStockValuationLayerRevaluation(TestStockValuationCommon):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockValuationLayerRevaluation, cls).setUpClass()
+ cls.stock_input_account, cls.stock_output_account, cls.stock_valuation_account, cls.expense_account, cls.stock_journal = _create_accounting_data(cls.env)
+ cls.product1.write({
+ 'property_account_expense_id': cls.expense_account.id,
+ })
+ cls.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': cls.stock_input_account.id,
+ 'property_stock_account_output_categ_id': cls.stock_output_account.id,
+ 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
+ 'property_stock_journal': cls.stock_journal.id,
+ })
+
+ cls.product1.categ_id.property_valuation = 'real_time'
+
+ def test_stock_valuation_layer_revaluation_avco(self):
+ self.product1.categ_id.property_cost_method = 'average'
+ context = {
+ 'default_product_id': self.product1.id,
+ 'default_company_id': self.env.company.id,
+ 'default_added_value': 0.0
+ }
+ # Quantity of product1 is zero, raise
+ with self.assertRaises(UserError):
+ Form(self.env['stock.valuation.layer.revaluation'].with_context(context)).save()
+
+ self._make_in_move(self.product1, 10, unit_cost=2)
+ self._make_in_move(self.product1, 10, unit_cost=4)
+
+ self.assertEqual(self.product1.standard_price, 3)
+ self.assertEqual(self.product1.quantity_svl, 20)
+
+ old_layers = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc")
+
+ self.assertEqual(len(old_layers), 2)
+ self.assertEqual(old_layers[0].remaining_value, 40)
+
+ revaluation_wizard = Form(self.env['stock.valuation.layer.revaluation'].with_context(context))
+ revaluation_wizard.added_value = 20
+ revaluation_wizard.account_id = self.stock_valuation_account
+ revaluation_wizard.save().action_validate_revaluation()
+
+ # Check standard price change
+ self.assertEqual(self.product1.standard_price, 4)
+ self.assertEqual(self.product1.quantity_svl, 20)
+
+ # Check the creation of stock.valuation.layer
+ new_layer = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc", limit=1)
+ self.assertEqual(new_layer.value, 20)
+
+ # Check the remaing value of current layers
+ self.assertEqual(old_layers[0].remaining_value, 50)
+ self.assertEqual(sum(slv.remaining_value for slv in old_layers), 80)
+
+ # Check account move
+ self.assertTrue(bool(new_layer.account_move_id))
+ self.assertEqual(len(new_layer.account_move_id.line_ids), 2)
+
+ self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("debit")), 20)
+ self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("credit")), 20)
+
+ credit_lines = [l for l in new_layer.account_move_id.line_ids if l.credit > 0]
+ self.assertEqual(len(credit_lines), 1)
+ self.assertEqual(credit_lines[0].account_id.id, self.stock_valuation_account.id)
+
+ def test_stock_valuation_layer_revaluation_avco_rounding(self):
+ self.product1.categ_id.property_cost_method = 'average'
+ context = {
+ 'default_product_id': self.product1.id,
+ 'default_company_id': self.env.company.id,
+ 'default_added_value': 0.0
+ }
+ # Quantity of product1 is zero, raise
+ with self.assertRaises(UserError):
+ Form(self.env['stock.valuation.layer.revaluation'].with_context(context)).save()
+
+ self._make_in_move(self.product1, 1, unit_cost=1)
+ self._make_in_move(self.product1, 1, unit_cost=1)
+ self._make_in_move(self.product1, 1, unit_cost=1)
+
+ self.assertEqual(self.product1.standard_price, 1)
+ self.assertEqual(self.product1.quantity_svl, 3)
+
+ old_layers = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc")
+
+ self.assertEqual(len(old_layers), 3)
+ self.assertEqual(old_layers[0].remaining_value, 1)
+
+ revaluation_wizard = Form(self.env['stock.valuation.layer.revaluation'].with_context(context))
+ revaluation_wizard.added_value = 1
+ revaluation_wizard.account_id = self.stock_valuation_account
+ revaluation_wizard.save().action_validate_revaluation()
+
+ # Check standard price change
+ self.assertEqual(self.product1.standard_price, 1.33)
+ self.assertEqual(self.product1.quantity_svl, 3)
+
+ # Check the creation of stock.valuation.layer
+ new_layer = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc", limit=1)
+ self.assertEqual(new_layer.value, 1)
+
+ # Check the remaing value of current layers
+ self.assertEqual(sum(slv.remaining_value for slv in old_layers), 4)
+ self.assertTrue(1.34 in old_layers.mapped("remaining_value"))
+
+ # Check account move
+ self.assertTrue(bool(new_layer.account_move_id))
+ self.assertEqual(len(new_layer.account_move_id.line_ids), 2)
+
+ self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("debit")), 1)
+ self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("credit")), 1)
+
+ credit_lines = [l for l in new_layer.account_move_id.line_ids if l.credit > 0]
+ self.assertEqual(len(credit_lines), 1)
+ self.assertEqual(credit_lines[0].account_id.id, self.stock_valuation_account.id)
+
+ def test_stock_valuation_layer_revaluation_fifo(self):
+ self.product1.categ_id.property_cost_method = 'fifo'
+ context = {
+ 'default_product_id': self.product1.id,
+ 'default_company_id': self.env.company.id,
+ 'default_added_value': 0.0
+ }
+ # Quantity of product1 is zero, raise
+ with self.assertRaises(UserError):
+ Form(self.env['stock.valuation.layer.revaluation'].with_context(context)).save()
+
+ self._make_in_move(self.product1, 10, unit_cost=2)
+ self._make_in_move(self.product1, 10, unit_cost=4)
+
+ self.assertEqual(self.product1.standard_price, 2)
+ self.assertEqual(self.product1.quantity_svl, 20)
+
+ old_layers = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc")
+
+ self.assertEqual(len(old_layers), 2)
+ self.assertEqual(old_layers[0].remaining_value, 40)
+
+ revaluation_wizard = Form(self.env['stock.valuation.layer.revaluation'].with_context(context))
+ revaluation_wizard.added_value = 20
+ revaluation_wizard.account_id = self.stock_valuation_account
+ revaluation_wizard.save().action_validate_revaluation()
+
+ self.assertEqual(self.product1.standard_price, 2)
+
+ # Check the creation of stock.valuation.layer
+ new_layer = self.env['stock.valuation.layer'].search([('product_id', '=', self.product1.id)], order="create_date desc, id desc", limit=1)
+ self.assertEqual(new_layer.value, 20)
+
+ # Check the remaing value of current layers
+ self.assertEqual(old_layers[0].remaining_value, 50)
+ self.assertEqual(sum(slv.remaining_value for slv in old_layers), 80)
+
+ # Check account move
+ self.assertTrue(bool(new_layer.account_move_id))
+ self.assertTrue(len(new_layer.account_move_id.line_ids), 2)
+
+ self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("debit")), 20)
+ self.assertEqual(sum(new_layer.account_move_id.line_ids.mapped("credit")), 20)
+
+ credit_lines = [l for l in new_layer.account_move_id.line_ids if l.credit > 0]
+ self.assertEqual(len(credit_lines), 1)
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
new file mode 100644
index 00000000..2d0d6036
--- /dev/null
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -0,0 +1,3748 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import timedelta
+
+from odoo.exceptions import UserError
+from odoo.fields import Datetime
+from odoo.tests.common import Form, SavepointCase
+
+
+def _create_accounting_data(env):
+ """Create the accounts and journals used in stock valuation.
+
+ :param env: environment used to create the records
+ :return: an input account, an output account, a valuation account, an expense account, a stock journal
+ """
+ stock_input_account = env['account.account'].create({
+ 'name': 'Stock Input',
+ 'code': 'StockIn',
+ 'user_type_id': env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ stock_output_account = env['account.account'].create({
+ 'name': 'Stock Output',
+ 'code': 'StockOut',
+ 'user_type_id': env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ stock_valuation_account = env['account.account'].create({
+ 'name': 'Stock Valuation',
+ 'code': 'Stock Valuation',
+ 'user_type_id': env.ref('account.data_account_type_current_assets').id,
+ 'reconcile': True,
+ })
+ expense_account = env['account.account'].create({
+ 'name': 'Expense Account',
+ 'code': 'Expense Account',
+ 'user_type_id': env.ref('account.data_account_type_expenses').id,
+ 'reconcile': True,
+ })
+ stock_journal = env['account.journal'].create({
+ 'name': 'Stock Journal',
+ 'code': 'STJTEST',
+ 'type': 'general',
+ })
+ return stock_input_account, stock_output_account, stock_valuation_account, expense_account, stock_journal
+
+
+class TestStockValuation(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockValuation, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
+ cls.partner = cls.env['res.partner'].create({'name': 'xxx'})
+ cls.owner1 = cls.env['res.partner'].create({'name': 'owner1'})
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.product1 = cls.env['product.product'].create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'default_code': 'prda',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.product2 = cls.env['product.product'].create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.inventory_user = cls.env['res.users'].create({
+ 'name': 'Pauline Poivraisselle',
+ 'login': 'pauline',
+ 'email': 'p.p@example.com',
+ 'notification_type': 'inbox',
+ 'groups_id': [(6, 0, [cls.env.ref('stock.group_stock_user').id])]
+ })
+
+ cls.stock_input_account, cls.stock_output_account, cls.stock_valuation_account, cls.expense_account, cls.stock_journal = _create_accounting_data(cls.env)
+ cls.product1.categ_id.property_valuation = 'real_time'
+ cls.product2.categ_id.property_valuation = 'real_time'
+ cls.product1.write({
+ 'property_account_expense_id': cls.expense_account.id,
+ })
+ cls.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': cls.stock_input_account.id,
+ 'property_stock_account_output_categ_id': cls.stock_output_account.id,
+ 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
+ 'property_stock_journal': cls.stock_journal.id,
+ })
+
+ def _get_stock_input_move_lines(self):
+ return self.env['account.move.line'].search([
+ ('account_id', '=', self.stock_input_account.id),
+ ], order='date, id')
+
+ def _get_stock_output_move_lines(self):
+ return self.env['account.move.line'].search([
+ ('account_id', '=', self.stock_output_account.id),
+ ], order='date, id')
+
+ def _get_stock_valuation_move_lines(self):
+ return self.env['account.move.line'].search([
+ ('account_id', '=', self.stock_valuation_account.id),
+ ], order='date, id')
+
+
+ def _make_in_move(self, product, quantity, unit_cost=None):
+ """ 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.env.ref('stock.stock_location_stock').id,
+ 'product_uom': self.env.ref('uom.product_uom_unit').id,
+ 'product_uom_qty': quantity,
+ 'price_unit': unit_cost,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+
+ in_move._action_confirm()
+ in_move._action_assign()
+ in_move.move_line_ids.qty_done = quantity
+ in_move._action_done()
+
+ return in_move.with_context(svl=True)
+
+ def _make_out_move(self, product, quantity):
+ """ 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.env.ref('stock.stock_location_stock').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.env.ref('stock.picking_type_out').id,
+ })
+ out_move._action_confirm()
+ out_move._action_assign()
+ out_move.move_line_ids.qty_done = quantity
+ out_move._action_done()
+ return out_move.with_context(svl=True)
+
+ def test_realtime(self):
+ """ Stock moves update stock value with product x cost price,
+ price change updates the stock value based on current stock level.
+ """
+ # Enter 10 products while price is 5.0
+ self.product1.standard_price = 5.0
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ # Set price to 6.0
+ self.product1.standard_price = 6.0
+ stock_aml, price_change_aml = self._get_stock_valuation_move_lines()
+ self.assertEqual(stock_aml.debit, 50)
+ self.assertEqual(price_change_aml.debit, 10)
+ self.assertEqual(price_change_aml.ref, 'prda')
+ self.assertEqual(price_change_aml.product_id, self.product1)
+
+ def test_fifo_perpetual_1(self):
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # receive 10 units @ 10.00 per unit
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ # stock_account values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+
+ # account values for move1
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 1)
+ move1_input_aml = input_aml[-1]
+ self.assertEqual(move1_input_aml.debit, 0)
+ self.assertEqual(move1_input_aml.credit, 100)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 1)
+ self.assertEqual(move1_valuation_aml.debit, 100)
+ self.assertEqual(move1_valuation_aml.credit, 0)
+ self.assertEqual(move1_valuation_aml.product_id.id, self.product1.id)
+ self.assertEqual(move1_valuation_aml.quantity, 10)
+ self.assertEqual(move1_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 0)
+
+ # ---------------------------------------------------------------------
+ # receive 10 units @ 8.00 per unit
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 8.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 8.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ # stock_account values for move2
+ self.assertEqual(move2.stock_valuation_layer_ids.unit_cost, 8.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 80.0)
+
+ # account values for move2
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 2)
+ move2_input_aml = input_aml[-1]
+ self.assertEqual(move2_input_aml.debit, 0)
+ self.assertEqual(move2_input_aml.credit, 80)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 2)
+ self.assertEqual(move2_valuation_aml.debit, 80)
+ self.assertEqual(move2_valuation_aml.credit, 0)
+ self.assertEqual(move2_valuation_aml.product_id.id, self.product1.id)
+ self.assertEqual(move2_valuation_aml.quantity, 10)
+ self.assertEqual(move2_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 0)
+
+ # ---------------------------------------------------------------------
+ # sale 3 units
+ # ---------------------------------------------------------------------
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 3 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 3.0
+ move3._action_done()
+
+ # stock_account values for move3
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out move
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -30.0) # took 3 items from move 1 @ 10.00 per unit
+
+ # account values for move3
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 2)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move3_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 3)
+ self.assertEqual(move3_valuation_aml.debit, 0)
+ self.assertEqual(move3_valuation_aml.credit, 30)
+ self.assertEqual(move3_valuation_aml.product_id.id, self.product1.id)
+ # FIXME sle
+ #self.assertEqual(move3_valuation_aml.quantity, -3)
+ self.assertEqual(move3_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ move3_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 1)
+ self.assertEqual(move3_output_aml.debit, 30)
+ self.assertEqual(move3_output_aml.credit, 0)
+
+ # ---------------------------------------------------------------------
+ # Increase received quantity of move1 from 10 to 12, it should create
+ # a new stock layer at the top of the queue.
+ # ---------------------------------------------------------------------
+ move1.quantity_done = 12
+
+ # stock_account values for move3
+ self.assertEqual(move1.stock_valuation_layer_ids.sorted()[-1].unit_cost, 10.0)
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('remaining_qty')), 9.0)
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('value')), 120.0) # move 1 is now 10@10 + 2@10
+
+ # account values for move1
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 3)
+ move1_correction_input_aml = input_aml[-1]
+ self.assertEqual(move1_correction_input_aml.debit, 0)
+ self.assertEqual(move1_correction_input_aml.credit, 20)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_correction_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 4)
+ self.assertEqual(move1_correction_valuation_aml.debit, 20)
+ self.assertEqual(move1_correction_valuation_aml.credit, 0)
+ self.assertEqual(move1_correction_valuation_aml.product_id.id, self.product1.id)
+ self.assertEqual(move1_correction_valuation_aml.quantity, 2)
+ self.assertEqual(move1_correction_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 1)
+
+ # ---------------------------------------------------------------------
+ # Sale 9 units, the units available from the previous increase are not sent
+ # immediately as the new layer is at the top of the queue.
+ # ---------------------------------------------------------------------
+ move4 = self.env['stock.move'].create({
+ 'name': 'Sale 9 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 9.0,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 9.0
+ move4._action_done()
+
+ # stock_account values for move4
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out move
+ self.assertEqual(move4.stock_valuation_layer_ids.value, -86.0) # took 9 items from move 1 @ 10.00 per unit
+
+ # account values for move4
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 3)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move4_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 5)
+ self.assertEqual(move4_valuation_aml.debit, 0)
+ self.assertEqual(move4_valuation_aml.credit, 86)
+ self.assertEqual(move4_valuation_aml.product_id.id, self.product1.id)
+ # FIXME sle
+ #self.assertEqual(move4_valuation_aml.quantity, -9)
+ self.assertEqual(move4_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ move4_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 2)
+ self.assertEqual(move4_output_aml.debit, 86)
+ self.assertEqual(move4_output_aml.credit, 0)
+
+ # ---------------------------------------------------------------------
+ # Sale 20 units, we fall in negative stock for 10 units. Theses are
+ # valued at the last FIFO cost and the total is negative.
+ # ---------------------------------------------------------------------
+ move5 = self.env['stock.move'].create({
+ 'name': 'Sale 20 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20.0,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 20.0
+ move5._action_done()
+
+ # stock_account values for move5
+ # (took 8 from the second receipt and 2 from the increase of the first receipt)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, -10.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.value, -184.0)
+
+ # account values for move5
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 3)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move5_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 6)
+ self.assertEqual(move5_valuation_aml.debit, 0)
+ self.assertEqual(move5_valuation_aml.credit, 184)
+ self.assertEqual(move5_valuation_aml.product_id.id, self.product1.id)
+ #self.assertEqual(move5_valuation_aml.quantity, -20)
+ self.assertEqual(move5_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ move5_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 3)
+ self.assertEqual(move5_output_aml.debit, 184)
+ self.assertEqual(move5_output_aml.credit, 0)
+
+ # ---------------------------------------------------------------------
+ # Receive 10 units @ 12.00 to counterbalance the negative, the vacuum
+ # will be called directly: 10@10 should be revalued 10@12
+ # ---------------------------------------------------------------------
+ move6 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 12.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 12.0,
+ })
+ move6._action_confirm()
+ move6._action_assign()
+ move6.move_line_ids.qty_done = 10.0
+ move6._action_done()
+
+ # stock_account values for move6
+ self.assertEqual(move6.stock_valuation_layer_ids.unit_cost, 12.0)
+ self.assertEqual(move6.stock_valuation_layer_ids.remaining_qty, 0.0) # already consumed by the next vacuum
+ self.assertEqual(move6.stock_valuation_layer_ids.value, 120)
+
+ # vacuum aml, 10@10 should have been 10@12, get rid of 20
+ valuation_aml = self._get_stock_valuation_move_lines()
+ vacuum_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 8)
+ self.assertEqual(vacuum_valuation_aml.balance, -20)
+ self.assertEqual(vacuum_valuation_aml.product_id.id, self.product1.id)
+ self.assertEqual(vacuum_valuation_aml.quantity, 0)
+ self.assertEqual(vacuum_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ output_aml = self._get_stock_output_move_lines()
+ vacuum_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 4)
+ self.assertEqual(vacuum_output_aml.balance, 20)
+
+ # ---------------------------------------------------------------------
+ # Edit move6, receive less: 2 in negative stock
+ # ---------------------------------------------------------------------
+ move6.quantity_done = 8
+
+ # stock_account values for move6
+ self.assertEqual(move6.stock_valuation_layer_ids.sorted()[-1].remaining_qty, -2)
+ self.assertEqual(move6.stock_valuation_layer_ids.sorted()[-1].value, -20)
+
+ # account values for move1
+ input_aml = self._get_stock_input_move_lines()
+ move6_correction_input_aml = input_aml[-1]
+ self.assertEqual(move6_correction_input_aml.debit, 20)
+ self.assertEqual(move6_correction_input_aml.credit, 0)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move6_correction_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move6_correction_valuation_aml.debit, 0)
+ self.assertEqual(move6_correction_valuation_aml.credit, 20)
+ self.assertEqual(move6_correction_valuation_aml.product_id.id, self.product1.id)
+ # FIXME sle
+ #self.assertEqual(move6_correction_valuation_aml.quantity, -2)
+ self.assertEqual(move6_correction_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ # -----------------------------------------------------------
+ # receive 4 to counterbalance now
+ # -----------------------------------------------------------
+ move7 = self.env['stock.move'].create({
+ 'name': 'IN 4 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ 'price_unit': 15.0,
+ })
+ move7._action_confirm()
+ move7._action_assign()
+ move7.move_line_ids.qty_done = 4.0
+ move7._action_done()
+
+ # account values after vacuum
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 7)
+ move6_correction2_input_aml = input_aml[-1]
+ self.assertEqual(move6_correction2_input_aml.debit, 10)
+ self.assertEqual(move6_correction2_input_aml.credit, 0)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move6_correction2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 11)
+ self.assertEqual(move6_correction2_valuation_aml.debit, 0)
+ self.assertEqual(move6_correction2_valuation_aml.credit, 10)
+ self.assertEqual(move6_correction2_valuation_aml.product_id.id, self.product1.id)
+ self.assertEqual(move6_correction2_valuation_aml.quantity, 0)
+ self.assertEqual(move6_correction_valuation_aml.product_uom_id.id, self.uom_unit.id)
+
+ # ---------------------------------------------------------------------
+ # Ending
+ # ---------------------------------------------------------------------
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertEqual(self.product1.value_svl, 30)
+ # check on accounting entries
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 30)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 380)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 380)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 350)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 320)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ def test_fifo_perpetual_2(self):
+ """ Normal fifo flow (no negative handling) """
+ # http://accountingexplained.com/financial/inventories/fifo-method
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # Beginning Inventory: 68 units @ 15.00 per unit
+ move1 = self.env['stock.move'].create({
+ 'name': '68 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 68.0,
+ 'price_unit': 15,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 68.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 1020.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 68.0)
+
+ # Purchase 140 units @ 15.50 per unit
+ move2 = self.env['stock.move'].create({
+ 'name': '140 units @ 15.50 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 140.0,
+ 'price_unit': 15.50,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 140.0
+ move2._action_done()
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 2170.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 68.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 140.0)
+
+ # Sale 94 units @ 19.00 per unit
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 94 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 94.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 94.0
+ move3._action_done()
+
+
+ # note: it' ll have to get 68 units from the first batch and 26 from the second one
+ # so its value should be -((68*15) + (26*15.5)) = -1423
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -1423.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 114)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ # Purchase 40 units @ 16.00 per unit
+ move4 = self.env['stock.move'].create({
+ 'name': '140 units @ 15.50 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 40.0,
+ 'price_unit': 16,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 40.0
+ move4._action_done()
+
+ self.assertEqual(move4.stock_valuation_layer_ids.value, 640.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 114)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 40.0)
+
+ # Purchase 78 units @ 16.50 per unit
+ move5 = self.env['stock.move'].create({
+ 'name': 'Purchase 78 units @ 16.50 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 78.0,
+ 'price_unit': 16.5,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 78.0
+ move5._action_done()
+
+ self.assertEqual(move5.stock_valuation_layer_ids.value, 1287.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 114)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 40.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, 78.0)
+
+ # Sale 116 units @ 19.50 per unit
+ move6 = self.env['stock.move'].create({
+ 'name': 'Sale 116 units @ 19.50 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 116.0,
+ })
+ move6._action_confirm()
+ move6._action_assign()
+ move6.move_line_ids.qty_done = 116.0
+ move6._action_done()
+
+ # note: it' ll have to get 114 units from the move2 and 2 from move4
+ # so its value should be -((114*15.5) + (2*16)) = 1735
+ self.assertEqual(move6.stock_valuation_layer_ids.value, -1799.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 38.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, 78.0)
+ self.assertEqual(move6.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ # Sale 62 units @ 21 per unit
+ move7 = self.env['stock.move'].create({
+ 'name': 'Sale 62 units @ 21 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 62.0,
+ })
+ move7._action_confirm()
+ move7._action_assign()
+ move7.move_line_ids.qty_done = 62.0
+ move7._action_done()
+
+ # note: it' ll have to get 38 units from the move4 and 24 from move5
+ # so its value should be -((38*16) + (24*16.5)) = 608 + 396
+ self.assertEqual(move7.stock_valuation_layer_ids.value, -1004.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, 54.0)
+ self.assertEqual(move6.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move7.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ # send 10 units in our transit location, the valorisation should not be impacted
+ transit_location = self.env['stock.location'].search([
+ ('company_id', '=', self.env.company.id),
+ ('usage', '=', 'transit'),
+ ('active', '=', False)
+ ], limit=1)
+ transit_location.active = True
+ move8 = self.env['stock.move'].create({
+ 'name': 'Send 10 units in transit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': transit_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move8._action_confirm()
+ move8._action_assign()
+ move8.move_line_ids.qty_done = 10.0
+ move8._action_done()
+
+ self.assertEqual(move8.stock_valuation_layer_ids.value, 0.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, 54.0)
+ self.assertEqual(move6.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move7.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move8.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in internal moves
+
+ # Sale 10 units @ 16.5 per unit
+ move9 = self.env['stock.move'].create({
+ 'name': 'Sale 10 units @ 16.5 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move9._action_confirm()
+ move9._action_assign()
+ move9.move_line_ids.qty_done = 10.0
+ move9._action_done()
+
+ # note: it' ll have to get 10 units from move5 so its value should
+ # be -(10*16.50) = -165
+ self.assertEqual(move9.stock_valuation_layer_ids.value, -165.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, 44.0)
+ self.assertEqual(move6.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move7.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move8.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in internal moves
+ self.assertEqual(move9.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ def test_fifo_perpetual_3(self):
+ """ Normal fifo flow (no negative handling) """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # in 10 @ 100
+ move1 = self.env['stock.move'].create({
+ 'name': 'in 10 @ 100',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 100,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 1000.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+
+ # in 10 @ 80
+ move2 = self.env['stock.move'].create({
+ 'name': 'in 10 @ 80',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 80,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 800.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 10.0)
+
+ # out 15
+ move3 = self.env['stock.move'].create({
+ 'name': 'out 15',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 15.0
+ move3._action_done()
+
+
+ # note: it' ll have to get 10 units from move1 and 5 from move2
+ # so its value should be -((10*100) + (5*80)) = -1423
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -1400.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 5)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ # in 5 @ 60
+ move4 = self.env['stock.move'].create({
+ 'name': 'in 5 @ 60',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'price_unit': 60,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 5.0
+ move4._action_done()
+
+ self.assertEqual(move4.stock_valuation_layer_ids.value, 300.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 5)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 5.0)
+
+ # out 7
+ move5 = self.env['stock.move'].create({
+ 'name': 'out 7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 7.0,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 7.0
+ move5._action_done()
+
+ # note: it' ll have to get 5 units from the move2 and 2 from move4
+ # so its value should be -((5*80) + (2*60)) = 520
+ self.assertEqual(move5.stock_valuation_layer_ids.value, -520.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 3.0)
+ self.assertEqual(move5.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ def test_fifo_perpetual_4(self):
+ """ Fifo and return handling. """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # in 8 @ 10
+ move1 = self.env['stock.move'].create({
+ 'name': 'in 8 @ 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 8.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 8.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 80.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 8.0)
+
+ # in 4 @ 16
+ move2 = self.env['stock.move'].create({
+ 'name': 'in 4 @ 16',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ 'price_unit': 16,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 4.0
+ move2._action_done()
+
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 64)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 4.0)
+
+ # out 10
+ out_pick = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': self.env['res.partner'].search([], limit=1).id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move3 = self.env['stock.move'].create({
+ 'name': 'out 10',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'picking_id': out_pick.id,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 10.0
+ move3._action_done()
+
+
+ # note: it' ll have to get 8 units from move1 and 2 from move2
+ # so its value should be -((8*10) + (2*16)) = -116
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -112.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 2)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+
+ # in 2 @ 6
+ move4 = self.env['stock.move'].create({
+ 'name': 'in 2 @ 6',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'price_unit': 6,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 2.0
+ move4._action_done()
+
+ self.assertEqual(move4.stock_valuation_layer_ids.value, 12.0)
+
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 2)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in out moves
+ self.assertEqual(move4.stock_valuation_layer_ids.remaining_qty, 2.0)
+
+ self.assertEqual(self.product1.standard_price, 16)
+
+ # return
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=out_pick.ids, active_id=out_pick.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0 # Return only 2
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.move_lines[0].move_line_ids[0].qty_done = 1.0
+ return_pick.with_user(self.inventory_user)._action_done()
+
+ self.assertEqual(self.product1.standard_price, 16)
+
+ self.assertAlmostEqual(return_pick.move_lines.stock_valuation_layer_ids.unit_cost, 11.2)
+
+ def test_fifo_negative_1(self):
+ """ Send products that you do not have. Value the first outgoing move to the standard
+ price, receive in multiple times the delivered quantity and run _fifo_vacuum to compensate.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # We expect the user to set manually set a standard price to its products if its first
+ # transfer is sending products that he doesn't have.
+ self.product1.product_tmpl_id.standard_price = 8.0
+
+ # ---------------------------------------------------------------------
+ # Send 50 units you don't have
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': '50 out',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 50.0,
+ 'price_unit': 0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 50.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, -400.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, -50.0) # normally unused in out moves, but as it moved negative stock we mark it
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 8)
+
+ # account values for move1
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move1_valuation_aml.debit, 0)
+ self.assertEqual(move1_valuation_aml.credit, 400)
+ output_aml = self._get_stock_output_move_lines()
+ move1_output_aml = output_aml[-1]
+ self.assertEqual(move1_output_aml.debit, 400)
+ self.assertEqual(move1_output_aml.credit, 0)
+
+ # ---------------------------------------------------------------------
+ # Receive 40 units @ 15
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': '40 in @15',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 40.0,
+ 'price_unit': 15.0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 40.0,
+ })]
+ })
+ move2._action_confirm()
+ move2._action_done()
+
+ # stock values for move2
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 600.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0)
+ self.assertEqual(move2.stock_valuation_layer_ids.unit_cost, 15.0)
+
+ # ---------------------------------------------------------------------
+ # The vacuum ran
+ # ---------------------------------------------------------------------
+ # account values after vacuum
+ valuation_aml = self._get_stock_valuation_move_lines()
+ vacuum1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(vacuum1_valuation_aml.debit, 0)
+ # 280 was credited more in valuation (we compensated 40 items here, so initially 40 were
+ # valued at 8 -> 320 in credit but now we actually sent 40@15 = 600, so the difference is
+ # 280 more credited)
+ self.assertEqual(vacuum1_valuation_aml.credit, 280)
+ output_aml = self._get_stock_output_move_lines()
+ vacuum1_output_aml = output_aml[-1]
+ self.assertEqual(vacuum1_output_aml.debit, 280)
+ self.assertEqual(vacuum1_output_aml.credit, 0)
+
+ # ---------------------------------------------------------------------
+ # Receive 20 units @ 25
+ # ---------------------------------------------------------------------
+ move3 = self.env['stock.move'].create({
+ 'name': '20 in @25',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20.0,
+ 'price_unit': 25.0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 20.0
+ })]
+ })
+ move3._action_confirm()
+ move3._action_done()
+
+ # ---------------------------------------------------------------------
+ # The vacuum ran
+ # ---------------------------------------------------------------------
+
+ # stock values for move1-3
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('value')), -850.0) # 40@15 + 10@25
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('remaining_qty')), 0.0)
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), 600.0)
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('remaining_qty')), 0.0)
+ self.assertEqual(sum(move3.stock_valuation_layer_ids.mapped('value')), 500.0)
+ self.assertEqual(sum(move3.stock_valuation_layer_ids.mapped('remaining_qty')), 10.0)
+
+ # account values after vacuum
+ valuation_aml = self._get_stock_valuation_move_lines()
+ vacuum2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(vacuum2_valuation_aml.debit, 0)
+ # there is still 10@8 to compensate with 10@25 -> 170 to credit more in the valuation account
+ self.assertEqual(vacuum2_valuation_aml.credit, 170)
+ output_aml = self._get_stock_output_move_lines()
+ vacuum2_output_aml = output_aml[-1]
+ self.assertEqual(vacuum2_output_aml.debit, 170)
+ self.assertEqual(vacuum2_output_aml.credit, 0)
+
+ # ---------------------------------------------------------------------
+ # Ending
+ # ---------------------------------------------------------------------
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.value_svl, 250)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 1100)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 1100)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 850)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 850)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ def test_fifo_negative_2(self):
+ """ Receives 10 units, send more, the extra quantity should be valued at the last fifo
+ price, running the vacuum should not do anything. Receive 2 units at the price the two
+ extra units were sent, check that no accounting entries are created.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # Receive 10@10
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ # account values for move1
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move1_valuation_aml.debit, 100)
+ self.assertEqual(move1_valuation_aml.credit, 0)
+ input_aml = self._get_stock_input_move_lines()
+ move1_input_aml = input_aml[-1]
+ self.assertEqual(move1_input_aml.debit, 0)
+ self.assertEqual(move1_input_aml.credit, 100)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Send 12
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': '12 out (2 negative)',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ 'price_unit': 0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 12.0,
+ })]
+ })
+ move2._action_confirm()
+ move2._action_done()
+
+ # stock values for move2
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -120.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, -2.0)
+
+ # account values for move2
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move2_valuation_aml.debit, 0)
+ self.assertEqual(move2_valuation_aml.credit, 120)
+ output_aml = self._get_stock_output_move_lines()
+ move2_output_aml = output_aml[-1]
+ self.assertEqual(move2_output_aml.debit, 120)
+ self.assertEqual(move2_output_aml.credit, 0)
+
+ self.assertEqual(len(move2.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Run the vacuum
+ # ---------------------------------------------------------------------
+ self.product1._run_fifo_vacuum()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -120.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, -2.0)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertEqual(len(move2.account_move_ids), 1)
+
+ self.assertEqual(self.product1.quantity_svl, -2)
+ self.assertEqual(self.product1.value_svl, -20)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 100)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 100)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 120)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ # Now receive exactly the extra units at exactly the price sent, no
+ # accounting entries should be created after the vacuum.
+ # ---------------------------------------------------------------------
+ # Receive 2@10
+ # ---------------------------------------------------------------------
+ move3 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 2.0,
+ })]
+ })
+ move3._action_confirm()
+ move3._action_done()
+
+ # ---------------------------------------------------------------------
+ # Ending
+ # ---------------------------------------------------------------------
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -120.0)
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
+ self.assertEqual(move3.stock_valuation_layer_ids.value, 20)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertEqual(len(move2.account_move_ids), 1)
+ self.assertEqual(len(move3.account_move_ids), 1) # the created account move is due to the receipt
+
+ # nothing should have changed in the accounting regarding the output
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 120)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 120)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 120)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 120)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ def test_fifo_negative_3(self):
+ """ Receives 10 units, send 10 units, then send more: the extra quantity should be valued
+ at the last fifo price, running the vacuum should not do anything.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # Receive 10@10
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ # account values for move1
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move1_valuation_aml.debit, 100)
+ self.assertEqual(move1_valuation_aml.credit, 0)
+ input_aml = self._get_stock_input_move_lines()
+ move1_input_aml = input_aml[-1]
+ self.assertEqual(move1_input_aml.debit, 0)
+ self.assertEqual(move1_input_aml.credit, 100)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Send 10
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': '10 out',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move2._action_confirm()
+ move2._action_done()
+
+ # stock values for move2
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -100.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0.0)
+
+ # account values for move2
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move2_valuation_aml.debit, 0)
+ self.assertEqual(move2_valuation_aml.credit, 100)
+ output_aml = self._get_stock_output_move_lines()
+ move2_output_aml = output_aml[-1]
+ self.assertEqual(move2_output_aml.debit, 100)
+ self.assertEqual(move2_output_aml.credit, 0)
+
+ self.assertEqual(len(move2.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Send 21
+ # ---------------------------------------------------------------------
+ # FIXME sle last fifo price not updated on the product?
+ move3 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 21.0,
+ 'price_unit': 0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 21.0,
+ })]
+ })
+ move3._action_confirm()
+ move3._action_done()
+
+ # stock values for move3
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -210.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -21.0)
+
+ # account values for move3
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move3_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move3_valuation_aml.debit, 0)
+ self.assertEqual(move3_valuation_aml.credit, 210)
+ output_aml = self._get_stock_output_move_lines()
+ move3_output_aml = output_aml[-1]
+ self.assertEqual(move3_output_aml.debit, 210)
+ self.assertEqual(move3_output_aml.credit, 0)
+
+ self.assertEqual(len(move3.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Run the vacuum
+ # ---------------------------------------------------------------------
+ self.product1._run_fifo_vacuum()
+ self.assertEqual(len(move3.account_move_ids), 1)
+
+ # the vacuum shouldn't do anything in this case
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -100.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -210.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -21.0)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertEqual(len(move2.account_move_ids), 1)
+ self.assertEqual(len(move3.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Ending
+ # ---------------------------------------------------------------------
+ self.assertEqual(self.product1.quantity_svl, -21)
+ self.assertEqual(self.product1.value_svl, -210)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 100)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 100)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 310)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 310)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ def test_fifo_add_move_in_done_picking_1(self):
+ """ The flow is:
+
+ product2 std price = 20
+ IN01 10@10 product1
+ IN01 10@20 product2
+ IN01 correction 10@20 -> 11@20 (product2)
+ DO01 11 product2
+ DO02 1 product2
+ DO02 correction 1 -> 2 (negative stock)
+ IN03 2@30 product2
+ vacuum
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # Receive 10@10
+ # ---------------------------------------------------------------------
+ receipt = self.env['stock.picking'].create({
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_in').id,
+ })
+
+ move1 = self.env['stock.move'].create({
+ 'picking_id': receipt.id,
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ # ---------------------------------------------------------------------
+ # Add a stock move, receive 10@20 of another product
+ # ---------------------------------------------------------------------
+ self.product2.categ_id.property_cost_method = 'fifo'
+ self.product2.standard_price = 20
+ move2 = self.env['stock.move'].create({
+ 'picking_id': receipt.id,
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product2.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product2.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move2._action_done()
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 200.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.unit_cost, 20.0)
+
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product2.quantity_svl, 10)
+ self.assertEqual(self.product2.value_svl, 200)
+
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 300)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 0)
+
+ # ---------------------------------------------------------------------
+ # Edit the previous stock move, receive 11
+ # ---------------------------------------------------------------------
+ move2.quantity_done = 11
+
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), 220.0) # after correction, the move should be valued at 11@20
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('remaining_qty')), 11.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.sorted()[-1].unit_cost, 20.0)
+
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 0)
+
+ # ---------------------------------------------------------------------
+ # Send 11 product 2
+ # ---------------------------------------------------------------------
+ delivery = self.env['stock.picking'].create({
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'partner_id': self.partner.id,
+ 'picking_type_id': self.env.ref('stock.picking_type_out').id,
+ })
+ move3 = self.env['stock.move'].create({
+ 'picking_id': delivery.id,
+ 'name': '11 out',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product2.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 11.0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product2.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 11.0,
+ })]
+ })
+
+ move3._action_confirm()
+ move3._action_done()
+
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -220.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.unit_cost, 20.0)
+ self.assertEqual(self.product2.qty_available, 0)
+ self.assertEqual(self.product2.quantity_svl, 0)
+
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 220)
+
+ # ---------------------------------------------------------------------
+ # Add one move of product 2, this'll make some negative stock.
+ # ---------------------------------------------------------------------
+
+ # FIXME: uncomment when negative stock is handled
+ #move4 = self.env['stock.move'].create({
+ # 'picking_id': delivery.id,
+ # 'name': '1 out',
+ # 'location_id': self.stock_location.id,
+ # 'location_dest_id': self.customer_location.id,
+ # 'product_id': self.product2.id,
+ # 'product_uom': self.uom_unit.id,
+ # 'product_uom_qty': 1.0,
+ # 'state': 'done', # simulate default_get override
+ # 'move_line_ids': [(0, 0, {
+ # 'product_id': self.product2.id,
+ # 'location_id': self.stock_location.id,
+ # 'location_dest_id': self.customer_location.id,
+ # 'product_uom_id': self.uom_unit.id,
+ # 'qty_done': 1.0,
+ # })]
+ #})
+ #self.assertEqual(move4.value, -20.0)
+ #self.assertEqual(move4.remaining_qty, -1.0)
+ #self.assertEqual(move4.price_unit, -20.0)
+
+ #self.assertEqual(self.product2.qty_available, -1)
+
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 240)
+
+ ## ---------------------------------------------------------------------
+ ## edit the created move, add 1
+ ## ---------------------------------------------------------------------
+ #move4.quantity_done = 2
+
+ #self.assertEqual(self.product2.qty_available, -2)
+ #self.assertEqual(move4.value, -40.0)
+ #self.assertEqual(move4.remaining_qty, -2.0)
+ #self.assertEqual(move4.price_unit, -20.0)
+
+ #self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ #self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 320) # 10*10 + 11*20
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 260)
+ #self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 260)
+ #self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ #self.env['stock.move']._run_fifo_vacuum()
+
+ #self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ #self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 320) # 10*10 + 11*20
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320)
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 260)
+ #self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 260)
+ #self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ ## ---------------------------------------------------------------------
+ ## receive 2 products 2 @ 30
+ ## ---------------------------------------------------------------------
+ #move5 = self.env['stock.move'].create({
+ # 'picking_id': receipt.id,
+ # 'name': '10 in',
+ # 'location_id': self.supplier_location.id,
+ # 'location_dest_id': self.stock_location.id,
+ # 'product_id': self.product2.id,
+ # 'product_uom': self.uom_unit.id,
+ # 'product_uom_qty': 2.0,
+ # 'price_unit': 30,
+ # 'move_line_ids': [(0, 0, {
+ # 'product_id': self.product2.id,
+ # 'location_id': self.supplier_location.id,
+ # 'location_dest_id': self.stock_location.id,
+ # 'product_uom_id': self.uom_unit.id,
+ # 'qty_done': 2.0,
+ # })]
+ #})
+ #move5._action_confirm()
+ #move5._action_done()
+
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 380)
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 260)
+
+ ## ---------------------------------------------------------------------
+ ## run vacuum
+ ## ---------------------------------------------------------------------
+ #self.env['stock.move']._run_fifo_vacuum()
+
+ #self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ #self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 380) # 10*10 + 11*20
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 380)
+ #self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 280) # 260/
+ #self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 280)
+ #self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ #self.assertEqual(self.product2.qty_available, 0)
+ #self.assertEqual(self.product2.stock_value, 0)
+ #self.assertEqual(move4.remaining_value, 0)
+ #self.assertEqual(move4.value, -60) # after correction, the move is valued -(2*30)
+
+ def test_fifo_add_moveline_in_done_move_1(self):
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # Receive 10@10
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+
+ # ---------------------------------------------------------------------
+ # Add a new move line to receive 10 more
+ # ---------------------------------------------------------------------
+ self.assertEqual(len(move1.move_line_ids), 1)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 10,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('value')), 200.0)
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('remaining_qty')), 20.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.sorted()[-1].unit_cost, 10.0)
+
+ self.assertEqual(len(move1.account_move_ids), 2)
+
+ self.assertEqual(self.product1.quantity_svl, 20)
+ self.assertEqual(self.product1.value_svl, 200)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 200)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 200)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 0)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ def test_fifo_edit_done_move1(self):
+ """ Increase OUT done move while quantities are available.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # Receive 10@10
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': 'receive 10@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ # account values for move1
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move1_valuation_aml.debit, 100)
+ self.assertEqual(move1_valuation_aml.credit, 0)
+ input_aml = self._get_stock_input_move_lines()
+ move1_input_aml = input_aml[-1]
+ self.assertEqual(move1_input_aml.debit, 0)
+ self.assertEqual(move1_input_aml.credit, 100)
+
+ self.assertEqual(len(move1.account_move_ids), 1)
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 10.0)
+ self.assertEqual(self.product1.value_svl, 100)
+
+ # ---------------------------------------------------------------------
+ # Receive 10@12
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': 'receive 10@12',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 12,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move2._action_confirm()
+ move2._action_done()
+
+ # stock values for move2
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 120.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.unit_cost, 12.0)
+
+ # account values for move2
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move2_valuation_aml.debit, 120)
+ self.assertEqual(move2_valuation_aml.credit, 0)
+ input_aml = self._get_stock_input_move_lines()
+ move2_input_aml = input_aml[-1]
+ self.assertEqual(move2_input_aml.debit, 0)
+ self.assertEqual(move2_input_aml.credit, 120)
+
+ self.assertEqual(len(move2.account_move_ids), 1)
+
+ self.assertAlmostEqual(self.product1.qty_available, 20.0)
+ self.assertAlmostEqual(self.product1.quantity_svl, 20.0)
+ self.assertEqual(self.product1.value_svl, 220)
+
+ # ---------------------------------------------------------------------
+ # Send 8
+ # ---------------------------------------------------------------------
+ move3 = self.env['stock.move'].create({
+ 'name': '12 out (2 negative)',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 8.0,
+ 'price_unit': 0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 8.0,
+ })]
+ })
+ move3._action_confirm()
+ move3._action_done()
+
+ # stock values for move3
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -80.0)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, 0.0)
+
+ # account values for move3
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move3_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move3_valuation_aml.debit, 0) # FIXME sle shiiiiiiieeeeet with_context out move doesn't work?
+ output_aml = self._get_stock_output_move_lines()
+ move3_output_aml = output_aml[-1]
+ self.assertEqual(move3_output_aml.debit, 80)
+ self.assertEqual(move3_output_aml.credit, 0)
+
+ self.assertEqual(len(move3.account_move_ids), 1)
+
+ self.assertAlmostEqual(self.product1.qty_available, 12.0)
+ self.assertAlmostEqual(self.product1.quantity_svl, 12.0)
+ self.assertEqual(self.product1.value_svl, 140)
+
+ # ---------------------------------------------------------------------
+ # Edit last move, send 14 instead
+ # it should send 2@10 and 4@12
+ # ---------------------------------------------------------------------
+ move3.quantity_done = 14
+ self.assertEqual(move3.product_qty, 14)
+ # old value: -80 -(8@10)
+ # new value: -148 => -(10@10 + 4@12)
+ self.assertEqual(sum(move3.stock_valuation_layer_ids.mapped('value')), -148)
+
+ # account values for move3
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move3_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move3_valuation_aml.debit, 0)
+ output_aml = self._get_stock_output_move_lines()
+ move3_output_aml = output_aml[-1]
+ self.assertEqual(move3_output_aml.debit, 68)
+ self.assertEqual(move3_output_aml.credit, 0)
+
+ self.assertEqual(len(move3.account_move_ids), 2)
+
+ self.assertEqual(self.product1.value_svl, 72)
+
+ # ---------------------------------------------------------------------
+ # Ending
+ # ---------------------------------------------------------------------
+ self.assertEqual(self.product1.qty_available, 6)
+ self.assertAlmostEqual(self.product1.quantity_svl, 6.0)
+ self.assertEqual(self.product1.value_svl, 72)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
+ self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 220)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 220)
+ self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 148)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 148)
+ self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0)
+
+ def test_fifo_edit_done_move2(self):
+ """ Decrease, then increase OUT done move while quantities are available.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # Receive 10@10
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': 'receive 10@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1._action_confirm()
+ move1._action_done()
+
+ # stock values for move1
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 10.0)
+ self.assertEqual(move1.stock_valuation_layer_ids.unit_cost, 10.0)
+
+ # ---------------------------------------------------------------------
+ # Send 10
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': '12 out (2 negative)',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move2._action_confirm()
+ move2._action_done()
+
+ # stock values for move2
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -100.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0.0)
+
+ # ---------------------------------------------------------------------
+ # Actually, send 8 in the last move
+ # ---------------------------------------------------------------------
+ move2.quantity_done = 8
+
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -80.0) # the move actually sent 8@10
+
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 2)
+
+ self.product1.qty_available = 2
+ self.product1.value_svl = 20
+ self.product1.quantity_svl = 2
+
+ # ---------------------------------------------------------------------
+ # Actually, send 10 in the last move
+ # ---------------------------------------------------------------------
+ move2.quantity_done = 10
+
+ self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -100.0) # the move actually sent 10@10
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
+
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.value_svl, 0)
+
+ def test_fifo_standard_price_upate_1(self):
+ product = self.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self._make_in_move(product, 3, unit_cost=17)
+ self._make_in_move(product, 1, unit_cost=23)
+ self._make_out_move(product, 3)
+ self.assertEqual(product.standard_price, 23)
+
+ def test_fifo_standard_price_upate_2(self):
+ product = self.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self._make_in_move(product, 5, unit_cost=17)
+ self._make_in_move(product, 1, unit_cost=23)
+ self._make_out_move(product, 4)
+ self.assertEqual(product.standard_price, 17)
+
+ def test_fifo_standard_price_upate_3(self):
+ """Standard price must be set on move in if no product and if first move."""
+ product = self.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': self.env.ref('product.product_category_all').id,
+ })
+ product.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self._make_in_move(product, 5, unit_cost=17)
+ self._make_in_move(product, 1, unit_cost=23)
+ self.assertEqual(product.standard_price, 17)
+ self._make_out_move(product, 4)
+ self.assertEqual(product.standard_price, 17)
+ self._make_out_move(product, 1)
+ self.assertEqual(product.standard_price, 23)
+ self._make_out_move(product, 1)
+ self.assertEqual(product.standard_price, 23)
+ self._make_in_move(product, 1, unit_cost=77)
+ self.assertEqual(product.standard_price, 77)
+
+ def test_average_perpetual_1(self):
+ # http://accountingexplained.com/financial/inventories/avco-method
+ self.product1.categ_id.property_cost_method = 'average'
+
+ # Beginning Inventory: 60 units @ 15.00 per unit
+ move1 = self.env['stock.move'].create({
+ 'name': '60 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 60.0,
+ 'price_unit': 15,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 60.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 900.0)
+
+ # Purchase 140 units @ 15.50 per unit
+ move2 = self.env['stock.move'].create({
+ 'name': '140 units @ 15.50 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 140.0,
+ 'price_unit': 15.50,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 140.0
+ move2._action_done()
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 2170.0)
+
+ # Sale 190 units @ 15.35 per unit
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 190 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 190.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 190.0
+ move3._action_done()
+
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -2916.5)
+
+ # Purchase 70 units @ $16.00 per unit
+ move4 = self.env['stock.move'].create({
+ 'name': '70 units @ $16.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 70.0,
+ 'price_unit': 16.00,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 70.0
+ move4._action_done()
+
+ self.assertEqual(move4.stock_valuation_layer_ids.value, 1120.0)
+
+ # Sale 30 units @ $19.50 per unit
+ move5 = self.env['stock.move'].create({
+ 'name': '30 units @ $19.50 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 30.0,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 30.0
+ move5._action_done()
+
+ self.assertEqual(move5.stock_valuation_layer_ids.value, -477.5)
+
+ # Receives 10 units but assign them to an owner, the valuation should not be impacted.
+ move6 = self.env['stock.move'].create({
+ 'name': '10 units to an owner',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 99,
+ })
+ move6._action_confirm()
+ move6._action_assign()
+ move6.move_line_ids.owner_id = self.owner1.id
+ move6.move_line_ids.qty_done = 10.0
+ move6._action_done()
+
+ self.assertEqual(move6.stock_valuation_layer_ids.value, 0)
+
+ # Sale 50 units @ $19.50 per unit (no stock anymore)
+ move7 = self.env['stock.move'].create({
+ 'name': '50 units @ $19.50 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 50.0,
+ })
+ move7._action_confirm()
+ move7._action_assign()
+ move7.move_line_ids.qty_done = 50.0
+ move7._action_done()
+
+ self.assertEqual(move7.stock_valuation_layer_ids.value, -796.0)
+ self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
+ self.assertAlmostEqual(self.product1.value_svl, 0.0)
+
+ def test_average_perpetual_2(self):
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 10 units at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+ self.assertEqual(self.product1.standard_price, 10)
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Receive 10 units at 15',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 15,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+ self.assertEqual(self.product1.standard_price, 12.5)
+
+ move3 = self.env['stock.move'].create({
+ 'name': 'Deliver 15 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 15.0
+ move3._action_done()
+ self.assertEqual(self.product1.standard_price, 12.5)
+
+ move4 = self.env['stock.move'].create({
+ 'name': 'Deliver 10 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 10.0
+ move4._action_done()
+ # note: 5 units were sent estimated at 12.5 (negative stock)
+ self.assertEqual(self.product1.standard_price, 12.5)
+ self.assertEqual(self.product1.quantity_svl, -5)
+ self.assertEqual(self.product1.value_svl, -62.5)
+
+ move2.move_line_ids.qty_done = 20
+ # incrementing the receipt triggered the vacuum, the negative stock is corrected
+ self.assertEqual(self.product1.stock_valuation_layer_ids[-1].value, -12.5)
+
+ self.assertEqual(self.product1.quantity_svl, 5)
+ self.assertEqual(self.product1.value_svl, 75)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_average_perpetual_3(self):
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 10 units at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Receive 10 units at 15',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 15,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ move3 = self.env['stock.move'].create({
+ 'name': 'Deliver 15 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 15.0
+ move3._action_done()
+
+ move4 = self.env['stock.move'].create({
+ 'name': 'Deliver 10 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 10.0
+ move4._action_done()
+ move2.move_line_ids.qty_done = 0
+ self.assertEqual(self.product1.value_svl, -187.5)
+
+ def test_average_perpetual_4(self):
+ """receive 1@10, receive 1@5 insteadof 3@5"""
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1._action_done()
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Receive 3 units at 5',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ 'price_unit': 5,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 1.0
+ move2._action_done()
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 2.0)
+ self.assertAlmostEqual(self.product1.standard_price, 7.5)
+
+ def test_average_perpetual_5(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
+ self.assertAlmostEqual(self.product1.value_svl, 0.0)
+
+ def test_average_perpetual_6(self):
+ """ Batch validation of moves """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Receive 1 units at 5',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 5,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 1.0
+
+ # Receive both at the same time
+ (move1 | move2)._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 7.5)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertEqual(self.product1.value_svl, 15)
+
+ def test_average_perpetual_7(self):
+ """ Test edit in the past. Receive 5@10, receive 10@20, edit the first move to receive
+ 15 instead.
+ """
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 5@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1.quantity_done = 5
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 10)
+ self.assertAlmostEqual(move1.stock_valuation_layer_ids.value, 50)
+ self.assertAlmostEqual(self.product1.quantity_svl, 5)
+ self.assertAlmostEqual(self.product1.value_svl, 50)
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'IN 10@20',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 20,
+ })
+ move2._action_confirm()
+ move2.quantity_done = 10
+ move2._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 16.67)
+ self.assertAlmostEqual(move2.stock_valuation_layer_ids.value, 200)
+ self.assertAlmostEqual(self.product1.quantity_svl, 15)
+ self.assertAlmostEqual(self.product1.value_svl, 250)
+
+ move1.move_line_ids.qty_done = 15
+
+ self.assertAlmostEqual(self.product1.standard_price, 14.0)
+ self.assertAlmostEqual(len(move1.stock_valuation_layer_ids), 2)
+ self.assertAlmostEqual(move1.stock_valuation_layer_ids.sorted()[-1].value, 100)
+ self.assertAlmostEqual(self.product1.quantity_svl, 25)
+ self.assertAlmostEqual(self.product1.value_svl, 350)
+
+ def test_average_perpetual_8(self):
+ """ Receive 1@10, then dropship 1@20, finally return the dropship. Dropship should not
+ impact the price.
+ """
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 1@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1.quantity_done = 1
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 10)
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'IN 1@20',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ 'price_unit': 20,
+ })
+ move2._action_confirm()
+ move2.quantity_done = 1
+ move2._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 10.0)
+
+ move3 = self.env['stock.move'].create({
+ 'name': 'IN 1@20',
+ 'location_id': self.customer_location.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1,
+ 'price_unit': 20,
+ })
+ move3._action_confirm()
+ move3.quantity_done = 1
+ move3._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 10.0)
+
+ def test_average_perpetual_9(self):
+ """ When a product has an available quantity of -5, edit an incoming shipment and increase
+ the received quantity by 5 units.
+ """
+ self.product1.categ_id.property_cost_method = 'average'
+ # receive 10
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 5@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1.quantity_done = 10
+ move1._action_done()
+
+ # deliver 15
+ move2 = self.env['stock.move'].create({
+ 'name': 'Deliver 10 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 15.0
+ move2._action_done()
+
+ # increase the receipt to 15
+ move1.move_line_ids.qty_done = 15
+
+ def test_average_stock_user(self):
+ """ deliver an average product as a stock user. """
+ self.product1.categ_id.property_cost_method = 'average'
+ # receive 10
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 5@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1.quantity_done = 10
+ move1._action_done()
+
+ # sell 15
+ move2 = self.env['stock.move'].create({
+ 'name': 'Deliver 10 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 15.0
+ move2.with_user(self.inventory_user)._action_done()
+
+ def test_average_negative_1(self):
+ """ Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
+ """
+ self.product1.categ_id.property_cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 10 units at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'send 20 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 20.0
+ move2._action_done()
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 2)
+ self.assertEqual(move2_valuation_aml.debit, 0)
+ self.assertEqual(move2_valuation_aml.credit, 200)
+
+ move2.quantity_done = 10.0
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 3)
+ self.assertEqual(move2_valuation_aml.debit, 100)
+ self.assertEqual(move2_valuation_aml.credit, 0)
+
+ move2.quantity_done = 11.0
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 4)
+ self.assertEqual(move2_valuation_aml.debit, 0)
+ self.assertEqual(move2_valuation_aml.credit, 10)
+
+ def test_average_negative_2(self):
+ """ Send goods that you don't have in stock and never received any unit.
+ """
+ self.product1.categ_id.property_cost_method = 'average'
+
+ # set a standard price
+ self.product1.standard_price = 99
+
+ # send 10 units that we do not have
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_average_negative_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1.quantity_done = 10.0
+ move1._action_done()
+ self.assertEqual(move1.stock_valuation_layer_ids.value, -990.0) # as no move out were done for this product, fallback on the standard price
+
+ def test_average_negative_3(self):
+ """ Send goods that you don't have in stock but received and send some units before.
+ """
+ self.product1.categ_id.property_cost_method= 'average'
+
+ # set a standard price
+ self.product1.standard_price = 99
+
+ # Receives 10 produts at 10
+ move1 = self.env['stock.move'].create({
+ 'name': '68 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+
+ # send 10 products
+ move2 = self.env['stock.move'].create({
+ 'name': 'Sale 94 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -100.0)
+ self.assertEqual(move2.stock_valuation_layer_ids.remaining_qty, 0.0) # unused in average move
+
+ # send 10 products again
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 94 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move3._action_confirm()
+ move3.quantity_done = 10.0
+ move3._action_done()
+
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -100.0) # as no move out were done for this product, fallback on latest cost
+
+ def test_average_negative_4(self):
+ self.product1.categ_id.property_cost_method = 'average'
+
+ # set a standard price
+ self.product1.standard_price = 99
+
+ # Receives 10 produts at 10
+ move1 = self.env['stock.move'].create({
+ 'name': '68 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+
+ def test_average_negative_5(self):
+ self.product1.categ_id.property_cost_method = 'average'
+
+ # in 10 @ 10
+ move1 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(self.product1.standard_price, 10)
+
+ # in 10 @ 20
+ move2 = self.env['stock.move'].create({
+ 'name': '10 units @ 20.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 20,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 200.0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ # send 5
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move3._action_confirm()
+ move3.quantity_done = 5.0
+ move3._action_done()
+
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -75.0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ # send 30
+ move4 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 30.0,
+ })
+ move4._action_confirm()
+ move4.quantity_done = 30.0
+ move4._action_done()
+
+ self.assertEqual(move4.stock_valuation_layer_ids.value, -450.0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ # in 20 @ 20
+ move5 = self.env['stock.move'].create({
+ 'name': '20 units @ 20.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20.0,
+ 'price_unit': 20,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 20.0
+ move5._action_done()
+ self.assertEqual(move5.stock_valuation_layer_ids.value, 400.0)
+
+ # Move 4 is now fixed, it initially sent 30@15 but the 5 last units were negative and estimated
+ # at 15 (1125). The new receipt made these 5 units sent at 20 (1500), so a 450 value is added
+ # to move4.
+ self.assertEqual(move4.stock_valuation_layer_ids[0].value, -450)
+
+ # So we have 5@20 in stock.
+ self.assertEqual(self.product1.quantity_svl, 5)
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.standard_price, 20)
+
+ # send 5 products to empty the inventory, the average price should not go to 0
+ move6 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move6._action_confirm()
+ move6.quantity_done = 5.0
+ move6._action_done()
+
+ self.assertEqual(move6.stock_valuation_layer_ids.value, -100.0)
+ self.assertEqual(self.product1.standard_price, 20)
+
+ # in 10 @ 10, the new average price should be 10
+ move7 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move7._action_confirm()
+ move7._action_assign()
+ move7.move_line_ids.qty_done = 10.0
+ move7._action_done()
+
+ self.assertEqual(move7.stock_valuation_layer_ids.value, 100.0)
+ self.assertEqual(self.product1.standard_price, 10)
+
+ def test_average_manual_1(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.categ_id.property_cost_method = 'average'
+ self.product1.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
+ self.assertAlmostEqual(self.product1.value_svl, 0.0)
+
+ def test_standard_perpetual_1(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.categ_id.property_cost_method = 'standard'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.qty_available, 1.0)
+ self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
+ self.assertAlmostEqual(self.product1.value_svl, 0.0)
+
+ def test_standard_manual_1(self):
+ ''' Set owner on incoming move => no valuation '''
+ self.product1.categ_id.property_cost_method = 'standard'
+ self.product1.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+ move1.move_line_ids.owner_id = self.owner1.id
+ move1._action_done()
+
+ self.assertAlmostEqual(self.product1.qty_available, 1.0)
+ self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
+ self.assertAlmostEqual(self.product1.value_svl, 0.0)
+
+ def test_standard_manual_2(self):
+ """Validate a receipt as a regular stock user."""
+ self.product1.categ_id.property_cost_method = 'standard'
+ self.product1.categ_id.property_valuation = 'manual_periodic'
+
+ self.product1.standard_price = 10.0
+
+ move1 = self.env['stock.move'].with_user(self.inventory_user).create({
+ 'name': 'IN 10 units',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ def test_standard_perpetual_2(self):
+ """Validate a receipt as a regular stock user."""
+ self.product1.categ_id.property_cost_method = 'standard'
+ self.product1.categ_id.property_valuation = 'real_time'
+
+ self.product1.standard_price = 10.0
+
+ move1 = self.env['stock.move'].with_user(self.inventory_user).create({
+ 'name': 'IN 10 units',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ def test_change_cost_method_1(self):
+ """ Change the cost method from FIFO to AVCO.
+ """
+ # ---------------------------------------------------------------------
+ # Use FIFO, make some operations
+ # ---------------------------------------------------------------------
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # receive 10@10
+ move1 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ # receive 10@15
+ move2 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 15,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ # sell 1
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 1.0
+ move3._action_done()
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 19)
+ self.assertEqual(self.product1.value_svl, 240)
+
+ # ---------------------------------------------------------------------
+ # Change the production valuation to AVCO
+ # ---------------------------------------------------------------------
+ self.product1.categ_id.property_cost_method = 'average'
+
+ # valuation should stay to ~240
+ self.assertAlmostEqual(self.product1.quantity_svl, 19)
+ self.assertAlmostEqual(self.product1.value_svl, 285, delta=0.03)
+
+ # an accounting entry should be created
+ # FIXME sle check it
+
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_change_cost_method_2(self):
+ """ Change the cost method from FIFO to standard.
+ """
+ # ---------------------------------------------------------------------
+ # Use FIFO, make some operations
+ # ---------------------------------------------------------------------
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ # receive 10@10
+ move1 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+
+ # receive 10@15
+ move2 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 15,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2._action_done()
+
+ # sell 1
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 1.0
+ move3._action_done()
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 19)
+ self.assertEqual(self.product1.value_svl, 240)
+
+ # ---------------------------------------------------------------------
+ # Change the production valuation to AVCO
+ # ---------------------------------------------------------------------
+ self.product1.categ_id.property_cost_method = 'standard'
+
+ # valuation should stay to ~240
+ self.assertAlmostEqual(self.product1.value_svl, 285, delta=0.03)
+ self.assertAlmostEqual(self.product1.quantity_svl, 19)
+
+ # no accounting entry should be created
+ # FIXME sle check it
+
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_fifo_sublocation_valuation_1(self):
+ """ Set the main stock as a view location. Receive 2 units of a
+ product, put 1 unit in an internal sublocation and the second
+ one in a scrap sublocation. Only a single unit, the one in the
+ internal sublocation, should be valued. Then, send these two
+ quants to a customer, only the one in the internal location
+ should be valued.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ view_location = self.env['stock.location'].create({'name': 'view', 'usage': 'view'})
+ subloc1 = self.env['stock.location'].create({
+ 'name': 'internal',
+ 'usage': 'internal',
+ 'location_id': view_location.id,
+ })
+ # sane settings for a scrap location, company_id doesn't matter
+ subloc2 = self.env['stock.location'].create({
+ 'name': 'scrap',
+ 'usage': 'inventory',
+ 'location_id': view_location.id,
+ 'scrap_location': True,
+ })
+
+ move1 = self.env['stock.move'].create({
+ 'name': '2 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ move1.write({'move_line_ids': [
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': subloc1.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': subloc2.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ ]})
+
+ move1._action_done()
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 10)
+ self.assertEqual(move1.stock_valuation_layer_ids.remaining_qty, 1)
+ self.assertAlmostEqual(self.product1.qty_available, 0.0)
+ self.assertAlmostEqual(self.product1.quantity_svl, 1.0)
+ self.assertEqual(self.product1.value_svl, 10)
+ self.assertTrue(len(move1.account_move_ids), 1)
+
+ move2 = self.env['stock.move'].create({
+ 'name': '2 units out',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+
+ move2.write({'move_line_ids': [
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': subloc1.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': subloc2.id,
+ 'location_dest_id': self.supplier_location.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ ]})
+ move2._action_done()
+ self.assertEqual(move2.stock_valuation_layer_ids.value, -10)
+
+ def test_move_in_or_out(self):
+ """ Test a few combination of move and their move lines and
+ check their valuation. A valued move should be IN or OUT.
+ Creating a move that is IN and OUT should be forbidden.
+ """
+ # an internal move should be considered as OUT if any of its move line
+ # is moved in a scrap location
+ scrap = self.env['stock.location'].create({
+ 'name': 'scrap',
+ 'usage': 'inventory',
+ 'location_id': self.stock_location.id,
+ 'scrap_location': True,
+ })
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'internal but out move',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.write({'move_line_ids': [
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': scrap.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ ]})
+ self.assertEqual(move1._is_out(), True)
+
+ # a move should be considered as invalid if some of its move lines are
+ # entering the company and some are leaving
+ customer1 = self.env['stock.location'].create({
+ 'name': 'customer',
+ 'usage': 'customer',
+ 'location_id': self.stock_location.id,
+ })
+ supplier1 = self.env['stock.location'].create({
+ 'name': 'supplier',
+ 'usage': 'supplier',
+ 'location_id': self.stock_location.id,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'internal but in and out move',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.write({'move_line_ids': [
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': customer1.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ (0, None, {
+ 'product_id': self.product1.id,
+ 'qty_done': 1,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': customer1.id,
+ 'product_uom_id': self.uom_unit.id
+ }),
+ ]})
+ self.assertEqual(move2._is_in(), True)
+ self.assertEqual(move2._is_out(), True)
+ with self.assertRaises(UserError):
+ move2._action_done()
+
+ def test_at_date_standard_1(self):
+ self.product1.categ_id.property_cost_method = 'standard'
+
+ now = Datetime.now()
+ date1 = now - timedelta(days=8)
+ date2 = now - timedelta(days=7)
+ date3 = now - timedelta(days=6)
+ date4 = now - timedelta(days=5)
+ date5 = now - timedelta(days=4)
+ date6 = now - timedelta(days=3)
+ date7 = now - timedelta(days=2)
+ date8 = now - timedelta(days=1)
+
+ # set the standard price to 10
+ self.product1.standard_price = 10.0
+
+ # receive 10
+ move1 = self.env['stock.move'].create({
+ 'name': 'in 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10
+ move1._action_done()
+ move1.date = date2
+ move1.stock_valuation_layer_ids._write({'create_date': date2})
+
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.value_svl, 100)
+
+ # receive 20
+ move2 = self.env['stock.move'].create({
+ 'name': 'in 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 20
+ move2._action_done()
+ move2.date = date3
+ move2.stock_valuation_layer_ids._write({'create_date': date3})
+
+ self.assertEqual(self.product1.quantity_svl, 30)
+ self.assertEqual(self.product1.value_svl, 300)
+
+ # send 15
+ move3 = self.env['stock.move'].create({
+ 'name': 'out 10',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 15
+ move3._action_done()
+ move3.date = date4
+ move3.stock_valuation_layer_ids._write({'create_date': date4})
+
+ self.assertEqual(self.product1.quantity_svl, 15)
+ self.assertEqual(self.product1.value_svl, 150)
+
+ # set the standard price to 5
+ self.product1.standard_price = 5
+ self.product1.stock_valuation_layer_ids.sorted()[-1]._write({'create_date': date5})
+
+ self.assertEqual(self.product1.quantity_svl, 15)
+ self.assertEqual(self.product1.value_svl, 75)
+
+ # send 20
+ move4 = self.env['stock.move'].create({
+ 'name': 'out 10',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 20
+ move4._action_done()
+ move4.date = date6
+ move4.stock_valuation_layer_ids._write({'create_date': date6})
+
+ self.assertEqual(self.product1.quantity_svl, -5)
+ self.assertEqual(self.product1.value_svl, -25)
+
+ # set the standard price to 7.5
+ self.product1.standard_price = 7.5
+ self.product1.stock_valuation_layer_ids.sorted()[-1]._write({'create_date': date7})
+
+ # receive 100
+ move5 = self.env['stock.move'].create({
+ 'name': 'in 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 100
+ move5._action_done()
+ move5.date = date8
+ move5.stock_valuation_layer_ids._write({'create_date': date8})
+
+ self.assertEqual(self.product1.quantity_svl, 95)
+ self.assertEqual(self.product1.value_svl, 712.5)
+
+ # Quantity available at date
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).quantity_svl, 0)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 10)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).quantity_svl, 30)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).quantity_svl, 15)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).quantity_svl, 15)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).quantity_svl, -5)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date7)).quantity_svl, -5)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).quantity_svl, 95)
+
+ # Valuation at date
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).value_svl, 0)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 100)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).value_svl, 300)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).value_svl, 150)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).value_svl, 75)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, -25)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).value_svl, 712.5)
+
+ # edit the done quantity of move1, decrease it
+ move1.quantity_done = 5
+
+ # the change is only visible right now
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 10)
+ self.assertEqual(self.product1.quantity_svl, 90)
+ # as when we decrease a quantity on a recreipt, we consider it as a out move with the price
+ # of today, the value will be decrease of 100 - (5*7.5)
+ self.assertEqual(sum(move1.stock_valuation_layer_ids.mapped('value')), 62.5)
+ # but the change is still only visible right now
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 100)
+
+ # edit move 4, send 15 instead of 20
+ move4.quantity_done = 15
+ # -(20*5) + (5*7.5)
+ self.assertEqual(sum(move4.stock_valuation_layer_ids.mapped('value')), -62.5)
+
+ # the change is only visible right now
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, -25)
+
+ self.assertEqual(self.product1.quantity_svl, 95)
+ self.assertEqual(self.product1.value_svl, 712.5)
+
+ def test_at_date_fifo_1(self):
+ """ Make some operations at different dates, check that the results of the valuation at
+ date wizard are consistent. Afterwards, edit the done quantity of some operations. The
+ valuation at date results should take these changes into account.
+ """
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ now = Datetime.now()
+ date1 = now - timedelta(days=8)
+ date2 = now - timedelta(days=7)
+ date3 = now - timedelta(days=6)
+ date4 = now - timedelta(days=5)
+ date5 = now - timedelta(days=4)
+ date6 = now - timedelta(days=3)
+
+ # receive 10@10
+ move1 = self.env['stock.move'].create({
+ 'name': 'in 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10
+ move1._action_done()
+ move1.date = date1
+ move1.stock_valuation_layer_ids._write({'create_date': date1})
+
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.value_svl, 100)
+
+ # receive 10@12
+ move2 = self.env['stock.move'].create({
+ 'name': 'in 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 12,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10
+ move2._action_done()
+ move2.date = date2
+ move2.stock_valuation_layer_ids._write({'create_date': date2})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 20)
+ self.assertEqual(self.product1.value_svl, 220)
+
+ # send 15
+ move3 = self.env['stock.move'].create({
+ 'name': 'out 10',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 15,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 15
+ move3._action_done()
+ move3.date = date3
+ move3.stock_valuation_layer_ids._write({'create_date': date3})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 5.0)
+ self.assertEqual(self.product1.value_svl, 60)
+
+ # send 20
+ move4 = self.env['stock.move'].create({
+ 'name': 'out 10',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 20
+ move4._action_done()
+ move4.date = date4
+ move4.stock_valuation_layer_ids._write({'create_date': date4})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, -15.0)
+ self.assertEqual(self.product1.value_svl, -180)
+
+ # receive 100@15
+ move5 = self.env['stock.move'].create({
+ 'name': 'in 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 100,
+ 'price_unit': 15,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 100
+ move5._action_done()
+ move5.date = date5
+ move5.stock_valuation_layer_ids._write({'create_date': date5})
+
+ # the vacuum ran
+ move4.stock_valuation_layer_ids.sorted()[-1]._write({'create_date': date6})
+
+ self.assertEqual(self.product1.quantity_svl, 85)
+ self.assertEqual(self.product1.value_svl, 1275)
+
+ # Edit the quantity done of move1, increase it.
+ move1.quantity_done = 20
+
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).quantity_svl, 10)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).value_svl, 100)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 20)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 220)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).quantity_svl, 5)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).value_svl, 60)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).quantity_svl, -15)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).value_svl, -180)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).quantity_svl, 85)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).value_svl, 1320)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).quantity_svl, 85)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date6)).value_svl, 1275)
+ self.assertEqual(self.product1.quantity_svl, 95)
+ self.assertEqual(self.product1.value_svl, 1375)
+
+ def test_at_date_fifo_2(self):
+ self.product1.categ_id.property_cost_method = 'fifo'
+
+ now = Datetime.now()
+ date1 = now - timedelta(days=8)
+ date2 = now - timedelta(days=7)
+ date3 = now - timedelta(days=6)
+ date4 = now - timedelta(days=5)
+ date5 = now - timedelta(days=4)
+
+ # receive 10@10
+ move1 = self.env['stock.move'].create({
+ 'name': 'in 10@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10
+ move1._action_done()
+ move1.date = date1
+ move1.stock_valuation_layer_ids._write({'create_date': date1})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 10.0)
+ self.assertEqual(self.product1.value_svl, 100)
+
+ # receive 10@15
+ move2 = self.env['stock.move'].create({
+ 'name': 'in 10@15',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 15,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 10
+ move2._action_done()
+ move2.date = date2
+ move2.stock_valuation_layer_ids._write({'create_date': date2})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 20.0)
+ self.assertEqual(self.product1.value_svl, 250)
+
+ # send 30
+ move3 = self.env['stock.move'].create({
+ 'name': 'out 30',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 30,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3.move_line_ids.qty_done = 30
+ move3._action_done()
+ move3.date = date3
+ move3.stock_valuation_layer_ids._write({'create_date': date3})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, -10.0)
+ self.assertEqual(self.product1.value_svl, -150)
+
+ # receive 10@20
+ move4 = self.env['stock.move'].create({
+ 'name': 'in 10@20',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 20,
+ })
+ move4._action_confirm()
+ move4._action_assign()
+ move4.move_line_ids.qty_done = 10
+ move4._action_done()
+ move4.date = date4
+ move3.stock_valuation_layer_ids.sorted()[-1]._write({'create_date': date4})
+ move4.stock_valuation_layer_ids._write({'create_date': date4})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
+ self.assertEqual(self.product1.value_svl, 0)
+
+ # receive 10@10
+ move5 = self.env['stock.move'].create({
+ 'name': 'in 10@10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10,
+ 'price_unit': 10,
+ })
+ move5._action_confirm()
+ move5._action_assign()
+ move5.move_line_ids.qty_done = 10
+ move5._action_done()
+ move5.date = date5
+ move5.stock_valuation_layer_ids._write({'create_date': date5})
+
+ self.assertAlmostEqual(self.product1.quantity_svl, 10.0)
+ self.assertEqual(self.product1.value_svl, 100)
+
+ # ---------------------------------------------------------------------
+ # ending: perpetual valuation
+ # ---------------------------------------------------------------------
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).quantity_svl, 10)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).value_svl, 100)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 20)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 250)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).quantity_svl, -10)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date3)).value_svl, -150)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).quantity_svl, 0)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date4)).value_svl, 0)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).quantity_svl, 10)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date5)).value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.value_svl, 100)
+
+ def test_inventory_fifo_1(self):
+ """ Make an inventory from a location with a company set, and ensure the product has a stock
+ value. When the product is sold, ensure there is no remaining quantity on the original move
+ and no stock value.
+ """
+ self.product1.standard_price = 15
+ self.product1.categ_id.property_cost_method = 'fifo'
+ inventory_location = self.product1.property_stock_inventory
+ inventory_location.company_id = self.env.company.id
+
+ # Start Inventory: 12 units
+ move1 = self.env['stock.move'].create({
+ 'name': 'Adjustment of 12 units',
+ 'location_id': inventory_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 12.0
+ move1._action_done()
+
+ self.assertAlmostEqual(move1.stock_valuation_layer_ids.value, 180.0)
+ self.assertAlmostEqual(move1.stock_valuation_layer_ids.remaining_qty, 12.0)
+ self.assertAlmostEqual(self.product1.value_svl, 180.0)
+
+ # Sell the 12 units
+ move2 = self.env['stock.move'].create({
+ 'name': 'Sell 12 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 12.0
+ move2._action_done()
+
+ self.assertAlmostEqual(move1.stock_valuation_layer_ids.remaining_qty, 0.0)
+ self.assertAlmostEqual(self.product1.value_svl, 0.0)
+
+ def test_at_date_average_1(self):
+ """ Set a company on the inventory loss, take items from there then put items there, check
+ the values and quantities at date.
+ """
+ now = Datetime.now()
+ date1 = now - timedelta(days=8)
+ date2 = now - timedelta(days=7)
+
+ self.product1.standard_price = 10
+ self.product1.product_tmpl_id.cost_method = 'average'
+ inventory_location = self.product1.property_stock_inventory
+ inventory_location.company_id = self.env.company.id
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Adjustment of 10 units',
+ 'location_id': inventory_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1._action_done()
+ move1.date = date1
+ move1.stock_valuation_layer_ids._write({'create_date': date1})
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Sell 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': inventory_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 5.0
+ move2._action_done()
+ move2.date = date2
+ move2.stock_valuation_layer_ids._write({'create_date': date2})
+
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).quantity_svl, 10)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).value_svl, 100)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 5)
+ self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 50)
diff --git a/addons/stock_account/tests/test_stockvaluationlayer.py b/addons/stock_account/tests/test_stockvaluationlayer.py
new file mode 100644
index 00000000..136bc967
--- /dev/null
+++ b/addons/stock_account/tests/test_stockvaluationlayer.py
@@ -0,0 +1,900 @@
+# -*- 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.addons.stock_account.tests.test_stockvaluation import _create_accounting_data
+from odoo.tests import Form, tagged
+from odoo.tests.common import SavepointCase, TransactionCase
+
+
+class TestStockValuationCommon(SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockValuationCommon, cls).setUpClass()
+ cls.stock_location = cls.env.ref('stock.stock_location_stock')
+ cls.customer_location = cls.env.ref('stock.stock_location_customers')
+ cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
+ cls.uom_unit = cls.env.ref('uom.product_uom_unit')
+ cls.product1 = cls.env['product.product'].create({
+ 'name': 'product1',
+ 'type': 'product',
+ 'categ_id': cls.env.ref('product.product_category_all').id,
+ })
+ cls.picking_type_in = cls.env.ref('stock.picking_type_in')
+ cls.picking_type_out = cls.env.ref('stock.picking_type_out')
+
+ def setUp(self):
+ super(TestStockValuationCommon, self).setUp()
+ # Counter automatically incremented by `_make_in_move` and `_make_out_move`.
+ self.days = 0
+
+ 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.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': quantity,
+ 'price_unit': unit_cost,
+ 'picking_type_id': self.picking_type_in.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.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': quantity,
+ 'picking_type_id': self.picking_type_out.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)
+
+ def _make_dropship_move(self, product, quantity, unit_cost=None):
+ dropshipped = self.env['stock.move'].create({
+ 'name': 'dropship %s units' % str(quantity),
+ 'product_id': product.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': quantity,
+ 'picking_type_id': self.picking_type_out.id,
+ })
+ if unit_cost:
+ dropshipped.price_unit = unit_cost
+ dropshipped._action_confirm()
+ dropshipped._action_assign()
+ dropshipped.move_line_ids.qty_done = quantity
+ dropshipped._action_done()
+ return dropshipped
+
+ def _make_return(self, move, quantity_to_return):
+ stock_return_picking = Form(self.env['stock.return.picking']\
+ .with_context(active_ids=[move.picking_id.id], active_id=move.picking_id.id, active_model='stock.picking'))
+ stock_return_picking = stock_return_picking.save()
+ stock_return_picking.product_return_moves.quantity = quantity_to_return
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.move_lines[0].move_line_ids[0].qty_done = quantity_to_return
+ return_pick._action_done()
+ return return_pick.move_lines
+
+
+class TestStockValuationStandard(TestStockValuationCommon):
+ def setUp(self):
+ super(TestStockValuationStandard, self).setUp()
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ def test_normal_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+
+ self.assertEqual(self.product1.value_svl, 50)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_change_in_past_increase_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 15
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_decrease_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_add_ml_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_increase_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_out_move(self.product1, 1)
+ move2.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 50)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_change_in_past_decrease_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_out_move(self.product1, 5)
+ move2.move_line_ids.qty_done = 1
+
+ self.assertEqual(self.product1.value_svl, 90)
+ self.assertEqual(self.product1.quantity_svl, 9)
+
+ def test_change_standard_price_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 15)
+
+ # change cost from 10 to 15
+ self.product1.standard_price = 15.0
+
+ self.assertEqual(self.product1.value_svl, 75)
+ self.assertEqual(self.product1.quantity_svl, 5)
+ self.assertEqual(self.product1.stock_valuation_layer_ids.sorted()[-1].description, 'Product value manually modified (from 10.0 to 15.0)')
+
+ def test_negative_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 10,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 50)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_dropship_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_dropship_move(self.product1, 10)
+
+ valuation_layers = self.product1.stock_valuation_layer_ids
+ self.assertEqual(len(valuation_layers), 2)
+ self.assertEqual(valuation_layers[0].value, 100)
+ self.assertEqual(valuation_layers[1].value, -100)
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_increase_dropship_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_dropship_move(self.product1, 10)
+ move1.move_line_ids.qty_done = 15
+
+ valuation_layers = self.product1.stock_valuation_layer_ids
+ self.assertEqual(len(valuation_layers), 4)
+ self.assertEqual(valuation_layers[0].value, 100)
+ self.assertEqual(valuation_layers[1].value, -100)
+ self.assertEqual(valuation_layers[2].value, 50)
+ self.assertEqual(valuation_layers[3].value, -50)
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_empty_stock_move_valorisation(self):
+ product1 = self.env['product.product'].create({
+ 'name': 'p1',
+ 'type': 'product',
+ })
+ product2 = self.env['product.product'].create({
+ 'name': 'p2',
+ 'type': 'product',
+ })
+ picking = self.env['stock.picking'].create({
+ 'picking_type_id': self.picking_type_in.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ })
+ for product in (product1, product2):
+ product.standard_price = 10
+ in_move = self.env['stock.move'].create({
+ 'name': 'in %s units @ %s per unit' % (2, str(10)),
+ 'product_id': product.id,
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 2,
+ 'price_unit': 10,
+ 'picking_type_id': self.picking_type_in.id,
+ 'picking_id': picking.id
+ })
+
+ picking.action_confirm()
+ # set quantity done only on one move
+ in_move.move_line_ids.qty_done = 2
+ res_dict = picking.button_validate()
+ wizard = self.env[(res_dict.get('res_model'))].with_context(res_dict.get('context')).browse(res_dict.get('res_id'))
+ res_dict_for_back_order = wizard.process()
+
+ self.assertTrue(product2.stock_valuation_layer_ids)
+ self.assertFalse(product1.stock_valuation_layer_ids)
+
+
+class TestStockValuationAVCO(TestStockValuationCommon):
+ def setUp(self):
+ super(TestStockValuationAVCO, self).setUp()
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+
+ def test_normal_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ self.assertEqual(self.product1.standard_price, 10)
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 100)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(move2.stock_valuation_layer_ids.value, 200)
+ move3 = self._make_out_move(self.product1, 15)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(move3.stock_valuation_layer_ids.value, -225)
+
+ self.assertEqual(self.product1.value_svl, 75)
+ self.assertEqual(self.product1.quantity_svl, 5)
+
+ def test_change_in_past_increase_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 15
+
+ self.assertEqual(self.product1.value_svl, 125)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_decrease_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move1.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_add_ml_in_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'move_id': move1.id,
+ 'product_id': move1.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 125)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.standard_price, 12.5)
+
+ def test_change_in_past_add_move_in_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)
+ move3 = self._make_out_move(self.product1, 15)
+ self.env['stock.move.line'].create({
+ 'product_id': move1.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move1.product_uom.id,
+ 'location_id': move1.location_id.id,
+ 'location_dest_id': move1.location_dest_id.id,
+ 'state': 'done',
+ 'picking_id': move1.picking_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 150)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_change_in_past_increase_out_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move3.move_line_ids.qty_done = 20
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_change_in_past_decrease_out_1(self):
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+ move3.move_line_ids.qty_done = 10
+
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 10)
+ self.assertEqual(self.product1.value_svl, 150)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_negative_1(self):
+ """ Ensures that, in AVCO, the `remaining_qty` field is computed and the vacuum is ran
+ when necessary.
+ """
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 30)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
+ move4 = self._make_in_move(self.product1, 10, unit_cost=30)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
+ move5 = self._make_in_move(self.product1, 10, unit_cost=40)
+
+ self.assertEqual(self.product1.value_svl, 400)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_negative_2(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.standard_price = 10
+ move1 = self._make_out_move(self.product1, 1, force_assign=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=15)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_negative_3(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_out_move(self.product1, 2, force_assign=True)
+ self.assertEqual(move1.stock_valuation_layer_ids.value, 0)
+ move2 = self._make_in_move(self.product1, 20, unit_cost=3.33)
+ self.assertEqual(move1.stock_valuation_layer_ids[1].value, -6.66)
+
+ self.assertEqual(self.product1.standard_price, 3.33)
+ self.assertEqual(self.product1.value_svl, 59.94)
+ self.assertEqual(self.product1.quantity_svl, 18)
+
+ def test_return_receipt_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+ move4 = self._make_return(move1, 1)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_return_delivery_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1, create_picking=True)
+ move4 = self._make_return(move3, 1)
+
+ self.assertEqual(self.product1.value_svl, 30)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 2)
+
+ def test_rereturn_receipt_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+ move4 = self._make_return(move1, 1) # -15, current avco
+ move5 = self._make_return(move4, 1) # +10, original move's price unit
+
+ self.assertEqual(self.product1.value_svl, 15)
+ self.assertEqual(self.product1.quantity_svl, 1)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 1)
+
+ def test_rereturn_delivery_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1, create_picking=True)
+ move4 = self._make_return(move3, 1)
+ move5 = self._make_return(move4, 1)
+
+ self.assertEqual(self.product1.value_svl, 15)
+ self.assertEqual(self.product1.quantity_svl, 1)
+ self.assertEqual(self.product1.standard_price, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 1)
+
+ def test_dropship_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_dropship_move(self.product1, 1, unit_cost=10)
+
+ self.assertEqual(self.product1.value_svl, 30)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ def test_rounding_slv_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=1.00)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=1.00)
+ move3 = self._make_in_move(self.product1, 1, unit_cost=1.01)
+
+ self.assertAlmostEqual(self.product1.value_svl, 3.01)
+
+ move_out = self._make_out_move(self.product1, 3, create_picking=True)
+
+ self.assertIn('Rounding Adjustment: -0.01', move_out.stock_valuation_layer_ids.description)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 1.00)
+
+ def test_rounding_slv_2(self):
+ self._make_in_move(self.product1, 1, unit_cost=1.02)
+ self._make_in_move(self.product1, 1, unit_cost=1.00)
+ self._make_in_move(self.product1, 1, unit_cost=1.00)
+
+ self.assertAlmostEqual(self.product1.value_svl, 3.02)
+
+ move_out = self._make_out_move(self.product1, 3, create_picking=True)
+
+ self.assertIn('Rounding Adjustment: +0.01', move_out.stock_valuation_layer_ids.description)
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+ self.assertEqual(self.product1.standard_price, 1.01)
+
+
+class TestStockValuationFIFO(TestStockValuationCommon):
+ def setUp(self):
+ super(TestStockValuationFIFO, self).setUp()
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+
+ def test_normal_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 15)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 5)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 5)
+
+ def test_negative_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 30)
+ self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
+ move4 = self._make_in_move(self.product1, 10, unit_cost=30)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
+ move5 = self._make_in_move(self.product1, 10, unit_cost=40)
+
+ self.assertEqual(self.product1.value_svl, 400)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_decrease_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move1.move_line_ids.qty_done = 10
+
+ self.assertEqual(self.product1.value_svl, 0)
+ self.assertEqual(self.product1.quantity_svl, 0)
+
+ def test_change_in_past_decrease_in_2(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 10)
+ move1.move_line_ids.qty_done = 10
+ move4 = self._make_in_move(self.product1, 20, unit_cost=15)
+
+ self.assertEqual(self.product1.value_svl, 150)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_increase_in_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ 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, 20)
+ move1.move_line_ids.qty_done = 20
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+
+ def test_change_in_past_increase_in_2(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=12)
+ move3 = self._make_out_move(self.product1, 15)
+ move4 = self._make_out_move(self.product1, 20)
+ move5 = self._make_in_move(self.product1, 100, unit_cost=15)
+ move1.move_line_ids.qty_done = 20
+
+ self.assertEqual(self.product1.value_svl, 1375)
+ self.assertEqual(self.product1.quantity_svl, 95)
+
+ def test_change_in_past_increase_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move3 = self._make_in_move(self.product1, 20, unit_cost=15)
+ move2.move_line_ids.qty_done = 25
+
+ self.assertEqual(self.product1.value_svl, 225)
+ self.assertEqual(self.product1.quantity_svl, 15)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 15)
+
+ def test_change_in_past_decrease_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 15)
+ move3 = self._make_in_move(self.product1, 20, unit_cost=15)
+ move2.move_line_ids.qty_done = 5
+
+ self.assertEqual(self.product1.value_svl, 450)
+ self.assertEqual(self.product1.quantity_svl, 35)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 35)
+
+ def test_change_in_past_add_ml_out_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 20, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10)
+ move3 = self._make_in_move(self.product1, 20, unit_cost=15)
+ self.env['stock.move.line'].create({
+ 'move_id': move2.id,
+ 'product_id': move2.product_id.id,
+ 'qty_done': 5,
+ 'product_uom_id': move2.product_uom.id,
+ 'location_id': move2.location_id.id,
+ 'location_dest_id': move2.location_dest_id.id,
+ })
+
+ self.assertEqual(self.product1.value_svl, 350)
+ self.assertEqual(self.product1.quantity_svl, 25)
+ self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 25)
+
+ def test_return_delivery_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_out_move(self.product1, 10, create_picking=True)
+ move3 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move4 = self._make_return(move2, 10)
+
+ self.assertEqual(self.product1.value_svl, 300)
+ self.assertEqual(self.product1.quantity_svl, 20)
+
+ def test_return_receipt_1(self):
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ 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)
+ move3 = self._make_return(move1, 2)
+
+ self.assertEqual(self.product1.value_svl, 280)
+ self.assertEqual(self.product1.quantity_svl, 18)
+
+ def test_rereturn_receipt_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10, create_picking=True)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+ move4 = self._make_return(move1, 1)
+ move5 = self._make_return(move4, 1)
+
+ self.assertEqual(self.product1.value_svl, 20)
+ self.assertEqual(self.product1.quantity_svl, 1)
+
+ def test_rereturn_delivery_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1, create_picking=True)
+ move4 = self._make_return(move3, 1)
+ move5 = self._make_return(move4, 1)
+
+ self.assertEqual(self.product1.value_svl, 10)
+ self.assertEqual(self.product1.quantity_svl, 1)
+
+ def test_dropship_1(self):
+ move1 = self._make_in_move(self.product1, 1, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 1, unit_cost=20)
+ move3 = self._make_dropship_move(self.product1, 1, unit_cost=10)
+
+ self.assertEqual(self.product1.value_svl, 30)
+ self.assertEqual(self.product1.quantity_svl, 2)
+ self.assertAlmostEqual(self.product1.standard_price, 10)
+
+
+class TestStockValuationChangeCostMethod(TestStockValuationCommon):
+ def test_standard_to_fifo_1(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.assertEqual(self.product1.value_svl, 190)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 5)
+ for svl in self.product1.stock_valuation_layer_ids.sorted()[-2:]:
+ self.assertEqual(svl.description, 'Costing method change for product category All: from standard to fifo.')
+
+ def test_standard_to_fifo_2(self):
+ """ We want the same result as `test_standard_to_fifo_1` but by changing the category of
+ `self.product1` to another one, not changing the current one.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 1)
+
+ cat2 = self.env['product.category'].create({'name': 'fifo'})
+ cat2.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id = cat2
+ self.assertEqual(self.product1.value_svl, 190)
+ self.assertEqual(self.product1.quantity_svl, 19)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 5)
+
+ def test_avco_to_fifo(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.assertEqual(self.product1.value_svl, 285)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ def test_fifo_to_standard(self):
+ """ The accounting impact of this cost method change is not neutral as we will use the last
+ fifo price as the new standard price.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.assertEqual(self.product1.value_svl, 380)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ def test_fifo_to_avco(self):
+ """ The accounting impact of this cost method change is not neutral as we will use the last
+ fifo price as the new AVCO.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'fifo'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.assertEqual(self.product1.value_svl, 380)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ def test_avco_to_standard(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ move1 = self._make_in_move(self.product1, 10, unit_cost=10)
+ move2 = self._make_in_move(self.product1, 10, unit_cost=20)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.assertEqual(self.product1.value_svl, 285)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+ def test_standard_to_avco(self):
+ """ The accounting impact of this cost method change is neutral.
+ """
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+
+ move1 = self._make_in_move(self.product1, 10)
+ move2 = self._make_in_move(self.product1, 10)
+ move3 = self._make_out_move(self.product1, 1)
+
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'average'
+ self.assertEqual(self.product1.value_svl, 190)
+ self.assertEqual(self.product1.quantity_svl, 19)
+
+
+class TestStockValuationChangeValuation(TestStockValuationCommon):
+ @classmethod
+ def setUpClass(cls):
+ super(TestStockValuationChangeValuation, cls).setUpClass()
+ cls.stock_input_account, cls.stock_output_account, cls.stock_valuation_account, cls.expense_account, cls.stock_journal = _create_accounting_data(cls.env)
+ cls.product1.categ_id.property_valuation = 'real_time'
+ cls.product1.write({
+ 'property_account_expense_id': cls.expense_account.id,
+ })
+ cls.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': cls.stock_input_account.id,
+ 'property_stock_account_output_categ_id': cls.stock_output_account.id,
+ 'property_stock_valuation_account_id': cls.stock_valuation_account.id,
+ 'property_stock_journal': cls.stock_journal.id,
+ })
+
+ def test_standard_manual_to_auto_1(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 0)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the replenish now that the category is perpetual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+ for svl in self.product1.stock_valuation_layer_ids.sorted()[-2:]:
+ self.assertEqual(svl.description, 'Valuation method change for product category All: from manual_periodic to real_time.')
+
+ def test_standard_manual_to_auto_2(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 0)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ cat2 = self.env['product.category'].create({'name': 'standard auto'})
+ cat2.property_cost_method = 'standard'
+ cat2.property_valuation = 'real_time'
+ cat2.write({
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+
+ # Try to change the product category with a `default_type` key in the context and
+ # check it doesn't break the account move generation.
+ self.product1.with_context(default_type='product').categ_id = cat2
+ self.assertEqual(self.product1.categ_id, cat2)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the replenish now that the category is perpetual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+
+ def test_standard_auto_to_manual_1(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the emptying now that the category is manual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 2)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+
+ def test_standard_auto_to_manual_2(self):
+ self.product1.product_tmpl_id.categ_id.property_cost_method = 'standard'
+ self.product1.product_tmpl_id.categ_id.property_valuation = 'real_time'
+ self.product1.product_tmpl_id.standard_price = 10
+ move1 = self._make_in_move(self.product1, 10)
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 1)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 1)
+
+ cat2 = self.env['product.category'].create({'name': 'fifo'})
+ cat2.property_cost_method = 'standard'
+ cat2.property_valuation = 'manual_periodic'
+ self.product1.with_context(debug=True).categ_id = cat2
+
+ self.assertEqual(self.product1.value_svl, 100)
+ self.assertEqual(self.product1.quantity_svl, 10)
+ # An accounting entry should only be created for the emptying now that the category is manual.
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids.mapped('account_move_id')), 2)
+ self.assertEqual(len(self.product1.stock_valuation_layer_ids), 3)
+