From 38dabdf584c54b7a5bb1a7b57f4dc0278bb186ec Mon Sep 17 00:00:00 2001 From: Sugesh G <73237300+Sugesh393@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:13:35 +0530 Subject: [PATCH 1/2] fix: use shipping_address_name for address validation in sales invoice (#46473) * fix: validate address and contact related to party * fix: solve unboundlocal error * refactor: improve variable scope * refactor: translatable strings * fix: use shipping_address_name for address validation in sales invoice * test: add new unit test for address and contact validation * chore: to avoid keyerror --------- Co-authored-by: ruthra kumar (cherry picked from commit 0bdb81db53136bea54e1e703be5a3c80ccb29be4) --- erpnext/controllers/accounts_controller.py | 40 +++++++++++++ .../tests/test_accounts_controller.py | 56 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 55889c3b4c3..21f339891bf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -271,6 +271,7 @@ class AccountsController(TransactionBase): self.set_total_in_words() self.set_default_letter_head() self.validate_company_in_accounting_dimension() + self.validate_party_address_and_contact() def set_default_letter_head(self): if hasattr(self, "letter_head") and not self.letter_head: @@ -441,6 +442,45 @@ class AccountsController(TransactionBase): ) ) + def validate_party_address_and_contact(self): + party, party_type = None, None + if self.get("customer"): + party, party_type = self.customer, "Customer" + billing_address, shipping_address = ( + self.get("customer_address"), + self.get("shipping_address_name"), + ) + self.validate_party_address(party, party_type, billing_address, shipping_address) + elif self.get("supplier"): + party, party_type = self.supplier, "Supplier" + billing_address = self.get("supplier_address") + self.validate_party_address(party, party_type, billing_address) + + if party and party_type: + self.validate_party_contact(party, party_type) + + def validate_party_address(self, party, party_type, billing_address, shipping_address=None): + if billing_address or shipping_address: + party_address = frappe.get_list( + "Dynamic Link", + {"link_doctype": party_type, "link_name": party, "parenttype": "Address"}, + pluck="parent", + ) + if billing_address and billing_address not in party_address: + frappe.throw(_("Billing Address does not belong to the {0}").format(party)) + elif shipping_address and shipping_address not in party_address: + frappe.throw(_("Shipping Address does not belong to the {0}").format(party)) + + def validate_party_contact(self, party, party_type): + if self.get("contact_person"): + contact = frappe.get_list( + "Dynamic Link", + {"link_doctype": party_type, "link_name": party, "parenttype": "Contact"}, + pluck="parent", + ) + if self.contact_person and self.contact_person not in contact: + frappe.throw(_("Contact Person does not belong to the {0}").format(party)) + def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index f959cbd0488..28536893ab5 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2175,3 +2175,59 @@ class TestAccountsController(FrappeTestCase): si_1 = create_sales_invoice(do_not_submit=True) si_1.items[0].project = project.name self.assertRaises(frappe.ValidationError, si_1.save) + + def test_party_billing_and_shipping_address(self): + from erpnext.crm.doctype.prospect.test_prospect import make_address + + customer_billing = make_address(address_title="Customer") + customer_billing.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"}) + customer_billing.save() + supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad") + supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"}) + supplier_billing.save() + + customer_shipping = make_address( + address_title="Customer", address_type="Shipping", address_line1="10" + ) + customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"}) + customer_shipping.save() + supplier_shipping = make_address( + address_title="Supplier", address_type="Shipping", address_line1="20", city="Ahmedabad" + ) + supplier_shipping.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"}) + supplier_shipping.save() + + si = create_sales_invoice(do_not_save=True) + si.customer_address = supplier_billing.name + self.assertRaises(frappe.ValidationError, si.save) + si.customer_address = customer_billing.name + si.save() + + si.shipping_address_name = supplier_shipping.name + self.assertRaises(frappe.ValidationError, si.save) + si.shipping_address_name = customer_shipping.name + si.reload() + si.save() + + pi = make_purchase_invoice(do_not_save=True) + pi.supplier_address = customer_shipping.name + self.assertRaises(frappe.ValidationError, pi.save) + pi.supplier_address = supplier_shipping.name + pi.save() + + def test_party_contact(self): + from frappe.contacts.doctype.contact.test_contact import create_contact + + customer_contact = create_contact(name="Customer", salutation="Mr", save=False) + customer_contact.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"}) + customer_contact.save() + + supplier_contact = create_contact(name="Supplier", salutation="Mr", save=False) + supplier_contact.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"}) + supplier_contact.save() + + si = create_sales_invoice(do_not_save=True) + si.contact_person = supplier_contact.name + self.assertRaises(frappe.ValidationError, si.save) + si.contact_person = customer_contact.name + si.save() From 3737b4a30028bfa0b080c471b41d0700659d9738 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 12 Mar 2025 17:34:23 +0530 Subject: [PATCH 2/2] refactor(test): unset billing address --- erpnext/selling/doctype/sales_order/test_sales_order.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 003ffd5ac82..16c7431c315 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2136,7 +2136,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(dn.items[0].rate, 90) - def test_credit_limit_on_so_reopning(self): + def test_credit_limit_on_so_reopening(self): # set credit limit company = "_Test Company" customer = frappe.get_doc("Customer", self.customer) @@ -2148,12 +2148,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so1 = make_sales_order(qty=9, rate=100, do_not_submit=True) so1.customer = self.customer + so1.customer_address = so1.shipping_address_name = None so1.save().submit() so1.update_status("Closed") so2 = make_sales_order(qty=9, rate=100, do_not_submit=True) so2.customer = self.customer + so2.customer_address = so2.shipping_address_name = None so2.save().submit() self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")