From 62ca71eb63e93fefc251b7087c97f5fcf2be0b25 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Wed, 29 Apr 2026 12:49:00 +0530 Subject: [PATCH 01/10] refactor(payment-request): move payment gateway related fields to custom fields for payments app compatibility --- .../payment_gateway_account/__init__.py | 0 .../payment_gateway_account.js | 21 ---- .../payment_gateway_account.json | 112 ------------------ .../payment_gateway_account.py | 50 -------- .../payment_gateway_account_dashboard.py | 6 - .../test_payment_gateway_account.py | 8 -- .../payment_request/payment_request.js | 5 +- .../payment_request/payment_request.json | 65 +++++----- .../payment_request/payment_request.py | 54 ++++++--- .../subscription_plan/subscription_plan.json | 12 +- .../subscription_plan/subscription_plan.py | 1 - 11 files changed, 79 insertions(+), 255 deletions(-) delete mode 100644 erpnext/accounts/doctype/payment_gateway_account/__init__.py delete mode 100644 erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js delete mode 100644 erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json delete mode 100644 erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py delete mode 100644 erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py delete mode 100644 erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py diff --git a/erpnext/accounts/doctype/payment_gateway_account/__init__.py b/erpnext/accounts/doctype/payment_gateway_account/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js deleted file mode 100644 index ab9a50f8285a..000000000000 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors -// License: GNU General Public License v3. See license.txt - -frappe.ui.form.on("Payment Gateway Account", { - refresh(frm) { - erpnext.utils.check_payments_app(); - if (!frm.doc.__islocal) { - frm.set_df_property("payment_gateway", "read_only", 1); - } - }, - - setup(frm) { - frm.set_query("payment_account", function () { - return { - filters: { - company: frm.doc.company, - }, - }; - }); - }, -}); diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json deleted file mode 100644 index cb258a89741d..000000000000 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "actions": [], - "creation": "2015-12-23 21:31:52.699821", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "payment_gateway", - "payment_channel", - "company", - "is_default", - "column_break_4", - "payment_account", - "currency", - "payment_request_message", - "message", - "message_examples" - ], - "fields": [ - { - "fieldname": "payment_gateway", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Payment Gateway", - "options": "Payment Gateway", - "reqd": 1 - }, - { - "default": "0", - "fieldname": "is_default", - "fieldtype": "Check", - "label": "Is Default" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "payment_account", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Payment Account", - "options": "Account", - "reqd": 1 - }, - { - "fetch_from": "payment_account.account_currency", - "fieldname": "currency", - "fieldtype": "Read Only", - "label": "Currency" - }, - { - "depends_on": "eval: doc.payment_channel == 'Email' || (!doc.payment_channel)", - "fieldname": "payment_request_message", - "fieldtype": "Section Break" - }, - { - "default": "Please click on the link below to make your payment", - "fieldname": "message", - "fieldtype": "Small Text", - "label": "Default Payment Request Message" - }, - { - "fieldname": "message_examples", - "fieldtype": "HTML", - "label": "Message Examples", - "options": "
Message Example
\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.
After all, life is beautiful and the time you have in hand should be spent to enjoy it!
So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n
\n" - }, - { - "default": "Email", - "fieldname": "payment_channel", - "fieldtype": "Select", - "label": "Payment Channel", - "options": "\nEmail\nPhone\nOther" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "print_hide": 1, - "remember_last_selected_value": 1, - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-07-14 16:49:55.210352", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Gateway Account", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py deleted file mode 100644 index 7c58949be8be..000000000000 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe.model.document import Document - - -class PaymentGatewayAccount(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - company: DF.Link - currency: DF.ReadOnly | None - is_default: DF.Check - message: DF.SmallText | None - payment_account: DF.Link - payment_channel: DF.Literal["", "Email", "Phone"] - payment_gateway: DF.Link - # end: auto-generated types - - def autoname(self): - abbr = frappe.db.get_value("Company", self.company, "abbr") - self.name = self.payment_gateway + " - " + self.currency + " - " + abbr - - def validate(self): - self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency") - - self.update_default_payment_gateway() - self.set_as_default_if_not_set() - - def update_default_payment_gateway(self): - if self.is_default: - frappe.db.set_value( - "Payment Gateway Account", - {"is_default": 1, "name": ["!=", self.name], "company": self.company}, - "is_default", - 0, - ) - - def set_as_default_if_not_set(self): - if not frappe.db.exists( - "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company} - ): - self.is_default = 1 diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py deleted file mode 100644 index d0aaee88350c..000000000000 --- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py +++ /dev/null @@ -1,6 +0,0 @@ -def get_data(): - return { - "fieldname": "payment_gateway_account", - "non_standard_fieldnames": {"Subscription Plan": "payment_gateway"}, - "transactions": [{"items": ["Payment Request"]}, {"items": ["Subscription Plan"]}], - } diff --git a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py deleted file mode 100644 index fe70292144f0..000000000000 --- a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -from erpnext.tests.utils import ERPNextTestSuite - - -class TestPaymentGatewayAccount(ERPNextTestSuite): - pass diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 9696a6bfc2ae..99e73f87c5fd 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -1,5 +1,4 @@ cur_frm.add_fetch("payment_gateway_account", "payment_account", "payment_account"); -cur_frm.add_fetch("payment_gateway_account", "payment_gateway", "payment_gateway"); cur_frm.add_fetch("payment_gateway_account", "message", "message"); frappe.ui.form.on("Payment Request", { @@ -13,7 +12,8 @@ frappe.ui.form.on("Payment Request", { frm.set_query("payment_gateway_account", function () { return { filters: { - company: frm.doc.company, + parent: frm.doc.payment_gateway, + parenttype: "Payment Gateway", }, }; }); @@ -80,6 +80,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) { }); frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) { + frm.toggle_reqd("payment_gateway", frm.doc.is_a_subscription); frm.toggle_reqd("payment_gateway_account", frm.doc.is_a_subscription); frm.toggle_reqd("subscription_plans", frm.doc.is_a_subscription); diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 17d8b74aa14e..18f01f1c0e3b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -48,23 +48,24 @@ "email_to", "subject", "column_break_9", - "payment_gateway_account", "status", "make_sales_invoice", "section_break_10", "message", "message_examples", "mute_email", - "section_break_7", + "payment_gateway_details", "payment_gateway", + "payment_gateway_account", "payment_account", "payment_channel", - "payment_order", - "amended_from", "column_break_pnyv", "payment_url", - "column_break_iiuv", - "phone_number" + "phone_number", + "section_break_jfix", + "amended_from", + "column_break_ruax", + "payment_order" ], "fields": [ { @@ -298,13 +299,6 @@ "fieldname": "column_break_9", "fieldtype": "Column Break" }, - { - "depends_on": "eval: doc.payment_request_type == 'Inward'", - "fieldname": "payment_gateway_account", - "fieldtype": "Link", - "label": "Payment Gateway Account", - "options": "Payment Gateway Account" - }, { "default": "Draft", "fieldname": "status", @@ -363,24 +357,17 @@ "read_only": 1 }, { - "collapsible": 1, - "collapsible_depends_on": "doc.payment_gateway_account", "depends_on": "eval: doc.payment_request_type == 'Inward'", - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "label": "Payment Gateway Details" - }, - { - "fetch_from": "payment_gateway_account.payment_gateway", "fieldname": "payment_gateway", - "fieldtype": "Read Only", - "label": "Payment Gateway" + "fieldtype": "Link", + "label": "Payment Gateway", + "options": "Payment Gateway" }, { - "fetch_from": "payment_gateway_account.payment_account", "fieldname": "payment_account", - "fieldtype": "Read Only", + "fieldtype": "Link", "label": "Payment Account", + "options": "Account", "read_only": 1 }, { @@ -452,10 +439,6 @@ "label": "Party Name", "read_only": 1 }, - { - "fieldname": "column_break_iiuv", - "fieldtype": "Column Break" - }, { "fieldname": "phone_number", "fieldtype": "Data", @@ -471,6 +454,28 @@ "label": "Payment Reference", "options": "Payment Reference", "read_only": 1 + }, + { + "fieldname": "payment_gateway_account", + "fieldtype": "Link", + "label": "Payment Gateway Account", + "options": "Payment Gateway Account" + }, + { + "collapsible": 1, + "collapsible_depends_on": "doc.payment_gateway", + "depends_on": "eval: doc.payment_request_type == 'Inward'", + "fieldname": "payment_gateway_details", + "fieldtype": "Section Break", + "label": "Payment Gateway Details" + }, + { + "fieldname": "section_break_jfix", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_ruax", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -478,7 +483,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-02-27 19:11:03.308896", + "modified": "2026-04-29 00:54:51.353583", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index a74f3808142a..bc8684c45ece 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -95,9 +95,9 @@ class PaymentRequest(Document): party_account_currency: DF.Link | None party_name: DF.Data | None party_type: DF.Link | None - payment_account: DF.ReadOnly | None + payment_account: DF.Link | None payment_channel: DF.Literal["", "Email", "Phone", "Other"] - payment_gateway: DF.ReadOnly | None + payment_gateway: DF.Link | None payment_gateway_account: DF.Link | None payment_order: DF.Link | None payment_reference: DF.Table[PaymentReference] @@ -197,13 +197,16 @@ def validate_currency(self): frappe.throw(_("Transaction currency must be same as Payment Gateway currency")) def validate_subscription_details(self): + if "payments" not in frappe.get_installed_apps(): + return + if self.is_a_subscription: amount = 0 for subscription_plan in self.subscription_plans: - payment_gateway = frappe.db.get_value( - "Subscription Plan", subscription_plan.plan, "payment_gateway" + payment_gateway_account = frappe.db.get_value( + "Subscription Plan", subscription_plan.plan, "payment_gateway_account" ) - if payment_gateway != self.payment_gateway_account: + if payment_gateway_account != self.payment_gateway_account: frappe.throw( _( "The payment gateway account in plan {0} is different from the payment gateway account in this payment request" @@ -403,6 +406,9 @@ def _get_address_fields(self, address_name): } def request_phone_payment(self): + if "payments" not in frappe.get_installed_apps(): + return + controller = _get_payment_gateway_controller(self.payment_gateway) request_amount = self.get_request_amount() @@ -451,6 +457,9 @@ def make_invoice(self): si.submit() def payment_gateway_validation(self): + if "payments" not in frappe.get_installed_apps(): + return False + try: controller = _get_payment_gateway_controller(self.payment_gateway) if hasattr(controller, "on_payment_request_submission"): @@ -461,10 +470,16 @@ def payment_gateway_validation(self): return False def set_payment_request_url(self): + if "payments" not in frappe.get_installed_apps(): + return + if self.payment_account and self.payment_gateway and self.payment_gateway_validation(): self.payment_url = self.get_payment_url() def get_payment_url(self): + if "payments" not in frappe.get_installed_apps(): + return False + if self.reference_doctype != "Fees": data = frappe.db.get_value( self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1 @@ -737,7 +752,7 @@ def make_payment_request(**args): if not args.get("company"): args.company = ref_doc.company - gateway_account = get_gateway_details(args) or frappe._dict() + gateway_account = get_payment_gateway_account_details(args) or frappe._dict() # Schedule-based PRs are allowed only if no Payment Entry exists for this document. # Any existing Payment Entry forces legacy (amount-based) flow. @@ -857,10 +872,6 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo pr.update( { - "payment_gateway_account": gateway_account.get("name"), - "payment_gateway": gateway_account.get("payment_gateway"), - "payment_account": gateway_account.get("payment_account"), - "payment_channel": gateway_account.get("payment_channel"), "payment_request_type": args.get("payment_request_type"), "currency": ref_doc.currency, "party_account_currency": party_account_currency, @@ -889,6 +900,17 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo } ) + # specific fields to payments app + if "payments" in frappe.get_installed_apps(): + pr.update( + { + "payment_gateway": gateway_account.get("payment_gateway"), + "payment_gateway_account": gateway_account.get("name"), + "payment_account": gateway_account.get("payment_account"), + "payment_channel": gateway_account.get("payment_channel"), + } + ) + if selected_payment_schedules: apply_payment_references(pr, payment_reference) @@ -1081,19 +1103,19 @@ def get_existing_payment_request_amount(ref_doc, statuses: list | None = None) - return os_amount_in_transaction_currency -def get_gateway_details(args): # nosemgrep +def get_payment_gateway_account_details(args): # nosemgrep """ - Return gateway and payment account of default payment gateway + Fetch details of the default or specified Payment Gateway Account """ - gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company}) - return get_payment_gateway_account(gateway_account) + if "payments" not in frappe.get_installed_apps(): + return + filter = args.get("payment_gateway_account", {"is_default": 1, "company": args.company}) -def get_payment_gateway_account(filter): return frappe.db.get_value( "Payment Gateway Account", filter, - ["name", "payment_gateway", "payment_account", "payment_channel", "message"], + ["name", "payment_account", "payment_channel", "message"], as_dict=1, ) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index d8f57e21cdb6..15ab0b72ce34 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -23,7 +23,6 @@ "payment_plan_section", "product_price_id", "column_break_16", - "payment_gateway", "accounting_dimensions_section", "cost_center", "dimension_col_break" @@ -120,12 +119,6 @@ "fieldname": "column_break_16", "fieldtype": "Column Break" }, - { - "fieldname": "payment_gateway", - "fieldtype": "Link", - "label": "Payment Gateway", - "options": "Payment Gateway Account" - }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", @@ -149,7 +142,7 @@ } ], "links": [], - "modified": "2024-03-27 13:10:47.998597", + "modified": "2026-04-28 13:20:59.817207", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", @@ -193,8 +186,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 932caaa2db24..ce9179d9a542 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -27,7 +27,6 @@ class SubscriptionPlan(Document): cost_center: DF.Link | None currency: DF.Link item: DF.Link - payment_gateway: DF.Link | None plan_name: DF.Data price_determination: DF.Literal["", "Fixed Rate", "Based On Price List", "Monthly Rate"] price_list: DF.Link | None From 06d2f0394024faab72bbac1f61faa14b99d3af9c Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Wed, 29 Apr 2026 22:18:11 +0530 Subject: [PATCH 02/10] chore: integrate Payments custom fields in ERPNext after_install --- .../payment_request/payment_request.js | 31 ++--- .../payment_request/payment_request.json | 108 +++++------------- .../payment_request/payment_request.py | 30 +++-- erpnext/setup/install.py | 3 + 4 files changed, 63 insertions(+), 109 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 99e73f87c5fd..76b5e877ed7a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -1,6 +1,3 @@ -cur_frm.add_fetch("payment_gateway_account", "payment_account", "payment_account"); -cur_frm.add_fetch("payment_gateway_account", "message", "message"); - frappe.ui.form.on("Payment Request", { setup: function (frm) { frm.set_query("party_type", function () { @@ -9,14 +6,18 @@ frappe.ui.form.on("Payment Request", { }; }); - frm.set_query("payment_gateway_account", function () { - return { - filters: { - parent: frm.doc.payment_gateway, - parenttype: "Payment Gateway", - }, - }; - }); + if (frm.fields_dict.payment_gateway_account) { + frm.set_query("payment_gateway_account", function () { + return { + filters: { + parent: frm.doc.payment_gateway, + parenttype: "Payment Gateway", + }, + }; + }); + frm.set_df_property("payment_account", "read_only", 1); + frm.add_fetch("payment_gateway_account", "payment_account", "payment_account"); + } }, }); @@ -80,8 +81,12 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) { }); frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) { - frm.toggle_reqd("payment_gateway", frm.doc.is_a_subscription); - frm.toggle_reqd("payment_gateway_account", frm.doc.is_a_subscription); + if (frm.fields_dict.payment_gateway) { + frm.toggle_reqd("payment_gateway", frm.doc.is_a_subscription); + } + if (frm.fields_dict.payment_gateway_account) { + frm.toggle_reqd("payment_gateway_account", frm.doc.is_a_subscription); + } frm.toggle_reqd("subscription_plans", frm.doc.is_a_subscription); if (frm.doc.is_a_subscription && frm.doc.reference_doctype && frm.doc.reference_name) { diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 18f01f1c0e3b..4d20b5051bda 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -19,6 +19,7 @@ "column_break_4", "reference_doctype", "reference_name", + "make_sales_invoice", "payment_reference_section", "payment_reference", "transaction_details", @@ -43,25 +44,18 @@ "cost_center", "dimension_col_break", "project", - "recipient_and_message", + "payment_details_section", + "payment_account", + "recipient_details", "print_format", "email_to", "subject", "column_break_9", "status", - "make_sales_invoice", - "section_break_10", + "message_details", "message", "message_examples", "mute_email", - "payment_gateway_details", - "payment_gateway", - "payment_gateway_account", - "payment_account", - "payment_channel", - "column_break_pnyv", - "payment_url", - "phone_number", "section_break_jfix", "amended_from", "column_break_ruax", @@ -271,13 +265,6 @@ "options": "Project" }, { - "depends_on": "eval: doc.payment_request_type == 'Inward'", - "fieldname": "recipient_and_message", - "fieldtype": "Section Break", - "label": "Recipient Message And Payment Details" - }, - { - "depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)", "fieldname": "print_format", "fieldtype": "Select", "label": "Print Format" @@ -289,7 +276,6 @@ "label": "To" }, { - "depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)", "fieldname": "subject", "fieldtype": "Data", "in_global_search": 1, @@ -319,18 +305,11 @@ "read_only": 1 }, { - "depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel == \"Email\" || (!doc.payment_channel)", - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, - { - "depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)", "fieldname": "message", "fieldtype": "Text", "label": "Message" }, { - "depends_on": "eval: doc.payment_channel == \"Email\" || (!doc.payment_channel)", "fieldname": "message_examples", "fieldtype": "HTML", "label": "Message Examples", @@ -348,37 +327,6 @@ "read_only": 1, "report_hide": 1 }, - { - "fieldname": "payment_url", - "fieldtype": "Data", - "label": "Payment URL", - "length": 500, - "options": "URL", - "read_only": 1 - }, - { - "depends_on": "eval: doc.payment_request_type == 'Inward'", - "fieldname": "payment_gateway", - "fieldtype": "Link", - "label": "Payment Gateway", - "options": "Payment Gateway" - }, - { - "fieldname": "payment_account", - "fieldtype": "Link", - "label": "Payment Account", - "options": "Account", - "read_only": 1 - }, - { - "depends_on": "eval: doc.payment_channel==\"Phone\"", - "fetch_from": "payment_gateway_account.payment_channel", - "fieldname": "payment_channel", - "fieldtype": "Select", - "label": "Payment Channel", - "options": "\nEmail\nPhone\nOther", - "read_only": 1 - }, { "fieldname": "payment_order", "fieldtype": "Link", @@ -422,10 +370,6 @@ "options": "Company", "read_only": 1 }, - { - "fieldname": "column_break_pnyv", - "fieldtype": "Column Break" - }, { "fieldname": "party_account_currency", "fieldtype": "Link", @@ -439,11 +383,6 @@ "label": "Party Name", "read_only": 1 }, - { - "fieldname": "phone_number", - "fieldtype": "Data", - "label": "Phone Number" - }, { "fieldname": "payment_reference_section", "fieldtype": "Section Break" @@ -456,26 +395,35 @@ "read_only": 1 }, { - "fieldname": "payment_gateway_account", - "fieldtype": "Link", - "label": "Payment Gateway Account", - "options": "Payment Gateway Account" + "fieldname": "section_break_jfix", + "fieldtype": "Section Break" }, { - "collapsible": 1, - "collapsible_depends_on": "doc.payment_gateway", - "depends_on": "eval: doc.payment_request_type == 'Inward'", - "fieldname": "payment_gateway_details", + "fieldname": "column_break_ruax", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: !doc.payment_channel ? doc.payment_request_type == \"Inward\" : doc.payment_channel == \"Email\"", + "fieldname": "recipient_details", "fieldtype": "Section Break", - "label": "Payment Gateway Details" + "label": "Recipient Details" }, { - "fieldname": "section_break_jfix", - "fieldtype": "Section Break" + "depends_on": "eval: !doc.payment_channel ? doc.payment_request_type == \"Inward\" : doc.payment_channel == \"Email\"", + "fieldname": "message_details", + "fieldtype": "Section Break", + "label": "Message Details" }, { - "fieldname": "column_break_ruax", - "fieldtype": "Column Break" + "fieldname": "payment_details_section", + "fieldtype": "Section Break", + "label": "Payment Details" + }, + { + "fieldname": "payment_account", + "fieldtype": "Link", + "label": "Payment Account", + "options": "Account" } ], "grid_page_length": 50, @@ -483,7 +431,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-04-29 00:54:51.353583", + "modified": "2026-04-29 20:28:38.207170", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index bc8684c45ece..de615dee0f83 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -96,14 +96,9 @@ class PaymentRequest(Document): party_name: DF.Data | None party_type: DF.Link | None payment_account: DF.Link | None - payment_channel: DF.Literal["", "Email", "Phone", "Other"] - payment_gateway: DF.Link | None - payment_gateway_account: DF.Link | None payment_order: DF.Link | None payment_reference: DF.Table[PaymentReference] payment_request_type: DF.Literal["Outward", "Inward"] - payment_url: DF.Data | None - phone_number: DF.Data | None print_format: DF.Literal[None] project: DF.Link | None reference_doctype: DF.Link | None @@ -194,7 +189,7 @@ def validate_currency(self): if self.payment_account and ref_doc.currency != frappe.get_cached_value( "Account", self.payment_account, "account_currency" ): - frappe.throw(_("Transaction currency must be same as Payment Gateway currency")) + frappe.throw(_("Transaction currency must be same as Payment Account currency")) def validate_subscription_details(self): if "payments" not in frappe.get_installed_apps(): @@ -251,15 +246,16 @@ def before_submit(self): elif self.payment_request_type == "Inward": self.status = "Requested" - if self.payment_request_type == "Inward" and self.payment_gateway: - if _is_v2_gateway(self.payment_gateway): + if self.payment_request_type == "Inward": + if self.payment_gateway and _is_v2_gateway(self.payment_gateway): # New PaymentController flow (v2 gateways) self._process_v2_gateway() elif self.payment_channel == "Phone": # Legacy v1 phone payment - phone payments do not generate email/link # communications as the payment is initiated directly via phone channel - self.request_phone_payment() - return + if hasattr(self, "payment_channel") and self.payment_channel == "Phone": + self.request_phone_payment() + return else: # Legacy v1 URL payment self.set_payment_request_url() @@ -512,7 +508,7 @@ def get_payment_url(self): ) def set_as_paid(self): - if self.payment_channel == "Phone": + if hasattr(self, "payment_channel") and self.payment_channel == "Phone": self.db_set({"status": "Paid", "outstanding_amount": 0}) else: @@ -628,7 +624,7 @@ def get_message(self): context = { "doc": frappe.get_doc(self.reference_doctype, self.reference_name), - "payment_url": self.payment_url, + "payment_url": self.payment_url if hasattr(self, "payment_url") else "", "payment_request": self, } @@ -806,7 +802,9 @@ def make_payment_request(**args): if selected_payment_schedules and not has_payment_entry: grand_total = sum(row.get("payment_amount") for row in selected_payment_schedules) else: - grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) + grand_total = get_amount( + ref_doc, args.get("payment_account") or gateway_account.get("payment_account") + ) if not grand_total: frappe.throw(_("Payment Entry is already created")) @@ -896,7 +894,7 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo or args.order_type == "Shopping Cart" # compat for webshop app or gateway_account.get("payment_channel", "Email") != "Email" ), - "phone_number": args.get("phone_number") if args.get("phone_number") else None, + "payment_account": args.get("payment_account") or gateway_account.get("payment_account"), } ) @@ -905,9 +903,9 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo pr.update( { "payment_gateway": gateway_account.get("payment_gateway"), - "payment_gateway_account": gateway_account.get("name"), - "payment_account": gateway_account.get("payment_account"), + "payment_gateway_account": gateway_account.get("gateway_name"), "payment_channel": gateway_account.get("payment_channel"), + "phone_number": args.get("phone_number"), } ) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 043ac0a71930..c76248d2a9fb 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -42,6 +42,9 @@ def after_install(): toggle_hidden_fields() frappe.db.commit() + if "payments" in frappe.get_installed_apps(): + frappe.get_attr("payments.utils.make_custom_fields_erpnext")() + def make_default_operations(): for operation in ["Assembly"]: From fc302cdad71d8b99f1460684325ada29f294f414 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sat, 9 May 2026 18:38:15 +0530 Subject: [PATCH 03/10] refactor: decouple payment request from payment gateway account --- .../payment_request/payment_request.js | 289 +++++++++++------- .../payment_request/payment_request.json | 26 +- .../payment_request/payment_request.py | 236 ++++++++------ 3 files changed, 336 insertions(+), 215 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 76b5e877ed7a..c4bec94e621f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -1,139 +1,208 @@ frappe.ui.form.on("Payment Request", { - setup: function (frm) { + setup(frm) { frm.set_query("party_type", function () { return { query: "erpnext.setup.doctype.party_type.party_type.get_party_type", }; }); - if (frm.fields_dict.payment_gateway_account) { - frm.set_query("payment_gateway_account", function () { + if (frm.fields_dict.payment_account) { + frm.set_query("payment_account", function () { return { + query: "erpnext.accounts.doctype.payment_request.payment_request.get_payment_account", filters: { - parent: frm.doc.payment_gateway, - parenttype: "Payment Gateway", + payment_gateway: frm.doc.payment_gateway, + company: frm.doc.company, }, }; }); - frm.set_df_property("payment_account", "read_only", 1); - frm.add_fetch("payment_gateway_account", "payment_account", "payment_account"); } - }, -}); -frappe.ui.form.on("Payment Request", "onload", function (frm, dt, dn) { - if (frm.doc.reference_doctype) { - frappe.call({ - method: "erpnext.accounts.doctype.payment_request.payment_request.get_print_format_list", - args: { ref_doctype: frm.doc.reference_doctype }, - callback: function (r) { - set_field_options("print_format", r.message["print_format"]); - }, - }); - } -}); + if (frm.doc.payment_request_type == "Outward") { + frm.set_query("bank_account", function () { + return { + filters: { + party_type: frm.doc.party_type, + party: frm.doc.party, + }, + }; + }); + } else { + frm.set_query("bank_account", function () { + return { + filters: { + is_company_account: 1, + company: frm.doc.company, + }, + }; + }); + } + }, -frappe.ui.form.on("Payment Request", "refresh", function (frm) { - if (frm.doc.status == "Failed") { - frm.set_intro(__("Failure: {0}", [frm.doc.failed_reason]), "red"); - } - - if ( - frm.doc.payment_request_type == "Inward" && - frm.doc.payment_channel !== "Phone" && - !["Initiated", "Paid"].includes(frm.doc.status) && - !frm.doc.__islocal && - frm.doc.docstatus == 1 - ) { - frm.add_custom_button(__("Resend Payment Email"), function () { + onload(frm) { + if (frm.doc.reference_doctype) { frappe.call({ - method: "erpnext.accounts.doctype.payment_request.payment_request.resend_payment_email", - args: { docname: frm.doc.name }, - freeze: true, - freeze_message: __("Sending"), + method: "erpnext.accounts.doctype.payment_request.payment_request.get_print_format_list", + args: { ref_doctype: frm.doc.reference_doctype }, callback: function (r) { - if (!r.exc) { - frappe.msgprint(__("Message Sent")); - } + set_field_options("print_format", r.message["print_format"]); }, }); - }); - } + } + }, + + refresh(frm) { + if (frm.doc.status == "Failed") { + frm.set_intro(__("Failure: {0}", [frm.doc.failed_reason]), "red"); + } + + if ( + frm.doc.payment_request_type == "Inward" && + frm.doc.payment_channel !== "Phone" && + !["Initiated", "Paid"].includes(frm.doc.status) && + !frm.doc.__islocal && + frm.doc.docstatus == 1 + ) { + frm.add_custom_button(__("Resend Payment Email"), function () { + frappe.call({ + method: "erpnext.accounts.doctype.payment_request.payment_request.resend_payment_email", + args: { docname: frm.doc.name }, + freeze: true, + freeze_message: __("Sending"), + callback: function (r) { + if (!r.exc) { + frappe.msgprint(__("Message Sent")); + } + }, + }); + }); + } + + if ( + frm.doc.payment_request_type == "Outward" && + ["Initiated", "Partially Paid"].includes(frm.doc.status) + ) { + frm.add_custom_button(__("Create Payment Entry"), function () { + frappe.call({ + method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", + args: { docname: frm.doc.name }, + freeze: true, + callback: function (r) { + if (!r.exc) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + }, + }); + }).addClass("btn-primary"); + } + }, + + bank_account(frm) { + if (frm.doc.bank_account) { + frm.set_value("payment_gateway", null); + frm.set_value("payment_account", null); + frm.set_value("payment_channel", null); + frm.set_value("phone_number", null); + } + }, - if ( - frm.doc.payment_request_type == "Outward" && - ["Initiated", "Partially Paid"].includes(frm.doc.status) - ) { - frm.add_custom_button(__("Create Payment Entry"), function () { + payment_gateway(frm) { + if (frm.doc.payment_gateway) { + frm.set_value("bank_account", null); + } + frm.set_value("payment_account", null); + frm.set_value("payment_channel", null); + frm.set_value("phone_number", null); + }, + + payment_account(frm) { + if (!frm.doc.payment_gateway || !frm.doc.payment_account || !frm.doc.company) { + frm.set_value("payment_channel", null); + return; + } + + frappe.db.get_value( + "Payment Gateway Account", + { + parent: frm.doc.payment_gateway, + payment_account: frm.doc.payment_account, + company: frm.doc.company, + }, + "payment_channel", + (r) => { + if (frm.fields_dict.payment_channel) { + frm.set_value("payment_channel", r?.payment_channel || null); + } + }, + "Payment Gateway" + ); + }, + + is_a_subscription(frm) { + if (frm.fields_dict.payment_gateway) { + frm.toggle_reqd("payment_gateway", frm.doc.is_a_subscription); + } + + frm.toggle_reqd("subscription_plans", frm.doc.is_a_subscription); + + if (frm.doc.is_a_subscription && frm.doc.reference_doctype && frm.doc.reference_name) { frappe.call({ - method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", - args: { docname: frm.doc.name }, + method: "erpnext.accounts.doctype.payment_request.payment_request.get_subscription_details", + args: { + reference_doctype: frm.doc.reference_doctype, + reference_name: frm.doc.reference_name, + }, freeze: true, - callback: function (r) { - if (!r.exc) { - var doc = frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); + callback: function (data) { + if (!data.exc) { + $.each(data.message || [], function (i, v) { + var d = frappe.model.add_child( + frm.doc, + "Subscription Plan Detail", + "subscription_plans" + ); + + d.qty = v.qty; + d.plan = v.plan; + }); + + frm.refresh_field("subscription_plans"); } }, }); - }).addClass("btn-primary"); - } -}); + } + }, -frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) { - if (frm.fields_dict.payment_gateway) { - frm.toggle_reqd("payment_gateway", frm.doc.is_a_subscription); - } - if (frm.fields_dict.payment_gateway_account) { - frm.toggle_reqd("payment_gateway_account", frm.doc.is_a_subscription); - } - frm.toggle_reqd("subscription_plans", frm.doc.is_a_subscription); - - if (frm.doc.is_a_subscription && frm.doc.reference_doctype && frm.doc.reference_name) { - frappe.call({ - method: "erpnext.accounts.doctype.payment_request.payment_request.get_subscription_details", - args: { reference_doctype: frm.doc.reference_doctype, reference_name: frm.doc.reference_name }, - freeze: true, - callback: function (data) { - if (!data.exc) { - $.each(data.message || [], function (i, v) { - var d = frappe.model.add_child( - frm.doc, - "Subscription Plan Detail", - "subscription_plans" - ); - d.qty = v.qty; - d.plan = v.plan; - }); - frm.refresh_field("subscription_plans"); - } - }, - }); - } -}); + calculate_total_amount_by_selected_rows(frm) { + if (frm.doc.docstatus !== 0) { + frappe.msgprint(__("Cannot fetch selected rows for submitted Payment Request")); + return; + } + + const selected = frm.get_selected()?.payment_reference || []; -frappe.ui.form.on("Payment Request", "calculate_total_amount_by_selected_rows", function (frm) { - if (frm.doc.docstatus !== 0) { - frappe.msgprint(__("Cannot fetch selected rows for submitted Payment Request")); - return; - } - const selected = frm.get_selected()?.payment_reference || []; - if (!selected.length) { - frappe.throw(__("No rows selected")); - } - let total = 0; - selected.forEach((name) => { - const row = frm.doc.payment_reference.find((d) => d.name === name); - if (row) { - row.manually_selected = 1; - - total += row.amount; + if (!selected.length) { + frappe.throw(__("No rows selected")); } - }); - frm.doc.payment_reference.forEach((row) => { - row.auto_selected = 0; - }); - frm.set_value("grand_total", total); - frm.refresh_field("grand_total"); - frm.save(); + + let total = 0; + + selected.forEach((name) => { + const row = frm.doc.payment_reference.find((d) => d.name === name); + + if (row) { + row.manually_selected = 1; + total += row.amount; + } + }); + + frm.doc.payment_reference.forEach((row) => { + row.auto_selected = 0; + }); + + frm.set_value("grand_total", total); + frm.refresh_field("grand_total"); + frm.save(); + }, }); diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 4d20b5051bda..cf53dd289fc0 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -44,8 +44,6 @@ "cost_center", "dimension_col_break", "project", - "payment_details_section", - "payment_account", "recipient_details", "print_format", "email_to", @@ -68,6 +66,7 @@ "fieldtype": "Select", "label": "Payment Request Type", "options": "Outward\nInward", + "read_only": 1, "reqd": 1 }, { @@ -106,13 +105,15 @@ "fieldname": "party_type", "fieldtype": "Link", "label": "Party Type", - "options": "DocType" + "options": "DocType", + "read_only": 1 }, { "fieldname": "party", "fieldtype": "Dynamic Link", "label": "Party", - "options": "party_type" + "options": "party_type", + "read_only": 1 }, { "fieldname": "column_break_4", @@ -187,7 +188,7 @@ "options": "Subscription Plan Detail" }, { - "collapsible": 1, + "depends_on": "eval: !doc.payment_gateway", "fieldname": "bank_account_details", "fieldtype": "Section Break", "label": "Bank Account Details" @@ -403,27 +404,18 @@ "fieldtype": "Column Break" }, { + "collapsible": 1, "depends_on": "eval: !doc.payment_channel ? doc.payment_request_type == \"Inward\" : doc.payment_channel == \"Email\"", "fieldname": "recipient_details", "fieldtype": "Section Break", "label": "Recipient Details" }, { + "collapsible": 1, "depends_on": "eval: !doc.payment_channel ? doc.payment_request_type == \"Inward\" : doc.payment_channel == \"Email\"", "fieldname": "message_details", "fieldtype": "Section Break", "label": "Message Details" - }, - { - "fieldname": "payment_details_section", - "fieldtype": "Section Break", - "label": "Payment Details" - }, - { - "fieldname": "payment_account", - "fieldtype": "Link", - "label": "Payment Account", - "options": "Account" } ], "grid_page_length": 50, @@ -431,7 +423,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-04-29 20:28:38.207170", + "modified": "2026-05-06 01:25:11.783984", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index de615dee0f83..b9933537cfdf 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -3,6 +3,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import DocType from frappe.query_builder.functions import Sum from frappe.utils import flt, nowdate from frappe.utils.background_jobs import enqueue @@ -95,7 +96,6 @@ class PaymentRequest(Document): party_account_currency: DF.Link | None party_name: DF.Data | None party_type: DF.Link | None - payment_account: DF.Link | None payment_order: DF.Link | None payment_reference: DF.Table[PaymentReference] payment_request_type: DF.Literal["Outward", "Inward"] @@ -127,11 +127,16 @@ def validate(self): if self.get("__islocal"): self.status = "Draft" self.validate_reference_document() + self.validate_payment_flows() self.validate_against_payment_reference() self.validate_payment_request_amount() - # self.validate_currency() + self.validate_currency() self.validate_subscription_details() + def validate_payment_flows(self): + if self.bank_account and self.payment_gateway: + frappe.throw(_("Select either Bank Account or Payment Gateway.")) + def validate_against_payment_reference(self): if not self.payment_reference: return @@ -185,6 +190,9 @@ def validate_payment_request_amount(self): ) def validate_currency(self): + if not self.get("payment_account"): + return + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) if self.payment_account and ref_doc.currency != frappe.get_cached_value( "Account", self.payment_account, "account_currency" @@ -192,32 +200,34 @@ def validate_currency(self): frappe.throw(_("Transaction currency must be same as Payment Account currency")) def validate_subscription_details(self): - if "payments" not in frappe.get_installed_apps(): + if not self.is_a_subscription: return - if self.is_a_subscription: - amount = 0 - for subscription_plan in self.subscription_plans: - payment_gateway_account = frappe.db.get_value( - "Subscription Plan", subscription_plan.plan, "payment_gateway_account" + if not self.get("payment_gateway"): + return + + amount = 0 + for subscription_plan in self.subscription_plans: + payment_account = frappe.db.get_value( + "Subscription Plan", subscription_plan.plan, "payment_account" + ) + if payment_account != self.payment_account: + frappe.throw( + _( + "The payment account in plan {0} is different from the payment account in this payment request" + ).format(subscription_plan.name) ) - if payment_gateway_account != self.payment_gateway_account: - frappe.throw( - _( - "The payment gateway account in plan {0} is different from the payment gateway account in this payment request" - ).format(subscription_plan.name) - ) - rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty) + rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty) - amount += rate + amount += rate - if amount != self.grand_total: - frappe.msgprint( - _( - "The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document." - ).format(self.grand_total, amount) - ) + if amount != self.grand_total: + frappe.msgprint( + _( + "The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document." + ).format(self.grand_total, amount) + ) def before_submit(self): if ( @@ -247,17 +257,15 @@ def before_submit(self): self.status = "Requested" if self.payment_request_type == "Inward": - if self.payment_gateway and _is_v2_gateway(self.payment_gateway): - # New PaymentController flow (v2 gateways) + if not getattr(self, "payment_gateway", None): + return + + if _is_v2_gateway(self.payment_gateway): self._process_v2_gateway() - elif self.payment_channel == "Phone": - # Legacy v1 phone payment - phone payments do not generate email/link - # communications as the payment is initiated directly via phone channel - if hasattr(self, "payment_channel") and self.payment_channel == "Phone": - self.request_phone_payment() - return + elif getattr(self, "payment_channel", None) == "Phone": + self.request_phone_payment() + return else: - # Legacy v1 URL payment self.set_payment_request_url() if not (self.mute_email or self.flags.mute_email): @@ -402,7 +410,7 @@ def _get_address_fields(self, address_name): } def request_phone_payment(self): - if "payments" not in frappe.get_installed_apps(): + if not self.get("payment_gateway"): return controller = _get_payment_gateway_controller(self.payment_gateway) @@ -452,29 +460,29 @@ def make_invoice(self): si = si.insert(ignore_permissions=True) si.submit() - def payment_gateway_validation(self): - if "payments" not in frappe.get_installed_apps(): - return False - + def validate_payment_gateway(self): try: controller = _get_payment_gateway_controller(self.payment_gateway) - if hasattr(controller, "on_payment_request_submission"): - return controller.on_payment_request_submission(self) - else: - return True except Exception: return False + if hasattr(controller, "on_payment_request_submission"): + return controller.on_payment_request_submission(self) + + return True + def set_payment_request_url(self): - if "payments" not in frappe.get_installed_apps(): + if not self.get("payment_gateway"): return - if self.payment_account and self.payment_gateway and self.payment_gateway_validation(): - self.payment_url = self.get_payment_url() + if not self.validate_payment_gateway(): + return + + self.payment_url = self.get_payment_url() def get_payment_url(self): - if "payments" not in frappe.get_installed_apps(): - return False + if not self.get("payment_gateway"): + return if self.reference_doctype != "Fees": data = frappe.db.get_value( @@ -487,6 +495,7 @@ def get_payment_url(self): data.update({"company": frappe.defaults.get_defaults().company}) controller = _get_payment_gateway_controller(self.payment_gateway) + controller.validate_transaction_currency(self.currency) if hasattr(controller, "validate_minimum_transaction_amount"): @@ -543,12 +552,12 @@ def create_payment_entry(self, submit=True): exchange_rate = ref_doc.get("conversion_rate") bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total")) - # outstanding amount is already in Part's account currency + # outstanding amount is already in Party's account currency payment_entry = get_payment_entry( self.reference_doctype, self.reference_name, party_amount=party_amount, - bank_account=self.payment_account, + bank_account=self.account or self.payment_account, # company bank account or payment account bank_amount=bank_amount, created_from_payment_request=True, ) @@ -594,7 +603,7 @@ def create_payment_entry(self, submit=True): return payment_entry def send_email(self): - """send email with payment link""" + """send email with optional payment link""" email_args = { "recipients": self.email_to, "sender": None, @@ -620,7 +629,7 @@ def send_email(self): ) def get_message(self): - """return message with payment gateway link""" + """return message with optional payment gateway link""" context = { "doc": frappe.get_doc(self.reference_doctype, self.reference_name), @@ -883,27 +892,30 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo "company": ref_doc.get("company"), "party_type": party_type, "party": args.get("party") or ref_doc.get("customer"), - "bank_account": bank_account, + "bank_account": bank_account + if args.get("payment_request_type") == "Outward" + else args.get("bank_account"), "party_name": args.get("party_name") or ref_doc.get("customer_name"), "make_sales_invoice": ( args.make_sales_invoice # new standard or args.order_type == "Shopping Cart" # compat for webshop app ), "mute_email": ( - args.mute_email # new standard - or args.order_type == "Shopping Cart" # compat for webshop app - or gateway_account.get("payment_channel", "Email") != "Email" + args.mute_email + or args.order_type == "Shopping Cart" + or ( + gateway_account.get("payment_channel") + and gateway_account.get("payment_channel") != "Email" + ) ), - "payment_account": args.get("payment_account") or gateway_account.get("payment_account"), } ) - # specific fields to payments app - if "payments" in frappe.get_installed_apps(): + if args.get("payment_gateway") or gateway_account.get("payment_account"): pr.update( { - "payment_gateway": gateway_account.get("payment_gateway"), - "payment_gateway_account": gateway_account.get("gateway_name"), + "payment_gateway": args.get("payment_gateway"), + "payment_account": args.get("payment_account") or gateway_account.get("payment_account"), "payment_channel": gateway_account.get("payment_channel"), "phone_number": args.get("phone_number"), } @@ -990,44 +1002,47 @@ def get_existing_payment_entry(ref_docname): def get_amount(ref_doc, payment_account=None): """get amount based on doctype""" grand_total = 0 - dt = ref_doc.doctype + + def currency_adjust(amount): + if ref_doc.party_account_currency == ref_doc.currency: + return amount + return flt(amount / ref_doc.conversion_rate) + + # so/po if dt in ["Sales Order", "Purchase Order"]: advance_amount = flt(ref_doc.advance_paid) + if ref_doc.party_account_currency != ref_doc.currency: - advance_amount = flt(flt(ref_doc.advance_paid) / ref_doc.conversion_rate) + advance_amount = currency_adjust(advance_amount) grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - advance_amount + # si/pi elif dt in ["Sales Invoice", "Purchase Invoice"]: - if ( - dt == "Sales Invoice" - and ref_doc.is_pos - and ref_doc.payments - and any( - [ - payment.type == "Phone" and payment.account == payment_account - for payment in ref_doc.payments - ] - ) + pos_phone_match = ( + dt == "Sales Invoice" and ref_doc.is_pos and ref_doc.get("payments") and payment_account + ) + + if pos_phone_match and any( + p.type == "Phone" and p.account == payment_account for p in ref_doc.payments ): grand_total = sum( - [ - payment.amount - for payment in ref_doc.payments - if payment.type == "Phone" and payment.account == payment_account - ] + p.amount for p in ref_doc.payments if p.type == "Phone" and p.account == payment_account ) + else: - if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.outstanding_amount) - else: - grand_total = flt(flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate) + grand_total = currency_adjust(flt(ref_doc.outstanding_amount)) + + # pos invoice elif dt == "POS Invoice": - for pay in ref_doc.payments: - if pay.type == "Phone" and pay.account == payment_account: - grand_total = pay.amount - break + if ref_doc.get("payments") and payment_account: + for pay in ref_doc.payments: + if pay.type == "Phone" and pay.account == payment_account: + grand_total = pay.amount + break + + # fees elif dt == "Fees": grand_total = ref_doc.outstanding_amount @@ -1103,17 +1118,28 @@ def get_existing_payment_request_amount(ref_doc, statuses: list | None = None) - def get_payment_gateway_account_details(args): # nosemgrep """ - Fetch details of the default or specified Payment Gateway Account + Fetch details of the specified or default Payment Gateway Account """ - if "payments" not in frappe.get_installed_apps(): + if not args.get("payment_gateway"): return - filter = args.get("payment_gateway_account", {"is_default": 1, "company": args.company}) + filters = { + "parent": args.get("payment_gateway"), + "parenttype": "Payment Gateway", + } + + if args.get("company"): + filters["company"] = args.get("company") + + if args.get("payment_account"): + filters["payment_account"] = args.get("payment_account") + else: + filters["is_default"] = 1 return frappe.db.get_value( "Payment Gateway Account", - filter, - ["name", "payment_account", "payment_channel", "message"], + filters, + ["payment_account", "payment_channel", "message"], as_dict=1, ) @@ -1375,3 +1401,37 @@ def get_existing_payment_references(reference_name): ).run(as_dict=True) return result + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_payment_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): + PGA = DocType("Payment Gateway Account") + get_payment_account_query = frappe.qb.from_(PGA).select(PGA.payment_account) + + condition_list = [] + + if filters: + payment_gateway = filters.get("payment_gateway") + company = filters.get("company") + + if not payment_gateway: + return [] + + condition_list.append(PGA.parent == payment_gateway) + + if company: + condition_list.append(PGA.company == company) + else: + return [] + + for condition in condition_list: + get_payment_account_query = get_payment_account_query.where(condition) + + get_payment_account_query = get_payment_account_query.where(PGA.payment_account.like(f"%{txt}%")) + get_payment_account_query = get_payment_account_query.limit(page_len) + get_payment_account_query = get_payment_account_query.offset(start) + + result = get_payment_account_query.run() + + return result From a550215baa9d6e011b0ff53bcc95903e91f6cccf Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sat, 9 May 2026 18:40:34 +0530 Subject: [PATCH 04/10] feat: add payment account filtering for subscription plan --- .../subscription_plan/subscription_plan.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js index 125dc7dd9ae9..dc560f0798b5 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js @@ -2,6 +2,23 @@ // For license information, please see license.txt frappe.ui.form.on("Subscription Plan", { + setup: function (frm) { + if (frm.fields_dict.payment_account) { + frm.set_query("payment_account", function () { + return { + query: "erpnext.accounts.doctype.payment_request.payment_request.get_payment_account", + filters: { + payment_gateway: frm.doc.payment_gateway, + }, + }; + }); + } + }, + + payment_gateway(frm) { + frm.set_value("payment_account", null); + }, + price_determination: function (frm) { frm.toggle_reqd("cost", frm.doc.price_determination === "Fixed rate"); frm.toggle_reqd("price_list", frm.doc.price_determination === "Based on price list"); From 5e2162561c88bd71aa7ad96a7c9f32f25d939a74 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sat, 9 May 2026 18:43:16 +0530 Subject: [PATCH 05/10] fix: update pos invoice payment gateway account lookup --- .../doctype/pos_invoice/pos_invoice.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 2925def84085..b65c3b2784af 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -815,12 +815,13 @@ def create_payment_request(self): return pay_req def get_new_payment_request(self, mop): - payment_gateway_account = frappe.db.get_value( + payment_gateway = frappe.db.get_value( "Payment Gateway Account", { "payment_account": mop.account, }, - ["name"], + ["parent", "payment_account"], + as_dict=1, ) args = { @@ -828,7 +829,8 @@ def get_new_payment_request(self, mop): "dn": self.name, "recipient_id": self.contact_mobile, "mode_of_payment": mop.mode_of_payment, - "payment_gateway_account": payment_gateway_account, + "payment_gateway": payment_gateway.parent, + "payment_account": payment_gateway.payment_account, "payment_request_type": "Inward", "party_type": "Customer", "party": self.customer, @@ -837,18 +839,10 @@ def get_new_payment_request(self, mop): return make_payment_request(**args) def get_existing_payment_request(self, pay): - payment_gateway_account = frappe.db.get_value( - "Payment Gateway Account", - { - "payment_account": pay.account, - }, - ["name"], - ) - filters = { "reference_doctype": "POS Invoice", "reference_name": self.name, - "payment_gateway_account": payment_gateway_account, + "payment_account": pay.account, "email_to": self.contact_mobile, } pr = frappe.db.get_value("Payment Request", filters=filters) From 4c1d8c49c77d6c803babe17c8ba3954f0caf4e58 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sat, 9 May 2026 18:44:35 +0530 Subject: [PATCH 06/10] fix: update payment gateway account creation logic --- erpnext/accounts/utils.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index bc7794b880b8..d934b22ebc2c 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1516,13 +1516,20 @@ def create_payment_gateway_account(gateway, payment_channel="Email", company=Non bank_account = create_bank_account({"company_name": company, "bank_account": _(gateway)}) if not bank_account: - frappe.msgprint(_("Payment Gateway Account not created, please create one manually.")) + frappe.msgprint( + _("Payment Gateway Account not created, please create one under a Payment Gateway manually.") + ) return # if payment gateway account exists, return if frappe.db.exists( "Payment Gateway Account", - {"payment_gateway": gateway, "currency": bank_account.account_currency}, + { + "parent": gateway, + "parenttype": "Payment Gateway", + "company": company, + "payment_account": bank_account.name, + }, ): return @@ -1530,8 +1537,10 @@ def create_payment_gateway_account(gateway, payment_channel="Email", company=Non frappe.get_doc( { "doctype": "Payment Gateway Account", + "parent": gateway, + "parenttype": "Payment Gateway", + "parentfield": "payment_gateway_account", "is_default": 1, - "payment_gateway": gateway, "payment_account": bank_account.name, "currency": bank_account.account_currency, "payment_channel": payment_channel, From d4cafacd18f3d9e630017257390f30b70c753827 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Sat, 9 May 2026 18:46:05 +0530 Subject: [PATCH 07/10] feat: add setup and cleanup hooks for payments custom fields --- erpnext/hooks.py | 1 + erpnext/setup/install.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index bba172e498b7..56d9bf300dd8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -64,6 +64,7 @@ setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages" after_install = "erpnext.setup.install.after_install" +before_uninstall = "erpnext.setup.install.before_uninstall" boot_session = "erpnext.startup.boot.boot_session" notification_config = "erpnext.startup.notifications.get_notification_config" diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index c76248d2a9fb..9710a7256da7 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -43,7 +43,12 @@ def after_install(): frappe.db.commit() if "payments" in frappe.get_installed_apps(): - frappe.get_attr("payments.utils.make_custom_fields_erpnext")() + frappe.get_attr("payments.utils.make_payments_erpnext_custom_fields")() + + +def before_uninstall(): + if "payments" in frappe.get_installed_apps(): + frappe.get_attr("payments.utils.delete_payments_erpnext_custom_fields")() def make_default_operations(): From 4b63b47ab738a122d720598f53e3eb46cb41baf7 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Mon, 11 May 2026 00:47:25 +0530 Subject: [PATCH 08/10] test: update payment request test cases --- .../payment_request/test_payment_request.py | 197 +++++++++++------- .../purchase_invoice/test_purchase_invoice.py | 1 + 2 files changed, 119 insertions(+), 79 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 6694df135665..1e5f94a31c6d 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -22,42 +22,48 @@ PAYMENT_URL = "https://example.com/payment" payment_gateways = [ - {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}, - {"doctype": "Payment Gateway", "gateway": "_Test Gateway Phone"}, - {"doctype": "Payment Gateway", "gateway": "_Test Gateway Other"}, -] - -payment_method = [ { - "doctype": "Payment Gateway Account", - "is_default": 1, - "payment_gateway": "_Test Gateway", - "payment_account": "_Test Bank - _TC", - "currency": "INR", - "company": "_Test Company", - }, - { - "doctype": "Payment Gateway Account", - "payment_gateway": "_Test Gateway", - "payment_account": "_Test Bank USD - _TC", - "currency": "USD", - "company": "_Test Company", + "doctype": "Payment Gateway", + "gateway_name": "_Test Gateway", + "payment_gateway_account": [ + { + "is_default": 1, + "payment_account": "_Test Bank - _TC", + "currency": "INR", + "company": "_Test Company", + }, + { + "payment_account": "_Test Bank USD - _TC", + "currency": "USD", + "company": "_Test Company", + }, + ], }, { - "doctype": "Payment Gateway Account", - "payment_gateway": "_Test Gateway Other", - "payment_account": "_Test Bank USD - _TC", - "payment_channel": "Other", - "currency": "USD", - "company": "_Test Company", + "doctype": "Payment Gateway", + "gateway_name": "_Test Gateway Other", + "payment_gateway_account": [ + { + "is_default": 1, + "payment_account": "_Test Bank USD - _TC", + "payment_channel": "Other", + "currency": "USD", + "company": "_Test Company", + } + ], }, { - "doctype": "Payment Gateway Account", - "payment_gateway": "_Test Gateway Phone", - "payment_account": "_Test Bank USD - _TC", - "payment_channel": "Phone", - "currency": "USD", - "company": "_Test Company", + "doctype": "Payment Gateway", + "gateway_name": "_Test Gateway Phone", + "payment_gateway_account": [ + { + "is_default": 1, + "payment_account": "_Test Bank USD - _TC", + "payment_channel": "Phone", + "currency": "USD", + "company": "_Test Company", + } + ], }, ] @@ -65,21 +71,9 @@ class TestPaymentRequest(ERPNextTestSuite): def setUp(self): for payment_gateway in payment_gateways: - if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"): + if not frappe.db.exists("Payment Gateway", payment_gateway["gateway_name"]): frappe.get_doc(payment_gateway).insert(ignore_permissions=True) - for method in payment_method: - if not frappe.db.get_value( - "Payment Gateway Account", - { - "payment_gateway": method["payment_gateway"], - "currency": method["currency"], - "company": method["company"], - }, - "name", - ): - frappe.get_doc(method).insert(ignore_permissions=True) - send_email = patch( "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email", return_value=None, @@ -99,6 +93,31 @@ def setUp(self): self._get_payment_gateway_controller = _get_payment_gateway_controller.start() self.addCleanup(_get_payment_gateway_controller.stop) + def test_validate_payment_flows(self): + bank = frappe.new_doc("Bank") + bank.bank_name = "_Test Bank" + bank.insert() + + bank_account = frappe.new_doc("Bank Account") + bank_account.bank = bank.name + bank_account.account_name = "_Test Bank Account" + bank_account.insert() + + so = make_sales_order(do_not_save=True) + so.save() + pr = make_payment_request( + dt="Sales Order", + dn=so.name, + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", + return_doc=1, + ) + + pr.bank_account = bank_account.name + self.assertRaisesRegex( + frappe.ValidationError, "Select either Bank Account or Payment Gateway.", pr.save + ) + def test_payment_request_linkings(self): so_inr = make_sales_order(currency="INR", do_not_save=True) so_inr.disable_rounded_total = 1 @@ -108,7 +127,8 @@ def test_payment_request_linkings(self): dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", ) self.assertEqual(pr.reference_doctype, "Sales Order") @@ -117,12 +137,18 @@ def test_payment_request_linkings(self): conversion_rate = get_exchange_rate("USD", "INR") - si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) + si_usd = create_sales_invoice( + customer="_Test Customer 1", + currency="USD", + conversion_rate=conversion_rate, + debit_to="_Test Receivable USD - _TC", + ) pr = make_payment_request( dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", ) self.assertEqual(pr.reference_doctype, "Sales Invoice") @@ -135,7 +161,8 @@ def test_payment_channels(self): pr = make_payment_request( dt="Sales Order", dn=so.name, - payment_gateway_account="_Test Gateway Other - USD - _TC", + payment_gateway="_Test Gateway Other", + payment_account="_Test Bank USD - _TC", submit_doc=True, return_doc=True, ) @@ -150,7 +177,8 @@ def test_payment_channels(self): pr = make_payment_request( dt="Sales Order", dn=so.name, - payment_gateway_account="_Test Gateway - USD - _TC", # email channel + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", submit_doc=False, return_doc=True, ) @@ -168,7 +196,8 @@ def test_payment_channels(self): pr = make_payment_request( dt="Sales Order", dn=so.name, - payment_gateway_account="_Test Gateway Phone - USD - _TC", + payment_gateway="_Test Gateway Phone", + payment_account="_Test Bank USD - _TC", submit_doc=True, return_doc=True, ) @@ -185,7 +214,8 @@ def test_payment_channels(self): pr = make_payment_request( dt="Sales Order", dn=so.name, - payment_gateway_account="_Test Gateway - USD - _TC", # email channel + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", submit_doc=True, return_doc=True, ) @@ -206,7 +236,8 @@ def test_payment_channels(self): pr = make_payment_request( dt="Sales Order", dn=so.name, - payment_gateway_account="_Test Gateway - USD - _TC", # email channel + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", make_sales_invoice=True, mute_email=True, submit_doc=True, @@ -237,7 +268,8 @@ def test_payment_entry_against_purchase_invoice(self): party="_Test Supplier USD", recipient_id="user@example.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", submit_doc=1, return_doc=1, ) @@ -262,7 +294,8 @@ def test_multiple_payment_entry_against_purchase_invoice(self): dn=purchase_invoice.name, recipient_id="user@example.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", return_doc=1, ) @@ -281,7 +314,8 @@ def test_multiple_payment_entry_against_purchase_invoice(self): dn=purchase_invoice.name, recipient_id="user@example.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", return_doc=1, ) @@ -305,7 +339,8 @@ def test_payment_entry(self): dn=so_inr.name, recipient_id="saurabh@erpnext.com", mute_email=1, - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", submit_doc=1, return_doc=1, ) @@ -327,7 +362,8 @@ def test_payment_entry(self): dn=si_usd.name, recipient_id="saurabh@erpnext.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", submit_doc=1, return_doc=1, ) @@ -371,7 +407,8 @@ def test_status(self): dn=si_usd.name, recipient_id="saurabh@erpnext.com", mute_email=1, - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", submit_doc=1, return_doc=1, ) @@ -983,15 +1020,8 @@ class TestPaymentRequestV2Gateway(ERPNextTestSuite): def setUp(self): """Set up payment gateway fixtures for flow tests.""" for payment_gateway in payment_gateways: - if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"): + if not frappe.db.exists("Payment Gateway", payment_gateway["gateway_name"]): frappe.get_doc(payment_gateway).insert(ignore_permissions=True) - for method in payment_method: - if not frappe.db.get_value( - "Payment Gateway Account", - {"payment_gateway": method["payment_gateway"], "currency": method["currency"]}, - "name", - ): - frappe.get_doc(method).insert(ignore_permissions=True) def _mock_payments_modules(self, is_v2_gateway_return_value): """Helper to mock both payments and payments.utils modules. @@ -1237,7 +1267,8 @@ def test_v2_gateway_uses_process_v2_gateway(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=True, submit_doc=True, return_doc=True, @@ -1276,7 +1307,8 @@ def test_v1_gateway_uses_legacy_flow(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=True, submit_doc=True, return_doc=True, @@ -1311,8 +1343,8 @@ def test_process_v2_gateway_handles_initiate_failure(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", - mute_email=True, + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", submit_doc=False, return_doc=True, ) @@ -1345,7 +1377,8 @@ def test_process_v2_gateway_handles_none_psl(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=True, submit_doc=False, return_doc=True, @@ -1376,7 +1409,8 @@ def test_process_v2_gateway_sets_payment_session_log(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=True, submit_doc=False, return_doc=True, @@ -1559,7 +1593,8 @@ def test_v2_gateway_sends_email_when_not_muted(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=False, # Email should be sent submit_doc=True, return_doc=True, @@ -1589,7 +1624,8 @@ def test_v1_phone_payment_skips_email(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=False, submit_doc=False, return_doc=True, @@ -1624,7 +1660,6 @@ def test_no_payment_gateway_skips_payment_processing(self): ) # Clear the payment gateway pr.payment_gateway = None - pr.payment_gateway_account = None pr.submit() # Neither v1 nor v2 flow should be called @@ -1677,7 +1712,8 @@ def test_flags_mute_email_suppresses_communication(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=False, # Field says don't mute submit_doc=False, return_doc=True, @@ -1861,7 +1897,8 @@ def test_get_tx_data_multi_currency(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - USD - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank USD - _TC", mute_email=True, submit_doc=False, return_doc=True, @@ -1924,7 +1961,8 @@ def test_process_v2_gateway_sets_payment_url(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=True, submit_doc=False, return_doc=True, @@ -1956,7 +1994,8 @@ def test_process_v2_gateway_logs_error_on_failure(self): dt="Sales Order", dn=so.name, recipient_id="test@example.com", - payment_gateway_account="_Test Gateway - INR - _TC", + payment_gateway="_Test Gateway", + payment_account="_Test Bank - _TC", mute_email=True, submit_doc=False, return_doc=True, diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 509120acce38..87d812be7a1a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -3165,6 +3165,7 @@ def make_purchase_invoice(**args): "use_serial_batch_fields": args.get("use_serial_batch_fields") or 0, "batch_no": args.get("batch_no") if args.get("use_serial_batch_fields") else "", "serial_no": args.get("serial_no") if args.get("use_serial_batch_fields") else "", + "delivered_by_supplier": 0, }, ) From 26a07e1ae7c1989154a87804fe1d9bccee3ee977 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Mon, 11 May 2026 01:04:55 +0530 Subject: [PATCH 09/10] ci: fork branch checkout --- .github/helper/install.sh | 2 +- erpnext/patches.txt | 1 + .../migrate_payment_gateway_related_data.py | 118 ++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v16_0/migrate_payment_gateway_related_data.py diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 95e95b071534..86104ec09ebf 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -66,7 +66,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile -bench get-app payments --branch develop +bench get-app "https://github.com/Shllokkk/payments" --branch "payment-gateway-fix" bench get-app erpnext "${GITHUB_WORKSPACE}" if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b17841bade57..2a7efce839d4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -483,3 +483,4 @@ erpnext.patches.v16_0.packed_item_inv_dimen erpnext.patches.v16_0.set_not_applicable_on_german_item_tax_templates erpnext.patches.v16_0.clear_procedures_from_receivable_report erpnext.patches.v16_0.migrate_address_contact_custom_fields +erpnext.patches.v16_0.migrate_payment_gateway_related_data diff --git a/erpnext/patches/v16_0/migrate_payment_gateway_related_data.py b/erpnext/patches/v16_0/migrate_payment_gateway_related_data.py new file mode 100644 index 000000000000..d1df3e6a2f4e --- /dev/null +++ b/erpnext/patches/v16_0/migrate_payment_gateway_related_data.py @@ -0,0 +1,118 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + apps = frappe.get_installed_apps() + + if "erpnext" not in apps or "payments" not in apps: + return + + doc = frappe.get_doc("DocType", "Payment Gateway Account") + doc.save() + + from payments.utils import make_payments_erpnext_custom_fields + + make_payments_erpnext_custom_fields() + + PG = DocType("Payment Gateway") + + update_pg_query = frappe.qb.update(PG).set(PG.gateway_name, PG.gateway) + update_pg_query.run() + + PGA = DocType("Payment Gateway Account") + + update_pga_query = ( + frappe.qb.update(PGA) + .set(PGA.parent, PGA.payment_gateway) + .set(PGA.parenttype, "Payment Gateway") + .set(PGA.parentfield, "payment_gateway_account") + ) + update_pga_query.run() + + pg_list = frappe.get_all("Payment Gateway", pluck="name") + + for pg in pg_list: + pga_list = frappe.get_all( + "Payment Gateway Account", + filters={"parent": pg}, + fields=["name", "company", "is_default"], + order_by="creation desc", + ) + + company_map = {} + + for pga in pga_list: + if not pga.company: + continue + + company_map.setdefault(pga.company, []).append(pga) + + for company_pgas in company_map.values(): + if any(pga.is_default for pga in company_pgas): + continue + + frappe.db.set_value( + "Payment Gateway Account", + company_pgas[0].name, + "is_default", + 1, + update_modified=False, + ) + + # save each parent so idx gets reassigned correctly + for pg in pg_list: + frappe.get_doc("Payment Gateway", pg).save(ignore_permissions=True) + + sp_list = frappe.get_all( + "Subscription Plan", + fields=["name", "payment_gateway"], + ) + + for sp in sp_list: + if sp.payment_gateway: + doc_dict = frappe.db.get_value( + "Payment Gateway Account", + sp.payment_gateway, + ["parent", "payment_account"], + as_dict=True, + ) + + if not doc_dict: + continue + + frappe.db.set_value( + "Subscription Plan", + sp.name, + { + "payment_gateway": doc_dict.parent, + "payment_account": doc_dict.payment_account, + }, + ) + + pr_list = frappe.get_all( + "Payment Request", + fields=["name", "payment_gateway_account"], + ) + + for pr in pr_list: + if pr.payment_gateway_account: + doc_dict = frappe.db.get_value( + "Payment Gateway Account", + pr.payment_gateway_account, + ["parent", "payment_account", "payment_channel"], + as_dict=True, + ) + + if not doc_dict: + continue + + frappe.db.set_value( + "Payment Request", + pr.name, + { + "payment_gateway": doc_dict.parent, + "payment_account": doc_dict.payment_account, + "payment_channel": doc_dict.payment_channel, + }, + ) From f09c5998a805544879abcfe0ad9adc7580ab25ab Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Wed, 20 May 2026 15:32:11 +0530 Subject: [PATCH 10/10] feat: add cross-app compatibility checks for migration --- erpnext/hooks.py | 2 ++ erpnext/setup/utils.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 56d9bf300dd8..820d94deae1c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -66,6 +66,8 @@ after_install = "erpnext.setup.install.after_install" before_uninstall = "erpnext.setup.install.before_uninstall" +before_migrate = "erpnext.setup.utils.validate_payments_compatibility" + boot_session = "erpnext.startup.boot.boot_session" notification_config = "erpnext.startup.notifications.get_notification_config" get_help_messages = "erpnext.utilities.activation.get_help_messages" diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index b6ea4781372b..b025fd2dbf4b 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from importlib import import_module + import frappe from frappe import _ from frappe.utils import add_days, flt, get_datetime_str, nowdate @@ -218,3 +220,20 @@ def identity(x, *args, **kwargs): Use like this: `from erpnext.setup.utils import identity as _` """ return x + + +def validate_payments_compatibility(): + """Ensure Payments app is compatible before site migration.""" + + if "payments" not in frappe.get_installed_apps(): + return + + try: + payments_utils = import_module("payments.utils.utils") + except Exception: + frappe.throw(_("Unable to load Payments utilities.\n\n") + frappe.get_traceback()) + + if not hasattr(payments_utils, "make_payments_erpnext_custom_fields"): + frappe.throw( + _("Incompatible Payments app version detected. Please update the Payments app before migration.") + )