From 66fab12b901ac725a581bace215d5570121078f7 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 25 Mar 2026 09:59:42 +0200 Subject: [PATCH] Add 'Pay Immediately' option for New Loan adjustments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When creating a New Loan, a "Pay Immediately" checkbox (checked by default) processes the loan right away — creates PayrollRecord, sends payslip to Spark, and records the loan as paid. Unchecking it keeps the old behavior where the loan sits in Pending Payments. Also adds loan-only payslip detection (like advance-only) across all payslip views: email template, PDF template, and browser detail page show a clean "Loan Payslip" layout instead of "0 days worked". Co-Authored-By: Claude Opus 4.6 --- core/templates/core/email/payslip_email.html | 7 +- core/templates/core/payroll_dashboard.html | 23 ++++++- core/templates/core/payslip.html | 40 +++++++++++- core/templates/core/pdf/payslip_pdf.html | 8 ++- core/views.py | 69 ++++++++++++++++---- 5 files changed, 130 insertions(+), 17 deletions(-) diff --git a/core/templates/core/email/payslip_email.html b/core/templates/core/email/payslip_email.html index ad7a262..79e3856 100644 --- a/core/templates/core/email/payslip_email.html +++ b/core/templates/core/email/payslip_email.html @@ -29,7 +29,7 @@
PAYMENT TO BENEFICIARY
{{ record.worker.name }}
-
{% if is_advance %}Advance Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}
+
{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}
@@ -53,6 +53,11 @@ Advance Payment: {{ advance_description }} R {{ advance_amount|floatformat:2 }} + {% elif is_loan %} + + Loan Payment: {{ loan_description }} + R {{ loan_amount|floatformat:2 }} + {% else %} diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 2994b8a..0c3b74c 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -545,6 +545,22 @@ + + {# Pay Immediately — only shown for New Loan type #} + {# When checked, the loan is paid to the worker right away and #} + {# a payslip is emailed to Spark. When unchecked, the loan sits #} + {# in pending payments and gets included in the next pay cycle. #} + + {% elif is_loan %} + +
Loan Details
+
+ + + + + + + + + + + + + + + + + +
DateTypeDescriptionAmount
{{ loan_adj.date|date:"M d, Y" }}Loan Payment{{ loan_adj.description|default:"Worker loan" }}R {{ loan_adj.amount|floatformat:2 }}
+
+ + +
+
+ + + + + +
Loan Amount:R {{ loan_adj.amount|floatformat:2 }}
+
+
+ {% else %}
Work Log Details (Attendance)
diff --git a/core/templates/core/pdf/payslip_pdf.html b/core/templates/core/pdf/payslip_pdf.html index 6d2a5b0..717a32c 100644 --- a/core/templates/core/pdf/payslip_pdf.html +++ b/core/templates/core/pdf/payslip_pdf.html @@ -107,7 +107,7 @@
PAYMENT TO BENEFICIARY
{{ record.worker.name }}
-
{% if is_advance %}Advance Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}
+
{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}
@@ -132,6 +132,12 @@ Advance Payment: {{ advance_description }} R {{ advance_amount|floatformat:2 }} + {% elif is_loan %} + + + Loan Payment: {{ loan_description }} + R {{ loan_amount|floatformat:2 }} + {% else %} diff --git a/core/views.py b/core/views.py index c85ce56..f6a6848 100644 --- a/core/views.py +++ b/core/views.py @@ -1338,18 +1338,27 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount, total_amount = payroll_record.amount_paid - # === DETECT ADVANCE-ONLY PAYMENT === - # If the payment has 0 work logs and consists of only an Advance Payment - # adjustment, use the special advance payslip layout (shows the advance - # as a positive amount instead of the confusing "0 days + deduction" format). + # === DETECT STANDALONE PAYMENT (no work logs, single adjustment) === + # Advance-only or Loan-only payments use a cleaner payslip layout + # showing just the amount instead of "0 days worked + adjustment". advance_adj = None + loan_adj = None if log_count == 0: adjs_list = list(payroll_record.adjustments.all()) - if len(adjs_list) == 1 and adjs_list[0].type == 'Advance Payment': - advance_adj = adjs_list[0] + if len(adjs_list) == 1: + if adjs_list[0].type == 'Advance Payment': + advance_adj = adjs_list[0] + elif adjs_list[0].type == 'New Loan': + loan_adj = adjs_list[0] is_advance = advance_adj is not None - subject = f"{'Advance ' if is_advance else ''}Payslip for {worker.name} - {payroll_record.date}" + is_loan = loan_adj is not None + if is_advance: + subject = f"Advance Payslip for {worker.name} - {payroll_record.date}" + elif is_loan: + subject = f"Loan Payslip for {worker.name} - {payroll_record.date}" + else: + subject = f"Payslip for {worker.name} - {payroll_record.date}" # Context for both the HTML email body and the PDF attachment email_context = { @@ -1361,6 +1370,9 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount, 'is_advance': is_advance, 'advance_amount': advance_adj.amount if advance_adj else None, 'advance_description': advance_adj.description if advance_adj else '', + 'is_loan': is_loan, + 'loan_amount': loan_adj.amount if loan_adj else None, + 'loan_description': loan_adj.description if loan_adj else '', } # 1. Render HTML email body @@ -1760,6 +1772,10 @@ def add_adjustment(request): continue # === NEW LOAN — create a Loan record (loan_type='loan') === + # If "Pay Immediately" is checked (default), the loan is processed + # right away — PayrollRecord is created, payslip emailed to Spark, + # and the adjustment is marked as paid. If unchecked, the loan sits + # in Pending Payments and is included in the next pay cycle. if adj_type == 'New Loan': loan = Loan.objects.create( worker=worker, @@ -1770,6 +1786,30 @@ def add_adjustment(request): reason=description, ) + pay_immediately = request.POST.get('pay_immediately') == '1' + if pay_immediately: + # Create the adjustment and immediately mark it as paid + loan_adj = PayrollAdjustment.objects.create( + worker=worker, + type='New Loan', + amount=amount, + date=adj_date, + description=description, + loan=loan, + ) + payroll_record = PayrollRecord.objects.create( + worker=worker, + amount_paid=amount, + date=adj_date, + ) + loan_adj.payroll_record = payroll_record + loan_adj.save() + + # Send payslip email to Spark + _send_payslip_email(request, worker, payroll_record, 0, Decimal('0.00')) + created_count += 1 + continue # Skip the generic PayrollAdjustment creation below + # === ADVANCE PAYMENT — immediate payment + auto-repayment === # An advance is a salary prepayment — worker gets money now, and # the full amount is automatically deducted from their next salary. @@ -2196,13 +2236,16 @@ def payslip_detail(request, pk): # Calculate net adjustment amount (additive minus deductive) adjustments_net = record.amount_paid - base_pay - # === DETECT ADVANCE-ONLY PAYMENT === - # If payment has 0 work logs and a single Advance Payment adjustment, - # show a cleaner "advance payslip" layout instead of "0 days worked". + # === DETECT STANDALONE PAYMENT (no work logs, single adjustment) === + # Advance-only or Loan-only payments use a cleaner layout. adjs_list = list(adjustments) advance_adj = None - if logs.count() == 0 and len(adjs_list) == 1 and adjs_list[0].type == 'Advance Payment': - advance_adj = adjs_list[0] + loan_adj = None + if logs.count() == 0 and len(adjs_list) == 1: + if adjs_list[0].type == 'Advance Payment': + advance_adj = adjs_list[0] + elif adjs_list[0].type == 'New Loan': + loan_adj = adjs_list[0] context = { 'record': record, @@ -2214,6 +2257,8 @@ def payslip_detail(request, pk): 'deductive_types': DEDUCTIVE_TYPES, 'is_advance': advance_adj is not None, 'advance_adj': advance_adj, + 'is_loan': loan_adj is not None, + 'loan_adj': loan_adj, } return render(request, 'core/payslip.html', context)