[16.0] sale_order_batch: add tests

This commit is contained in:
Niels Göttsch 2026-05-12 15:18:17 +02:00 committed by madmooose
parent 12736d9972
commit 4b9796521e
9 changed files with 768 additions and 1 deletions

View file

@ -49,6 +49,7 @@ Changelog
- 16.0.2.2.1: Add lst_price to view - 16.0.2.2.1: Add lst_price to view
- 16.0.2.3.0: Add "Reset to Open" - 16.0.2.3.0: Add "Reset to Open"
- 16.0.2.3.1: Remove Sale Order from batch on cancelation - 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 Bug Tracker
=========== ===========

View file

@ -9,3 +9,4 @@
- 16.0.2.2.1: Add lst_price to view - 16.0.2.2.1: Add lst_price to view
- 16.0.2.3.0: Add "Reset to Open" - 16.0.2.3.0: Add "Reset to Open"
- 16.0.2.3.1: Remove Sale Order from batch on cancelation - 16.0.2.3.1: Remove Sale Order from batch on cancelation
- 16.0.2.4.0: Add Followers on In Progress & add tests

View file

@ -405,6 +405,7 @@ anymore as log as they are part of a batch. Only Sale Orders in state Qutotation
<li>16.0.2.2.1: Add lst_price to view</li> <li>16.0.2.2.1: Add lst_price to view</li>
<li>16.0.2.3.0: Add “Reset to Open”</li> <li>16.0.2.3.0: Add “Reset to Open”</li>
<li>16.0.2.3.1: Remove Sale Order from batch on cancelation</li> <li>16.0.2.3.1: Remove Sale Order from batch on cancelation</li>
<li>16.0.2.4.0: Add Followers on In Progress &amp; add tests</li>
</ul> </ul>
</div> </div>
<div class="section" id="bug-tracker"> <div class="section" id="bug-tracker">

View file

@ -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,
)

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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)

View file

@ -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)