@@ -111,15 +111,36 @@ def validate(self):
111111 if self .get ("__islocal" ):
112112 self .status = "Draft"
113113 self .validate_reference_document ()
114+ self .validate_against_payment_reference ()
114115 self .validate_payment_request_amount ()
115116 # self.validate_currency()
116117 self .validate_subscription_details ()
117118
119+ def validate_against_payment_reference (self ):
120+ if not self .payment_reference :
121+ return
122+
123+ expected = sum (flt (r .amount ) for r in self .payment_reference )
124+ if flt (expected , self .precision ("grand_total" )) != flt (self .grand_total ):
125+ frappe .throw (_ ("Grand Total must match sum of Payment References" ))
126+
127+ seen = set ()
128+ for r in self .payment_reference :
129+ if not r .payment_schedule :
130+ continue # legacy mode → skip
131+
132+ if r .payment_schedule in seen :
133+ frappe .throw (_ ("Duplicate Payment Schedule selected" ))
134+
135+ seen .add (r .payment_schedule )
136+
118137 def validate_reference_document (self ):
119138 if not self .reference_doctype or not self .reference_name :
120139 frappe .throw (_ ("To create a Payment Request reference document is required" ))
121140
122141 def validate_payment_request_amount (self ):
142+ if self .payment_reference :
143+ return
123144 if self .grand_total == 0 :
124145 frappe .throw (
125146 _ ("{0} cannot be zero" ).format (self .get_label_from_fieldname ("grand_total" )),
@@ -554,9 +575,63 @@ def make_payment_request(**args):
554575 ref_doc = args .ref_doc or frappe .get_doc (args .dt , args .dn )
555576 if not args .get ("company" ):
556577 args .company = ref_doc .company
578+
557579 gateway_account = get_gateway_details (args ) or frappe ._dict ()
558580
559- grand_total = get_amount (ref_doc , gateway_account .get ("payment_account" ))
581+ # Schedule-based PRs are allowed only if no Payment Entry exists for this document.
582+ # Any existing Payment Entry forces legacy (amount-based) flow.
583+ selected_payment_schedules = json .loads (args .get ("schedules" )) if args .get ("schedules" ) else []
584+
585+ # Backend guard:
586+ # If any Payment Entry exists, schedule-based PRs are not allowed.
587+ if selected_payment_schedules and get_existing_payment_entry (ref_doc .name ):
588+ frappe .throw (
589+ _ (
590+ "Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document."
591+ )
592+ )
593+
594+ has_payment_entry = bool (get_existing_payment_entry (ref_doc .name ))
595+
596+ payment_reference = []
597+
598+ if selected_payment_schedules :
599+ existing_payment_references = get_existing_payment_references (ref_doc .name )
600+
601+ if existing_payment_references :
602+ existing_ids = {r ["payment_schedule" ] for r in existing_payment_references }
603+ selected_ids = {r ["name" ] for r in selected_payment_schedules }
604+ duplicate_ids = existing_ids & selected_ids
605+
606+ if duplicate_ids :
607+ duplicate_schedules = []
608+ for row in selected_payment_schedules :
609+ if row ["name" ] in duplicate_ids :
610+ existing_ref = next (
611+ (r for r in existing_payment_references if r ["payment_schedule" ] == row ["name" ]),
612+ {},
613+ )
614+ existing_pr = existing_ref .get ("parent" )
615+ duplicate_schedules .append (
616+ f"Payment Term: { row .get ('payment_term' )} , "
617+ f"Due Date: { row .get ('due_date' )} , "
618+ f"Amount: { row .get ('payment_amount' )} "
619+ f"(already requested in PR { existing_pr } )"
620+ )
621+ frappe .throw (
622+ _ ("The following payment schedule(s) already exist:\n {0}" ).format (
623+ "\n " .join (duplicate_schedules )
624+ )
625+ )
626+
627+ payment_reference = set_payment_references (args .get ("schedules" ))
628+
629+ # Determine grand_total
630+ if selected_payment_schedules and not has_payment_entry :
631+ grand_total = sum (row .get ("payment_amount" ) for row in selected_payment_schedules )
632+ else :
633+ grand_total = get_amount (ref_doc , gateway_account .get ("payment_account" ))
634+
560635 if not grand_total :
561636 frappe .throw (_ ("Payment Entry is already created" ))
562637
@@ -566,7 +641,6 @@ def make_payment_request(**args):
566641 loyalty_amount = validate_loyalty_points (ref_doc , int (args .loyalty_points )) # sets fields on ref_doc
567642 ref_doc .db_update ()
568643 grand_total = grand_total - loyalty_amount
569-
570644 # fetches existing payment request `grand_total` amount
571645 existing_payment_request_amount = get_existing_payment_request_amount (ref_doc )
572646
@@ -586,21 +660,20 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo
586660 else :
587661 # If PR's are processed, cancel all of them.
588662 cancel_old_payment_requests (ref_doc .doctype , ref_doc .name )
589- else :
663+ elif not selected_payment_schedules :
590664 grand_total = validate_and_calculate_grand_total (grand_total , existing_payment_request_amount )
591-
592665 draft_payment_request = frappe .db .get_value (
593666 "Payment Request" ,
594667 {"reference_doctype" : ref_doc .doctype , "reference_name" : ref_doc .name , "docstatus" : 0 },
595668 )
596669
597670 if draft_payment_request :
598- frappe .db .set_value (
599- "Payment Request" , draft_payment_request , "grand_total" , grand_total , update_modified = False
600- )
601671 pr = frappe .get_doc ("Payment Request" , draft_payment_request )
602672
603- set_payment_references (pr , ref_doc )
673+ if selected_payment_schedules :
674+ apply_payment_references (pr , payment_reference )
675+ pr .save ()
676+
604677 else :
605678 bank_account = (
606679 get_party_bank_account (args .get ("party_type" ), args .get ("party" ))
@@ -621,8 +694,6 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo
621694 party_account = get_party_account (party_type , ref_doc .get (party_type .lower ()), ref_doc .company )
622695 party_account_currency = get_account_currency (party_account )
623696
624- set_payment_references (pr , ref_doc )
625-
626697 pr .update (
627698 {
628699 "payment_gateway_account" : gateway_account .get ("name" ),
@@ -657,7 +728,10 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo
657728 }
658729 )
659730
660- # Update dimensions
731+ if selected_payment_schedules :
732+ apply_payment_references (pr , payment_reference )
733+
734+ # Dimensions
661735 pr .update (
662736 {
663737 "cost_center" : ref_doc .get ("cost_center" ),
@@ -686,6 +760,51 @@ def validate_and_calculate_grand_total(grand_total, existing_payment_request_amo
686760 return pr .as_dict ()
687761
688762
763+ def apply_payment_references (pr , payment_reference ):
764+ existing_refs = pr .get ("payment_reference" ) or []
765+
766+ existing_ids = {r .get ("payment_schedule" ) for r in existing_refs if r .get ("payment_schedule" )}
767+ new_refs = [r for r in (payment_reference or []) if r .get ("payment_schedule" ) not in existing_ids ]
768+ pr .set ("payment_reference" , existing_refs + new_refs )
769+ pr .set ("grand_total" , sum (flt (r .get ("amount" )) for r in pr .get ("payment_reference" )))
770+
771+
772+ def set_payment_references (payment_schedules ):
773+ payment_schedules = json .loads (payment_schedules ) if payment_schedules else []
774+ payment_reference = []
775+
776+ for row in payment_schedules :
777+ payment_reference .append (
778+ {
779+ "payment_term" : row .get ("payment_term" ),
780+ "payment_schedule" : row .get ("name" ),
781+ "description" : row .get ("description" ),
782+ "due_date" : row .get ("due_date" ),
783+ "amount" : row .get ("payment_amount" ),
784+ }
785+ )
786+
787+ return payment_reference
788+
789+
790+ def get_existing_payment_entry (ref_docname ):
791+ pe = frappe .qb .DocType ("Payment Entry" )
792+ per = frappe .qb .DocType ("Payment Entry Reference" )
793+
794+ existing_pe = (
795+ frappe .qb .from_ (pe )
796+ .join (per )
797+ .on (per .parent == pe .name )
798+ .select (pe .name )
799+ .where (pe .docstatus < 2 )
800+ .where (per .reference_name == ref_docname )
801+ .limit (1 )
802+ .run ()
803+ )
804+
805+ return existing_pe
806+
807+
689808def get_amount (ref_doc , payment_account = None ):
690809 """get amount based on doctype"""
691810 grand_total = 0
@@ -1032,36 +1151,20 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
10321151 return res
10331152
10341153
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 )
1154+ @frappe .whitelist ()
1155+ def get_available_payment_schedules (reference_doctype , reference_name ):
1156+ ref_doc = frappe .get_doc (reference_doctype , reference_name )
10471157
1048- existing = existing_map .get (key )
1049- if existing and (existing .manually_selected or existing .auto_selected ):
1050- continue
1158+ if not hasattr (ref_doc , "payment_schedule" ) or not ref_doc .payment_schedule :
1159+ return []
10511160
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- )
1161+ if get_existing_payment_entry (reference_name ):
1162+ return []
10611163
1164+ existing_refs = get_existing_payment_references (reference_name )
1165+ existing_ids = {r ["payment_schedule" ] for r in existing_refs if r .get ("payment_schedule" )}
10621166
1063- def make_key (payment_term , due_date , amount ):
1064- return (payment_term , due_date , flt (amount ))
1167+ return [r for r in ref_doc .payment_schedule if r .name not in existing_ids ]
10651168
10661169
10671170def get_existing_payment_references (reference_name ):
@@ -1075,14 +1178,15 @@ def get_existing_payment_references(reference_name):
10751178 .select (
10761179 PRF .payment_term ,
10771180 PRF .due_date ,
1078- PRF .amount ,
1079- PRF .manually_selected ,
1080- PRF .auto_selected ,
1181+ PRF .amount .as_ ("payment_amount" ),
1182+ PRF .payment_schedule ,
10811183 PRF .parent ,
10821184 )
10831185 .where (PR .reference_name == reference_name )
1084- .where (PR .docstatus == 1 )
1085- .where (PR .status .isin (["Initiated" , "Partially Paid" , "Payment Ordered" , "Paid" ]))
1186+ .where (PR .docstatus < 2 )
1187+ .where (
1188+ PR .status .isin (["Draft" , "Requested" , "Initiated" , "Partially Paid" , "Payment Ordered" , "Paid" ])
1189+ )
10861190 ).run (as_dict = True )
10871191
10881192 return result
0 commit comments