From b409ded2f6648852de43e51a2f830b0a1bf57125 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Tue, 8 Sep 2026 16:21:55 +0530 Subject: [PATCH] fix(stock): recalculate delivery note billing after return (#58869) (cherry picked from commit f864333afab6c3b1dec7519c2e66b0bb17e548e6) # Conflicts: # erpnext/stock/doctype/delivery_note/services/billing_status.py --- .../doctype/delivery_note/delivery_note.py | 10 +++- .../delivery_note/test_delivery_note.py | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index fe132d589d4..92c4dc38386 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -740,6 +740,9 @@ class DeliveryNote(SellingController): def update_billing_status(self, update_modified=True): updated_delivery_notes = [self.name] + if self.is_return and self.return_against: + updated_delivery_notes.append(self.return_against) + for d in self.get("items"): if d.si_detail and not d.so_detail: d.db_set("billed_amt", d.amount, update_modified=update_modified) @@ -748,7 +751,12 @@ class DeliveryNote(SellingController): for dn in set(updated_delivery_notes): dn_doc = self if (dn == self.name) else frappe.get_doc("Delivery Note", dn) - dn_doc.update_billing_percentage(update_modified=update_modified) + update_dn_modified = update_modified and dn != self.return_against + dn_doc.update_billing_percentage(update_modified=update_dn_modified) + if dn == self.return_against: + dn_doc.load_from_db() + dn_doc.set_status(update=True, update_modified=False) + dn_doc.notify_update() self.load_from_db() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 56622562d7a..4ee8f3289af 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1049,6 +1049,56 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.status, "Completed") + def test_dn_is_completed_when_unbilled_item_is_returned(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + make_stock_entry(target="_Test Warehouse - _TC", qty=1, basic_rate=100) + make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=1, basic_rate=100) + + dn = create_delivery_note(do_not_submit=True) + dn.append( + "items", + { + "item_code": "_Test Item 2", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 100, + "conversion_factor": 1, + "allow_zero_valuation_rate": 1, + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + dn.submit() + + si = make_sales_invoice(dn.name) + si.set("items", [item for item in si.items if item.item_code == "_Test Item"]) + si.insert() + si.submit() + + dn.reload() + self.assertEqual(dn.per_billed, 50) + self.assertEqual(dn.status, "Partially Billed") + + return_dn = make_sales_return(dn.name) + return_dn.set("items", [item for item in return_dn.items if item.item_code == "_Test Item 2"]) + return_dn.insert() + # Mimic the submit request, which reconstructs the document from client data. + return_dn = frappe.get_doc(return_dn.as_dict()) + return_dn.submit() + + dn.reload() + self.assertEqual(dn.items[1].returned_qty, 1) + self.assertEqual(dn.per_billed, 100) + self.assertEqual(dn.status, "Completed") + + return_dn.cancel() + + dn.reload() + self.assertEqual(dn.items[1].returned_qty, 0) + self.assertEqual(dn.per_billed, 50) + self.assertEqual(dn.status, "Partially Billed") + def test_dn_billing_status_case2(self): # SO -> SI and SO -> DN1, DN2 from erpnext.selling.doctype.sales_order.sales_order import (