Merge pull request #47968 from aerele/validate-intercompany-rate

Add validation for inter company transactions rate
This commit is contained in:
ruthra kumar
2025-06-10 11:12:26 +05:30
committed by GitHub
4 changed files with 153 additions and 0 deletions

View File

@@ -38,6 +38,11 @@
"show_taxes_as_table_in_print",
"column_break_12",
"show_payment_schedule_in_print",
"item_price_settings_section",
"maintain_same_internal_transaction_rate",
"column_break_feyo",
"maintain_same_rate_action",
"role_to_override_stop_action",
"currency_exchange_section",
"allow_stale",
"column_break_yuug",
@@ -556,6 +561,37 @@
"fieldname": "legacy_section",
"fieldtype": "Section Break",
"label": "Legacy Fields"
},
{
"default": "0",
"fieldname": "maintain_same_internal_transaction_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout Internal Transaction"
},
{
"default": "Stop",
"depends_on": "maintain_same_internal_transaction_rate",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
},
{
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
},
{
"fieldname": "column_break_feyo",
"fieldtype": "Column Break"
}
],
"icon": "icon-cog",

View File

@@ -50,6 +50,8 @@ class AccountsSettings(Document):
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check
maintain_same_internal_transaction_rate: DF.Check
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
@@ -58,6 +60,7 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
role_to_override_stop_action: DF.Link | None
round_row_wise_tax: DF.Check
show_balance_in_coa: DF.Check
show_inclusive_tax_in_print: DF.Check

View File

@@ -64,6 +64,26 @@ class TestSalesInvoice(FrappeTestCase):
)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
@change_settings(
"Accounts Settings",
{"maintain_same_internal_transaction_rate": 1, "maintain_same_rate_action": "Stop"},
)
def test_invalid_rate_without_override(self):
from frappe import ValidationError
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
si = create_sales_invoice(
customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100
)
pi = make_inter_company_purchase_invoice(si.name)
pi.items[0].rate = 120
with self.assertRaises(ValidationError) as e:
pi.insert()
pi.submit()
self.assertIn("Rate must be same", str(e.exception))
def tearDown(self):
frappe.db.rollback()
@@ -4441,6 +4461,7 @@ def create_sales_invoice(**args):
si.conversion_rate = args.conversion_rate or 1
si.naming_series = args.naming_series or "T-SINV-"
si.cost_center = args.parent_cost_center
si.is_internal_customer = args.is_internal_customer or 0
bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
@@ -4643,6 +4664,12 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_supplier(
supplier_name="_Test Internal Supplier 3",
represents_company="_Test Company",
allowed_to_interact_with="_Test Company",
)
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Supplier", supplier_name):

View File

@@ -230,6 +230,8 @@ class AccountsController(TransactionBase):
self.validate_party_accounts()
self.validate_inter_company_reference()
# validate inter company transaction rate
self.validate_internal_transaction()
self.disable_pricing_rule_on_internal_transfer()
self.disable_tax_included_prices_for_internal_transfer()
@@ -740,6 +742,91 @@ class AccountsController(TransactionBase):
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
def validate_internal_transaction(self):
if not cint(
frappe.db.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate")
):
return
doctypes_list = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
if self.doctype in doctypes_list and (
self.get("is_internal_customer") or self.get("is_internal_supplier")
):
self.validate_internal_transaction_based_on_voucher_type()
def validate_internal_transaction_based_on_voucher_type(self):
order = ["Sales Order", "Purchase Order"]
invoice = ["Sales Invoice", "Purchase Invoice"]
if self.doctype in order and self.get("inter_company_order_reference"):
# Fetch the linked order
linked_doctype = "Sales Order" if self.doctype == "Purchase Order" else "Purchase Order"
self.validate_line_items(
linked_doctype,
"sales_order" if linked_doctype == "Sales Order" else "purchase_order",
"sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item",
)
elif self.doctype in invoice and self.get("inter_company_invoice_reference"):
# Fetch the linked invoice
linked_doctype = "Sales Invoice" if self.doctype == "Purchase Invoice" else "Purchase Invoice"
self.validate_line_items(
linked_doctype,
"sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice",
"sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item",
)
def validate_line_items(self, ref_dt, ref_dn_field, ref_link_field):
action, role_allowed_to_override = frappe.get_cached_value(
"Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
)
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
stop_actions = []
for d in self.get("items"):
if d.get(ref_link_field):
ref_rate = reference_details.get(d.get(ref_link_field))
if ref_rate is not None and abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
if action == "Stop":
user_roles = [
r["role"]
for r in frappe.get_all(
"Has Role", filters={"parent": frappe.session.user}, fields=["role"]
)
]
if role_allowed_to_override not in user_roles:
stop_actions.append(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx,
ref_dt,
self.inter_company_invoice_reference
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
else d.get(ref_dn_field),
d.rate,
ref_rate,
)
)
else:
frappe.msgprint(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx,
ref_dt,
self.inter_company_invoice_reference
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
else d.get(ref_dn_field),
d.rate,
ref_rate,
),
title=_("Warning"),
indicator="orange",
)
if stop_actions:
frappe.throw(stop_actions, as_list=True)
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1