From cff115cc1cf4bfb47b097eb5fd4514dcbbdef290 Mon Sep 17 00:00:00 2001 From: sayanthns Date: Fri, 26 Jun 2026 22:46:53 +0300 Subject: [PATCH] =?UTF-8?q?fix(dn-consolidation):=20flip=20clubbed=20DN=20?= =?UTF-8?q?status=20To=20Bill=20=E2=86=92=20Completed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple DNs are clubbed into one net-off Sales Invoice (consolidate_dns_to_si), the source DNs stayed on "To Bill" forever. Cause: the net-off SI omits SI Item.delivery_note (to avoid validate_multiple_billing), so ERPNext's update_billing_status sums zero linked rows and never advances per_billed. And nothing refreshed the custom_consolidated_si-linked DNs on submit (only the inter-company path refreshed its own custom_inter_company_si DNs). Fix: on Sales Invoice on_submit, mark every custom_consolidated_si-linked DN per_billed=100 / status=Completed (flat — a clubbed DN is fully invoiced). On cancel and on_trash, revert via ERPNext's update_billing_status (back to To Bill for a pure net-off DN; reflects any other real invoices otherwise) — done before clearing the stamp. No schema change. Regression test added. Co-Authored-By: Claude Opus 4.8 --- rmax_custom/api/delivery_note.py | 61 ++++++++++++++++++++-- rmax_custom/hooks.py | 1 + rmax_custom/inter_company_dn.py | 13 +++-- rmax_custom/tests/test_dn_consolidation.py | 18 +++++++ 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/rmax_custom/api/delivery_note.py b/rmax_custom/api/delivery_note.py index 03efb6a..3026678 100644 --- a/rmax_custom/api/delivery_note.py +++ b/rmax_custom/api/delivery_note.py @@ -695,12 +695,63 @@ def clear_consolidated_si_links(si_name): ) +def mark_consolidated_dns_billed(si_name): + """Mark every DN consolidated into this SI as fully billed → status Completed. + + The net-off consolidated SI omits ``SI Item.delivery_note``, so ERPNext's + ``update_billing_status`` sums zero linked rows and would leave each source + DN stuck on "To Bill". Set ``per_billed``/``status`` explicitly instead. + Flat 100% — a clubbed DN is treated as fully invoiced regardless of net-off. + No-op for a normal (non-consolidated) Sales Invoice. + """ + for dn_name in frappe.get_all( + "Delivery Note", + filters={"custom_consolidated_si": si_name}, + pluck="name", + ): + frappe.db.set_value( + "Delivery Note", dn_name, + {"per_billed": 100, "status": "Completed"}, + update_modified=False, + ) + + +def unmark_consolidated_dns_billed(si_name): + """Revert the billing flag on DNs consolidated into this SI when it is + cancelled or deleted. Re-runs ERPNext's own ``update_billing_status`` so the + DN reflects any OTHER real invoices (a pure net-off DN has none → "To Bill"). + Must be called BEFORE clearing ``custom_consolidated_si`` — it needs the link + to find the DNs. + """ + for dn_name in frappe.get_all( + "Delivery Note", + filters={"custom_consolidated_si": si_name}, + pluck="name", + ): + try: + dn = frappe.get_doc("Delivery Note", dn_name) + dn.update_billing_status(update_modified=False) + except Exception: + frappe.db.set_value( + "Delivery Note", dn_name, + {"per_billed": 0, "status": "To Bill"}, + update_modified=False, + ) + + +def sales_invoice_on_submit_mark_consolidated(doc, method=None): + """Sales Invoice ``on_submit``: flip every clubbed DN (linked via + ``custom_consolidated_si``) from "To Bill" to "Completed".""" + mark_consolidated_dns_billed(doc.name) + + def sales_invoice_on_trash_clear_consolidated(doc, method=None): - """Sales Invoice ``on_trash``: free the netted-off DN stamps so a DRAFT - consolidated SI can be deleted. ``on_trash`` runs before - ``check_if_doc_is_linked`` in ``frappe.delete_doc`` (verified against v15), - so clearing here lets the link-integrity check pass. The submitted-SI case - is handled separately on ``on_cancel``. No-op for non-consolidated SIs.""" + """Sales Invoice ``on_trash``: revert the clubbed DNs' billing flag, then + free the netted-off DN stamps so a DRAFT consolidated SI can be deleted. + ``on_trash`` runs before ``check_if_doc_is_linked`` in ``frappe.delete_doc`` + (verified against v15), so clearing here lets the link-integrity check pass. + No-op for non-consolidated SIs.""" + unmark_consolidated_dns_billed(doc.name) clear_consolidated_si_links(doc.name) diff --git a/rmax_custom/hooks.py b/rmax_custom/hooks.py index 7f3411a..0f3c338 100644 --- a/rmax_custom/hooks.py +++ b/rmax_custom/hooks.py @@ -222,6 +222,7 @@ "on_submit": [ "rmax_custom.inter_company.sales_invoice_on_submit", "rmax_custom.inter_company_dn.sales_invoice_on_submit", + "rmax_custom.api.delivery_note.sales_invoice_on_submit_mark_consolidated", ], "on_cancel": [ "rmax_custom.inter_company_dn.sales_invoice_on_cancel", diff --git a/rmax_custom/inter_company_dn.py b/rmax_custom/inter_company_dn.py index 23569ce..f1d9310 100644 --- a/rmax_custom/inter_company_dn.py +++ b/rmax_custom/inter_company_dn.py @@ -289,10 +289,15 @@ def sales_invoice_on_cancel(doc, method=None): if linked: frappe.db.commit() - # Clear the net-off consolidation stamp on every DN linked via - # custom_consolidated_si. (Set by api.delivery_note.consolidate_dns_to_si.) - # Shared with the on_trash path. Local import avoids a circular import. - from rmax_custom.api.delivery_note import clear_consolidated_si_links + # Revert the billing flag on the clubbed DNs, then clear the net-off + # consolidation stamp. (Set by api.delivery_note.consolidate_dns_to_si.) + # Order matters — unmark needs the link before it is cleared. Shared with the + # on_trash path. Local import avoids a circular import. + from rmax_custom.api.delivery_note import ( + clear_consolidated_si_links, + unmark_consolidated_dns_billed, + ) + unmark_consolidated_dns_billed(doc.name) clear_consolidated_si_links(doc.name) diff --git a/rmax_custom/tests/test_dn_consolidation.py b/rmax_custom/tests/test_dn_consolidation.py index 311d17b..36f239f 100644 --- a/rmax_custom/tests/test_dn_consolidation.py +++ b/rmax_custom/tests/test_dn_consolidation.py @@ -82,6 +82,24 @@ def test_si_cancel_clears_stamp(self): frappe.db.get_value("Delivery Note", dn.name, "custom_consolidated_si") ) + def test_clubbed_dn_billing_status_flips_on_submit_and_reverts(self): + # The net-off SI omits SI Item.delivery_note, so ERPNext's billing calc + # can't flip the clubbed DNs. on_submit must mark them Completed; cancel + # must revert to To Bill. + dn = _make_submitted_dn(self.customer, self.item, self.company, + self.warehouse, qty=5, rate=10) + self.assertEqual( + frappe.db.get_value("Delivery Note", dn.name, "status"), "To Bill" + ) + si = frappe.get_doc("Sales Invoice", consolidate_dns_to_si([dn.name])) + si.submit() + dn.reload() + self.assertEqual(flt(dn.per_billed), 100) + self.assertEqual(dn.status, "Completed") + si.cancel() + dn.reload() + self.assertEqual(dn.status, "To Bill") + def test_draft_si_delete_clears_stamp_and_succeeds(self): # Reproduces the bug: a DRAFT consolidated SI stamps the DN via # custom_consolidated_si; deleting it must clear the stamp (on_trash)