mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 23:18:40 +00:00
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:
@@ -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]))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}):
|
||||
|
||||
Reference in New Issue
Block a user