From 6a7c9f616e00543abacc742d87f5aff212a0ec4d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 9 Jun 2026 16:31:20 +0530 Subject: [PATCH] refactor(journal_entry): extract document builders into mapper.py Move the Payment Entry / Journal Entry builders (get_payment_entry and its against-order/against-invoice helpers, make_inter_company_journal_entry, make_reverse_journal_entry) into mapper.py. The whitelisted builders are re-exported from journal_entry.py so existing call paths -- including custom apps -- keep working, and the erpnext client calls now point at the mapper path. get_payment_entry imports the exchange-rate/bank-account helpers lazily to avoid a circular import with the re-export. --- .../doctype/journal_entry/journal_entry.js | 4 +- .../doctype/journal_entry/journal_entry.py | 226 +---------------- .../accounts/doctype/journal_entry/mapper.py | 240 ++++++++++++++++++ erpnext/public/js/controllers/transaction.js | 6 +- 4 files changed, 253 insertions(+), 223 deletions(-) create mode 100644 erpnext/accounts/doctype/journal_entry/mapper.py diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index b1dc0c477b7..1293a18ca0b 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -178,7 +178,7 @@ frappe.ui.form.on("Journal Entry", { voucher_type: frm.doc.voucher_type, company: args.company, }, - method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_inter_company_journal_entry", + method: "erpnext.accounts.doctype.journal_entry.mapper.make_inter_company_journal_entry", callback: function (r) { if (r.message) { var doc = frappe.model.sync(r.message)[0]; @@ -731,7 +731,7 @@ $.extend(erpnext.journal_entry, { reverse_journal_entry: function (frm) { frappe.model.open_mapped_doc({ - method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry", + method: "erpnext.accounts.doctype.journal_entry.mapper.make_reverse_journal_entry", frm: frm, }); }, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d6d79915b0e..1781d41b0dd 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1001,174 +1001,6 @@ def get_default_bank_cash_account( return frappe._dict() -@frappe.whitelist() -def get_payment_entry_against_order( - dt: str, - dn: str, - amount: float | None = None, - debit_in_account_currency: str | float | None = None, - journal_entry: bool = False, - bank_account: str | None = None, -): - ref_doc = frappe.get_doc(dt, dn) - - if flt(ref_doc.per_billed, 2) > 0: - frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) - - if dt == "Sales Order": - party_type = "Customer" - amount_field_party = "credit_in_account_currency" - amount_field_bank = "debit_in_account_currency" - else: - party_type = "Supplier" - amount_field_party = "debit_in_account_currency" - amount_field_bank = "credit_in_account_currency" - - party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company) - party_account_currency = get_account_currency(party_account) - - if not amount: - if party_account_currency == ref_doc.company_currency: - amount = flt(ref_doc.base_grand_total) - flt(ref_doc.advance_paid) - else: - amount = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) - - return get_payment_entry( - ref_doc, - { - "party_type": party_type, - "party_account": party_account, - "party_account_currency": party_account_currency, - "amount_field_party": amount_field_party, - "amount_field_bank": amount_field_bank, - "amount": amount, - "debit_in_account_currency": debit_in_account_currency, - "remarks": f"Advance Payment received against {dt} {dn}", - "is_advance": "Yes", - "bank_account": bank_account, - "journal_entry": journal_entry, - }, - ) - - -@frappe.whitelist() -def get_payment_entry_against_invoice( - dt: str, - dn: str, - amount: float | None = None, - debit_in_account_currency: str | None = None, - journal_entry: bool = False, - bank_account: str | None = None, -): - ref_doc = frappe.get_doc(dt, dn) - if dt == "Sales Invoice": - party_type = "Customer" - party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to - else: - party_type = "Supplier" - party_account = ref_doc.credit_to - - if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) or ( - dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0 - ): - amount_field_party = "credit_in_account_currency" - amount_field_bank = "debit_in_account_currency" - else: - amount_field_party = "debit_in_account_currency" - amount_field_bank = "credit_in_account_currency" - - return get_payment_entry( - ref_doc, - { - "party_type": party_type, - "party_account": party_account, - "party_account_currency": ref_doc.party_account_currency, - "amount_field_party": amount_field_party, - "amount_field_bank": amount_field_bank, - "amount": amount if amount else abs(ref_doc.outstanding_amount), - "debit_in_account_currency": debit_in_account_currency, - "remarks": f"Payment received against {dt} {dn}. {ref_doc.remarks}", - "is_advance": "No", - "bank_account": bank_account, - "journal_entry": journal_entry, - }, - ) - - -def get_payment_entry(ref_doc, args): - cost_center = ref_doc.get("cost_center") or frappe.get_cached_value( - "Company", ref_doc.company, "cost_center" - ) - exchange_rate = 1 - if args.get("party_account"): - # Modified to include the posting date for which the exchange rate is required. - # Assumed to be the posting date in the reference document - exchange_rate = get_exchange_rate( - ref_doc.get("posting_date") or ref_doc.get("transaction_date"), - args.get("party_account"), - args.get("party_account_currency"), - ref_doc.company, - ref_doc.doctype, - ref_doc.name, - ) - - je = frappe.new_doc("Journal Entry") - je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}) - - party_row = je.append( - "accounts", - { - "account": args.get("party_account"), - "party_type": args.get("party_type"), - "party": ref_doc.get(args.get("party_type").lower()), - "cost_center": cost_center, - "account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"), - "account_currency": args.get("party_account_currency") - or get_account_currency(args.get("party_account")), - "exchange_rate": exchange_rate, - args.get("amount_field_party"): args.get("amount"), - "is_advance": args.get("is_advance"), - "reference_type": ref_doc.doctype, - "reference_name": ref_doc.name, - }, - ) - - bank_row = je.append("accounts") - - # Make it bank_details - bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account")) - if bank_account: - bank_row.update(bank_account) - # Modified to include the posting date for which the exchange rate is required. - # Assumed to be the posting date of the reference date - bank_row.exchange_rate = get_exchange_rate( - ref_doc.get("posting_date") or ref_doc.get("transaction_date"), - bank_account["account"], - bank_account["account_currency"], - ref_doc.company, - ) - - bank_row.cost_center = cost_center - - amount = args.get("debit_in_account_currency") or args.get("amount") - - if bank_row.account_currency == args.get("party_account_currency"): - bank_row.set(args.get("amount_field_bank"), amount) - else: - bank_row.set(args.get("amount_field_bank"), amount * exchange_rate) - - # Multi currency check again - if party_row.account_currency != ref_doc.company_currency or ( - bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency - ): - je.multi_currency = 1 - - je.set_amounts_in_company_currency() - je.set_total_debit_credit() - - return je if args.get("journal_entry") else je.as_dict() - - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_against_jv( @@ -1393,52 +1225,12 @@ def get_average_exchange_rate(account: str): return exchange_rate -@frappe.whitelist() -def make_inter_company_journal_entry(name: str, voucher_type: str, company: str): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = voucher_type - journal_entry.company = company - journal_entry.posting_date = nowdate() - journal_entry.inter_company_journal_entry_reference = name - return journal_entry.as_dict() - - -@frappe.whitelist() -def make_reverse_journal_entry(source_name: str, target_doc: str | Document | None = None): - existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1}) - if existing_reverse: - frappe.throw( - _("A Reverse Journal Entry {0} already exists for this Journal Entry.").format( - get_link_to_form("Journal Entry", existing_reverse) - ) - ) - - from frappe.model.mapper import get_mapped_doc - - def post_process(source, target): - target.reversal_of = source.name - - doclist = get_mapped_doc( - "Journal Entry", - source_name, - { - "Journal Entry": {"doctype": "Journal Entry", "validation": {"docstatus": ["=", 1]}}, - "Journal Entry Account": { - "doctype": "Journal Entry Account", - "field_map": { - "account_currency": "account_currency", - "exchange_rate": "exchange_rate", - "debit_in_account_currency": "credit_in_account_currency", - "debit": "credit", - "credit_in_account_currency": "debit_in_account_currency", - "credit": "debit", - "reference_type": "reference_type", - "reference_name": "reference_name", - }, - }, - }, - target_doc, - post_process, - ) - - return doclist +# These document builders moved to mapper.py. They are re-exported here so existing +# call paths -- including custom apps that reference +# erpnext.accounts.doctype.journal_entry.journal_entry. -- keep working. +from erpnext.accounts.doctype.journal_entry.mapper import ( + get_payment_entry_against_invoice, + get_payment_entry_against_order, + make_inter_company_journal_entry, + make_reverse_journal_entry, +) diff --git a/erpnext/accounts/doctype/journal_entry/mapper.py b/erpnext/accounts/doctype/journal_entry/mapper.py new file mode 100644 index 00000000000..e9ac678987f --- /dev/null +++ b/erpnext/accounts/doctype/journal_entry/mapper.py @@ -0,0 +1,240 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +"""Document builders that map a source document to a Journal Entry or to a +Payment Entry raised against it.""" + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import flt, get_link_to_form, nowdate + +from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( + get_party_account_based_on_invoice_discounting, +) +from erpnext.accounts.party import get_party_account +from erpnext.accounts.utils import get_account_currency + + +@frappe.whitelist() +def get_payment_entry_against_order( + dt: str, + dn: str, + amount: float | None = None, + debit_in_account_currency: str | float | None = None, + journal_entry: bool = False, + bank_account: str | None = None, +): + ref_doc = frappe.get_doc(dt, dn) + + if flt(ref_doc.per_billed, 2) > 0: + frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) + + if dt == "Sales Order": + party_type = "Customer" + amount_field_party = "credit_in_account_currency" + amount_field_bank = "debit_in_account_currency" + else: + party_type = "Supplier" + amount_field_party = "debit_in_account_currency" + amount_field_bank = "credit_in_account_currency" + + party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company) + party_account_currency = get_account_currency(party_account) + + if not amount: + if party_account_currency == ref_doc.company_currency: + amount = flt(ref_doc.base_grand_total) - flt(ref_doc.advance_paid) + else: + amount = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) + + return get_payment_entry( + ref_doc, + { + "party_type": party_type, + "party_account": party_account, + "party_account_currency": party_account_currency, + "amount_field_party": amount_field_party, + "amount_field_bank": amount_field_bank, + "amount": amount, + "debit_in_account_currency": debit_in_account_currency, + "remarks": f"Advance Payment received against {dt} {dn}", + "is_advance": "Yes", + "bank_account": bank_account, + "journal_entry": journal_entry, + }, + ) + + +@frappe.whitelist() +def get_payment_entry_against_invoice( + dt: str, + dn: str, + amount: float | None = None, + debit_in_account_currency: str | None = None, + journal_entry: bool = False, + bank_account: str | None = None, +): + ref_doc = frappe.get_doc(dt, dn) + if dt == "Sales Invoice": + party_type = "Customer" + party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to + else: + party_type = "Supplier" + party_account = ref_doc.credit_to + + if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) or ( + dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0 + ): + amount_field_party = "credit_in_account_currency" + amount_field_bank = "debit_in_account_currency" + else: + amount_field_party = "debit_in_account_currency" + amount_field_bank = "credit_in_account_currency" + + return get_payment_entry( + ref_doc, + { + "party_type": party_type, + "party_account": party_account, + "party_account_currency": ref_doc.party_account_currency, + "amount_field_party": amount_field_party, + "amount_field_bank": amount_field_bank, + "amount": amount if amount else abs(ref_doc.outstanding_amount), + "debit_in_account_currency": debit_in_account_currency, + "remarks": f"Payment received against {dt} {dn}. {ref_doc.remarks}", + "is_advance": "No", + "bank_account": bank_account, + "journal_entry": journal_entry, + }, + ) + + +def get_payment_entry(ref_doc, args): + from erpnext.accounts.doctype.journal_entry.journal_entry import ( + get_default_bank_cash_account, + get_exchange_rate, + ) + + cost_center = ref_doc.get("cost_center") or frappe.get_cached_value( + "Company", ref_doc.company, "cost_center" + ) + exchange_rate = 1 + if args.get("party_account"): + # Modified to include the posting date for which the exchange rate is required. + # Assumed to be the posting date in the reference document + exchange_rate = get_exchange_rate( + ref_doc.get("posting_date") or ref_doc.get("transaction_date"), + args.get("party_account"), + args.get("party_account_currency"), + ref_doc.company, + ref_doc.doctype, + ref_doc.name, + ) + + je = frappe.new_doc("Journal Entry") + je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}) + + party_row = je.append( + "accounts", + { + "account": args.get("party_account"), + "party_type": args.get("party_type"), + "party": ref_doc.get(args.get("party_type").lower()), + "cost_center": cost_center, + "account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"), + "account_currency": args.get("party_account_currency") + or get_account_currency(args.get("party_account")), + "exchange_rate": exchange_rate, + args.get("amount_field_party"): args.get("amount"), + "is_advance": args.get("is_advance"), + "reference_type": ref_doc.doctype, + "reference_name": ref_doc.name, + }, + ) + + bank_row = je.append("accounts") + + # Make it bank_details + bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account")) + if bank_account: + bank_row.update(bank_account) + # Modified to include the posting date for which the exchange rate is required. + # Assumed to be the posting date of the reference date + bank_row.exchange_rate = get_exchange_rate( + ref_doc.get("posting_date") or ref_doc.get("transaction_date"), + bank_account["account"], + bank_account["account_currency"], + ref_doc.company, + ) + + bank_row.cost_center = cost_center + + amount = args.get("debit_in_account_currency") or args.get("amount") + + if bank_row.account_currency == args.get("party_account_currency"): + bank_row.set(args.get("amount_field_bank"), amount) + else: + bank_row.set(args.get("amount_field_bank"), amount * exchange_rate) + + # Multi currency check again + if party_row.account_currency != ref_doc.company_currency or ( + bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency + ): + je.multi_currency = 1 + + je.set_amounts_in_company_currency() + je.set_total_debit_credit() + + return je if args.get("journal_entry") else je.as_dict() + + +@frappe.whitelist() +def make_inter_company_journal_entry(name: str, voucher_type: str, company: str): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = voucher_type + journal_entry.company = company + journal_entry.posting_date = nowdate() + journal_entry.inter_company_journal_entry_reference = name + return journal_entry.as_dict() + + +@frappe.whitelist() +def make_reverse_journal_entry(source_name: str, target_doc: str | Document | None = None): + existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1}) + if existing_reverse: + frappe.throw( + _("A Reverse Journal Entry {0} already exists for this Journal Entry.").format( + get_link_to_form("Journal Entry", existing_reverse) + ) + ) + + from frappe.model.mapper import get_mapped_doc + + def post_process(source, target): + target.reversal_of = source.name + + doclist = get_mapped_doc( + "Journal Entry", + source_name, + { + "Journal Entry": {"doctype": "Journal Entry", "validation": {"docstatus": ["=", 1]}}, + "Journal Entry Account": { + "doctype": "Journal Entry Account", + "field_map": { + "account_currency": "account_currency", + "exchange_rate": "exchange_rate", + "debit_in_account_currency": "credit_in_account_currency", + "debit": "credit", + "credit_in_account_currency": "debit_in_account_currency", + "credit": "debit", + "reference_type": "reference_type", + "reference_name": "reference_name", + }, + }, + }, + target_doc, + post_process, + ) + + return doclist diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a05775a80b5..4fa4c936b3e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -3003,11 +3003,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if (this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry) { if (["Sales Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) { - method = - "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_invoice"; + method = "erpnext.accounts.doctype.journal_entry.mapper.get_payment_entry_against_invoice"; } else { - method = - "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_order"; + method = "erpnext.accounts.doctype.journal_entry.mapper.get_payment_entry_against_order"; } }