From 10109674a4505cb5bfde2b71809339774e778b1f Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Fri, 14 Aug 2026 15:04:31 +0530 Subject: [PATCH] feat(selling): add shipping contact person to sales order, delivery note and sales invoice (#58159) --- .../doctype/sales_invoice/sales_invoice.json | 46 ++++++++++++++++- .../doctype/sales_invoice/sales_invoice.py | 4 ++ erpnext/accounts/party.py | 16 +++++- erpnext/public/js/utils/sales_common.js | 1 + .../doctype/sales_order/sales_order.json | 46 ++++++++++++++++- .../doctype/sales_order/sales_order.py | 4 ++ .../doctype/sales_order/test_sales_order.py | 50 +++++++++++++++++++ .../doctype/delivery_note/delivery_note.json | 38 +++++++++++++- .../doctype/delivery_note/delivery_note.py | 4 ++ 9 files changed, 204 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index a9fd383ca50..4813cbfcdf1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -170,6 +170,10 @@ "shipping_address_section", "shipping_address_name", "shipping_address", + "shipping_contact_person", + "shipping_contact_display", + "shipping_contact_mobile", + "shipping_contact_email", "shipping_addr_col_break", "dispatch_address_name", "dispatch_address", @@ -589,6 +593,46 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "shipping_contact_person", + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "fetch_from": "shipping_contact_person.full_name", + "fieldname": "shipping_contact_display", + "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact", + "read_only": 1 + }, + { + "fetch_from": "shipping_contact_person.mobile_no", + "fieldname": "shipping_contact_mobile", + "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact Mobile No", + "options": "Phone", + "read_only": 1 + }, + { + "fetch_from": "shipping_contact_person.email_id", + "fieldname": "shipping_contact_email", + "fieldtype": "Data", + "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "company_address", "fieldtype": "Link", @@ -2361,7 +2405,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2026-08-12 12:00:00.000000", + "modified": "2026-08-14 12:43:19.480555", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 17a27986ffc..d756eedeb5f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -202,6 +202,10 @@ class SalesInvoice(SellingController): set_warehouse: DF.Link | None shipping_address: DF.TextEditor | None shipping_address_name: DF.Link | None + shipping_contact_display: DF.SmallText | None + shipping_contact_email: DF.Data | None + shipping_contact_mobile: DF.SmallText | None + shipping_contact_person: DF.Link | None shipping_rule: DF.Link | None status: DF.Literal[ "", diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 6bf27f6ee6b..ef1c6c41cb7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -156,7 +156,7 @@ def _get_party_details( dispatch_address, ignore_permissions=ignore_permissions, ) - set_contact_details(party_details, party, party_type) + set_contact_details(party_details, party, party_type, doctype) set_other_values(party_details, party, party_type) set_price_list(party_details, party, party_type, price_list, pos_profile) @@ -358,10 +358,22 @@ def complete_contact_details(party_details): party_details.update(contact_details) -def set_contact_details(party_details, party, party_type): +def set_contact_details(party_details, party, party_type, doctype=None): party_details.contact_person = get_default_contact(party_type, party.name) complete_contact_details(party_details) + # the shipping contact is picked by the user, so it has no default to fall back on; + # blank it instead of carrying the previous party's contact over + if doctype and frappe.get_meta(doctype).has_field("shipping_contact_person"): + party_details.update( + { + "shipping_contact_person": None, + "shipping_contact_display": None, + "shipping_contact_mobile": None, + "shipping_contact_email": None, + } + ) + def set_other_values(party_details, party, party_type): # copy diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 478c8481602..14834067e74 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -49,6 +49,7 @@ erpnext.sales_common = { ); me.frm.set_query("contact_person", erpnext.queries.contact_query); + me.frm.set_query("shipping_contact_person", erpnext.queries.contact_query); me.frm.set_query("company_contact_person", erpnext.queries.company_contact_query); me.frm.set_query("customer_address", erpnext.queries.address_query); me.frm.set_query("shipping_address_name", erpnext.queries.address_query); diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index c506e682d8f..39f880a4cc5 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -113,6 +113,10 @@ "shipping_address_column", "shipping_address_name", "shipping_address", + "shipping_contact_person", + "shipping_contact_display", + "shipping_contact_mobile", + "shipping_contact_email", "column_break_93", "dispatch_address_name", "dispatch_address", @@ -459,6 +463,46 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "shipping_contact_person", + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "fetch_from": "shipping_contact_person.full_name", + "fieldname": "shipping_contact_display", + "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact", + "read_only": 1 + }, + { + "fetch_from": "shipping_contact_person.mobile_no", + "fieldname": "shipping_contact_mobile", + "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact Mobile No", + "options": "Phone", + "read_only": 1 + }, + { + "fetch_from": "shipping_contact_person.email_id", + "fieldname": "shipping_contact_email", + "fieldtype": "Data", + "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, + "label": "Shipping Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "customer_group", "fieldtype": "Link", @@ -1782,7 +1826,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-08-12 12:00:00.000000", + "modified": "2026-08-14 12:43:19.480555", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d7a255f2791..812fc89a5d0 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -146,6 +146,10 @@ class SalesOrder(SellingController): set_warehouse: DF.Link | None shipping_address: DF.TextEditor | None shipping_address_name: DF.Link | None + shipping_contact_display: DF.SmallText | None + shipping_contact_email: DF.Data | None + shipping_contact_mobile: DF.SmallText | None + shipping_contact_person: DF.Link | None shipping_rule: DF.Link | None skip_delivery_note: DF.Check status: DF.Literal[ diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 2dd1a73dea6..48221f17f22 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -11,6 +11,7 @@ from frappe.query_builder.functions import Sum from frappe.tests import change_settings from frappe.utils import add_days, flt, getdate, nowdate, today +from erpnext.accounts.party import get_party_details from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( make_maintenance_schedule, @@ -3411,6 +3412,55 @@ class TestSalesOrder(ERPNextTestSuite): finally: frappe.db.set_value("Item", "_Test Item", "grant_commission", original) + def test_shipping_contact_person_flows_to_delivery_note_and_sales_invoice(self): + billing_contact = "_Test Contact for _Test Customer-_Test Customer" + shipping_contact = "_Test Contact 2 for _Test Customer-_Test Customer" + + so = make_sales_order(customer="_Test Customer", do_not_submit=True) + so.contact_person = billing_contact + so.shipping_contact_person = shipping_contact + so.save() + + # fetch_from fills the display fields off the shipping contact, not the billing one + self.assertEqual(so.shipping_contact_display, "_Test Contact 2 for _Test Customer") + self.assertEqual(so.shipping_contact_email, "test_contact_two_customer@example.com") + + so.submit() + + dn = make_delivery_note(so.name) + self.assertEqual(dn.contact_person, billing_contact) + self.assertEqual(dn.shipping_contact_person, shipping_contact) + + si = make_sales_invoice(so.name) + self.assertEqual(si.shipping_contact_person, shipping_contact) + + def test_get_party_details_blanks_shipping_contact(self): + # unlike the billing contact there is no default to fall back on, so the keys must + # still come back as None — that is what clears the previous party's contact on the form + party_details = get_party_details( + party="_Test Customer", + party_type="Customer", + company="_Test Company", + doctype="Sales Order", + ) + for fieldname in ( + "shipping_contact_person", + "shipping_contact_display", + "shipping_contact_mobile", + "shipping_contact_email", + ): + self.assertIn(fieldname, party_details) + self.assertIsNone(party_details[fieldname]) + + # purchase transactions have no such field, so the keys must not be added there + purchase_details = get_party_details( + party="_Test Supplier", + party_type="Supplier", + company="_Test Company", + doctype="Purchase Order", + ) + self.assertNotIn("shipping_contact_person", purchase_details) + def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")): diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 9736b70978d..3da609b0bbd 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -105,6 +105,10 @@ "shipping_address_section", "shipping_address_name", "shipping_address", + "shipping_contact_person", + "shipping_contact_display", + "shipping_contact_mobile", + "shipping_contact_email", "column_break_95", "dispatch_address_name", "dispatch_address", @@ -356,6 +360,38 @@ "label": "Shipping Address", "read_only": 1 }, + { + "fieldname": "shipping_contact_person", + "fieldtype": "Link", + "label": "Shipping Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "fetch_from": "shipping_contact_person.full_name", + "fieldname": "shipping_contact_display", + "fieldtype": "Small Text", + "label": "Shipping Contact", + "read_only": 1 + }, + { + "fetch_from": "shipping_contact_person.mobile_no", + "fieldname": "shipping_contact_mobile", + "fieldtype": "Small Text", + "label": "Shipping Contact Mobile No", + "options": "Phone", + "read_only": 1 + }, + { + "fetch_from": "shipping_contact_person.email_id", + "fieldname": "shipping_contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Shipping Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "contact_person", "fieldtype": "Link", @@ -1473,7 +1509,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2026-08-12 12:00:00.000000", + "modified": "2026-08-14 12:43:19.480555", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 1cc3579003f..6131758b18f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -119,6 +119,10 @@ class DeliveryNote(SellingController): set_warehouse: DF.Link | None shipping_address: DF.TextEditor | None shipping_address_name: DF.Link | None + shipping_contact_display: DF.SmallText | None + shipping_contact_email: DF.Data | None + shipping_contact_mobile: DF.SmallText | None + shipping_contact_person: DF.Link | None shipping_rule: DF.Link | None status: DF.Literal[ "",