diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7bfd275ee2a..7f524c82912 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -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])) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index aca8491946d..68cb91a979c 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -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")