Merge pull request #56181 from mihir-kandoi/pg-delivery-note

refactor(stock): port Delivery Note & Delivery Trip raw SQL to qb/ORM (Postgres compat)
This commit is contained in:
Mihir Kandoi
2026-06-20 00:21:37 +05:30
committed by GitHub
6 changed files with 171 additions and 56 deletions

View File

@@ -261,12 +261,10 @@ class DeliveryNote(SellingController):
def set_actual_qty(self):
for d in self.get("items"):
if d.item_code and d.warehouse:
actual_qty = frappe.db.sql(
"""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s""",
(d.item_code, d.warehouse),
actual_qty = frappe.db.get_value(
"Bin", {"item_code": d.item_code, "warehouse": d.warehouse}, "actual_qty"
)
d.actual_qty = actual_qty and flt(actual_qty[0][0]) or 0
d.actual_qty = flt(actual_qty) or 0
def so_required(self):
"""check in manage account if sales order required or not"""
@@ -385,11 +383,10 @@ class DeliveryNote(SellingController):
def validate_proj_cust(self):
"""check for does customer belong to same project as entered.."""
if self.project and self.customer:
res = frappe.db.sql(
"""select name from `tabProject`
where name = %s and (customer = %s or
ifnull(customer,'')='')""",
(self.project, self.customer),
res = frappe.get_all(
"Project",
filters={"name": self.project},
or_filters=[["customer", "=", self.customer], ["customer", "is", "not set"]],
)
if not res:
frappe.throw(
@@ -604,20 +601,20 @@ class DeliveryNote(SellingController):
PackingService(self).validate_packed_qty()
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
"""select t1.name
from `tabSales Invoice` t1,`tabSales Invoice Item` t2
where t1.name = t2.parent and t2.delivery_note = %s and t1.docstatus = 1""",
(self.name),
submit_rv = frappe.get_all(
"Sales Invoice Item",
filters={"delivery_note": self.name, "docstatus": 1},
fields=["parent"],
as_list=True,
)
if submit_rv:
frappe.throw(_("Sales Invoice {0} has already been submitted").format(submit_rv[0][0]))
submit_in = frappe.db.sql(
"""select t1.name
from `tabInstallation Note` t1, `tabInstallation Note Item` t2
where t1.name = t2.parent and t2.prevdoc_docname = %s and t1.docstatus = 1""",
(self.name),
submit_in = frappe.get_all(
"Installation Note Item",
filters={"prevdoc_docname": self.name, "docstatus": 1},
fields=["parent"],
as_list=True,
)
if submit_in:
frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0]))

View File

@@ -104,12 +104,12 @@ def update_billed_amount_based_on_so(so_detail: str, update_modified: bool = Tru
billed_against_so -= billed_amt_against_dn
else:
# Get billed amount directly against Delivery Note
billed_amt_against_dn = frappe.db.sql(
"""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""",
dnd.name,
billed_amt_against_dn = frappe.get_all(
"Sales Invoice Item",
filters={"dn_detail": dnd.name, "docstatus": 1},
fields=[{"SUM": "amount", "as": "amount"}],
)
billed_amt_against_dn = billed_amt_against_dn and billed_amt_against_dn[0][0] or 0
billed_amt_against_dn = billed_amt_against_dn[0].amount or 0 if billed_amt_against_dn else 0
# Distribute billed amount directly against SO between DNs based on FIFO
if billed_against_so and billed_amt_against_dn < dnd.amount:

View File

@@ -50,14 +50,12 @@ class PackingService:
def cancel_packing_slips(self) -> None:
"""Cancel submitted packing slips related to this delivery note"""
res = frappe.db.sql(
"""SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s
AND docstatus = 1""",
self.doc.name,
res = frappe.get_all(
"Packing Slip", filters={"delivery_note": self.doc.name, "docstatus": 1}, pluck="name"
)
if res:
for r in res:
ps = frappe.get_doc("Packing Slip", r[0])
ps = frappe.get_doc("Packing Slip", r)
ps.cancel()
frappe.msgprint(_("Packing Slip(s) cancelled"))

View File

@@ -3376,6 +3376,68 @@ class TestDeliveryNote(ERPNextTestSuite):
dn.items[0].stock_qty = 2
dn.save()
def test_validate_proj_cust_matches_project_customer(self):
"""validate_proj_cust must reject a DN whose customer differs from the project's customer,
and accept one when the project has no customer (the ifnull(customer,'')='' / `is not set`
branch of the converted or_filters)."""
mismatch_project = frappe.get_doc(
{
"doctype": "Project",
"project_name": "_Test DN Project Mismatch",
"company": "_Test Company",
"customer": "_Test Customer 1",
}
).insert()
dn = create_delivery_note(customer="_Test Customer", do_not_save=True)
dn.project = mismatch_project.name
with self.assertRaises(frappe.ValidationError) as cm:
dn.insert()
self.assertIn("does not belong to project", str(cm.exception))
# A project with no customer must pass via the empty-string/NULL or_filters branch.
open_project = frappe.get_doc(
{
"doctype": "Project",
"project_name": "_Test DN Project No Customer",
"company": "_Test Company",
}
).insert()
self.assertFalse(open_project.customer)
dn2 = create_delivery_note(customer="_Test Customer", do_not_save=True)
dn2.project = open_project.name
dn2.insert() # must not raise
self.assertTrue(dn2.name)
def test_check_next_docstatus_blocks_cancel_with_submitted_invoice(self):
"""check_next_docstatus must block cancelling a DN once a submitted Sales Invoice draws from
it — covers the converted child-table get_all (Sales Invoice Item, docstatus=1)."""
dn = create_delivery_note() # submitted, simple _Test Item
si = make_sales_invoice(dn.name)
si.insert()
si.submit()
dn.load_from_db()
with self.assertRaises(frappe.ValidationError) as cm:
dn.cancel()
self.assertIn("has already been submitted", str(cm.exception))
def test_cancel_packing_slips_cancels_submitted_slips(self):
"""cancel_packing_slips must cancel the DN's submitted Packing Slips — covers the converted
get_all(pluck=name) lookup and the pluck-aware iteration."""
from erpnext.stock.doctype.delivery_note.mapper import make_packing_slip
from erpnext.stock.doctype.delivery_note.services.packing import PackingService
dn = create_delivery_note(do_not_submit=True) # draft, so a Packing Slip can be mapped
ps = make_packing_slip(dn.name)
ps.save()
ps.submit()
dn.submit()
self.assertEqual(frappe.db.get_value("Packing Slip", ps.name, "docstatus"), 1)
PackingService(dn).cancel_packing_slips()
self.assertEqual(frappe.db.get_value("Packing Slip", ps.name, "docstatus"), 2)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -315,19 +315,15 @@ def get_contact_and_address(name: str):
def get_default_contact(out, name):
contact_persons = frappe.db.sql(
"""
SELECT parent,
(SELECT is_primary_contact FROM tabContact c WHERE c.name = dl.parent) AS is_primary_contact
FROM
`tabDynamic Link` dl
WHERE
dl.link_doctype='Customer'
AND dl.link_name=%s
AND dl.parenttype = 'Contact'
""",
(name),
as_dict=1,
dl = frappe.qb.DocType("Dynamic Link")
contact = frappe.qb.DocType("Contact")
contact_persons = (
frappe.qb.from_(dl)
.left_join(contact)
.on(contact.name == dl.parent)
.select(dl.parent, contact.is_primary_contact)
.where((dl.link_doctype == "Customer") & (dl.link_name == name) & (dl.parenttype == "Contact"))
.run(as_dict=1)
)
if contact_persons:
@@ -341,19 +337,15 @@ def get_default_contact(out, name):
def get_default_address(out, name):
shipping_addresses = frappe.db.sql(
"""
SELECT parent,
(SELECT is_shipping_address FROM tabAddress a WHERE a.name=dl.parent) AS is_shipping_address
FROM
`tabDynamic Link` dl
WHERE
dl.link_doctype='Customer'
AND dl.link_name=%s
AND dl.parenttype = 'Address'
""",
(name),
as_dict=1,
dl = frappe.qb.DocType("Dynamic Link")
address = frappe.qb.DocType("Address")
shipping_addresses = (
frappe.qb.from_(dl)
.left_join(address)
.on(address.name == dl.parent)
.select(dl.parent, address.is_shipping_address)
.where((dl.link_doctype == "Customer") & (dl.link_name == name) & (dl.parenttype == "Address"))
.run(as_dict=1)
)
if shipping_addresses:

View File

@@ -8,6 +8,7 @@ from frappe.utils import add_days, flt, now_datetime, nowdate
import erpnext
from erpnext.stock.doctype.delivery_trip.delivery_trip import (
get_contact_and_address,
get_default_contact,
notify_customers,
)
from erpnext.tests.utils import ERPNextTestSuite
@@ -108,6 +109,71 @@ class TestDeliveryTrip(ERPNextTestSuite):
self.delivery_trip.save()
self.assertEqual(self.delivery_trip.status, "Completed")
def test_get_contact_and_address_returns_linked_contact_and_address(self):
"""get_contact_and_address (the converted Dynamic Link queries) must return a real Contact
and Address that are actually linked to the customer — pins the converted query's output."""
out = get_contact_and_address("_Test Customer")
self.assertTrue(out.contact_person and out.contact_person.parent)
self.assertTrue(frappe.db.exists("Contact", out.contact_person.parent))
self.assertTrue(
frappe.db.exists(
"Dynamic Link",
{
"parenttype": "Contact",
"parent": out.contact_person.parent,
"link_doctype": "Customer",
"link_name": "_Test Customer",
},
)
)
self.assertTrue(out.shipping_address and out.shipping_address.parent)
self.assertTrue(frappe.db.exists("Address", out.shipping_address.parent))
self.assertTrue(
frappe.db.exists(
"Dynamic Link",
{
"parenttype": "Address",
"parent": out.shipping_address.parent,
"link_doctype": "Customer",
"link_name": "_Test Customer",
},
)
)
def test_get_default_contact_keeps_orphaned_dynamic_link(self):
"""The converted get_default_contact uses a LEFT join, matching the original correlated
subquery: a Dynamic Link whose parent Contact no longer exists must STILL be returned
(is_primary_contact NULL). An inner join would silently drop it and return None."""
customer = "_Test Customer 2"
# A Contact linked to the customer, then orphan its Dynamic Link by deleting the Contact row.
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": "_Test Orphan Link Contact",
"links": [{"link_doctype": "Customer", "link_name": customer}],
}
).insert()
orphan_parent = contact.name
frappe.db.delete("Contact", {"name": orphan_parent})
self.assertFalse(frappe.db.exists("Contact", orphan_parent))
self.assertTrue(
frappe.db.exists(
"Dynamic Link",
{"parenttype": "Contact", "parent": orphan_parent, "link_name": customer},
)
)
out = frappe._dict()
result = get_default_contact(out, customer)
# LEFT join keeps the orphaned-link row; an inner join would have returned None.
self.assertIsNotNone(result)
self.assertEqual(result.parent, orphan_parent)
self.assertIsNone(result.is_primary_contact)
def create_address(driver):
if not frappe.db.exists("Address", {"address_title": "_Test Address for Driver"}):