diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/sale_stock/tests | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/sale_stock/tests')
| -rw-r--r-- | addons/sale_stock/tests/__init__.py | 10 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_anglo_saxon_valuation.py | 1124 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_anglo_saxon_valuation_reconciliation.py | 145 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_sale_order_dates.py | 115 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_sale_stock.py | 1020 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_sale_stock_lead_time.py | 199 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_sale_stock_multicompany.py | 86 | ||||
| -rw-r--r-- | addons/sale_stock/tests/test_sale_stock_report.py | 57 |
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) |
