1818from frappe .permissions import has_permission
1919from frappe .query_builder .custom import ConstantColumn
2020from frappe .query_builder .functions import Coalesce , NullIf , Sum
21- from frappe .utils .data import flt , get_datetime , getdate , now , nowdate
21+ from frappe .utils .data import add_months , flt , get_datetime , get_last_day , getdate , now , nowdate
2222from frappe .utils .file_manager import remove_all , save_file
2323from frappe .utils .password import get_decrypted_password
2424from frappe .utils .print_format import read_multi_pdf
@@ -59,6 +59,7 @@ def onload(self) -> None:
5959 self .set_onload (
6060 "is_approver_user" , settings .approver_role in frappe .get_roles (frappe .session .user )
6161 )
62+ self .set_onload ("payment_discount_account" , getattr (settings , "payment_discount_account" , None ))
6263
6364 def validate (self ) -> None :
6465 gl_account = frappe .get_value ("Bank Account" , self .bank_account , "account" )
@@ -350,6 +351,7 @@ def create_payment_entries(self, transactions: list[frappe._dict]) -> list[frapp
350351 f"via { _group [0 ].mode_of_payment } { self .get_formatted ('posting_date' )} "
351352 )
352353
354+ total_discount_amount = 0.0
353355 for reference in group :
354356 if not reference :
355357 continue
@@ -360,6 +362,12 @@ def create_payment_entries(self, transactions: list[frappe._dict]) -> list[frapp
360362 ):
361363 if frappe .get_value (reference .doctype , reference .name , "on_hold" ):
362364 frappe .db .set_value (reference .doctype , reference .name , "on_hold" , 0 )
365+
366+ discount_amount = 0.0
367+ if reference .doctype == "Purchase Invoice" and reference .payment_term :
368+ discount_amount , has_discount = calculate_payment_term_discount (reference , self .posting_date )
369+ total_discount_amount += discount_amount
370+
363371 if reference .doctype == "Journal Entry" :
364372 reference_name = reference .ref_number
365373 else :
@@ -381,11 +389,22 @@ def create_payment_entries(self, transactions: list[frappe._dict]) -> list[frapp
381389 total_amount += reference .amount
382390 reference .check_number = pe .reference_no
383391 _references .append (reference )
384- pe .received_amount = total_amount
385- pe .base_received_amount = total_amount
386- pe .paid_amount = total_amount
387- pe .base_paid_amount = total_amount
388- pe .base_grand_total = total_amount
392+ pe .received_amount = total_amount - total_discount_amount
393+ pe .base_received_amount = total_amount - total_discount_amount
394+ pe .paid_amount = total_amount - total_discount_amount
395+ pe .base_paid_amount = total_amount - total_discount_amount
396+ pe .base_grand_total = total_amount - total_discount_amount
397+
398+ if total_discount_amount > 0 and settings .payment_discount_account :
399+ pe .append (
400+ "deductions" ,
401+ {
402+ "account" : settings .payment_discount_account ,
403+ "amount" : - total_discount_amount ,
404+ "cost_center" : frappe .get_value ("Company" , self .company , "cost_center" ),
405+ },
406+ )
407+
389408 if not pe .get ("references" ): # already paid or cancelled
390409 continue
391410 try :
@@ -552,6 +571,45 @@ def create_and_attach_positive_pay(self):
552571 )
553572
554573
574+ def calculate_payment_term_discount (transaction , payment_date ):
575+ if not transaction .payment_term :
576+ return 0.0 , False
577+
578+ payment_term_doc = frappe .get_doc ("Payment Term" , transaction .payment_term )
579+ if not payment_term_doc .discount or payment_term_doc .discount <= 0 :
580+ return 0.0 , False
581+
582+ posting_date = (
583+ getdate (transaction .posting_date )
584+ if isinstance (transaction .posting_date , str )
585+ else transaction .posting_date
586+ )
587+
588+ if payment_term_doc .discount_validity_based_on == "Day(s) after invoice date" :
589+ discount_end_date = posting_date + datetime .timedelta (days = payment_term_doc .discount_validity )
590+ elif payment_term_doc .discount_validity_based_on == "Day(s) after the end of the invoice month" :
591+ last_day_of_month = get_last_day (posting_date )
592+ discount_end_date = last_day_of_month + datetime .timedelta (
593+ days = payment_term_doc .discount_validity
594+ )
595+ elif payment_term_doc .discount_validity_based_on == "Month(s) after the end of the invoice month" :
596+ last_day_of_month = get_last_day (posting_date )
597+ discount_end_date = get_last_day (
598+ add_months (last_day_of_month , payment_term_doc .discount_validity )
599+ )
600+
601+ if payment_date > discount_end_date :
602+ return 0.0 , False
603+
604+ discount_amount = 0.0
605+ if payment_term_doc .discount_type == "Percentage" :
606+ discount_amount = transaction .amount * (payment_term_doc .discount / 100 )
607+ else :
608+ discount_amount = min (payment_term_doc .discount , transaction .amount )
609+
610+ return discount_amount , True
611+
612+
555613@frappe .whitelist ()
556614def check_for_draft_check_run (company : str , bank_account : str , payable_account : str ) -> str :
557615 existing = frappe .get_value (
@@ -766,7 +824,15 @@ def get_entries(doc: CheckRun | str) -> dict:
766824
767825 file_preview_allowed = False if len (transactions ) > settings .file_preview_threshold else True
768826
827+ posting_date = getattr (doc , "posting_date" , None )
769828 for transaction in transactions :
829+ if transaction .doctype == "Purchase Invoice" and transaction .payment_term and posting_date :
830+ discount_amount , has_discount = calculate_payment_term_discount (transaction , posting_date )
831+ transaction .discount_amount = transaction .amount - discount_amount if has_discount else 0.0
832+ transaction .has_discount = has_discount
833+ else :
834+ transaction .has_discount = False
835+
770836 if file_preview_allowed :
771837 doc_name = transaction .ref_number if transaction .ref_number else transaction .name
772838 transaction .attachments = [
@@ -796,6 +862,14 @@ def get_entries(doc: CheckRun | str) -> dict:
796862 if transaction .due_date and settings .show_due_date == "Show Days Past Due" :
797863 transaction .due_date = (getdate (nowdate ()) - transaction .due_date ).days
798864
865+ has_discounts = any (t .get ("has_discount" ) for t in transactions )
866+ if has_discounts and settings and not settings .payment_discount_account :
867+ frappe .throw (
868+ frappe ._ (
869+ "Payment Discount Account is required in Check Run Settings when processing invoices with payment term discounts. Please configure the Payment Discount Account in Check Run Settings."
870+ )
871+ )
872+
799873 outstanding_transaction = []
800874 if not isinstance (doc , CheckRun ):
801875 if db_doc :
0 commit comments