diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 53e0647b4e6..5408ebd283a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -28,7 +28,12 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger ) from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center -from erpnext.accounts.party import get_due_date, get_party_account, get_party_details +from erpnext.accounts.party import ( + CROSS_PARTY_FIELD_NO_MAP, + get_due_date, + get_party_account, + get_party_details, +) from erpnext.accounts.utils import ( get_account_currency, refresh_subscription_status, @@ -2821,7 +2826,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "doctype": target_doctype, "postprocess": update_details, "set_target_warehouse": "set_from_warehouse", - "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"], + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"], }, doctype + " Item": item_field_map, }, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9a15c1feb14..1be06706100 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2918,6 +2918,34 @@ class TestSalesInvoice(ERPNextTestSuite): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_inter_company_transaction_does_not_inherit_party_fields(self): + """ + Party-derived fields on SI (from Customer) must not leak into the mapped PI. + """ + si = create_sales_invoice( + company="Wind Power LLC", + customer="_Test Internal Customer", + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + cost_center="Main - WP", + currency="USD", + do_not_save=1, + ) + si.selling_price_list = "_Test Price List Rest of the World" + si.tax_category = "_Test Tax Category 1" + si.language = "ar" + si.payment_terms_template = "_Test Payment Term Template" + si.submit() + + pi = make_inter_company_transaction("Sales Invoice", si.name) + + supplier = frappe.get_doc("Supplier", "_Test Internal Supplier") + self.assertEqual(pi.tax_category or None, supplier.tax_category or None) + self.assertEqual(pi.language or None, supplier.language or None) + self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None) + def test_inter_company_transaction_without_default_warehouse(self): "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." # setup diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 5a3c6647261..a40d1eb49aa 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -48,6 +48,25 @@ SALES_TRANSACTION_TYPES = { } TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES +# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the +# source and target documents belong to different parties (e.g. Sales Order → +# Purchase Order or inter-company Sales Invoice → Purchase Invoice). +CROSS_PARTY_FIELD_NO_MAP = [ + "tax_category", + "tax_id", + "tax_withholding_category", + "taxes_and_charges", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "shipping_address", + "dispatch_address", + "payment_terms_template", + "language", +] + class DuplicatePartyAccountError(frappe.ValidationError): pass diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 71df3d8d081..48f2bbf5e6d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -20,7 +20,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_linked_doc, validate_inter_company_party, ) -from erpnext.accounts.party import get_party_account +from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_party_account from erpnext.controllers.selling_controller import SellingController from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( validate_against_blanket_order, @@ -1612,7 +1612,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.shipping_rule = "" target.tc_name = "" target.terms = "" - target.payment_terms_template = "" target.payment_schedule = [] default_price_list = frappe.get_value("Supplier", supplier, "default_price_list") @@ -1679,16 +1678,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): { "Sales Order": { "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "dispatch_address", - ], + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP], "validation": {"docstatus": ["=", 1]}, }, "Sales Order Item": { diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 3da58733edb..20d9affe11d 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -25,6 +25,7 @@ from erpnext.selling.doctype.sales_order.sales_order import ( make_delivery_note, make_material_request, make_production_plan, + make_purchase_order, make_raw_material_request, make_sales_invoice, make_work_orders, @@ -1197,9 +1198,6 @@ class TestSalesOrder(ERPNextTestSuite): def test_drop_shipping(self): from erpnext.buying.doctype.purchase_order.purchase_order import update_status - from erpnext.selling.doctype.sales_order.sales_order import ( - make_purchase_order, - ) from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items @@ -1293,9 +1291,6 @@ class TestSalesOrder(ERPNextTestSuite): so.cancel() def test_drop_shipping_partial_order(self): - from erpnext.selling.doctype.sales_order.sales_order import ( - make_purchase_order, - ) from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items @@ -1353,10 +1348,6 @@ class TestSalesOrder(ERPNextTestSuite): def test_drop_shipping_full_for_default_suppliers(self): """Test if multiple POs are generated in one go against different default suppliers.""" - from erpnext.selling.doctype.sales_order.sales_order import ( - make_purchase_order, - ) - if not frappe.db.exists("Item", "_Test Item for Drop Shipping 1"): make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1}) @@ -1397,8 +1388,6 @@ class TestSalesOrder(ERPNextTestSuite): Tests if the the Product Bundles in the Items table of Sales Orders are replaced with their child items(from the Packed Items table) on creating a Purchase Order from it. """ - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order - product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) @@ -1427,8 +1416,6 @@ class TestSalesOrder(ERPNextTestSuite): """ Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order """ - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order - product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) @@ -2698,8 +2685,6 @@ class TestSalesOrder(ERPNextTestSuite): self.assertEqual(so.status, "To Deliver and Bill") def test_item_tax_transfer_from_sales_to_purchase(self): - from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order - item_tax = frappe.new_doc("Item Tax Template") item_tax.title = "Test Item Tax Template" item_tax.company = "_Test Company" @@ -2729,6 +2714,33 @@ class TestSalesOrder(ERPNextTestSuite): po.submit() self.assertEqual(po.taxes[0].tax_amount, 2) + def test_make_purchase_order_does_not_inherit_party_fields(self): + """ + Customer-derived fields must not leak from a drop-ship SO into the PO. + """ + so_items = [ + { + "item_code": "_Test Item", + "warehouse": "", + "qty": 1, + "rate": 100, + "delivered_by_supplier": 1, + "supplier": "_Test Supplier", + } + ] + so = make_sales_order(item_list=so_items, do_not_submit=True) + so.tax_category = "_Test Tax Category 1" + so.language = "ar" + so.payment_terms_template = "_Test Payment Term Template" + so.submit() + + po = make_purchase_order(so.name, selected_items=so_items)[0] + + supplier = frappe.get_doc("Supplier", "_Test Supplier") + self.assertEqual(po.tax_category or None, supplier.tax_category or None) + self.assertEqual(po.language or None, supplier.language or None) + self.assertEqual(po.payment_terms_template or None, supplier.payment_terms or None) + def test_pending_quantity_after_update_item_during_invoice_creation(self): so = make_sales_order(qty=30, rate=100) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0be8c8f864d..8fcc66c19dd 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -15,7 +15,7 @@ from frappe.query_builder import DocType from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt -from erpnext.accounts.party import get_due_date +from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_due_date from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes from erpnext.controllers.selling_controller import SellingController @@ -1306,8 +1306,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): doctype: { "doctype": target_doctype, "postprocess": update_details, - "field_no_map": ["taxes_and_charges", "set_warehouse"], - "field_map": {"shipping_address_name": "shipping_address"}, + "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse"], }, doctype + " Item": { "doctype": target_doctype + " Item", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0ae83404a61..96280d00a05 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1051,6 +1051,40 @@ class TestPurchaseReceipt(ERPNextTestSuite): pr.cancel() + def test_inter_company_purchase_receipt_does_not_inherit_party_fields(self): + """ + Party-derived fields on DN (from Customer) must not leak into the mapped PR. + """ + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + dn = create_delivery_note( + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=1, + rate=100, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + do_not_submit=True, + ) + # Stamp customer-side party fields onto the DN + dn.tax_category = "_Test Tax Category 2" + dn.language = "ar" + dn.submit() + + pr = make_inter_company_purchase_receipt(dn.name) + + supplier = frappe.get_doc("Supplier", "_Test Internal Supplier 2") + self.assertEqual(pr.tax_category or None, supplier.tax_category or None) + self.assertEqual(pr.language or None, supplier.language or None) + def test_lcv_for_internal_transfer(self): from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note