From 4b9796521e7a716c66df38a92357406a359354ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20G=C3=B6ttsch?= Date: Tue, 12 May 2026 15:18:17 +0200 Subject: [PATCH] [16.0] sale_order_batch: add tests --- sale_order_batch/README.rst | 1 + sale_order_batch/readme/HISTORY.rst | 3 +- .../static/description/index.html | 1 + sale_order_batch/tests/__init__.py | 7 + sale_order_batch/tests/test_sale_order.py | 158 +++++++++++ .../tests/test_sale_order_batch.py | 249 ++++++++++++++++++ .../test_sale_order_batch_cancel_wizard.py | 86 ++++++ .../tests/test_sale_order_batch_product.py | 119 +++++++++ .../tests/test_sale_order_line.py | 145 ++++++++++ 9 files changed, 768 insertions(+), 1 deletion(-) create mode 100644 sale_order_batch/tests/__init__.py create mode 100644 sale_order_batch/tests/test_sale_order.py create mode 100644 sale_order_batch/tests/test_sale_order_batch.py create mode 100644 sale_order_batch/tests/test_sale_order_batch_cancel_wizard.py create mode 100644 sale_order_batch/tests/test_sale_order_batch_product.py create mode 100644 sale_order_batch/tests/test_sale_order_line.py diff --git a/sale_order_batch/README.rst b/sale_order_batch/README.rst index 66e750a..6252b2c 100644 --- a/sale_order_batch/README.rst +++ b/sale_order_batch/README.rst @@ -49,6 +49,7 @@ Changelog - 16.0.2.2.1: Add lst_price to view - 16.0.2.3.0: Add "Reset to Open" - 16.0.2.3.1: Remove Sale Order from batch on cancelation +- 16.0.2.4.0: Add Followers on In Progress & add tests Bug Tracker =========== diff --git a/sale_order_batch/readme/HISTORY.rst b/sale_order_batch/readme/HISTORY.rst index 592dc2e..cdc713f 100644 --- a/sale_order_batch/readme/HISTORY.rst +++ b/sale_order_batch/readme/HISTORY.rst @@ -8,4 +8,5 @@ - 16.0.2.2.0: Add invoice status and menues - 16.0.2.2.1: Add lst_price to view - 16.0.2.3.0: Add "Reset to Open" -- 16.0.2.3.1: Remove Sale Order from batch on cancelation \ No newline at end of file +- 16.0.2.3.1: Remove Sale Order from batch on cancelation +- 16.0.2.4.0: Add Followers on In Progress & add tests \ No newline at end of file diff --git a/sale_order_batch/static/description/index.html b/sale_order_batch/static/description/index.html index bd90800..537c93b 100644 --- a/sale_order_batch/static/description/index.html +++ b/sale_order_batch/static/description/index.html @@ -405,6 +405,7 @@ anymore as log as they are part of a batch. Only Sale Orders in state Qutotation
  • 16.0.2.2.1: Add lst_price to view
  • 16.0.2.3.0: Add “Reset to Open”
  • 16.0.2.3.1: Remove Sale Order from batch on cancelation
  • +
  • 16.0.2.4.0: Add Followers on In Progress & add tests
  • diff --git a/sale_order_batch/tests/__init__.py b/sale_order_batch/tests/__init__.py new file mode 100644 index 0000000..c3a148c --- /dev/null +++ b/sale_order_batch/tests/__init__.py @@ -0,0 +1,7 @@ +from . import ( + test_sale_order, + test_sale_order_batch, + test_sale_order_batch_cancel_wizard, + test_sale_order_batch_product, + test_sale_order_line, +) diff --git a/sale_order_batch/tests/test_sale_order.py b/sale_order_batch/tests/test_sale_order.py new file mode 100644 index 0000000..730e4e2 --- /dev/null +++ b/sale_order_batch/tests/test_sale_order.py @@ -0,0 +1,158 @@ +from odoo.exceptions import UserError +from odoo.tests import TransactionCase + + +class TestSaleOrder(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context={**cls.env.context, "tracking_disable": True}) + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.product = cls.env["product.product"].create( + {"name": "Test Service", "type": "service", "list_price": 100.0} + ) + + def _create_order(self, **kwargs): + vals = { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": 1, + "price_unit": 100.0, + }, + ) + ], + } + vals.update(kwargs) + return self.env["sale.order"].create(vals) + + def _create_batch(self): + return self.env["sale.order.batch"].create({}) + + # --- _get_current_batch --- + + def test_get_current_batch_finds_open_batch(self): + batch = self._create_batch() + order = self._create_order() + self.assertEqual(order._get_current_batch(), batch) + + def test_get_current_batch_ignores_closed_batch(self): + batch = self._create_batch() + batch.action_confirm() + order = self._create_order() + self.assertNotEqual(order._get_current_batch(), batch) + + # --- action_add_to_batch --- + + def test_action_add_to_batch_creates_new_batch(self): + order = self._create_order() + order.action_add_to_batch() + self.assertTrue(order.batch_id) + + def test_action_add_to_batch_uses_existing_open_batch(self): + batch = self._create_batch() + order = self._create_order() + order.action_add_to_batch() + self.assertEqual(order.batch_id, batch) + + def test_action_add_to_batch_skips_already_batched_order(self): + batch1 = self._create_batch() + order = self._create_order(batch_id=batch1.id) + order.action_add_to_batch() + self.assertEqual(order.batch_id, batch1) + + def test_action_add_to_batch_skips_confirmed_order(self): + order = self._create_order() + order.with_context(bypass_batch=True).action_confirm() + order.action_add_to_batch() + self.assertFalse(order.batch_id) + + # --- action_view_sale_order_batch --- + + def test_action_view_sale_order_batch_returns_form_action(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + result = order.action_view_sale_order_batch() + self.assertEqual(result["res_model"], "sale.order.batch") + self.assertEqual(result["res_id"], batch.id) + self.assertEqual(result["view_mode"], "form") + + # --- action_confirm --- + + def test_action_confirm_raises_user_error_when_in_batch(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + with self.assertRaises(UserError): + order.action_confirm() + + def test_action_confirm_raises_for_multiple_batched_orders(self): + batch = self._create_batch() + order1 = self._create_order(batch_id=batch.id) + order2 = self._create_order(batch_id=batch.id) + with self.assertRaises(UserError): + (order1 | order2).action_confirm() + + def test_action_confirm_works_with_bypass_batch_context(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + order.with_context(bypass_batch=True).action_confirm() + self.assertEqual(order.state, "sale") + + def test_action_confirm_works_without_batch(self): + order = self._create_order() + order.action_confirm() + self.assertEqual(order.state, "sale") + + # --- action_quotation_send --- + + def test_action_quotation_send_raises_user_error_when_in_batch(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + with self.assertRaises(UserError): + order.action_quotation_send() + + def test_action_quotation_send_raises_for_multiple_batched_orders(self): + batch = self._create_batch() + order1 = self._create_order(batch_id=batch.id) + order2 = self._create_order(batch_id=batch.id) + with self.assertRaises(UserError): + (order1 | order2).action_quotation_send() + + def test_action_quotation_send_works_with_bypass_batch_context(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + result = order.with_context(bypass_batch=True).action_quotation_send() + self.assertIsNotNone(result) + + def test_action_quotation_send_works_without_batch(self): + order = self._create_order() + result = order.action_quotation_send() + self.assertIsNotNone(result) + + # --- action_cancel --- + + def test_action_cancel_clears_batch_id(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + order.action_cancel() + self.assertFalse(order.batch_id) + + # --- write --- + + def test_write_batch_id_updates_batch_products(self): + batch = self._create_batch() + order = self._create_order() + self.assertFalse(order.order_line.batch_product_id) + order.write({"batch_id": batch.id}) + self.assertTrue(order.order_line.batch_product_id) + + def test_write_without_batch_id_does_not_change_products(self): + batch = self._create_batch() + order = self._create_order(batch_id=batch.id) + batch_product = order.order_line.batch_product_id + order.write({"note": "test note"}) + self.assertEqual(order.order_line.batch_product_id, batch_product) diff --git a/sale_order_batch/tests/test_sale_order_batch.py b/sale_order_batch/tests/test_sale_order_batch.py new file mode 100644 index 0000000..cbbe648 --- /dev/null +++ b/sale_order_batch/tests/test_sale_order_batch.py @@ -0,0 +1,249 @@ +from odoo.tests import TransactionCase + + +class TestSaleOrderBatch(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context={**cls.env.context, "tracking_disable": True}) + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.product = cls.env["product.product"].create( + { + "name": "Test Service Product", + "type": "service", + "list_price": 100.0, + "invoice_policy": "order", + } + ) + cls.product2 = cls.env["product.product"].create( + { + "name": "Test Service Product 2", + "type": "service", + "list_price": 200.0, + "invoice_policy": "order", + } + ) + + def _create_batch(self, **kwargs): + return self.env["sale.order.batch"].create(kwargs) + + def _create_order(self, batch=None, product=None, qty=1, price=100.0): + if product is None: + product = self.product + vals = { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_uom_qty": qty, + "price_unit": price, + }, + ) + ], + } + if batch: + vals["batch_id"] = batch.id + return self.env["sale.order"].create(vals) + + def test_create_generates_sequence_name(self): + batch = self._create_batch() + self.assertTrue(batch.name.startswith("SOB")) + self.assertNotEqual(batch.name, "New") + + def test_create_keeps_explicit_name(self): + batch = self.env["sale.order.batch"].create({"name": "MY-BATCH-001"}) + self.assertEqual(batch.name, "MY-BATCH-001") + + def test_compute_sale_order_count_with_orders(self): + batch = self._create_batch() + self._create_order(batch=batch) + self._create_order(batch=batch) + self.assertEqual(batch.sale_order_count, 2) + + def test_compute_amount_total(self): + batch = self._create_batch() + order1 = self._create_order(batch=batch, price=100.0) + order2 = self._create_order(batch=batch, price=200.0) + self.assertAlmostEqual( + batch.amount_total, order1.amount_total + order2.amount_total + ) + + def test_compute_product_count_with_products(self): + batch = self._create_batch() + self._create_order(batch=batch, product=self.product) + self._create_order(batch=batch, product=self.product2) + self.assertEqual(batch.product_count, 2) + + def test_compute_invoice_ids_with_invoice(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + order.with_context(bypass_batch=True).action_confirm() + invoice = order._create_invoices() + self.assertIn(invoice, batch.invoice_ids) + self.assertEqual(batch.invoice_count, 1) + + def test_compute_partner_credit_warning_no_warnings(self): + batch = self._create_batch() + self._create_order(batch=batch) + self.assertEqual(batch.partner_credit_warning, "") + + # --- action_view_source_sale_orders --- + + def test_action_view_source_sale_orders_zero(self): + batch = self._create_batch() + result = batch.action_view_source_sale_orders() + self.assertEqual(result["type"], "ir.actions.act_window_close") + + def test_action_view_source_sale_orders_one(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + result = batch.action_view_source_sale_orders() + self.assertEqual(result["res_id"], order.id) + self.assertIn("views", result) + + def test_action_view_source_sale_orders_multiple(self): + batch = self._create_batch() + self._create_order(batch=batch) + self._create_order(batch=batch) + result = batch.action_view_source_sale_orders() + self.assertIn("domain", result) + + # --- action_view_products --- + + def test_action_view_products_zero(self): + batch = self._create_batch() + result = batch.action_view_products() + self.assertEqual(result["type"], "ir.actions.act_window_close") + + def test_action_view_products_one(self): + batch = self._create_batch() + self._create_order(batch=batch) + result = batch.action_view_products() + self.assertIn("res_id", result) + self.assertIn("views", result) + self.assertEqual(result["res_id"], batch.product_ids.id) + + def test_action_view_products_multiple(self): + batch = self._create_batch() + self._create_order(batch=batch, product=self.product) + self._create_order(batch=batch, product=self.product2) + result = batch.action_view_products() + self.assertIn("domain", result) + + # --- action_view_invoice --- + + def test_action_view_invoice_zero(self): + batch = self._create_batch() + result = batch.action_view_invoice() + self.assertEqual(result["type"], "ir.actions.act_window_close") + + def test_action_view_invoice_one(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + order.with_context(bypass_batch=True).action_confirm() + order._create_invoices() + result = batch.action_view_invoice() + self.assertIn("res_id", result) + self.assertIn("views", result) + + def test_action_view_invoice_multiple(self): + batch = self._create_batch() + order1 = self._create_order(batch=batch) + order2 = self._create_order(batch=batch) + order1.with_context(bypass_batch=True).action_confirm() + order2.with_context(bypass_batch=True).action_confirm() + order1._create_invoices() + order2._create_invoices() + result = batch.action_view_invoice() + self.assertIn("domain", result) + + # --- action_in_progress --- + + def test_action_in_progress_sets_state(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.action_in_progress() + self.assertEqual(batch.state, "in_progress") + + def test_action_in_progress_sends_quotation(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + batch.action_in_progress() + self.assertEqual(order.state, "sent") + + # --- action_confirm --- + + def test_action_confirm_sets_batch_closed(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.action_confirm() + self.assertEqual(batch.state, "closed") + + def test_action_confirm_confirms_orders(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + batch.action_confirm() + self.assertEqual(order.state, "sale") + + # --- action_open --- + + def test_action_open_resets_state(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.action_in_progress() + batch.action_open() + self.assertEqual(batch.state, "open") + + def test_action_open_resets_orders_to_draft(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + batch.action_in_progress() + batch.action_open() + self.assertEqual(order.state, "draft") + + # --- _show_cancel_wizard --- + + def test_show_cancel_wizard_disabled_by_context(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.action_in_progress() + result = batch.with_context(disable_cancel_warning=True)._show_cancel_wizard() + self.assertFalse(result) + + def test_show_cancel_wizard_no_orders(self): + batch = self._create_batch() + self.assertFalse(batch._show_cancel_wizard()) + + def test_show_cancel_wizard_with_sent_orders(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.action_in_progress() + self.assertTrue(batch._show_cancel_wizard()) + + # --- action_cancel --- + + def test_action_cancel_returns_wizard_for_sent_orders(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.action_in_progress() + result = batch.action_cancel() + self.assertIsNotNone(result) + self.assertEqual(result.get("res_model"), "sale.order.batch.cancel.wizard") + + def test_action_cancel_without_wizard_sets_cancel(self): + batch = self._create_batch() + self._create_order(batch=batch) + batch.with_context(disable_cancel_warning=True).action_cancel() + self.assertEqual(batch.state, "cancel") + + # --- _action_cancel --- + + def test_action_cancel_directly(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + batch._action_cancel() + self.assertEqual(batch.state, "cancel") + self.assertEqual(order.state, "cancel") diff --git a/sale_order_batch/tests/test_sale_order_batch_cancel_wizard.py b/sale_order_batch/tests/test_sale_order_batch_cancel_wizard.py new file mode 100644 index 0000000..a63f211 --- /dev/null +++ b/sale_order_batch/tests/test_sale_order_batch_cancel_wizard.py @@ -0,0 +1,86 @@ +from odoo.tests import TransactionCase + + +class TestSaleOrderBatchCancelWizard(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context={**cls.env.context, "tracking_disable": True}) + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.product = cls.env["product.product"].create( + {"name": "Test Product", "type": "service"} + ) + + def _create_batch_with_order(self): + batch = self.env["sale.order.batch"].create({}) + order = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "batch_id": batch.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": 1, + "price_unit": 10.0, + }, + ) + ], + } + ) + return batch, order + + def _create_wizard(self, batch): + return ( + self.env["sale.order.batch.cancel.wizard"] + .with_context(active_ids=[batch.id]) + .create({"batch_ids": [(4, batch.id)]}) + ) + + # --- _compute_order_ids --- + + def test_compute_order_ids(self): + batch, order = self._create_batch_with_order() + wizard = self._create_wizard(batch) + self.assertIn(order, wizard.order_ids) + + def test_compute_order_ids_multiple_batches(self): + batch1, order1 = self._create_batch_with_order() + batch2, order2 = self._create_batch_with_order() + wizard = ( + self.env["sale.order.batch.cancel.wizard"] + .with_context(active_ids=[batch1.id, batch2.id]) + .create({"batch_ids": [(4, batch1.id), (4, batch2.id)]}) + ) + self.assertIn(order1, wizard.order_ids) + self.assertIn(order2, wizard.order_ids) + + # --- action_cancel --- + + def test_action_cancel_sets_batch_state_cancel(self): + batch, order = self._create_batch_with_order() + wizard = self._create_wizard(batch) + wizard.action_cancel() + self.assertEqual(batch.state, "cancel") + + def test_action_cancel_cancels_orders(self): + batch, order = self._create_batch_with_order() + wizard = self._create_wizard(batch) + wizard.action_cancel() + self.assertEqual(order.state, "cancel") + + def test_action_cancel_returns_window_close(self): + batch, order = self._create_batch_with_order() + wizard = self._create_wizard(batch) + result = wizard.action_cancel() + self.assertEqual(result["type"], "ir.actions.act_window_close") + + # --- action_discard --- + + def test_action_discard_returns_window_close(self): + batch, order = self._create_batch_with_order() + wizard = self._create_wizard(batch) + result = wizard.action_discard() + self.assertEqual(result["type"], "ir.actions.act_window_close") diff --git a/sale_order_batch/tests/test_sale_order_batch_product.py b/sale_order_batch/tests/test_sale_order_batch_product.py new file mode 100644 index 0000000..6bcc379 --- /dev/null +++ b/sale_order_batch/tests/test_sale_order_batch_product.py @@ -0,0 +1,119 @@ +from odoo.tests import TransactionCase + + +class TestSaleOrderBatchProduct(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context={**cls.env.context, "tracking_disable": True}) + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.product = cls.env["product.product"].create( + {"name": "Test Product", "type": "service"} + ) + + def _create_batch_with_order(self, qty=1): + batch = self.env["sale.order.batch"].create({}) + order = self.env["sale.order"].create( + { + "partner_id": self.partner.id, + "batch_id": batch.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": qty, + "price_unit": 10.0, + }, + ) + ], + } + ) + return batch, order, batch.product_ids[0] + + # --- _compute_uom_qty --- + + def test_compute_uom_qty_single_line(self): + batch, order, batch_product = self._create_batch_with_order(qty=3) + self.assertAlmostEqual(batch_product.product_uom_qty, 3.0) + + def test_compute_uom_qty_multiple_lines(self): + batch, order, batch_product = self._create_batch_with_order(qty=3) + self.env["sale.order.line"].create( + { + "order_id": order.id, + "product_id": self.product.id, + "product_uom_qty": 2, + "price_unit": 10.0, + } + ) + self.assertAlmostEqual(batch_product.product_uom_qty, 5.0) + + # --- _compute_product_packaging_qty --- + + def test_compute_product_packaging_qty_without_packaging(self): + batch, order, batch_product = self._create_batch_with_order() + if not batch_product.product_packaging_id: + self.assertEqual(batch_product.product_packaging_qty, 1) + + def test_compute_product_packaging_qty_with_packaging(self): + packaging = self.env["product.packaging"].create( + { + "name": "Box of 10", + "product_id": self.product.id, + "qty": 10.0, + "sales": True, + } + ) + batch, order, batch_product = self._create_batch_with_order(qty=5) + self.assertEqual(batch_product.product_packaging_id, packaging) + self.assertEqual(batch_product.product_packaging_qty, 10.0) + + # --- _compute_product_packaging_id --- + + def test_compute_product_packaging_id_suggests_first_packaging(self): + packaging = self.env["product.packaging"].create( + { + "name": "Box of 6", + "product_id": self.product.id, + "qty": 6.0, + "sales": True, + } + ) + batch, order, batch_product = self._create_batch_with_order(qty=4) + self.assertEqual(batch_product.product_packaging_id, packaging) + + def test_compute_product_packaging_id_clears_wrong_product_packaging(self): + product2 = self.env["product.product"].create( + {"name": "Other Product", "type": "service"} + ) + packaging2 = self.env["product.packaging"].create( + { + "name": "Box of 5", + "product_id": product2.id, + "qty": 5.0, + "sales": True, + } + ) + batch, order, batch_product = self._create_batch_with_order() + batch_product.product_packaging_id = packaging2 + batch_product._compute_product_packaging_id() + self.assertFalse(batch_product.product_packaging_id) + + def test_compute_product_packaging_id_no_packaging_when_no_qty(self): + self.env["product.packaging"].create( + { + "name": "Box of 12", + "product_id": self.product.id, + "qty": 12.0, + "sales": True, + } + ) + batch = self.env["sale.order.batch"].create({}) + batch_product = self.env["sale.order.batch.product"].create( + {"batch_id": batch.id, "product_id": self.product.id} + ) + # With no lines, product_uom_qty is 0 — packaging should not be suggested + self.assertFalse(batch_product.product_uom_qty) + self.assertFalse(batch_product.product_packaging_id) diff --git a/sale_order_batch/tests/test_sale_order_line.py b/sale_order_batch/tests/test_sale_order_line.py new file mode 100644 index 0000000..ce76417 --- /dev/null +++ b/sale_order_batch/tests/test_sale_order_line.py @@ -0,0 +1,145 @@ +from odoo.tests import TransactionCase + + +class TestSaleOrderLine(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context={**cls.env.context, "tracking_disable": True}) + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.product = cls.env["product.product"].create( + {"name": "Product A", "type": "service"} + ) + cls.product2 = cls.env["product.product"].create( + {"name": "Product B", "type": "service"} + ) + + def _create_batch(self): + return self.env["sale.order.batch"].create({}) + + def _create_order(self, batch=None, product=None, qty=1): + if product is None: + product = self.product + vals = { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_uom_qty": qty, + "price_unit": 10.0, + }, + ) + ], + } + if batch: + vals["batch_id"] = batch.id + return self.env["sale.order"].create(vals) + + # --- create --- + + def test_create_line_with_batch_links_batch_product(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line = order.order_line + self.assertTrue(line.batch_product_id) + self.assertEqual(line.batch_product_id.product_id, self.product) + self.assertEqual(line.batch_product_id.batch_id, batch) + + def test_create_line_without_batch_has_no_batch_product(self): + order = self._create_order() + self.assertFalse(order.order_line.batch_product_id) + + def test_create_two_lines_same_product_share_batch_product(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line2 = self.env["sale.order.line"].create( + { + "order_id": order.id, + "product_id": self.product.id, + "product_uom_qty": 2, + "price_unit": 10.0, + } + ) + self.assertEqual(order.order_line[0].batch_product_id, line2.batch_product_id) + + def test_create_two_lines_different_products_get_different_batch_products(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line2 = self.env["sale.order.line"].create( + { + "order_id": order.id, + "product_id": self.product2.id, + "product_uom_qty": 1, + "price_unit": 10.0, + } + ) + self.assertNotEqual( + order.order_line[0].batch_product_id, line2.batch_product_id + ) + self.assertEqual(batch.product_count, 2) + + # --- write --- + + def test_write_product_id_updates_batch_product(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line = order.order_line + old_batch_product = line.batch_product_id + line.write({"product_id": self.product2.id}) + self.assertNotEqual(line.batch_product_id, old_batch_product) + self.assertEqual(line.batch_product_id.product_id, self.product2) + + def test_write_without_product_id_keeps_batch_product(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line = order.order_line + batch_product = line.batch_product_id + line.write({"product_uom_qty": 5}) + self.assertEqual(line.batch_product_id, batch_product) + + # --- unlink --- + + def test_unlink_last_line_removes_batch_product(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line = order.order_line + batch_product = line.batch_product_id + line.unlink() + self.assertFalse(batch_product.exists()) + + def test_unlink_line_keeps_batch_product_when_other_lines_exist(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line2 = self.env["sale.order.line"].create( + { + "order_id": order.id, + "product_id": self.product.id, + "product_uom_qty": 2, + "price_unit": 10.0, + } + ) + batch_product = order.order_line[0].batch_product_id + order.order_line[0].unlink() + self.assertTrue(batch_product.exists()) + self.assertIn(line2, batch_product.sale_order_line_ids) + + # --- _update_batch_product --- + + def test_update_batch_product_links_when_batch_assigned(self): + order = self._create_order() + line = order.order_line + self.assertFalse(line.batch_product_id) + batch = self._create_batch() + order.write({"batch_id": batch.id}) + self.assertTrue(line.batch_product_id) + + def test_update_batch_product_unlinks_when_batch_removed(self): + batch = self._create_batch() + order = self._create_order(batch=batch) + line = order.order_line + self.assertTrue(line.batch_product_id) + order.with_context(disable_cancel_warning=True).write({"batch_id": False}) + self.assertFalse(line.batch_product_id)