mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 06:01:46 +00:00
Merge pull request #55749 from nabinhait/refactor-je-validate-reference-doc
refactor(journal_entry): split validate_reference_doc into per-row methods
This commit is contained in:
@@ -34,6 +34,13 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
REFERENCE_PARTY_ACCOUNT_FIELDS = {
|
||||
"Sales Invoice": ["Customer", "Debit To"],
|
||||
"Purchase Invoice": ["Supplier", "Credit To"],
|
||||
"Sales Order": ["Customer"],
|
||||
"Purchase Order": ["Supplier"],
|
||||
}
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
pass
|
||||
@@ -743,105 +750,101 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def validate_reference_doc(self):
|
||||
"""Validates reference document"""
|
||||
field_dict = {
|
||||
"Sales Invoice": ["Customer", "Debit To"],
|
||||
"Purchase Invoice": ["Supplier", "Credit To"],
|
||||
"Sales Order": ["Customer"],
|
||||
"Purchase Order": ["Supplier"],
|
||||
}
|
||||
|
||||
self.reference_totals = {}
|
||||
self.reference_types = {}
|
||||
self.reference_accounts = {}
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if not d.reference_type:
|
||||
d.reference_name = None
|
||||
if not d.reference_name:
|
||||
d.reference_type = None
|
||||
if d.reference_type and d.reference_name and (d.reference_type in list(field_dict)):
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if d.reference_type in ("Sales Order", "Sales Invoice")
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
# check debit or credit type Sales / Purchase Order
|
||||
if d.reference_type == "Sales Order" and flt(d.debit) > 0:
|
||||
frappe.throw(
|
||||
_("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type)
|
||||
)
|
||||
|
||||
if d.reference_type == "Purchase Order" and flt(d.credit) > 0:
|
||||
frappe.throw(
|
||||
_("Row {0}: Credit entry can not be linked with a {1}").format(
|
||||
d.idx, d.reference_type
|
||||
)
|
||||
)
|
||||
|
||||
# set totals
|
||||
if d.reference_name not in self.reference_totals:
|
||||
self.reference_totals[d.reference_name] = 0.0
|
||||
|
||||
if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):
|
||||
self.reference_totals[d.reference_name] += flt(d.get(dr_or_cr))
|
||||
|
||||
self.reference_types[d.reference_name] = d.reference_type
|
||||
self.reference_accounts[d.reference_name] = d.account
|
||||
|
||||
against_voucher = frappe.db.get_value(
|
||||
d.reference_type, d.reference_name, [scrub(dt) for dt in field_dict.get(d.reference_type)]
|
||||
)
|
||||
|
||||
if not against_voucher:
|
||||
frappe.throw(_("Row {0}: Invalid reference {1}").format(d.idx, d.reference_name))
|
||||
|
||||
# check if party and account match
|
||||
if d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if (
|
||||
self.voucher_type in ("Deferred Revenue", "Deferred Expense")
|
||||
and d.reference_detail_no
|
||||
):
|
||||
debit_or_credit = "Debit" if d.debit else "Credit"
|
||||
party_account = get_deferred_booking_accounts(
|
||||
d.reference_type, d.reference_detail_no, debit_or_credit
|
||||
)
|
||||
against_voucher = ["", against_voucher[1]]
|
||||
else:
|
||||
if d.reference_type == "Sales Invoice":
|
||||
party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or against_voucher[1]
|
||||
)
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
|
||||
if (
|
||||
against_voucher[0] != cstr(d.party) or party_account != d.account
|
||||
) and self.voucher_type != "Exchange Gain Or Loss":
|
||||
frappe.throw(
|
||||
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
|
||||
d.idx,
|
||||
field_dict.get(d.reference_type)[0],
|
||||
field_dict.get(d.reference_type)[1],
|
||||
d.reference_type,
|
||||
d.reference_name,
|
||||
)
|
||||
)
|
||||
|
||||
# check if party matches for Sales / Purchase Order
|
||||
if d.reference_type in ("Sales Order", "Purchase Order"):
|
||||
# set totals
|
||||
if against_voucher != d.party:
|
||||
frappe.throw(
|
||||
_("Row {0}: {1} {2} does not match with {3}").format(
|
||||
d.idx, d.party_type, d.party, d.reference_type
|
||||
)
|
||||
)
|
||||
self._normalize_reference_fields(d)
|
||||
if not self._has_party_reference(d):
|
||||
continue
|
||||
self._validate_order_direction(d)
|
||||
self._register_reference(d)
|
||||
self._validate_reference_party_and_account(d)
|
||||
|
||||
self.validate_orders()
|
||||
self.validate_invoices()
|
||||
|
||||
def _normalize_reference_fields(self, row):
|
||||
if not row.reference_type:
|
||||
row.reference_name = None
|
||||
if not row.reference_name:
|
||||
row.reference_type = None
|
||||
|
||||
def _has_party_reference(self, row):
|
||||
return bool(
|
||||
row.reference_type and row.reference_name and row.reference_type in REFERENCE_PARTY_ACCOUNT_FIELDS
|
||||
)
|
||||
|
||||
def _reference_amount_field(self, row):
|
||||
if row.reference_type in ("Sales Order", "Sales Invoice"):
|
||||
return "credit_in_account_currency"
|
||||
return "debit_in_account_currency"
|
||||
|
||||
def _validate_order_direction(self, row):
|
||||
if row.reference_type == "Sales Order" and flt(row.debit) > 0:
|
||||
frappe.throw(
|
||||
_("Row {0}: Debit entry can not be linked with a {1}").format(row.idx, row.reference_type)
|
||||
)
|
||||
if row.reference_type == "Purchase Order" and flt(row.credit) > 0:
|
||||
frappe.throw(
|
||||
_("Row {0}: Credit entry can not be linked with a {1}").format(row.idx, row.reference_type)
|
||||
)
|
||||
|
||||
def _register_reference(self, row):
|
||||
if row.reference_name not in self.reference_totals:
|
||||
self.reference_totals[row.reference_name] = 0.0
|
||||
if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):
|
||||
self.reference_totals[row.reference_name] += flt(row.get(self._reference_amount_field(row)))
|
||||
self.reference_types[row.reference_name] = row.reference_type
|
||||
self.reference_accounts[row.reference_name] = row.account
|
||||
|
||||
def _validate_reference_party_and_account(self, row):
|
||||
party_fields = REFERENCE_PARTY_ACCOUNT_FIELDS[row.reference_type]
|
||||
against_voucher = frappe.db.get_value(
|
||||
row.reference_type, row.reference_name, [scrub(f) for f in party_fields]
|
||||
)
|
||||
if not against_voucher:
|
||||
frappe.throw(_("Row {0}: Invalid reference {1}").format(row.idx, row.reference_name))
|
||||
|
||||
if row.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
self._validate_invoice_party_and_account(row, against_voucher, party_fields)
|
||||
elif row.reference_type in ("Sales Order", "Purchase Order"):
|
||||
self._validate_order_party(row, against_voucher)
|
||||
|
||||
def _validate_invoice_party_and_account(self, row, against_voucher, party_fields):
|
||||
party_account, against_party = self._resolve_invoice_party_account(row, against_voucher)
|
||||
if self.voucher_type == "Exchange Gain Or Loss":
|
||||
return
|
||||
if against_party != cstr(row.party) or party_account != row.account:
|
||||
frappe.throw(
|
||||
_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
|
||||
row.idx, party_fields[0], party_fields[1], row.reference_type, row.reference_name
|
||||
)
|
||||
)
|
||||
|
||||
def _resolve_invoice_party_account(self, row, against_voucher):
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and row.reference_detail_no:
|
||||
debit_or_credit = "Debit" if row.debit else "Credit"
|
||||
party_account = get_deferred_booking_accounts(
|
||||
row.reference_type, row.reference_detail_no, debit_or_credit
|
||||
)
|
||||
return party_account, ""
|
||||
if row.reference_type == "Sales Invoice":
|
||||
party_account = (
|
||||
get_party_account_based_on_invoice_discounting(row.reference_name) or against_voucher[1]
|
||||
)
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
return party_account, against_voucher[0]
|
||||
|
||||
def _validate_order_party(self, row, against_voucher):
|
||||
if against_voucher != row.party:
|
||||
frappe.throw(
|
||||
_("Row {0}: {1} {2} does not match with {3}").format(
|
||||
row.idx, row.party_type, row.party, row.reference_type
|
||||
)
|
||||
)
|
||||
|
||||
def validate_orders(self):
|
||||
"""Validate totals, closed and docstatus for orders"""
|
||||
for reference_name, total in self.reference_totals.items():
|
||||
|
||||
@@ -609,6 +609,85 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
def test_validate_reference_doc_debit_against_sales_order_throws(self):
|
||||
"""Characterize: a debit entry linked to a Sales Order is rejected."""
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
sales_order = make_sales_order()
|
||||
jv = make_journal_entry("Debtors - _TC", "_Test Cash - _TC", 100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = "_Test Customer"
|
||||
jv.accounts[0].reference_type = "Sales Order"
|
||||
jv.accounts[0].reference_name = sales_order.name
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Debit entry can not be linked", jv.insert)
|
||||
|
||||
def test_validate_reference_doc_credit_against_purchase_order_throws(self):
|
||||
"""Characterize: a credit entry linked to a Purchase Order is rejected."""
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
purchase_order = create_purchase_order()
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Creditors - _TC", 100, save=False)
|
||||
jv.accounts[1].party_type = "Supplier"
|
||||
jv.accounts[1].party = "_Test Supplier"
|
||||
jv.accounts[1].reference_type = "Purchase Order"
|
||||
jv.accounts[1].reference_name = purchase_order.name
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Credit entry can not be linked", jv.insert)
|
||||
|
||||
def test_validate_reference_doc_nonexistent_reference_rejected(self):
|
||||
"""Characterize: a JE referencing a non-existent invoice is rejected by link validation.
|
||||
|
||||
Note: the controller's own "Invalid reference" branch is unreachable in normal flow
|
||||
because Frappe link validation rejects the missing reference before validate_reference_doc.
|
||||
"""
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
|
||||
jv.accounts[1].party_type = "Customer"
|
||||
jv.accounts[1].party = "_Test Customer"
|
||||
jv.accounts[1].reference_type = "Sales Invoice"
|
||||
jv.accounts[1].reference_name = "NON-EXISTENT-SI"
|
||||
self.assertRaises(frappe.LinkValidationError, jv.insert)
|
||||
|
||||
def test_validate_reference_doc_invoice_party_mismatch_throws(self):
|
||||
"""Characterize: an invoice reference whose party differs from the row party is rejected."""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
invoice = create_sales_invoice(rate=500)
|
||||
other_customer = make_customer("_Test JE Mismatch Customer")
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
|
||||
jv.accounts[1].party_type = "Customer"
|
||||
jv.accounts[1].party = other_customer
|
||||
jv.accounts[1].reference_type = "Sales Invoice"
|
||||
jv.accounts[1].reference_name = invoice.name
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Party / Account does not match", jv.insert)
|
||||
|
||||
def test_validate_reference_doc_order_party_mismatch_throws(self):
|
||||
"""Characterize: a Sales Order reference whose party differs from the row party is rejected."""
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
sales_order = make_sales_order()
|
||||
other_customer = make_customer("_Test JE Mismatch Customer")
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
|
||||
jv.accounts[1].party_type = "Customer"
|
||||
jv.accounts[1].party = other_customer
|
||||
jv.accounts[1].is_advance = "Yes"
|
||||
jv.accounts[1].reference_type = "Sales Order"
|
||||
jv.accounts[1].reference_name = sales_order.name
|
||||
self.assertRaisesRegex(frappe.ValidationError, "does not match", jv.insert)
|
||||
|
||||
def test_validate_reference_doc_populates_reference_side_effects(self):
|
||||
"""Characterize: a valid invoice reference populates reference_totals/types/accounts."""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
invoice = create_sales_invoice(rate=500)
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
|
||||
jv.accounts[1].party_type = "Customer"
|
||||
jv.accounts[1].party = "_Test Customer"
|
||||
jv.accounts[1].reference_type = "Sales Invoice"
|
||||
jv.accounts[1].reference_name = invoice.name
|
||||
jv.insert()
|
||||
self.assertEqual(jv.reference_totals[invoice.name], 100.0)
|
||||
self.assertEqual(jv.reference_types[invoice.name], "Sales Invoice")
|
||||
self.assertEqual(jv.reference_accounts[invoice.name], "Debtors - _TC")
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
Reference in New Issue
Block a user