fix: prevent leakage of party-derived fields in cross doctype transactions (#55336)

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
Lakshit Jain
2026-06-03 12:56:37 +05:30
committed by GitHub
parent a2a2e1020b
commit 260cec3b86
7 changed files with 120 additions and 33 deletions

View File

@@ -29,7 +29,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_party_details, get_due_date, get_party_account
from erpnext.accounts.party import (
CROSS_PARTY_FIELD_NO_MAP,
_get_party_details,
get_due_date,
get_party_account,
)
from erpnext.accounts.utils import (
get_account_currency,
update_voucher_outstanding,
@@ -2883,7 +2888,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,
},

View File

@@ -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

View File

@@ -49,6 +49,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

View File

@@ -21,7 +21,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,
@@ -1743,7 +1743,6 @@ def make_purchase_order(
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")
@@ -1810,16 +1809,7 @@ def make_purchase_order(
{
"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": {

View File

@@ -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,
@@ -1163,9 +1164,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
@@ -1259,9 +1257,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
@@ -1319,10 +1314,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})
@@ -1363,8 +1354,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})
@@ -1393,8 +1382,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})
@@ -2664,8 +2651,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"
@@ -2695,6 +2680,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)

View File

@@ -16,7 +16,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
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
@@ -1390,8 +1390,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",

View File

@@ -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