mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-09 13:26:39 +00:00
feat(payment_request): add option to calculate request amount using payment schedule
(cherry picked from commit 60108590b0)
This commit is contained in:
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user