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)