summaryrefslogtreecommitdiff
path: root/addons/sale_stock/tests
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/sale_stock/tests
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/sale_stock/tests')
-rw-r--r--addons/sale_stock/tests/__init__.py10
-rw-r--r--addons/sale_stock/tests/test_anglo_saxon_valuation.py1124
-rw-r--r--addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py145
-rw-r--r--addons/sale_stock/tests/test_sale_order_dates.py115
-rw-r--r--addons/sale_stock/tests/test_sale_stock.py1020
-rw-r--r--addons/sale_stock/tests/test_sale_stock_lead_time.py199
-rw-r--r--addons/sale_stock/tests/test_sale_stock_multicompany.py86
-rw-r--r--addons/sale_stock/tests/test_sale_stock_report.py57
8 files changed, 2756 insertions, 0 deletions
diff --git a/addons/sale_stock/tests/__init__.py b/addons/sale_stock/tests/__init__.py
new file mode 100644
index 00000000..f4f97c3a
--- /dev/null
+++ b/addons/sale_stock/tests/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_anglo_saxon_valuation
+from . import test_anglo_saxon_valuation_reconciliation
+from . import test_sale_stock
+from . import test_sale_stock_lead_time
+from . import test_sale_stock_report
+from . import test_sale_order_dates
+from . import test_sale_stock_multicompany
diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py
new file mode 100644
index 00000000..d9f55183
--- /dev/null
+++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py
@@ -0,0 +1,1124 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import Form, tagged
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.exceptions import UserError
+
+
+@tagged('post_install', '-at_install')
+class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.env.user.company_id.anglo_saxon_accounting = True
+
+ cls.product = cls.env['product.product'].create({
+ 'name': 'product',
+ 'type': 'product',
+ 'categ_id': cls.stock_account_product_categ.id,
+ })
+
+ def _inv_adj_two_units(self):
+ inventory = self.env['stock.inventory'].create({
+ 'name': 'test',
+ 'location_ids': [(4, self.company_data['default_warehouse'].lot_stock_id.id)],
+ 'product_ids': [(4, self.product.id)],
+ })
+ inventory.action_start()
+ self.env['stock.inventory.line'].create({
+ 'inventory_id': inventory.id,
+ 'location_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_id': self.product.id,
+ 'product_qty': 2,
+ })
+ inventory.action_validate()
+
+ def _so_and_confirm_two_units(self):
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 2.0,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': 12,
+ 'tax_id': False, # no love taxes amls
+ })],
+ })
+ sale_order.action_confirm()
+ return sale_order
+
+ def _fifo_in_one_eight_one_ten(self):
+ # Put two items in stock.
+ in_move_1 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 1,
+ 'price_unit': 8,
+ })
+ in_move_1._action_confirm()
+ in_move_1.quantity_done = 1
+ in_move_1._action_done()
+ in_move_2 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 1,
+ 'price_unit': 10,
+ })
+ in_move_2._action_confirm()
+ in_move_2.quantity_done = 1
+ in_move_2._action_done()
+
+ # -------------------------------------------------------------------------
+ # Standard Ordered
+ # -------------------------------------------------------------------------
+ def test_standard_ordered_invoice_pre_delivery(self):
+ """Standard price set to 10. Get 2 units in stock. Sale order 2@12. Standard price set
+ to 14. Invoice 2 without delivering. The amount in Stock OUT and COGS should be 14*2.
+ """
+ self.product.categ_id.property_cost_method = 'standard'
+ self.product.invoice_policy = 'order'
+ self.product.standard_price = 10.0
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # standard price to 14
+ self.product.standard_price = 14.0
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 28)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 28)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_standard_ordered_invoice_post_partial_delivery_1(self):
+ """Standard price set to 10. Get 2 units in stock. Sale order 2@12. Deliver 1, invoice 1,
+ change the standard price to 14, deliver one, change the standard price to 16, invoice 1.
+ The amounts used in Stock OUT and COGS should be 10 then 14."""
+ self.product.categ_id.property_cost_method = 'standard'
+ self.product.invoice_policy = 'order'
+ self.product.standard_price = 10.0
+
+ # Put two items in stock.
+ sale_order = self._so_and_confirm_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # Invoice 1
+ invoice = sale_order._create_invoices()
+ invoice_form = Form(invoice)
+ with invoice_form.invoice_line_ids.edit(0) as invoice_line:
+ invoice_line.quantity = 1
+ invoice_form.save()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 10)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 10)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 12)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 12)
+
+ # change the standard price to 14
+ self.product.standard_price = 14.0
+
+ # deliver the backorder
+ sale_order.picking_ids[0].move_lines.quantity_done = 1
+ sale_order.picking_ids[0].button_validate()
+
+ # change the standard price to 16
+ self.product.standard_price = 16.0
+
+ # invoice 1
+ invoice2 = sale_order._create_invoices()
+ invoice2.action_post()
+ amls = invoice2.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 14)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 14)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 12)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 12)
+
+ def test_standard_ordered_invoice_post_delivery(self):
+ """Standard price set to 10. Get 2 units in stock. Sale order 2@12. Deliver 1, change the
+ standard price to 14, deliver one, invoice 2. The amounts used in Stock OUT and COGS should
+ be 12*2."""
+ self.product.categ_id.property_cost_method = 'standard'
+ self.product.invoice_policy = 'order'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # change the standard price to 14
+ self.product.standard_price = 14.0
+
+ # deliver the backorder
+ sale_order.picking_ids.filtered('backorder_id').move_lines.quantity_done = 1
+ sale_order.picking_ids.filtered('backorder_id').button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 24)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 24)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ # -------------------------------------------------------------------------
+ # Standard Delivered
+ # -------------------------------------------------------------------------
+ def test_standard_delivered_invoice_pre_delivery(self):
+ """Not possible to invoice pre delivery."""
+ self.product.categ_id.property_cost_method = 'standard'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Invoice the sale order.
+ # Nothing delivered = nothing to invoice.
+ with self.assertRaises(UserError):
+ sale_order._create_invoices()
+
+ def test_standard_delivered_invoice_post_partial_delivery(self):
+ """Standard price set to 10. Get 2 units in stock. Sale order 2@12. Deliver 1, invoice 1,
+ change the standard price to 14, deliver one, change the standard price to 16, invoice 1.
+ The amounts used in Stock OUT and COGS should be 10 then 14."""
+ self.product.categ_id.property_cost_method = 'standard'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ sale_order = self._so_and_confirm_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # Invoice 1
+ invoice = sale_order._create_invoices()
+ invoice_form = Form(invoice)
+ with invoice_form.invoice_line_ids.edit(0) as invoice_line:
+ invoice_line.quantity = 1
+ invoice_form.save()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 10)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 10)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 12)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 12)
+
+ # change the standard price to 14
+ self.product.standard_price = 14.0
+
+ # deliver the backorder
+ sale_order.picking_ids[0].move_lines.quantity_done = 1
+ sale_order.picking_ids[0].button_validate()
+
+ # change the standard price to 16
+ self.product.standard_price = 16.0
+
+ # invoice 1
+ invoice2 = sale_order._create_invoices()
+ invoice2.action_post()
+ amls = invoice2.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 14)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 14)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 12)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 12)
+
+ def test_standard_delivered_invoice_post_delivery(self):
+ """Standard price set to 10. Get 2 units in stock. Sale order 2@12. Deliver 1, change the
+ standard price to 14, deliver one, invoice 2. The amounts used in Stock OUT and COGS should
+ be 12*2."""
+ self.product.categ_id.property_cost_method = 'standard'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # change the standard price to 14
+ self.product.standard_price = 14.0
+
+ # deliver the backorder
+ sale_order.picking_ids.filtered('backorder_id').move_lines.quantity_done = 1
+ sale_order.picking_ids.filtered('backorder_id').button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 24)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 24)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ # -------------------------------------------------------------------------
+ # AVCO Ordered
+ # -------------------------------------------------------------------------
+ def test_avco_ordered_invoice_pre_delivery(self):
+ """Standard price set to 10. Sale order 2@12. Invoice without delivering."""
+ self.product.categ_id.property_cost_method = 'average'
+ self.product.invoice_policy = 'order'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 20)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 20)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_avco_ordered_invoice_post_partial_delivery(self):
+ """Standard price set to 10. Sale order 2@12. Invoice after delivering 1."""
+ self.product.categ_id.property_cost_method = 'average'
+ self.product.invoice_policy = 'order'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 20)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 20)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_avco_ordered_invoice_post_delivery(self):
+ """Standard price set to 10. Sale order 2@12. Invoice after full delivery."""
+ self.product.categ_id.property_cost_method = 'average'
+ self.product.invoice_policy = 'order'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 2
+ sale_order.picking_ids.button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 20)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 20)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ # -------------------------------------------------------------------------
+ # AVCO Delivered
+ # -------------------------------------------------------------------------
+ def test_avco_delivered_invoice_pre_delivery(self):
+ """Standard price set to 10. Sale order 2@12. Invoice without delivering. """
+ self.product.categ_id.property_cost_method = 'average'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Invoice the sale order.
+ # Nothing delivered = nothing to invoice.
+ with self.assertRaises(UserError):
+ sale_order._create_invoices()
+
+ def test_avco_delivered_invoice_post_partial_delivery(self):
+ """Standard price set to 10. Sale order 2@12. Invoice after delivering 1."""
+ self.product.categ_id.property_cost_method = 'average'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 10)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 10)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 12)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 12)
+
+ def test_avco_delivered_invoice_post_delivery(self):
+ """Standard price set to 10. Sale order 2@12. Invoice after full delivery."""
+ self.product.categ_id.property_cost_method = 'average'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ # Put two items in stock.
+ self._inv_adj_two_units()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 2
+ sale_order.picking_ids.button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 20)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 20)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ # -------------------------------------------------------------------------
+ # FIFO Ordered
+ # -------------------------------------------------------------------------
+ def test_fifo_ordered_invoice_pre_delivery(self):
+ """Receive at 8 then at 10. Sale order 2@12. Invoice without delivering.
+ As no standard price is set, the Stock OUT and COGS amounts are 0."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'order'
+
+ self._fifo_in_one_eight_one_ten()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertAlmostEqual(stock_out_aml.credit, 16)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertAlmostEqual(cogs_aml.debit, 16)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_fifo_ordered_invoice_post_partial_delivery(self):
+ """Receive 1@8, 1@10, so 2@12, standard price 12, deliver 1, invoice 2: the COGS amount
+ should be 20: 1 really delivered at 10 and the other valued at the standard price 10."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'order'
+
+ self._fifo_in_one_eight_one_ten()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # upate the standard price to 12
+ self.product.standard_price = 12
+
+ # Invoice 2
+ invoice = sale_order._create_invoices()
+ invoice_form = Form(invoice)
+ with invoice_form.invoice_line_ids.edit(0) as invoice_line:
+ invoice_line.quantity = 2
+ invoice_form.save()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 20)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 20)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_fifo_ordered_invoice_post_delivery(self):
+ """Receive at 8 then at 10. Sale order 2@12. Invoice after delivering everything."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'order'
+
+ self._fifo_in_one_eight_one_ten()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 2
+ sale_order.picking_ids.button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 18)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 18)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ # -------------------------------------------------------------------------
+ # FIFO Delivered
+ # -------------------------------------------------------------------------
+ def test_fifo_delivered_invoice_pre_delivery(self):
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ self._fifo_in_one_eight_one_ten()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Invoice the sale order.
+ # Nothing delivered = nothing to invoice.
+ with self.assertRaises(UserError):
+ invoice_id = sale_order._create_invoices()
+
+ def test_fifo_delivered_invoice_post_partial_delivery(self):
+ """Receive 1@8, 1@10, so 2@12, standard price 12, deliver 1, invoice 2: the price used should be 10:
+ one at 8 and one at 10."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+
+ self._fifo_in_one_eight_one_ten()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ wiz = sale_order.picking_ids.button_validate()
+ wiz = Form(self.env[wiz['res_model']].with_context(wiz['context'])).save()
+ wiz.process()
+
+ # upate the standard price to 12
+ self.product.standard_price = 12
+
+ # Invoice 2
+ invoice = sale_order._create_invoices()
+ invoice_form = Form(invoice)
+ with invoice_form.invoice_line_ids.edit(0) as invoice_line:
+ invoice_line.quantity = 2
+ invoice_form.save()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 20)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 20)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_fifo_delivered_invoice_post_delivery(self):
+ """Receive at 8 then at 10. Sale order 2@12. Invoice after delivering everything."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ self._fifo_in_one_eight_one_ten()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self._so_and_confirm_two_units()
+
+ # Deliver one.
+ sale_order.picking_ids.move_lines.quantity_done = 2
+ sale_order.picking_ids.button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 18)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 18)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 24)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 24)
+
+ def test_fifo_delivered_invoice_post_delivery_2(self):
+ """Receive at 8 then at 10. Sale order 10@12 and deliver without receiving the 2 missing.
+ receive 2@12. Invoice."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ in_move_1 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 8,
+ 'price_unit': 10,
+ })
+ in_move_1._action_confirm()
+ in_move_1.quantity_done = 8
+ in_move_1._action_done()
+
+ # Create and confirm a sale order for 2@12
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 10.0,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': 12,
+ 'tax_id': False, # no love taxes amls
+ })],
+ })
+ sale_order.action_confirm()
+
+ # Deliver 10
+ sale_order.picking_ids.move_lines.quantity_done = 10
+ sale_order.picking_ids.button_validate()
+
+ # Make the second receipt
+ in_move_2 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 2,
+ 'price_unit': 12,
+ })
+ in_move_2._action_confirm()
+ in_move_2.quantity_done = 2
+ in_move_2._action_done()
+ self.assertEqual(self.product.stock_valuation_layer_ids[-1].value, -4) # we sent two at 10 but they should have been sent at 12
+ self.assertEqual(self.product.stock_valuation_layer_ids[-1].quantity, 0)
+ self.assertEqual(sale_order.order_line.move_ids.stock_valuation_layer_ids[-1].quantity, 0)
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Check the resulting accounting entries
+ amls = invoice.line_ids
+ self.assertEqual(len(amls), 4)
+ stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml.debit, 0)
+ self.assertEqual(stock_out_aml.credit, 104)
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 104)
+ self.assertEqual(cogs_aml.credit, 0)
+ receivable_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml.debit, 120)
+ self.assertEqual(receivable_aml.credit, 0)
+ income_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml.debit, 0)
+ self.assertEqual(income_aml.credit, 120)
+
+ def test_fifo_delivered_invoice_post_delivery_3(self):
+ """Receive 5@8, receive 8@12, sale 1@20, deliver, sale 6@20, deliver. Make sure no rouding
+ issues appear on the second invoice."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+
+ # +5@8
+ in_move_1 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 5,
+ 'price_unit': 8,
+ })
+ in_move_1._action_confirm()
+ in_move_1.quantity_done = 5
+ in_move_1._action_done()
+
+ # +8@12
+ in_move_2 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 8,
+ 'price_unit': 12,
+ })
+ in_move_2._action_confirm()
+ in_move_2.quantity_done = 8
+ in_move_2._action_done()
+
+ # sale 1@20, deliver, invoice
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 1,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': 20,
+ 'tax_id': False,
+ })],
+ })
+ sale_order.action_confirm()
+ sale_order.picking_ids.move_lines.quantity_done = 1
+ sale_order.picking_ids.button_validate()
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # sale 6@20, deliver, invoice
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 6,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': 20,
+ 'tax_id': False,
+ })],
+ })
+ sale_order.action_confirm()
+ sale_order.picking_ids.move_lines.quantity_done = 6
+ sale_order.picking_ids.button_validate()
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # check the last anglo saxon invoice line
+ amls = invoice.line_ids
+ cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml.debit, 56)
+ self.assertEqual(cogs_aml.credit, 0)
+
+ def test_fifo_delivered_invoice_post_delivery_4(self):
+ """Receive 8@10. Sale order 10@12. Deliver and also invoice it without receiving the 2 missing.
+ Now, receive 2@12. Make sure price difference is correctly reflected in expense account."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+ self.product.standard_price = 10
+
+ in_move_1 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 8,
+ 'price_unit': 10,
+ })
+ in_move_1._action_confirm()
+ in_move_1.quantity_done = 8
+ in_move_1._action_done()
+
+ # Create and confirm a sale order for 10@12
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 10.0,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': 12,
+ 'tax_id': False, # no love taxes amls
+ })],
+ })
+ sale_order.action_confirm()
+
+ # Deliver 10
+ sale_order.picking_ids.move_lines.quantity_done = 10
+ sale_order.picking_ids.button_validate()
+
+ # Invoice the sale order.
+ invoice = sale_order._create_invoices()
+ invoice.action_post()
+
+ # Make the second receipt
+ in_move_2 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 2,
+ 'price_unit': 12,
+ })
+ in_move_2._action_confirm()
+ in_move_2.quantity_done = 2
+ in_move_2._action_done()
+
+ # check the last anglo saxon move line
+ revalued_anglo_expense_amls = sale_order.picking_ids.mapped('move_lines.stock_valuation_layer_ids')[-1].stock_move_id.account_move_ids[-1].mapped('line_ids')
+ revalued_cogs_aml = revalued_anglo_expense_amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(revalued_cogs_aml.debit, 4, 'Price difference should have correctly reflected in expense account.')
+
+ def test_fifo_delivered_invoice_post_delivery_with_return(self):
+ """Receive 2@10. SO1 2@12. Return 1 from SO1. SO2 1@12. Receive 1@20.
+ Re-deliver returned from SO1. Invoice after delivering everything."""
+ self.product.categ_id.property_cost_method = 'fifo'
+ self.product.invoice_policy = 'delivery'
+
+ # Receive 2@10.
+ in_move_1 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 2,
+ 'price_unit': 10,
+ })
+ in_move_1._action_confirm()
+ in_move_1.quantity_done = 2
+ in_move_1._action_done()
+
+ # Create, confirm and deliver a sale order for 2@12 (SO1)
+ so_1 = self._so_and_confirm_two_units()
+ so_1.picking_ids.move_lines.quantity_done = 2
+ so_1.picking_ids.button_validate()
+
+ # Return 1 from SO1
+ stock_return_picking_form = Form(
+ self.env['stock.return.picking'].with_context(
+ active_ids=so_1.picking_ids.ids, active_id=so_1.picking_ids.ids[0], active_model='stock.picking')
+ )
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.action_assign()
+ return_pick.move_lines.quantity_done = 1
+ return_pick._action_done()
+
+ # Create, confirm and deliver a sale order for 1@12 (SO2)
+ so_2 = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 1.0,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': 12,
+ 'tax_id': False, # no love taxes amls
+ })],
+ })
+ so_2.action_confirm()
+ so_2.picking_ids.move_lines.quantity_done = 1
+ so_2.picking_ids.button_validate()
+
+ # Receive 1@20
+ in_move_2 = self.env['stock.move'].create({
+ 'name': 'a',
+ 'product_id': self.product.id,
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_uom': self.product.uom_id.id,
+ 'product_uom_qty': 1,
+ 'price_unit': 20,
+ })
+ in_move_2._action_confirm()
+ in_move_2.quantity_done = 1
+ in_move_2._action_done()
+
+ # Re-deliver returned 1 from SO1
+ stock_redeliver_picking_form = Form(
+ self.env['stock.return.picking'].with_context(
+ active_ids=return_pick.ids, active_id=return_pick.ids[0], active_model='stock.picking')
+ )
+ stock_redeliver_picking = stock_redeliver_picking_form.save()
+ stock_redeliver_picking.product_return_moves.quantity = 1.0
+ stock_redeliver_picking_action = stock_redeliver_picking.create_returns()
+ redeliver_pick = self.env['stock.picking'].browse(stock_redeliver_picking_action['res_id'])
+ redeliver_pick.action_assign()
+ redeliver_pick.move_lines.quantity_done = 1
+ redeliver_pick._action_done()
+
+ # Invoice the sale orders
+ invoice_1 = so_1._create_invoices()
+ invoice_1.action_post()
+ invoice_2 = so_2._create_invoices()
+ invoice_2.action_post()
+
+ # Check the resulting accounting entries
+ amls_1 = invoice_1.line_ids
+ self.assertEqual(len(amls_1), 4)
+ stock_out_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml_1.debit, 0)
+ self.assertEqual(stock_out_aml_1.credit, 30)
+ cogs_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml_1.debit, 30)
+ self.assertEqual(cogs_aml_1.credit, 0)
+ receivable_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml_1.debit, 24)
+ self.assertEqual(receivable_aml_1.credit, 0)
+ income_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml_1.debit, 0)
+ self.assertEqual(income_aml_1.credit, 24)
+
+ amls_2 = invoice_2.line_ids
+ self.assertEqual(len(amls_2), 4)
+ stock_out_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
+ self.assertEqual(stock_out_aml_2.debit, 0)
+ self.assertEqual(stock_out_aml_2.credit, 10)
+ cogs_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
+ self.assertEqual(cogs_aml_2.debit, 10)
+ self.assertEqual(cogs_aml_2.credit, 0)
+ receivable_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable'])
+ self.assertEqual(receivable_aml_2.debit, 12)
+ self.assertEqual(receivable_aml_2.credit, 0)
+ income_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue'])
+ self.assertEqual(income_aml_2.debit, 0)
+ self.assertEqual(income_aml_2.credit, 12)
diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py
new file mode 100644
index 00000000..05a07a56
--- /dev/null
+++ b/addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.tests import Form, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestValuationReconciliation(ValuationReconciliationTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ # Set the invoice_policy to delivery to have an accurate COGS entry.
+ cls.test_product_delivery.invoice_policy = 'delivery'
+
+ def _create_sale(self, product, date, quantity=1.0):
+ rslt = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'order_line': [
+ (0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': quantity,
+ 'product_uom': product.uom_po_id.id,
+ 'price_unit': 66.0,
+ })],
+ 'date_order': date,
+ })
+ rslt.action_confirm()
+ return rslt
+
+ def _create_invoice_for_so(self, sale_order, product, date, quantity=1.0):
+ rslt = self.env['account.move'].create({
+ 'partner_id': self.partner_a.id,
+ 'currency_id': self.currency_data['currency'].id,
+ 'move_type': 'out_invoice',
+ 'invoice_date': date,
+ 'invoice_line_ids': [(0, 0, {
+ 'name': 'test line',
+ 'account_id': self.company_data['default_account_revenue'].id,
+ 'price_unit': 66.0,
+ 'quantity': quantity,
+ 'discount': 0.0,
+ 'product_uom_id': product.uom_id.id,
+ 'product_id': product.id,
+ 'sale_line_ids': [(6, 0, sale_order.order_line.ids)],
+ })],
+ })
+
+ sale_order.invoice_ids += rslt
+ return rslt
+
+ def _set_initial_stock_for_product(self, product):
+ move1 = self.env['stock.move'].create({
+ 'name': 'Initial stock',
+ 'location_id': self.env.ref('stock.stock_location_suppliers').id,
+ 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
+ 'product_uom_qty': 11,
+ 'price_unit': 13,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 11
+ move1._action_done()
+
+ def test_shipment_invoice(self):
+ """ Tests the case into which we send the goods to the customer before
+ making the invoice
+ """
+ test_product = self.test_product_delivery
+ self._set_initial_stock_for_product(test_product)
+
+ sale_order = self._create_sale(test_product, '2108-01-01')
+ self._process_pickings(sale_order.picking_ids)
+
+ invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-12')
+ invoice.action_post()
+ picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
+ self.check_reconciliation(invoice, picking, operation='sale')
+
+ def test_invoice_shipment(self):
+ """ Tests the case into which we make the invoice first, and then send
+ the goods to our customer.
+ """
+ test_product = self.test_product_delivery
+ #since the invoice come first, the COGS will use the standard price on product
+ self.test_product_delivery.standard_price = 13
+ self._set_initial_stock_for_product(test_product)
+
+ sale_order = self._create_sale(test_product, '2018-01-01')
+
+ invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03')
+ invoice.action_post()
+
+ self._process_pickings(sale_order.picking_ids)
+
+ picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)])
+ self.check_reconciliation(invoice, picking, operation='sale')
+
+ #return the goods and refund the invoice
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking.ids, active_id=picking.ids[0],
+ active_model='stock.picking'))
+ stock_return_picking = stock_return_picking_form.save()
+ stock_return_picking.product_return_moves.quantity = 1.0
+ stock_return_picking_action = stock_return_picking.create_returns()
+ return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
+ return_pick.action_assign()
+ return_pick.move_lines.quantity_done = 1
+ return_pick._action_done()
+ refund_invoice_wiz = self.env['account.move.reversal'].with_context(active_model='account.move', active_ids=[invoice.id]).create({
+ 'reason': 'test_invoice_shipment_refund',
+ 'refund_method': 'cancel',
+ })
+ refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
+ self.assertEqual(invoice.payment_state, 'reversed', "Invoice should be in 'reversed' state.")
+ self.assertEqual(refund_invoice.payment_state, 'paid', "Refund should be in 'paid' state.")
+ self.check_reconciliation(refund_invoice, return_pick, operation='sale')
+
+ def test_multiple_shipments_invoices(self):
+ """ Tests the case into which we deliver part of the goods first, then 2 invoices at different rates, and finally the remaining quantities
+ """
+ test_product = self.test_product_delivery
+ self._set_initial_stock_for_product(test_product)
+
+ sale_order = self._create_sale(test_product, '2018-01-01', quantity=5)
+
+ self._process_pickings(sale_order.picking_ids, quantity=2.0)
+ picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order="id asc", limit=1)
+
+ invoice = self._create_invoice_for_so(sale_order, test_product, '2018-02-03', quantity=3)
+ invoice.action_post()
+ self.check_reconciliation(invoice, picking, full_reconcile=False, operation='sale')
+
+ invoice2 = self._create_invoice_for_so(sale_order, test_product, '2018-03-12', quantity=2)
+ invoice2.action_post()
+ self.check_reconciliation(invoice2, picking, full_reconcile=False, operation='sale')
+
+ self._process_pickings(sale_order.picking_ids.filtered(lambda x: x.state != 'done'), quantity=3.0)
+ picking = self.env['stock.picking'].search([('sale_id', '=', sale_order.id)], order='id desc', limit=1)
+ self.check_reconciliation(invoice2, picking, operation='sale')
diff --git a/addons/sale_stock/tests/test_sale_order_dates.py b/addons/sale_stock/tests/test_sale_order_dates.py
new file mode 100644
index 00000000..6e1103a3
--- /dev/null
+++ b/addons/sale_stock/tests/test_sale_order_dates.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from datetime import timedelta
+from odoo import fields
+from odoo.tests import common, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestSaleExpectedDate(ValuationReconciliationTestCommon):
+
+ def test_sale_order_expected_date(self):
+ """ Test expected date and effective date of Sales Orders """
+ Product = self.env['product.product']
+
+ product_A = Product.create({
+ 'name': 'Product A',
+ 'type': 'product',
+ 'sale_delay': 5,
+ 'uom_id': 1,
+ })
+ product_B = Product.create({
+ 'name': 'Product B',
+ 'type': 'product',
+ 'sale_delay': 10,
+ 'uom_id': 1,
+ })
+ product_C = Product.create({
+ 'name': 'Product C',
+ 'type': 'product',
+ 'sale_delay': 15,
+ 'uom_id': 1,
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_A, self.company_data['default_warehouse'].lot_stock_id, 10)
+ self.env['stock.quant']._update_available_quantity(product_B, self.company_data['default_warehouse'].lot_stock_id, 10)
+ self.env['stock.quant']._update_available_quantity(product_C, self.company_data['default_warehouse'].lot_stock_id, 10)
+
+ sale_order = self.env['sale.order'].create({
+ 'partner_id': self.env['res.partner'].create({'name': 'A Customer'}).id,
+ 'picking_policy': 'direct',
+ 'order_line': [
+ (0, 0, {'name': product_A.name, 'product_id': product_A.id, 'customer_lead': product_A.sale_delay, 'product_uom_qty': 5}),
+ (0, 0, {'name': product_B.name, 'product_id': product_B.id, 'customer_lead': product_B.sale_delay, 'product_uom_qty': 5}),
+ (0, 0, {'name': product_C.name, 'product_id': product_C.id, 'customer_lead': product_C.sale_delay, 'product_uom_qty': 5})
+ ],
+ })
+
+ # if Shipping Policy is set to `direct`(when SO is in draft state) then expected date should be
+ # current date + shortest lead time from all of it's order lines
+ expected_date = fields.Datetime.now() + timedelta(days=5)
+ self.assertAlmostEqual(expected_date, sale_order.expected_date,
+ msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
+
+ # if Shipping Policy is set to `one`(when SO is in draft state) then expected date should be
+ # current date + longest lead time from all of it's order lines
+ sale_order.write({'picking_policy': 'one'})
+ expected_date = fields.Datetime.now() + timedelta(days=15)
+ self.assertAlmostEqual(expected_date, sale_order.expected_date,
+ msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
+
+ sale_order.action_confirm()
+
+ # Setting confirmation date of SO to 5 days from today so that the expected/effective date could be checked
+ # against real confirmation date
+ confirm_date = fields.Datetime.now() + timedelta(days=5)
+ sale_order.write({'date_order': confirm_date})
+
+ # if Shipping Policy is set to `one`(when SO is confirmed) then expected date should be
+ # SO confirmation date + longest lead time from all of it's order lines
+ expected_date = confirm_date + timedelta(days=15)
+ self.assertAlmostEqual(expected_date, sale_order.expected_date,
+ msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
+
+ # if Shipping Policy is set to `direct`(when SO is confirmed) then expected date should be
+ # SO confirmation date + shortest lead time from all of it's order lines
+ sale_order.write({'picking_policy': 'direct'})
+ expected_date = confirm_date + timedelta(days=5)
+ self.assertAlmostEqual(expected_date, sale_order.expected_date,
+ msg="Wrong expected date on sale order!", delta=timedelta(seconds=1))
+
+ # Check effective date, it should be date on which the first shipment successfully delivered to customer
+ picking = sale_order.picking_ids[0]
+ for ml in picking.move_line_ids:
+ ml.qty_done = ml.product_uom_qty
+ picking._action_done()
+ self.assertEqual(picking.state, 'done', "Picking not processed correctly!")
+ self.assertEqual(fields.Date.today(), sale_order.effective_date, "Wrong effective date on sale order!")
+
+ def test_sale_order_commitment_date(self):
+
+ # In order to test the Commitment Date feature in Sales Orders in Odoo,
+ # I copy a demo Sales Order with committed Date on 2010-07-12
+ new_order = self.env['sale.order'].create({
+ 'partner_id': self.env['res.partner'].create({'name': 'A Partner'}).id,
+ 'order_line': [(0, 0, {
+ 'name': "A product",
+ 'product_id': self.env['product.product'].create({
+ 'name': 'A product',
+ 'type': 'product',
+ }).id,
+ 'product_uom_qty': 1,
+ 'price_unit': 750,
+ })],
+ 'commitment_date': '2010-07-12',
+ })
+ # I confirm the Sales Order.
+ new_order.action_confirm()
+ # I verify that the Procurements and Stock Moves have been generated with the correct date
+ security_delay = timedelta(days=new_order.company_id.security_lead)
+ commitment_date = fields.Datetime.from_string(new_order.commitment_date)
+ right_date = commitment_date - security_delay
+ for line in new_order.order_line:
+ self.assertEqual(line.move_ids[0].date, right_date, "The expected date for the Stock Move is wrong")
diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py
new file mode 100644
index 00000000..a68c8988
--- /dev/null
+++ b/addons/sale_stock/tests/test_sale_stock.py
@@ -0,0 +1,1020 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime, timedelta
+
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.addons.sale.tests.common import TestSaleCommon
+from odoo.exceptions import UserError
+from odoo.tests import Form, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon):
+
+ def _get_new_sale_order(self, amount=10.0, product=False):
+ """ Creates and returns a sale order with one default order line.
+
+ :param float amount: quantity of product for the order line (10 by default)
+ """
+ product = product or self.company_data['product_delivery_no']
+ sale_order_vals = {
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'order_line': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': amount,
+ 'product_uom': product.uom_id.id,
+ 'price_unit': product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ sale_order = self.env['sale.order'].create(sale_order_vals)
+ return sale_order
+
+ def test_00_sale_stock_invoice(self):
+ """
+ Test SO's changes when playing around with stock moves, quants, pack operations, pickings
+ and whatever other model there is in stock with "invoice on delivery" products
+ """
+ self.so = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': p.name,
+ 'product_id': p.id,
+ 'product_uom_qty': 2,
+ 'product_uom': p.uom_id.id,
+ 'price_unit': p.list_price,
+ }) for p in (
+ self.company_data['product_order_no'],
+ self.company_data['product_service_delivery'],
+ self.company_data['product_service_order'],
+ self.company_data['product_delivery_no'],
+ )],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ 'picking_policy': 'direct',
+ })
+
+ # confirm our standard so, check the picking
+ self.so.action_confirm()
+ self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" storable products')
+ # invoice on order
+ self.so._create_invoices()
+
+ # deliver partially, check the so's invoice_status and delivered quantities
+ self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "nothing to invoice" after invoicing')
+ pick = self.so.picking_ids
+ pick.move_lines.write({'quantity_done': 1})
+ wiz_act = pick.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+ self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" after partial delivery')
+ del_qties = [sol.qty_delivered for sol in self.so.order_line]
+ del_qties_truth = [1.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line]
+ self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery')
+ # invoice on delivery: only storable products
+ inv_1 = self.so._create_invoices()
+ self.assertTrue(all([il.product_id.invoice_policy == 'delivery' for il in inv_1.invoice_line_ids]),
+ 'Sale Stock: invoice should only contain "invoice on delivery" products')
+
+ # complete the delivery and check invoice_status again
+ self.assertEqual(self.so.invoice_status, 'no',
+ 'Sale Stock: so invoice_status should be "nothing to invoice" after partial delivery and invoicing')
+ self.assertEqual(len(self.so.picking_ids), 2, 'Sale Stock: number of pickings should be 2')
+ pick_2 = self.so.picking_ids.filtered('backorder_id')
+ pick_2.move_lines.write({'quantity_done': 1})
+ self.assertTrue(pick_2.button_validate(), 'Sale Stock: second picking should be final without need for a backorder')
+ self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" after complete delivery')
+ del_qties = [sol.qty_delivered for sol in self.so.order_line]
+ del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line]
+ self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after complete delivery')
+ # Without timesheet, we manually set the delivered qty for the product serv_del
+ self.so.order_line.sorted()[1]['qty_delivered'] = 2.0
+ # There is a bug with `new` and `_origin`
+ # If you create a first new from a record, then change a value on the origin record, than create another new,
+ # this other new wont have the updated value of the origin record, but the one from the previous new
+ # Here the problem lies in the use of `new` in `move = self_ctx.new(new_vals)`,
+ # and the fact this method is called multiple times in the same transaction test case.
+ # Here, we update `qty_delivered` on the origin record, but the `new` records which are in cache with this order line
+ # as origin are not updated, nor the fields that depends on it.
+ self.so.flush()
+ for field in self.env['sale.order.line']._fields.values():
+ for res_id in list(self.env.cache._data[field]):
+ if not res_id:
+ self.env.cache._data[field].pop(res_id)
+ inv_id = self.so._create_invoices()
+ self.assertEqual(self.so.invoice_status, 'invoiced',
+ 'Sale Stock: so invoice_status should be "fully invoiced" after complete delivery and invoicing')
+
+ def test_01_sale_stock_order(self):
+ """
+ Test SO's changes when playing around with stock moves, quants, pack operations, pickings
+ and whatever other model there is in stock with "invoice on order" products
+ """
+ # let's cheat and put all our products to "invoice on order"
+ self.so = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'order_line': [(0, 0, {
+ 'name': p.name,
+ 'product_id': p.id,
+ 'product_uom_qty': 2,
+ 'product_uom': p.uom_id.id,
+ 'price_unit': p.list_price,
+ }) for p in (
+ self.company_data['product_order_no'],
+ self.company_data['product_service_delivery'],
+ self.company_data['product_service_order'],
+ self.company_data['product_delivery_no'],
+ )],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ 'picking_policy': 'direct',
+ })
+ for sol in self.so.order_line:
+ sol.product_id.invoice_policy = 'order'
+ # confirm our standard so, check the picking
+ self.so.order_line._compute_product_updatable()
+ self.assertTrue(self.so.order_line.sorted()[0].product_updatable)
+ self.so.action_confirm()
+ self.so.order_line._compute_product_updatable()
+ self.assertFalse(self.so.order_line.sorted()[0].product_updatable)
+ self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on order" storable products')
+ # let's do an invoice for a deposit of 5%
+
+ advance_product = self.env['product.product'].create({
+ 'name': 'Deposit',
+ 'type': 'service',
+ 'invoice_policy': 'order',
+ })
+ adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({
+ 'advance_payment_method': 'percentage',
+ 'amount': 5.0,
+ 'product_id': advance_product.id,
+ })
+ act = adv_wiz.with_context(open_invoices=True).create_invoices()
+ inv = self.env['account.move'].browse(act['res_id'])
+ self.assertEqual(inv.amount_untaxed, self.so.amount_untaxed * 5.0 / 100.0, 'Sale Stock: deposit invoice is wrong')
+ self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so should be to invoice after invoicing deposit')
+ # invoice on order: everything should be invoiced
+ self.so._create_invoices(final=True)
+ self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so should be fully invoiced after second invoice')
+
+ # deliver, check the delivered quantities
+ pick = self.so.picking_ids
+ pick.move_lines.write({'quantity_done': 2})
+ self.assertTrue(pick.button_validate(), 'Sale Stock: complete delivery should not need a backorder')
+ del_qties = [sol.qty_delivered for sol in self.so.order_line]
+ del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line]
+ self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery')
+ # invoice on delivery: nothing to invoice
+ with self.assertRaises(UserError):
+ self.so._create_invoices()
+
+ def test_02_sale_stock_return(self):
+ """
+ Test a SO with a product invoiced on delivery. Deliver and invoice the SO, then do a return
+ of the picking. Check that a refund invoice is well generated.
+ """
+ # intial so
+ self.product = self.company_data['product_delivery_no']
+ so_vals = {
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'order_line': [(0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': self.product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ self.so = self.env['sale.order'].create(so_vals)
+
+ # confirm our standard so, check the picking
+ self.so.action_confirm()
+ self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" storable products')
+
+ # invoice in on delivery, nothing should be invoiced
+ self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s".' % self.so.invoice_status)
+
+ # deliver completely
+ pick = self.so.picking_ids
+ pick.move_lines.write({'quantity_done': 5})
+ pick.button_validate()
+
+ # Check quantity delivered
+ del_qty = sum(sol.qty_delivered for sol in self.so.order_line)
+ self.assertEqual(del_qty, 5.0, 'Sale Stock: delivered quantity should be 5.0 instead of %s after complete delivery' % del_qty)
+
+ # Check invoice
+ self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" before invoicing' % self.so.invoice_status)
+ self.inv_1 = self.so._create_invoices()
+ self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so invoice_status should be "invoiced" instead of "%s" after invoicing' % self.so.invoice_status)
+ self.assertEqual(len(self.inv_1), 1, 'Sale Stock: only one invoice instead of "%s" should be created' % len(self.inv_1))
+ self.assertEqual(self.inv_1.amount_untaxed, self.inv_1.amount_untaxed, 'Sale Stock: amount in SO and invoice should be the same')
+ self.inv_1.action_post()
+
+ # Create return picking
+ stock_return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=pick.ids, active_id=pick.sorted().ids[0],
+ active_model='stock.picking'))
+ return_wiz = stock_return_picking_form.save()
+ return_wiz.product_return_moves.quantity = 2.0 # Return only 2
+ return_wiz.product_return_moves.to_refund = True # Refund these 2
+ res = return_wiz.create_returns()
+ return_pick = self.env['stock.picking'].browse(res['res_id'])
+
+ # Validate picking
+ return_pick.move_lines.write({'quantity_done': 2})
+ return_pick.button_validate()
+
+ # Check invoice
+ self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" after picking return' % self.so.invoice_status)
+ self.assertAlmostEqual(self.so.order_line.sorted()[0].qty_delivered, 3.0, msg='Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line.sorted()[0].qty_delivered)
+ # let's do an invoice with refunds
+ adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({
+ 'advance_payment_method': 'delivered',
+ })
+ adv_wiz.with_context(open_invoices=True).create_invoices()
+ self.inv_2 = self.so.invoice_ids.filtered(lambda r: r.state == 'draft')
+ self.assertAlmostEqual(self.inv_2.invoice_line_ids.sorted()[0].quantity, 2.0, msg='Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids.sorted()[0].quantity)
+ self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s" after invoicing the return' % self.so.invoice_status)
+
+ def test_03_sale_stock_delivery_partial(self):
+ """
+ Test a SO with a product invoiced on delivery. Deliver partially and invoice the SO, when
+ the SO is set on 'done', the SO should be fully invoiced.
+ """
+ # intial so
+ self.product = self.company_data['product_delivery_no']
+ so_vals = {
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'order_line': [(0, 0, {
+ 'name': self.product.name,
+ 'product_id': self.product.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': self.product.uom_id.id,
+ 'price_unit': self.product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ self.so = self.env['sale.order'].create(so_vals)
+
+ # confirm our standard so, check the picking
+ self.so.action_confirm()
+ self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" storable products')
+
+ # invoice in on delivery, nothing should be invoiced
+ self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "nothing to invoice"')
+
+ # deliver partially
+ pick = self.so.picking_ids
+ pick.move_lines.write({'quantity_done': 4})
+ res_dict = pick.button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process_cancel_backorder()
+
+ # Check quantity delivered
+ del_qty = sum(sol.qty_delivered for sol in self.so.order_line)
+ self.assertEqual(del_qty, 4.0, 'Sale Stock: delivered quantity should be 4.0 after partial delivery')
+
+ # Check invoice
+ self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" before invoicing')
+ self.inv_1 = self.so._create_invoices()
+ self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" after invoicing')
+ self.assertEqual(len(self.inv_1), 1, 'Sale Stock: only one invoice should be created')
+ self.assertEqual(self.inv_1.amount_untaxed, self.inv_1.amount_untaxed, 'Sale Stock: amount in SO and invoice should be the same')
+
+ self.so.action_done()
+ self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so invoice_status should be "invoiced" when set to done')
+
+ def test_04_create_picking_update_saleorderline(self):
+ """
+ Test that updating multiple sale order lines after a successful delivery creates a single picking containing
+ the new move lines.
+ """
+ # sell two products
+ item1 = self.company_data['product_order_no'] # consumable
+ item1.type = 'consu'
+ item2 = self.company_data['product_delivery_no'] # storable
+ item2.type = 'product' # storable
+
+ self.so = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 1, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ (0, 0, {'name': item2.name, 'product_id': item2.id, 'product_uom_qty': 1, 'product_uom': item2.uom_id.id, 'price_unit': item2.list_price}),
+ ],
+ })
+ self.so.action_confirm()
+
+ # deliver them
+ # One of the move is for a consumable product, thus is assigned. The second one is for a
+ # storable product, thus is unavailable. Hitting `button_validate` will first ask to
+ # process all the reserved quantities and, if the user chose to process, a second wizard
+ # will ask to create a backorder for the unavailable product.
+ self.assertEqual(len(self.so.picking_ids), 1)
+ res_dict = self.so.picking_ids.sorted()[0].button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ self.assertEqual(wizard._name, 'stock.immediate.transfer')
+ res_dict = wizard.process()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ self.assertEqual(wizard._name, 'stock.backorder.confirmation')
+ wizard.process()
+
+ # Now, the original picking is done and there is a new one (the backorder).
+ self.assertEqual(len(self.so.picking_ids), 2)
+ for picking in self.so.picking_ids:
+ move = picking.move_lines
+ if picking.backorder_id:
+ self.assertEqual(move.product_id.id, item2.id)
+ self.assertEqual(move.state, 'confirmed')
+ else:
+ self.assertEqual(picking.move_lines.product_id.id, item1.id)
+ self.assertEqual(move.state, 'done')
+
+ # update the two original sale order lines
+ self.so.write({
+ 'order_line': [
+ (1, self.so.order_line.sorted()[0].id, {'product_uom_qty': 2}),
+ (1, self.so.order_line.sorted()[1].id, {'product_uom_qty': 2}),
+ ]
+ })
+ # a single picking should be created for the new delivery
+ self.assertEqual(len(self.so.picking_ids), 2)
+ backorder = self.so.picking_ids.filtered(lambda p: p.backorder_id)
+ self.assertEqual(len(backorder.move_lines), 2)
+ for backorder_move in backorder.move_lines:
+ if backorder_move.product_id.id == item1.id:
+ self.assertEqual(backorder_move.product_qty, 1)
+ elif backorder_move.product_id.id == item2.id:
+ self.assertEqual(backorder_move.product_qty, 2)
+
+ # add a new sale order lines
+ self.so.write({
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 1, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ ]
+ })
+ self.assertEqual(sum(backorder.move_lines.filtered(lambda m: m.product_id.id == item1.id).mapped('product_qty')), 2)
+
+ def test_05_create_picking_update_saleorderline(self):
+ """ Same test than test_04 but only with enough products in stock so that the reservation
+ is successful.
+ """
+ # sell two products
+ item1 = self.company_data['product_order_no'] # consumable
+ item1.type = 'consu' # consumable
+ item2 = self.company_data['product_delivery_no'] # storable
+ item2.type = 'product' # storable
+
+ self.env['stock.quant']._update_available_quantity(item2, self.company_data['default_warehouse'].lot_stock_id, 2)
+ self.so = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 1, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ (0, 0, {'name': item2.name, 'product_id': item2.id, 'product_uom_qty': 1, 'product_uom': item2.uom_id.id, 'price_unit': item2.list_price}),
+ ],
+ })
+ self.so.action_confirm()
+
+ # deliver them
+ self.assertEqual(len(self.so.picking_ids), 1)
+ res_dict = self.so.picking_ids.sorted()[0].button_validate()
+ wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
+ wizard.process()
+ self.assertEqual(self.so.picking_ids.sorted()[0].state, "done")
+
+ # update the two original sale order lines
+ self.so.write({
+ 'order_line': [
+ (1, self.so.order_line.sorted()[0].id, {'product_uom_qty': 2}),
+ (1, self.so.order_line.sorted()[1].id, {'product_uom_qty': 2}),
+ ]
+ })
+ # a single picking should be created for the new delivery
+ self.assertEqual(len(self.so.picking_ids), 2)
+
+ def test_05_confirm_cancel_confirm(self):
+ """ Confirm a sale order, cancel it, set to quotation, change the
+ partner, confirm it again: the second delivery order should have
+ the new partner.
+ """
+ item1 = self.company_data['product_order_no']
+ partner1 = self.partner_a.id
+ partner2 = self.env['res.partner'].create({'name': 'Another Test Partner'})
+ so1 = self.env['sale.order'].create({
+ 'partner_id': partner1,
+ 'order_line': [(0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': item1.uom_id.id,
+ 'price_unit': item1.list_price,
+ })],
+ })
+ so1.action_confirm()
+ self.assertEqual(len(so1.picking_ids), 1)
+ self.assertEqual(so1.picking_ids.partner_id.id, partner1)
+ so1.action_cancel()
+ so1.action_draft()
+ so1.partner_id = partner2
+ so1.partner_shipping_id = partner2 # set by an onchange
+ so1.action_confirm()
+ self.assertEqual(len(so1.picking_ids), 2)
+ picking2 = so1.picking_ids.filtered(lambda p: p.state != 'cancel')
+ self.assertEqual(picking2.partner_id.id, partner2.id)
+
+ def test_06_uom(self):
+ """ Sell a dozen of products stocked in units. Check that the quantities on the sale order
+ lines as well as the delivered quantities are handled in dozen while the moves themselves
+ are handled in units. Edit the ordered quantities, check that the quantites are correctly
+ updated on the moves. Edit the ir.config_parameter to propagate the uom of the sale order
+ lines to the moves and edit a last time the ordered quantities. Deliver, check the
+ quantities.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ item1 = self.company_data['product_order_no']
+
+ self.assertEqual(item1.uom_id.id, uom_unit.id)
+
+ # sell a dozen
+ so1 = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [(0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ })],
+ })
+ so1.action_confirm()
+
+ # the move should be 12 units
+ # note: move.product_qty = computed field, always in the uom of the quant
+ # move.product_uom_qty = stored field representing the initial demand in move.product_uom
+ move1 = so1.picking_ids.move_lines[0]
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 12)
+
+ # edit the so line, sell 2 dozen, the move should now be 24 units
+ so1.write({
+ 'order_line': [
+ (1, so1.order_line.id, {'product_uom_qty': 2}),
+ ]
+ })
+ # The above will create a second move, and then the two moves will be merged in _merge_moves`
+ # The picking moves are not well sorted because the new move has just been created, and this influences the resulting move,
+ # in which move the twos are merged.
+ # But, this doesn't seem really important which is the resulting move, but in this test we have to ensure
+ # we use the resulting move to compare the qty.
+ # ```
+ # for moves in moves_to_merge:
+ # # link all move lines to record 0 (the one we will keep).
+ # moves.mapped('move_line_ids').write({'move_id': moves[0].id})
+ # # merge move data
+ # moves[0].write(moves._merge_moves_fields())
+ # # update merged moves dicts
+ # moves_to_unlink |= moves[1:]
+ # ```
+ move1 = so1.picking_ids.move_lines[0]
+ self.assertEqual(move1.product_uom_qty, 24)
+ self.assertEqual(move1.product_uom.id, uom_unit.id)
+ self.assertEqual(move1.product_qty, 24)
+
+ # force the propagation of the uom, sell 3 dozen
+ self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
+ so1.write({
+ 'order_line': [
+ (1, so1.order_line.id, {'product_uom_qty': 3}),
+ ]
+ })
+ move2 = so1.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
+ self.assertEqual(move2.product_uom_qty, 1)
+ self.assertEqual(move2.product_uom.id, uom_dozen.id)
+ self.assertEqual(move2.product_qty, 12)
+
+ # deliver everything
+ move1.quantity_done = 24
+ move2.quantity_done = 1
+ so1.picking_ids.button_validate()
+
+ # check the delivered quantity
+ self.assertEqual(so1.order_line.qty_delivered, 3.0)
+
+ def test_07_forced_qties(self):
+ """ Make multiple sale order lines of the same product which isn't available in stock. On
+ the picking, create new move lines (through the detailed operations view). See that the move
+ lines are correctly dispatched through the moves.
+ """
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ uom_dozen = self.env.ref('uom.product_uom_dozen')
+ item1 = self.company_data['product_order_no']
+
+ self.assertEqual(item1.uom_id.id, uom_unit.id)
+
+ # sell a dozen
+ so1 = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ }),
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ }),
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 1,
+ 'product_uom': uom_dozen.id,
+ 'price_unit': item1.list_price,
+ }),
+ ],
+ })
+ so1.action_confirm()
+
+ self.assertEqual(len(so1.picking_ids.move_lines), 3)
+ so1.picking_ids.write({
+ 'move_line_ids': [
+ (0, 0, {
+ 'product_id': item1.id,
+ 'product_uom_qty': 0,
+ 'qty_done': 1,
+ 'product_uom_id': uom_dozen.id,
+ 'location_id': so1.picking_ids.location_id.id,
+ 'location_dest_id': so1.picking_ids.location_dest_id.id,
+ }),
+ (0, 0, {
+ 'product_id': item1.id,
+ 'product_uom_qty': 0,
+ 'qty_done': 1,
+ 'product_uom_id': uom_dozen.id,
+ 'location_id': so1.picking_ids.location_id.id,
+ 'location_dest_id': so1.picking_ids.location_dest_id.id,
+ }),
+ (0, 0, {
+ 'product_id': item1.id,
+ 'product_uom_qty': 0,
+ 'qty_done': 1,
+ 'product_uom_id': uom_dozen.id,
+ 'location_id': so1.picking_ids.location_id.id,
+ 'location_dest_id': so1.picking_ids.location_dest_id.id,
+ }),
+ ],
+ })
+ so1.picking_ids.button_validate()
+ self.assertEqual(so1.picking_ids.state, 'done')
+ self.assertEqual(so1.order_line.mapped('qty_delivered'), [1, 1, 1])
+
+ def test_08_quantities(self):
+ """Change the picking code of the receipts to internal. Make a SO for 10 units, go to the
+ picking and return 5, edit the SO line to 15 units.
+
+ The purpose of the test is to check the consistencies across the delivered quantities and the
+ procurement quantities.
+ """
+ # Change the code of the picking type receipt
+ self.env['stock.picking.type'].search([('code', '=', 'incoming')]).write({'code': 'internal'})
+
+ # Sell and deliver 10 units
+ item1 = self.company_data['product_order_no']
+ uom_unit = self.env.ref('uom.product_uom_unit')
+ so1 = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': item1.name,
+ 'product_id': item1.id,
+ 'product_uom_qty': 10,
+ 'product_uom': uom_unit.id,
+ 'price_unit': item1.list_price,
+ }),
+ ],
+ })
+ so1.action_confirm()
+
+ picking = so1.picking_ids
+ wiz_act = picking.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+
+ # Return 5 units
+ stock_return_picking_form = Form(self.env['stock.return.picking'].with_context(
+ active_ids=picking.ids,
+ active_id=picking.sorted().ids[0],
+ active_model='stock.picking'
+ ))
+ return_wiz = stock_return_picking_form.save()
+ for return_move in return_wiz.product_return_moves:
+ return_move.write({
+ 'quantity': 5,
+ 'to_refund': True
+ })
+ res = return_wiz.create_returns()
+ return_pick = self.env['stock.picking'].browse(res['res_id'])
+ wiz_act = return_pick.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+
+ self.assertEqual(so1.order_line.qty_delivered, 5)
+
+ # Deliver 15 instead of 10.
+ so1.write({
+ 'order_line': [
+ (1, so1.order_line.sorted()[0].id, {'product_uom_qty': 15}),
+ ]
+ })
+
+ # A new move of 10 unit (15 - 5 units)
+ self.assertEqual(so1.order_line.qty_delivered, 5)
+ self.assertEqual(so1.picking_ids.sorted('id')[-1].move_lines.product_qty, 10)
+
+ def test_09_qty_available(self):
+ """ create a sale order in warehouse1, change to warehouse2 and check the
+ available quantities on sale order lines are well updated """
+ # sell two products
+ item1 = self.company_data['product_order_no']
+ item1.type = 'product'
+
+ warehouse1 = self.company_data['default_warehouse']
+ self.env['stock.quant']._update_available_quantity(item1, warehouse1.lot_stock_id, 10)
+ self.env['stock.quant']._update_reserved_quantity(item1, warehouse1.lot_stock_id, 3)
+
+ warehouse2 = self.env['stock.warehouse'].create({
+ 'partner_id': self.partner_a.id,
+ 'name': 'Zizizatestwarehouse',
+ 'code': 'Test',
+ })
+ self.env['stock.quant']._update_available_quantity(item1, warehouse2.lot_stock_id, 5)
+ so = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 1, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ ],
+ })
+ line = so.order_line[0]
+ self.assertAlmostEqual(line.scheduled_date, datetime.now(), delta=timedelta(seconds=10))
+ self.assertEqual(line.virtual_available_at_date, 10)
+ self.assertEqual(line.free_qty_today, 7)
+ self.assertEqual(line.qty_available_today, 10)
+ self.assertEqual(line.warehouse_id, warehouse1)
+ self.assertEqual(line.qty_to_deliver, 1)
+ so.warehouse_id = warehouse2
+ # invalidate product cache to ensure qty_available is recomputed
+ # bc warehouse isn't in the depends_context of qty_available
+ line.product_id.invalidate_cache()
+ self.assertEqual(line.virtual_available_at_date, 5)
+ self.assertEqual(line.free_qty_today, 5)
+ self.assertEqual(line.qty_available_today, 5)
+ self.assertEqual(line.warehouse_id, warehouse2)
+ self.assertEqual(line.qty_to_deliver, 1)
+
+ def test_10_qty_available(self):
+ """create a sale order containing three times the same product. The
+ quantity available should be different for the 3 lines"""
+ item1 = self.company_data['product_order_no']
+ item1.type = 'product'
+ self.env['stock.quant']._update_available_quantity(item1, self.company_data['default_warehouse'].lot_stock_id, 10)
+ so = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'order_line': [
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 5, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 5, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ (0, 0, {'name': item1.name, 'product_id': item1.id, 'product_uom_qty': 5, 'product_uom': item1.uom_id.id, 'price_unit': item1.list_price}),
+ ],
+ })
+ self.assertEqual(so.order_line.mapped('free_qty_today'), [10, 5, 0])
+
+ def test_11_return_with_refund(self):
+ """ Creates a sale order, valids it and its delivery, then creates a
+ return. The return must refund by default and the sale order delivered
+ quantity must be updated.
+ """
+ # Creates a sale order for 10 products.
+ sale_order = self._get_new_sale_order()
+ # Valids the sale order, then valids the delivery.
+ sale_order.action_confirm()
+ self.assertTrue(sale_order.picking_ids)
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+ picking = sale_order.picking_ids
+ picking.move_lines.write({'quantity_done': 10})
+ picking.button_validate()
+
+ # Checks the delivery amount (must be 10).
+ self.assertEqual(sale_order.order_line.qty_delivered, 10)
+ # Creates a return from the delivery picking.
+ return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking.ids, active_id=picking.id,
+ active_model='stock.picking'))
+ return_wizard = return_picking_form.save()
+ # Checks the field `to_refund` is checked (must be checked by default).
+ self.assertEqual(return_wizard.product_return_moves.to_refund, True)
+ self.assertEqual(return_wizard.product_return_moves.quantity, 10)
+
+ # Valids the return picking.
+ res = return_wizard.create_returns()
+ return_picking = self.env['stock.picking'].browse(res['res_id'])
+ return_picking.move_lines.write({'quantity_done': 10})
+ return_picking.button_validate()
+ # Checks the delivery amount (must be 0).
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+
+ def test_12_return_without_refund(self):
+ """ Do the exact thing than in `test_11_return_with_refund` except we
+ set on False the refund and checks the sale order delivered quantity
+ isn't changed.
+ """
+ # Creates a sale order for 10 products.
+ sale_order = self._get_new_sale_order()
+ # Valids the sale order, then valids the delivery.
+ sale_order.action_confirm()
+ self.assertTrue(sale_order.picking_ids)
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+ picking = sale_order.picking_ids
+ picking.move_lines.write({'quantity_done': 10})
+ picking.button_validate()
+
+ # Checks the delivery amount (must be 10).
+ self.assertEqual(sale_order.order_line.qty_delivered, 10)
+ # Creates a return from the delivery picking.
+ return_picking_form = Form(self.env['stock.return.picking']
+ .with_context(active_ids=picking.ids, active_id=picking.id,
+ active_model='stock.picking'))
+ return_wizard = return_picking_form.save()
+ # Checks the field `to_refund` is checked, then unchecks it.
+ self.assertEqual(return_wizard.product_return_moves.to_refund, True)
+ self.assertEqual(return_wizard.product_return_moves.quantity, 10)
+ return_wizard.product_return_moves.to_refund = False
+ # Valids the return picking.
+ res = return_wizard.create_returns()
+ return_picking = self.env['stock.picking'].browse(res['res_id'])
+ return_picking.move_lines.write({'quantity_done': 10})
+ return_picking.button_validate()
+ # Checks the delivery amount (must still be 10).
+ self.assertEqual(sale_order.order_line.qty_delivered, 10)
+
+ def test_13_delivered_qty(self):
+ """ Creates a sale order, valids it and adds a new move line in the delivery for a
+ product with an invoicing policy on 'order', then checks a new SO line was created.
+ After that, creates a second sale order and does the same thing but with a product
+ with and invoicing policy on 'ordered'.
+ """
+ product_inv_on_delivered = self.company_data['product_delivery_no']
+ # Configure a product with invoicing policy on order.
+ product_inv_on_order = self.env['product.product'].create({
+ 'name': 'Shenaniffluffy',
+ 'type': 'consu',
+ 'invoice_policy': 'order',
+ 'list_price': 55.0,
+ })
+ # Creates a sale order for 3 products invoiced on qty. delivered.
+ sale_order = self._get_new_sale_order(amount=3)
+ # Confirms the sale order, then increases the delivered qty., adds a new
+ # line and valids the delivery.
+ sale_order.action_confirm()
+ self.assertTrue(sale_order.picking_ids)
+ self.assertEqual(len(sale_order.order_line), 1)
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+ picking = sale_order.picking_ids
+
+ picking_form = Form(picking)
+ with picking_form.move_line_ids_without_package.edit(0) as move:
+ move.qty_done = 5
+ with picking_form.move_line_ids_without_package.new() as new_move:
+ new_move.product_id = product_inv_on_order
+ new_move.qty_done = 5
+ picking = picking_form.save()
+ picking.button_validate()
+
+ # Check a new sale order line was correctly created.
+ self.assertEqual(len(sale_order.order_line), 2)
+ so_line_1 = sale_order.order_line[0]
+ so_line_2 = sale_order.order_line[1]
+ self.assertEqual(so_line_1.product_id.id, product_inv_on_delivered.id)
+ self.assertEqual(so_line_1.product_uom_qty, 3)
+ self.assertEqual(so_line_1.qty_delivered, 5)
+ self.assertEqual(so_line_1.price_unit, 70.0)
+ self.assertEqual(so_line_2.product_id.id, product_inv_on_order.id)
+ self.assertEqual(so_line_2.product_uom_qty, 0)
+ self.assertEqual(so_line_2.qty_delivered, 5)
+ self.assertEqual(
+ so_line_2.price_unit, 0,
+ "Shouldn't get the product price as the invoice policy is on qty. ordered")
+
+ # Creates a second sale order for 3 product invoiced on qty. ordered.
+ sale_order = self._get_new_sale_order(product=product_inv_on_order, amount=3)
+ # Confirms the sale order, then increases the delivered qty., adds a new
+ # line and valids the delivery.
+ sale_order.action_confirm()
+ self.assertTrue(sale_order.picking_ids)
+ self.assertEqual(len(sale_order.order_line), 1)
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+ picking = sale_order.picking_ids
+
+ picking_form = Form(picking)
+ with picking_form.move_line_ids_without_package.edit(0) as move:
+ move.qty_done = 5
+ with picking_form.move_line_ids_without_package.new() as new_move:
+ new_move.product_id = product_inv_on_delivered
+ new_move.qty_done = 5
+ picking = picking_form.save()
+ picking.button_validate()
+
+ # Check a new sale order line was correctly created.
+ self.assertEqual(len(sale_order.order_line), 2)
+ so_line_1 = sale_order.order_line[0]
+ so_line_2 = sale_order.order_line[1]
+ self.assertEqual(so_line_1.product_id.id, product_inv_on_order.id)
+ self.assertEqual(so_line_1.product_uom_qty, 3)
+ self.assertEqual(so_line_1.qty_delivered, 5)
+ self.assertEqual(so_line_1.price_unit, 55.0)
+ self.assertEqual(so_line_2.product_id.id, product_inv_on_delivered.id)
+ self.assertEqual(so_line_2.product_uom_qty, 0)
+ self.assertEqual(so_line_2.qty_delivered, 5)
+ self.assertEqual(
+ so_line_2.price_unit, 70.0,
+ "Should get the product price as the invoice policy is on qty. delivered")
+
+ def test_14_delivered_qty_in_multistep(self):
+ """ Creates a sale order with delivery in two-step. Process the pick &
+ ship and check we don't have extra SO line. Then, do the same but with
+ adding a extra move and check only one extra SO line was created.
+ """
+ # Set the delivery in two steps.
+ warehouse = self.company_data['default_warehouse']
+ warehouse.delivery_steps = 'pick_ship'
+ # Configure a product with invoicing policy on order.
+ product_inv_on_order = self.env['product.product'].create({
+ 'name': 'Shenaniffluffy',
+ 'type': 'consu',
+ 'invoice_policy': 'order',
+ 'list_price': 55.0,
+ })
+ # Create a sale order.
+ sale_order = self._get_new_sale_order()
+ # Confirms the sale order, then valids pick and delivery.
+ sale_order.action_confirm()
+ self.assertTrue(sale_order.picking_ids)
+ self.assertEqual(len(sale_order.order_line), 1)
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+ pick = sale_order.picking_ids.filtered(lambda p: p.picking_type_code == 'internal')
+ delivery = sale_order.picking_ids.filtered(lambda p: p.picking_type_code == 'outgoing')
+
+ picking_form = Form(pick)
+ with picking_form.move_line_ids_without_package.edit(0) as move:
+ move.qty_done = 10
+ pick = picking_form.save()
+ pick.button_validate()
+
+ picking_form = Form(delivery)
+ with picking_form.move_line_ids_without_package.edit(0) as move:
+ move.qty_done = 10
+ delivery = picking_form.save()
+ delivery.button_validate()
+
+ # Check no new sale order line was created.
+ self.assertEqual(len(sale_order.order_line), 1)
+ self.assertEqual(sale_order.order_line.product_uom_qty, 10)
+ self.assertEqual(sale_order.order_line.qty_delivered, 10)
+ self.assertEqual(sale_order.order_line.price_unit, 70.0)
+
+ # Creates a second sale order.
+ sale_order = self._get_new_sale_order()
+ # Confirms the sale order then add a new line for an another product in the pick/out.
+ sale_order.action_confirm()
+ self.assertTrue(sale_order.picking_ids)
+ self.assertEqual(len(sale_order.order_line), 1)
+ self.assertEqual(sale_order.order_line.qty_delivered, 0)
+ pick = sale_order.picking_ids.filtered(lambda p: p.picking_type_code == 'internal')
+ delivery = sale_order.picking_ids.filtered(lambda p: p.picking_type_code == 'outgoing')
+
+ picking_form = Form(pick)
+ with picking_form.move_line_ids_without_package.edit(0) as move:
+ move.qty_done = 10
+ with picking_form.move_line_ids_without_package.new() as new_move:
+ new_move.product_id = product_inv_on_order
+ new_move.qty_done = 10
+ pick = picking_form.save()
+ pick.button_validate()
+
+ picking_form = Form(delivery)
+ with picking_form.move_line_ids_without_package.edit(0) as move:
+ move.qty_done = 10
+ with picking_form.move_line_ids_without_package.new() as new_move:
+ new_move.product_id = product_inv_on_order
+ new_move.qty_done = 10
+ delivery = picking_form.save()
+ delivery.button_validate()
+
+ # Check a new sale order line was correctly created.
+ self.assertEqual(len(sale_order.order_line), 2)
+ so_line_1 = sale_order.order_line[0]
+ so_line_2 = sale_order.order_line[1]
+ self.assertEqual(so_line_1.product_id.id, self.company_data['product_delivery_no'].id)
+ self.assertEqual(so_line_1.product_uom_qty, 10)
+ self.assertEqual(so_line_1.qty_delivered, 10)
+ self.assertEqual(so_line_1.price_unit, 70.0)
+ self.assertEqual(so_line_2.product_id.id, product_inv_on_order.id)
+ self.assertEqual(so_line_2.product_uom_qty, 0)
+ self.assertEqual(so_line_2.qty_delivered, 10)
+ self.assertEqual(so_line_2.price_unit, 0)
+
+ def test_08_sale_return_qty_and_cancel(self):
+ """
+ Test a SO with a product on delivery with a 5 quantity.
+ Create two invoices: one for 3 quantity and one for 2 quantity
+ Then cancel Sale order, it won't raise any warning, it should be cancelled.
+ """
+ partner = self.partner_a
+ product = self.company_data['product_delivery_no']
+ so_vals = {
+ 'partner_id': partner.id,
+ 'partner_invoice_id': partner.id,
+ 'partner_shipping_id': partner.id,
+ 'order_line': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 5.0,
+ 'product_uom': product.uom_id.id,
+ 'price_unit': product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ so = self.env['sale.order'].create(so_vals)
+
+ # confirm the so
+ so.action_confirm()
+
+ # deliver partially
+ pick = so.picking_ids
+ pick.move_lines.write({'quantity_done': 3})
+
+ wiz_act = pick.button_validate()
+ wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
+ wiz.process()
+
+ # create invoice for 3 quantity and post it
+ inv_1 = so._create_invoices()
+ inv_1.action_post()
+ self.assertEqual(inv_1.state, 'posted', 'invoice should be in posted state')
+
+ pick_2 = so.picking_ids.filtered('backorder_id')
+ pick_2.move_lines.write({'quantity_done': 2})
+ pick_2.button_validate()
+
+ # create invoice for remaining 2 quantity
+ inv_2 = so._create_invoices()
+ self.assertEqual(inv_2.state, 'draft', 'invoice should be in draft state')
+
+ # check the status of invoices after cancelling the order
+ so.action_cancel()
+ wizard = self.env['sale.order.cancel'].with_context({'order_id': so.id}).create({'order_id': so.id})
+ wizard.action_cancel()
+ self.assertEqual(inv_1.state, 'posted', 'A posted invoice state should remain posted')
+ self.assertEqual(inv_2.state, 'cancel', 'A drafted invoice state should be cancelled')
+
+ def test_15_cancel_delivery(self):
+ """ Suppose the option "Lock Confirmed Sales" enabled and a product with the invoicing
+ policy set to "Delivered quantities". When cancelling the delivery of such a product, the
+ invoice status of the associated SO should be 'Nothing to Invoice'
+ """
+ group_auto_done = self.env['ir.model.data'].xmlid_to_object('sale.group_auto_done_setting')
+ self.env.user.groups_id = [(4, group_auto_done.id)]
+
+ product = self.product_a
+ partner = self.partner_a
+ so = self.env['sale.order'].create({
+ 'partner_id': partner.id,
+ 'partner_invoice_id': partner.id,
+ 'partner_shipping_id': partner.id,
+ 'order_line': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 2,
+ 'product_uom': product.uom_id.id,
+ 'price_unit': product.list_price
+ })],
+ 'pricelist_id': self.env.ref('product.list0').id,
+ })
+ so.action_confirm()
+ self.assertEqual(so.state, 'done')
+ so.picking_ids.action_cancel()
+
+ self.assertEqual(so.invoice_status, 'no')
diff --git a/addons/sale_stock/tests/test_sale_stock_lead_time.py b/addons/sale_stock/tests/test_sale_stock_lead_time.py
new file mode 100644
index 00000000..00339f1c
--- /dev/null
+++ b/addons/sale_stock/tests/test_sale_stock_lead_time.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.addons.sale.tests.common import TestSaleCommon
+from odoo import fields
+from odoo.tests import tagged
+
+from datetime import timedelta
+
+
+@tagged('post_install', '-at_install')
+class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ # Update the product_1 with type and Customer Lead Time
+ cls.test_product_order.sale_delay = 5.0
+
+ def test_00_product_company_level_delays(self):
+ """ In order to check schedule date, set product's Customer Lead Time
+ and company's Sales Safety Days."""
+
+ # Update company with Sales Safety Days
+ self.env.company.security_lead = 3.00
+
+ # Create sale order of product_1
+ order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ 'picking_policy': 'direct',
+ 'warehouse_id': self.company_data['default_warehouse'].id,
+ 'order_line': [(0, 0, {'name': self.test_product_order.name,
+ 'product_id': self.test_product_order.id,
+ 'product_uom_qty': 10,
+ 'product_uom': self.env.ref('uom.product_uom_unit').id,
+ 'customer_lead': self.test_product_order.sale_delay})]})
+
+ # Confirm our standard sale order
+ order.action_confirm()
+
+ # Check the picking crated or not
+ self.assertTrue(order.picking_ids, "Picking should be created.")
+
+ # Check schedule date of picking
+ out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead)
+ min_date = fields.Datetime.from_string(order.picking_ids[0].scheduled_date)
+ self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.')
+
+ def test_01_product_route_level_delays(self):
+ """ In order to check schedule dates, set product's Customer Lead Time
+ and warehouse route's delay."""
+
+ # Update warehouse_1 with Outgoing Shippings pick + pack + ship
+ self.company_data['default_warehouse'].write({'delivery_steps': 'pick_pack_ship'})
+
+ # Set delay on pull rule
+ for pull_rule in self.company_data['default_warehouse'].delivery_route_id.rule_ids:
+ pull_rule.write({'delay': 2})
+
+ # Create sale order of product_1
+ order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ 'picking_policy': 'direct',
+ 'warehouse_id': self.company_data['default_warehouse'].id,
+ 'order_line': [(0, 0, {'name': self.test_product_order.name,
+ 'product_id': self.test_product_order.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.env.ref('uom.product_uom_unit').id,
+ 'customer_lead': self.test_product_order.sale_delay})]})
+
+ # Confirm our standard sale order
+ order.action_confirm()
+
+ # Check the picking crated or not
+ self.assertTrue(order.picking_ids, "Pickings should be created.")
+
+ # Check schedule date of ship type picking
+ out = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].out_type_id)
+ out_min_date = fields.Datetime.from_string(out.scheduled_date)
+ out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=out.move_lines[0].rule_id.delay)
+ self.assertTrue(abs(out_min_date - out_date) <= timedelta(seconds=1), 'Schedule date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay.')
+
+ # Check schedule date of pack type picking
+ pack = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pack_type_id)
+ pack_min_date = fields.Datetime.from_string(pack.scheduled_date)
+ pack_date = out_date - timedelta(days=pack.move_lines[0].rule_id.delay)
+ self.assertTrue(abs(pack_min_date - pack_date) <= timedelta(seconds=1), 'Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.')
+
+ # Check schedule date of pick type picking
+ pick = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pick_type_id)
+ pick_min_date = fields.Datetime.from_string(pick.scheduled_date)
+ pick_date = pack_date - timedelta(days=pick.move_lines[0].rule_id.delay)
+ self.assertTrue(abs(pick_min_date - pick_date) <= timedelta(seconds=1), 'Schedule date of pick type picking should be equal to: Schedule date of pack type picking - pull rule delay.')
+
+ def test_02_delivery_date_propagation(self):
+ """ In order to check deadline date propagation, set product's Customer Lead Time
+ and warehouse route's delay in stock rules"""
+
+ # Example :
+ # -> Set Warehouse with Outgoing Shipments : pick + pack + ship
+ # -> Set Delay : 5 days on stock rules
+ # -> Set Customer Lead Time on product : 30 days
+ # -> Set Sales Safety Days : 2 days
+ # -> Create an SO and confirm it with confirmation Date : 12/18/2018
+
+ # -> Pickings : OUT -> Scheduled Date : 01/12/2019, Deadline Date: 01/14/2019
+ # PACK -> Scheduled Date : 01/07/2019, Deadline Date: 01/09/2019
+ # PICK -> Scheduled Date : 01/02/2019, Deadline Date: 01/04/2019
+
+ # -> Now, change commitment_date in the sale order = out_deadline_date + 5 days
+
+ # -> Deadline Date should be changed and Scheduled date should be unchanged:
+ # OUT -> Deadline Date : 01/19/2019
+ # PACK -> Deadline Date : 01/14/2019
+ # PICK -> Deadline Date : 01/09/2019
+
+ # Update company with Sales Safety Days
+ self.env.company.security_lead = 2.00
+
+ # Update warehouse_1 with Outgoing Shippings pick + pack + ship
+ self.company_data['default_warehouse'].write({'delivery_steps': 'pick_pack_ship'})
+
+ # Set delay on pull rule
+ self.company_data['default_warehouse'].delivery_route_id.rule_ids.write({'delay': 5})
+
+ # Update the product_1 with type and Customer Lead Time
+ self.test_product_order.write({'type': 'product', 'sale_delay': 30.0})
+
+ # Now, create sale order of product_1 with customer_lead set on product
+ order = self.env['sale.order'].create({
+ 'partner_id': self.partner_a.id,
+ 'partner_invoice_id': self.partner_a.id,
+ 'partner_shipping_id': self.partner_a.id,
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ 'picking_policy': 'direct',
+ 'warehouse_id': self.company_data['default_warehouse'].id,
+ 'order_line': [(0, 0, {'name': self.test_product_order.name,
+ 'product_id': self.test_product_order.id,
+ 'product_uom_qty': 5,
+ 'product_uom': self.env.ref('uom.product_uom_unit').id,
+ 'customer_lead': self.test_product_order.sale_delay})]})
+
+ # Confirm our standard sale order
+ order.action_confirm()
+
+ # Check the picking crated or not
+ self.assertTrue(order.picking_ids, "Pickings should be created.")
+
+ # Check schedule/deadline date of ship type picking
+ out = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].out_type_id)
+ deadline_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=out.move_lines[0].rule_id.delay)
+ self.assertAlmostEqual(
+ out.date_deadline, deadline_date, delta=timedelta(seconds=1),
+ msg='Deadline date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay.')
+ out_scheduled_date = deadline_date - timedelta(days=self.env.company.security_lead)
+ self.assertAlmostEqual(
+ out.scheduled_date, out_scheduled_date, delta=timedelta(seconds=1),
+ msg='Schedule date of ship type picking should be equal to: order date + Customer Lead Time - pull rule delay - security_lead')
+
+ # Check schedule/deadline date of pack type picking
+ pack = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pack_type_id)
+ pack_scheduled_date = out_scheduled_date - timedelta(days=pack.move_lines[0].rule_id.delay)
+ self.assertAlmostEqual(
+ pack.scheduled_date, pack_scheduled_date, delta=timedelta(seconds=1),
+ msg='Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.')
+ deadline_date -= timedelta(days=pack.move_lines[0].rule_id.delay)
+ self.assertAlmostEqual(
+ pack.date_deadline, deadline_date, delta=timedelta(seconds=1),
+ msg='Deadline date of pack type picking should be equal to: Deadline date of ship type picking - pull rule delay.')
+
+ # Check schedule/deadline date of pick type picking
+ pick = order.picking_ids.filtered(lambda r: r.picking_type_id == self.company_data['default_warehouse'].pick_type_id)
+ pick_scheduled_date = pack_scheduled_date - timedelta(days=pick.move_lines[0].rule_id.delay)
+ self.assertAlmostEqual(
+ pick.scheduled_date, pick_scheduled_date, delta=timedelta(seconds=1),
+ msg='Schedule date of pack type picking should be equal to: Schedule date of ship type picking - pull rule delay.')
+ deadline_date -= timedelta(days=pick.move_lines[0].rule_id.delay)
+ self.assertAlmostEqual(
+ pick.date_deadline, deadline_date, delta=timedelta(seconds=1),
+ msg='Deadline date of pack type picking should be equal to: Deadline date of ship type picking - pull rule delay.')
+
+ # Now change the commitment date (Delivery Date) of the sale order
+ new_deadline = deadline_date + timedelta(days=5)
+ order.write({'commitment_date': new_deadline})
+
+ # Now check date_deadline of pick, pack and out are forced
+ # TODO : add note in case of change of deadline and check
+ self.assertEqual(out.date_deadline, new_deadline)
+ new_deadline -= timedelta(days=pack.move_lines[0].rule_id.delay)
+ self.assertEqual(pack.date_deadline, new_deadline)
+ new_deadline -= timedelta(days=pick.move_lines[0].rule_id.delay)
+ self.assertEqual(pick.date_deadline, new_deadline)
diff --git a/addons/sale_stock/tests/test_sale_stock_multicompany.py b/addons/sale_stock/tests/test_sale_stock_multicompany.py
new file mode 100644
index 00000000..998fbdb9
--- /dev/null
+++ b/addons/sale_stock/tests/test_sale_stock_multicompany.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
+from odoo.addons.sale.tests.common import TestSaleCommon
+from odoo.tests import tagged
+
+
+@tagged('post_install', '-at_install')
+class TestSaleStockMultiCompany(TestSaleCommon, ValuationReconciliationTestCommon):
+
+ @classmethod
+ def setUpClass(cls, chart_template_ref=None):
+ super().setUpClass(chart_template_ref=chart_template_ref)
+
+ cls.warehouse_A = cls.company_data['default_warehouse']
+ cls.warehouse_A2 = cls.env['stock.warehouse'].create({
+ 'name': 'WH B',
+ 'code': 'WHB',
+ 'company_id': cls.env.company.id,
+ 'partner_id': cls.env.company.partner_id.id,
+ })
+ cls.warehouse_B = cls.company_data_2['default_warehouse']
+
+ cls.env.user.groups_id |= cls.env.ref('stock.group_stock_user')
+ cls.env.user.groups_id |= cls.env.ref('stock.group_stock_multi_locations')
+ cls.env.user.groups_id |= cls.env.ref('sales_team.group_sale_salesman')
+
+ cls.env.user.with_company(cls.company_data['company']).property_warehouse_id = cls.warehouse_A.id
+ cls.env.user.with_company(cls.company_data_2['company']).property_warehouse_id = cls.warehouse_B.id
+
+ def test_warehouse_definition_on_so(self):
+
+ partner = self.partner_a
+ product = self.test_product_order
+
+ sale_order_vals = {
+ 'partner_id': partner.id,
+ 'partner_invoice_id': partner.id,
+ 'partner_shipping_id': partner.id,
+ 'user_id': False,
+ 'company_id': self.env.company.id,
+ 'order_line': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 10,
+ 'product_uom': product.uom_id.id,
+ 'price_unit': product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ sale_order = self.env['sale.order']
+
+ so_no_user = sale_order.create(sale_order_vals)
+ self.assertFalse(so_no_user.user_id.property_warehouse_id)
+ self.assertEqual(so_no_user.warehouse_id.id, self.warehouse_A.id)
+
+ sale_order_vals2 = {
+ 'partner_id': partner.id,
+ 'partner_invoice_id': partner.id,
+ 'partner_shipping_id': partner.id,
+ 'company_id': self.env.company.id,
+ 'order_line': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 10,
+ 'product_uom': product.uom_id.id,
+ 'price_unit': product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ so_company_A = sale_order.with_company(self.env.company).create(sale_order_vals2)
+ self.assertEqual(so_company_A.warehouse_id.id, self.warehouse_A.id)
+
+ sale_order_vals3 = {
+ 'partner_id': partner.id,
+ 'partner_invoice_id': partner.id,
+ 'partner_shipping_id': partner.id,
+ 'company_id': self.company_data_2['company'].id,
+ 'order_line': [(0, 0, {
+ 'name': product.name,
+ 'product_id': product.id,
+ 'product_uom_qty': 10,
+ 'product_uom': product.uom_id.id,
+ 'price_unit': product.list_price})],
+ 'pricelist_id': self.company_data['default_pricelist'].id,
+ }
+ so_company_B = sale_order.with_company(self.company_data_2['company']).create(sale_order_vals3)
+ self.assertEqual(so_company_B.warehouse_id.id, self.warehouse_B.id)
diff --git a/addons/sale_stock/tests/test_sale_stock_report.py b/addons/sale_stock/tests/test_sale_stock_report.py
new file mode 100644
index 00000000..a132a844
--- /dev/null
+++ b/addons/sale_stock/tests/test_sale_stock_report.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, timedelta
+
+from odoo.tests.common import Form
+from odoo.addons.stock.tests.test_report import TestReportsCommon
+
+
+class TestSaleStockReports(TestReportsCommon):
+ def test_report_forecast_1_sale_order_replenishment(self):
+ """ Create and confirm two sale orders: one for the next week and one
+ for tomorrow. Then check in the report it's the most urgent who is
+ linked to the qty. on stock.
+ """
+ today = datetime.today()
+ # Put some quantity in stock.
+ quant_vals = {
+ 'product_id': self.product.id,
+ 'product_uom_id': self.product.uom_id.id,
+ 'location_id': self.stock_location.id,
+ 'quantity': 5,
+ 'reserved_quantity': 0,
+ }
+ self.env['stock.quant'].create(quant_vals)
+ # Create a first SO for the next week.
+ so_form = Form(self.env['sale.order'])
+ so_form.partner_id = self.partner
+ # so_form.validity_date = today + timedelta(days=7)
+ with so_form.order_line.new() as so_line:
+ so_line.product_id = self.product
+ so_line.product_uom_qty = 5
+ so_1 = so_form.save()
+ so_1.action_confirm()
+ so_1.picking_ids.scheduled_date = today + timedelta(days=7)
+
+ # Create a second SO for tomorrow.
+ so_form = Form(self.env['sale.order'])
+ so_form.partner_id = self.partner
+ # so_form.validity_date = today + timedelta(days=1)
+ with so_form.order_line.new() as so_line:
+ so_line.product_id = self.product
+ so_line.product_uom_qty = 5
+ so_2 = so_form.save()
+ so_2.action_confirm()
+ so_2.picking_ids.scheduled_date = today + timedelta(days=1)
+
+ report_values, docs, lines = self.get_report_forecast(product_template_ids=self.product_template.ids)
+ self.assertEqual(len(lines), 2)
+ line_1 = lines[0]
+ line_2 = lines[1]
+ self.assertEqual(line_1['quantity'], 5)
+ self.assertTrue(line_1['replenishment_filled'])
+ self.assertEqual(line_1['document_out'].id, so_2.id)
+ self.assertEqual(line_2['quantity'], 5)
+ self.assertEqual(line_2['replenishment_filled'], False)
+ self.assertEqual(line_2['document_out'].id, so_1.id)