feat(payment_request): add option to calculate request amount using payment schedule

(cherry picked from commit 60108590b0)
This commit is contained in:
Jatin3128
2025-12-06 20:56:44 +05:30
committed by Mergify
parent 807463e90c
commit 298ea33922
7 changed files with 293 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-02 17:50:08.648006",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_term",
"manually_selected",
"auto_selected",
"section_break_fjhh",
"description",
"section_break_mjlv",
"due_date",
"column_break_qghl",
"amount"
],
"fields": [
{
"fieldname": "payment_term",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Term",
"options": "Payment Term"
},
{
"collapsible": 1,
"fieldname": "section_break_fjhh",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"fieldname": "section_break_mjlv",
"fieldtype": "Section Break"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date"
},
{
"fieldname": "column_break_qghl",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"precision": "2"
},
{
"default": "0",
"fieldname": "manually_selected",
"fieldtype": "Check",
"hidden": 1,
"label": "Manually Selected"
},
{
"default": "1",
"fieldname": "auto_selected",
"fieldtype": "Check",
"hidden": 1,
"label": "Auto Selected"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-12-05 11:26:29.877050",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,28 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PaymentReference(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
amount: DF.Currency
auto_selected: DF.Check
description: DF.SmallText | None
due_date: DF.Date | None
manually_selected: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_term: DF.Link | None
# end: auto-generated types
pass

View File

@@ -105,3 +105,29 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
});
}
});
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;
}
});
frm.doc.payment_reference.forEach((row) => {
row.auto_selected = 0;
});
frm.set_value("grand_total", total);
frm.refresh_field("grand_total");
frm.save();
});

View File

@@ -19,6 +19,9 @@
"column_break_4",
"reference_doctype",
"reference_name",
"payment_reference_section",
"payment_reference",
"calculate_total_amount_by_selected_rows",
"transaction_details",
"grand_total",
"currency",
@@ -457,6 +460,21 @@
"fieldname": "phone_number",
"fieldtype": "Data",
"label": "Phone Number"
},
{
"fieldname": "payment_reference_section",
"fieldtype": "Section Break"
},
{
"fieldname": "calculate_total_amount_by_selected_rows",
"fieldtype": "Button",
"label": "Calculate Total Amount by Selected Rows"
},
{
"fieldname": "payment_reference",
"fieldtype": "Table",
"label": "Payment Reference",
"options": "Payment Reference"
}
],
"grid_page_length": 50,
@@ -464,7 +482,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-08-29 11:52:48.555415",
"modified": "2025-12-05 11:27:51.406257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -45,6 +45,7 @@ class PaymentRequest(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.payment_reference.payment_reference import PaymentReference
from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
SubscriptionPlanDetail,
)
@@ -78,6 +79,7 @@ class PaymentRequest(Document):
payment_gateway: DF.ReadOnly | 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
@@ -597,6 +599,8 @@ def make_payment_request(**args):
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", draft_payment_request)
set_payment_references(pr, ref_doc)
else:
bank_account = (
get_party_bank_account(args.get("party_type"), args.get("party"))
@@ -617,6 +621,8 @@ def make_payment_request(**args):
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
party_account_currency = get_account_currency(party_account)
set_payment_references(pr, ref_doc)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -1024,3 +1030,59 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
},
)
return res
def set_payment_references(payment_request, ref_doc):
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return
existing_refs = get_existing_payment_references(ref_doc.name)
existing_map = {make_key(r.payment_term, r.due_date, r.amount): r for r in existing_refs}
payment_request.reference = []
for row in ref_doc.payment_schedule:
key = make_key(row.payment_term, row.due_date, row.payment_amount)
existing = existing_map.get(key)
if existing and (existing.manually_selected or existing.auto_selected):
continue
payment_request.append(
"payment_reference",
{
"payment_term": row.payment_term,
"description": row.description,
"due_date": row.due_date,
"amount": row.payment_amount,
},
)
def make_key(payment_term, due_date, amount):
return (payment_term, due_date, flt(amount))
def get_existing_payment_references(reference_name):
PR = frappe.qb.DocType("Payment Request")
PRF = frappe.qb.DocType("Payment Reference")
result = (
frappe.qb.from_(PR)
.join(PRF)
.on(PR.name == PRF.parent)
.select(
PRF.payment_term,
PRF.due_date,
PRF.amount,
PRF.manually_selected,
PRF.auto_selected,
PRF.parent,
)
.where(PR.reference_name == reference_name)
.where(PR.docstatus == 1)
.where(PR.status.isin(["Initiated", "Partially Paid", "Payment Ordered", "Paid"]))
).run(as_dict=True)
return result

View File

@@ -851,3 +851,71 @@ class TestPaymentRequest(IntegrationTestCase):
pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_schedule_row_selection(self):
from frappe.utils import add_days, nowdate
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=86)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 33})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 33})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 20})
po.save()
po.submit()
pr1 = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
)
pr1.payment_reference[0].manually_selected = 1
pr1.payment_reference[1].auto_selected = 0
pr1.payment_reference[2].manually_selected = 1
pr1.grand_total = 53
pr1.submit()
pr2 = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
)
self.assertEqual(len(pr2.payment_reference), 1)
self.assertEqual(pr2.payment_reference[0].amount, 33)
def test_auto_selected_rows_are_not_reused(self):
from frappe.utils import add_days, nowdate
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=80)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 40})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 10})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 30})
po.save()
po.submit()
pr1 = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
)
pr1.submit()
with self.assertRaises(frappe.ValidationError):
make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
)