Skip to content

Commit 298ea33

Browse files
Jatin3128mergify[bot]
authored andcommitted
feat(payment_request): add option to calculate request amount using payment schedule
(cherry picked from commit 6010859)
1 parent 807463e commit 298ea33

7 files changed

Lines changed: 293 additions & 1 deletion

File tree

erpnext/accounts/doctype/payment_reference/__init__.py

Whitespace-only changes.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"actions": [],
3+
"allow_rename": 1,
4+
"creation": "2025-12-02 17:50:08.648006",
5+
"doctype": "DocType",
6+
"editable_grid": 1,
7+
"engine": "InnoDB",
8+
"field_order": [
9+
"payment_term",
10+
"manually_selected",
11+
"auto_selected",
12+
"section_break_fjhh",
13+
"description",
14+
"section_break_mjlv",
15+
"due_date",
16+
"column_break_qghl",
17+
"amount"
18+
],
19+
"fields": [
20+
{
21+
"fieldname": "payment_term",
22+
"fieldtype": "Link",
23+
"in_list_view": 1,
24+
"label": "Payment Term",
25+
"options": "Payment Term"
26+
},
27+
{
28+
"collapsible": 1,
29+
"fieldname": "section_break_fjhh",
30+
"fieldtype": "Section Break",
31+
"label": "Description"
32+
},
33+
{
34+
"fieldname": "description",
35+
"fieldtype": "Small Text",
36+
"in_list_view": 1,
37+
"label": "Description"
38+
},
39+
{
40+
"fieldname": "section_break_mjlv",
41+
"fieldtype": "Section Break"
42+
},
43+
{
44+
"fieldname": "due_date",
45+
"fieldtype": "Date",
46+
"in_list_view": 1,
47+
"label": "Due Date"
48+
},
49+
{
50+
"fieldname": "column_break_qghl",
51+
"fieldtype": "Column Break"
52+
},
53+
{
54+
"fieldname": "amount",
55+
"fieldtype": "Currency",
56+
"in_list_view": 1,
57+
"label": "Amount",
58+
"precision": "2"
59+
},
60+
{
61+
"default": "0",
62+
"fieldname": "manually_selected",
63+
"fieldtype": "Check",
64+
"hidden": 1,
65+
"label": "Manually Selected"
66+
},
67+
{
68+
"default": "1",
69+
"fieldname": "auto_selected",
70+
"fieldtype": "Check",
71+
"hidden": 1,
72+
"label": "Auto Selected"
73+
}
74+
],
75+
"grid_page_length": 50,
76+
"index_web_pages_for_search": 1,
77+
"istable": 1,
78+
"links": [],
79+
"modified": "2025-12-05 11:26:29.877050",
80+
"modified_by": "Administrator",
81+
"module": "Accounts",
82+
"name": "Payment Reference",
83+
"owner": "Administrator",
84+
"permissions": [],
85+
"row_format": "Dynamic",
86+
"rows_threshold_for_grid_search": 20,
87+
"sort_field": "creation",
88+
"sort_order": "DESC",
89+
"states": []
90+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
2+
# For license information, please see license.txt
3+
4+
# import frappe
5+
from frappe.model.document import Document
6+
7+
8+
class PaymentReference(Document):
9+
# begin: auto-generated types
10+
# This code is auto-generated. Do not modify anything in this block.
11+
12+
from typing import TYPE_CHECKING
13+
14+
if TYPE_CHECKING:
15+
from frappe.types import DF
16+
17+
amount: DF.Currency
18+
auto_selected: DF.Check
19+
description: DF.SmallText | None
20+
due_date: DF.Date | None
21+
manually_selected: DF.Check
22+
parent: DF.Data
23+
parentfield: DF.Data
24+
parenttype: DF.Data
25+
payment_term: DF.Link | None
26+
# end: auto-generated types
27+
28+
pass

erpnext/accounts/doctype/payment_request/payment_request.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,29 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
105105
});
106106
}
107107
});
108+
109+
frappe.ui.form.on("Payment Request", "calculate_total_amount_by_selected_rows", function (frm) {
110+
if (frm.doc.docstatus !== 0) {
111+
frappe.msgprint(__("Cannot fetch selected rows for submitted Payment Request"));
112+
return;
113+
}
114+
const selected = frm.get_selected()?.payment_reference || [];
115+
if (!selected.length) {
116+
frappe.throw(__("No rows selected"));
117+
}
118+
let total = 0;
119+
selected.forEach((name) => {
120+
const row = frm.doc.payment_reference.find((d) => d.name === name);
121+
if (row) {
122+
row.manually_selected = 1;
123+
124+
total += row.amount;
125+
}
126+
});
127+
frm.doc.payment_reference.forEach((row) => {
128+
row.auto_selected = 0;
129+
});
130+
frm.set_value("grand_total", total);
131+
frm.refresh_field("grand_total");
132+
frm.save();
133+
});

erpnext/accounts/doctype/payment_request/payment_request.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"column_break_4",
2020
"reference_doctype",
2121
"reference_name",
22+
"payment_reference_section",
23+
"payment_reference",
24+
"calculate_total_amount_by_selected_rows",
2225
"transaction_details",
2326
"grand_total",
2427
"currency",
@@ -457,14 +460,29 @@
457460
"fieldname": "phone_number",
458461
"fieldtype": "Data",
459462
"label": "Phone Number"
463+
},
464+
{
465+
"fieldname": "payment_reference_section",
466+
"fieldtype": "Section Break"
467+
},
468+
{
469+
"fieldname": "calculate_total_amount_by_selected_rows",
470+
"fieldtype": "Button",
471+
"label": "Calculate Total Amount by Selected Rows"
472+
},
473+
{
474+
"fieldname": "payment_reference",
475+
"fieldtype": "Table",
476+
"label": "Payment Reference",
477+
"options": "Payment Reference"
460478
}
461479
],
462480
"grid_page_length": 50,
463481
"in_create": 1,
464482
"index_web_pages_for_search": 1,
465483
"is_submittable": 1,
466484
"links": [],
467-
"modified": "2025-08-29 11:52:48.555415",
485+
"modified": "2025-12-05 11:27:51.406257",
468486
"modified_by": "Administrator",
469487
"module": "Accounts",
470488
"name": "Payment Request",

erpnext/accounts/doctype/payment_request/payment_request.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class PaymentRequest(Document):
4545
if TYPE_CHECKING:
4646
from frappe.types import DF
4747

48+
from erpnext.accounts.doctype.payment_reference.payment_reference import PaymentReference
4849
from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
4950
SubscriptionPlanDetail,
5051
)
@@ -78,6 +79,7 @@ class PaymentRequest(Document):
7879
payment_gateway: DF.ReadOnly | None
7980
payment_gateway_account: DF.Link | None
8081
payment_order: DF.Link | None
82+
payment_reference: DF.Table[PaymentReference]
8183
payment_request_type: DF.Literal["Outward", "Inward"]
8284
payment_url: DF.Data | None
8385
phone_number: DF.Data | None
@@ -597,6 +599,8 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo
597599
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
598600
)
599601
pr = frappe.get_doc("Payment Request", draft_payment_request)
602+
603+
set_payment_references(pr, ref_doc)
600604
else:
601605
bank_account = (
602606
get_party_bank_account(args.get("party_type"), args.get("party"))
@@ -617,6 +621,8 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo
617621
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
618622
party_account_currency = get_account_currency(party_account)
619623

624+
set_payment_references(pr, ref_doc)
625+
620626
pr.update(
621627
{
622628
"payment_gateway_account": gateway_account.get("name"),
@@ -1024,3 +1030,59 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
10241030
},
10251031
)
10261032
return res
1033+
1034+
1035+
def set_payment_references(payment_request, ref_doc):
1036+
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
1037+
return
1038+
1039+
existing_refs = get_existing_payment_references(ref_doc.name)
1040+
1041+
existing_map = {make_key(r.payment_term, r.due_date, r.amount): r for r in existing_refs}
1042+
1043+
payment_request.reference = []
1044+
1045+
for row in ref_doc.payment_schedule:
1046+
key = make_key(row.payment_term, row.due_date, row.payment_amount)
1047+
1048+
existing = existing_map.get(key)
1049+
if existing and (existing.manually_selected or existing.auto_selected):
1050+
continue
1051+
1052+
payment_request.append(
1053+
"payment_reference",
1054+
{
1055+
"payment_term": row.payment_term,
1056+
"description": row.description,
1057+
"due_date": row.due_date,
1058+
"amount": row.payment_amount,
1059+
},
1060+
)
1061+
1062+
1063+
def make_key(payment_term, due_date, amount):
1064+
return (payment_term, due_date, flt(amount))
1065+
1066+
1067+
def get_existing_payment_references(reference_name):
1068+
PR = frappe.qb.DocType("Payment Request")
1069+
PRF = frappe.qb.DocType("Payment Reference")
1070+
1071+
result = (
1072+
frappe.qb.from_(PR)
1073+
.join(PRF)
1074+
.on(PR.name == PRF.parent)
1075+
.select(
1076+
PRF.payment_term,
1077+
PRF.due_date,
1078+
PRF.amount,
1079+
PRF.manually_selected,
1080+
PRF.auto_selected,
1081+
PRF.parent,
1082+
)
1083+
.where(PR.reference_name == reference_name)
1084+
.where(PR.docstatus == 1)
1085+
.where(PR.status.isin(["Initiated", "Partially Paid", "Payment Ordered", "Paid"]))
1086+
).run(as_dict=True)
1087+
1088+
return result

erpnext/accounts/doctype/payment_request/test_payment_request.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,3 +851,71 @@ def test_payment_request_on_unreconcile(self):
851851
pr.load_from_db()
852852

853853
self.assertEqual(pr.grand_total, pi.outstanding_amount)
854+
855+
def test_payment_schedule_row_selection(self):
856+
from frappe.utils import add_days, nowdate
857+
858+
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=86)
859+
860+
po.payment_schedule = []
861+
862+
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 33})
863+
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 33})
864+
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 20})
865+
866+
po.save()
867+
po.submit()
868+
869+
pr1 = make_payment_request(
870+
dt="Purchase Order",
871+
dn=po.name,
872+
mute_email=1,
873+
submit_doc=False,
874+
return_doc=True,
875+
)
876+
pr1.payment_reference[0].manually_selected = 1
877+
pr1.payment_reference[1].auto_selected = 0
878+
pr1.payment_reference[2].manually_selected = 1
879+
pr1.grand_total = 53
880+
pr1.submit()
881+
882+
pr2 = make_payment_request(
883+
dt="Purchase Order",
884+
dn=po.name,
885+
mute_email=1,
886+
submit_doc=False,
887+
return_doc=True,
888+
)
889+
890+
self.assertEqual(len(pr2.payment_reference), 1)
891+
self.assertEqual(pr2.payment_reference[0].amount, 33)
892+
893+
def test_auto_selected_rows_are_not_reused(self):
894+
from frappe.utils import add_days, nowdate
895+
896+
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=80)
897+
po.payment_schedule = []
898+
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 40})
899+
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 10})
900+
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 30})
901+
po.save()
902+
po.submit()
903+
904+
pr1 = make_payment_request(
905+
dt="Purchase Order",
906+
dn=po.name,
907+
mute_email=1,
908+
submit_doc=False,
909+
return_doc=True,
910+
)
911+
912+
pr1.submit()
913+
914+
with self.assertRaises(frappe.ValidationError):
915+
make_payment_request(
916+
dt="Purchase Order",
917+
dn=po.name,
918+
mute_email=1,
919+
submit_doc=False,
920+
return_doc=True,
921+
)

0 commit comments

Comments
 (0)