Skip to content

Commit d1fec49

Browse files
authored
Merge pull request #373 from agritheory/368-invoices-with-valid-payment-term-discounts-are-not-applied-in-check-run
368 invoices with valid payment term discounts are not applied in check run
2 parents 06f26ab + c4cd05c commit d1fec49

File tree

8 files changed

+584
-15
lines changed

8 files changed

+584
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ Co-authored-by: agritheory <agritheory@users.noreply.github.com>
302302
([#142](https://github.com/agritheory/check_run/pull/142),
303303
[`f07ad5a`](https://github.com/agritheory/check_run/commit/f07ad5a1c4a9b7e424ad534ec40731f26f6feb0c))
304304

305-
- Only increment if check numer is numeric
305+
- Only increment if check number is numeric
306306
([#139](https://github.com/agritheory/check_run/pull/139),
307307
[`079aa52`](https://github.com/agritheory/check_run/commit/079aa52bf013e13d3350848ef011b74b99b64bf1))
308308

check_run/check_run/doctype/check_run/check_run.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from frappe.permissions import has_permission
1919
from frappe.query_builder.custom import ConstantColumn
2020
from 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
2222
from frappe.utils.file_manager import remove_all, save_file
2323
from frappe.utils.password import get_decrypted_password
2424
from 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()
556614
def 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:

check_run/check_run/doctype/check_run_settings/check_run_settings.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"approver_role",
1212
"column_break_3",
1313
"pay_to_account",
14+
"payment_discount_account",
1415
"print_format",
1516
"section_break_4",
1617
"include_purchase_invoices",
@@ -26,7 +27,6 @@
2627
"set_payment_entry_posting_date",
2728
"show_due_date",
2829
"number_of_invoices_per_voucher",
29-
"secondary_print_format",
3030
"file_preview_threshold",
3131
"allow_stand_alone_debit_notes",
3232
"default_modes_of_payment_section_section",
@@ -316,10 +316,16 @@
316316
"fieldtype": "Select",
317317
"label": "Show Due Date",
318318
"options": "Show Due Date\nShow Days Past Due"
319+
},
320+
{
321+
"fieldname": "payment_discount_account",
322+
"fieldtype": "Link",
323+
"label": "Payment Discount Account",
324+
"options": "Account"
319325
}
320326
],
321327
"links": [],
322-
"modified": "2025-06-22 14:56:43.637709",
328+
"modified": "2025-07-29 13:59:10.135620",
323329
"modified_by": "Administrator",
324330
"module": "Check Run",
325331
"name": "Check Run Settings",
@@ -352,6 +358,7 @@
352358
}
353359
],
354360
"quick_entry": 1,
361+
"row_format": "Dynamic",
355362
"sort_field": "modified",
356363
"sort_order": "DESC",
357364
"states": [],

check_run/public/js/check_run/CheckRun.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,11 @@
154154
</select>
155155
<span v-else>{{ transactions[item.name].mode_of_payment }}</span>
156156
</td>
157-
<td>{{ format_currency(item.amount, frm.pay_to_account_currency, 2) }}</td>
158-
<td>{{ item.due_date }}</td>
157+
<td v-if="item.has_discount">
158+
{{ format_currency(item.discount_amount, frm.pay_to_account_currency, 2) }}
159+
</td>
160+
<td v-else>{{ format_currency(item.amount, frm.pay_to_account_currency, 2) }}</td>
161+
<td>{{ datetime.str_to_user(item.due_date) }}</td>
159162
<td v-if="item.on_hold && frm.settings.automatically_release_on_hold_invoices == 0">
160163
<span style="font-weight: bold">On Hold</span>
161164
</td>

0 commit comments

Comments
 (0)