fix(stock): recalculate billing status of returned delivery notes (#58953)

This commit is contained in:
Raffael Meyer
2026-09-10 19:56:28 +02:00
committed by GitHub
parent 4671d1a665
commit be8208e7cb
3 changed files with 94 additions and 0 deletions

View File

@@ -521,3 +521,4 @@ erpnext.patches.v16_0.set_secondary_item_valuation_type
erpnext.patches.v16_0.append_fieldname_to_pos_search_fields
erpnext.patches.v16_0.set_supplier_quotation_order_status
erpnext.patches.v16_0.recalculate_holiday_list_totals
erpnext.patches.v16_0.recalculate_returned_delivery_note_billing_status

View File

@@ -0,0 +1,32 @@
import frappe
def execute():
"""Recalculate billing status of Delivery Notes left open by a return.
Returning the uninvoiced qty of a Delivery Note did not recalculate the original
Delivery Note, so it stayed "To Bill" / "Partially Billed" with nothing left to invoice.
"""
dn = frappe.qb.DocType("Delivery Note")
dn_item = frappe.qb.DocType("Delivery Note Item")
delivery_notes = (
frappe.qb.from_(dn)
.inner_join(dn_item)
.on(dn_item.parent == dn.name)
.select(dn.name)
.distinct()
.where(
(dn.docstatus == 1)
& (dn.is_return == 0)
& dn.status.isin(["To Bill", "Partially Billed"])
& (dn_item.returned_qty > 0)
)
.run(pluck=True)
)
for name in delivery_notes:
doc = frappe.get_doc("Delivery Note", name)
doc.update_billing_percentage(update_modified=False)
doc.load_from_db()
doc.set_status(update=True, update_modified=False)

View File

@@ -1110,6 +1110,67 @@ class TestDeliveryNote(ERPNextTestSuite):
self.assertEqual(dn.per_billed, 50)
self.assertEqual(dn.status, "Partially Billed")
def test_billing_status_repair_patch(self):
"""Returns submitted before #58869 left the original Delivery Note's per_billed stale.
The repair patch recalculates such notes: a directly invoiced one whose remaining
qty was returned becomes Completed, an uninvoiced Sales Order linked one goes back
to To Bill.
"""
from erpnext.patches.v16_0 import recalculate_returned_delivery_note_billing_status as patch
from erpnext.stock.doctype.delivery_note.mapper import make_sales_return
# Delivery Note invoiced for 2 of 5 qty, the remaining 3 returned -> fully billed
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
dn = create_delivery_note(qty=5)
si = make_sales_invoice(dn.name)
si.items[0].qty = 2
si.insert()
si.submit()
dn_return = make_sales_return(dn.name)
dn_return.items[0].qty = -3
dn_return.insert()
# Mimic the submit request, which reconstructs the document from client data.
frappe.get_doc(dn_return.as_dict()).submit()
dn.load_from_db()
self.assertEqual(dn.items[0].returned_qty, 3)
self.assertEqual(dn.per_billed, 100)
# Sales Order linked Delivery Note, nothing invoiced, partly returned -> unbilled
so = make_sales_order(qty=10)
so_dn = create_dn_against_so(so.name, delivered_qty=5)
so_dn_return = make_sales_return(so_dn.name)
so_dn_return.items[0].qty = -2
so_dn_return.insert()
frappe.get_doc(so_dn_return.as_dict()).submit()
so_dn.load_from_db()
self.assertEqual(so_dn.items[0].returned_qty, 2)
self.assertEqual(so_dn.per_billed, 0)
# Mimic the state left behind by a return submitted before the fix
for name, per_billed in ((dn.name, 40), (so_dn.name, 50)):
frappe.db.set_value(
"Delivery Note",
name,
{"per_billed": per_billed, "status": "Partially Billed"},
update_modified=False,
)
patch.execute()
dn.load_from_db()
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.status, "Completed")
so_dn.load_from_db()
self.assertEqual(so_dn.per_billed, 0)
self.assertEqual(so_dn.status, "To Bill")
def test_dn_billing_status_case2(self):
# SO -> SI and SO -> DN1, DN2
from erpnext.selling.doctype.sales_order.mapper import (