Add 'Pay Immediately' option for New Loan adjustments
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 <noreply@anthropic.com>
This commit is contained in:
parent
72d40971f1
commit
66fab12b90
@ -29,7 +29,7 @@
|
||||
<div class="header">
|
||||
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
|
||||
<div class="beneficiary-name">{{ record.worker.name }}</div>
|
||||
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
|
||||
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<!-- Beneficiary details -->
|
||||
@ -53,6 +53,11 @@
|
||||
<td>Advance Payment: {{ advance_description }}</td>
|
||||
<td style="text-align: right;">R {{ advance_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% elif is_loan %}
|
||||
<tr>
|
||||
<td>Loan Payment: {{ loan_description }}</td>
|
||||
<td style="text-align: right;">R {{ loan_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Base pay line -->
|
||||
<tr>
|
||||
|
||||
@ -545,6 +545,22 @@
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-control" rows="2" placeholder="Reason for this adjustment..."></textarea>
|
||||
</div>
|
||||
|
||||
{# 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. #}
|
||||
<div class="col-12" id="addAdjPayImmediatelyGroup" style="display: none;">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" name="pay_immediately"
|
||||
id="addAdjPayImmediately" value="1" checked>
|
||||
<label class="form-check-label" for="addAdjPayImmediately">
|
||||
<i class="fas fa-bolt me-1 text-warning"></i>
|
||||
<strong>Pay Immediately</strong>
|
||||
<span class="text-muted small">— send payslip to Spark now and record as paid</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -1295,9 +1311,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const adjSelectedCount = document.getElementById('adjSelectedCount');
|
||||
const addAdjWorkerError = document.getElementById('addAdjWorkerError');
|
||||
|
||||
// Show/hide project field based on adjustment type.
|
||||
// Show/hide project field and "Pay Immediately" checkbox based on adjustment type.
|
||||
// Also toggles the HTML "required" attribute so browser validation
|
||||
// only enforces the project when the type actually needs one.
|
||||
var addAdjPayImmediatelyGroup = document.getElementById('addAdjPayImmediatelyGroup');
|
||||
function toggleProjectField() {
|
||||
if (!addAdjType || !addAdjProjectGroup) return;
|
||||
// Loan types and repayments don't need a project.
|
||||
@ -1313,6 +1330,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
addAdjProject.value = ''; // Clear selection when hidden
|
||||
}
|
||||
}
|
||||
// "Pay Immediately" checkbox — only shown for New Loan type
|
||||
if (addAdjPayImmediatelyGroup) {
|
||||
addAdjPayImmediatelyGroup.style.display = (addAdjType.value === 'New Loan') ? '' : 'none';
|
||||
}
|
||||
}
|
||||
if (addAdjType) {
|
||||
addAdjType.addEventListener('change', toggleProjectField);
|
||||
|
||||
@ -28,10 +28,10 @@
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-1">Payment To Beneficiary:</h6>
|
||||
<h2 class="fw-bold text-dark mb-0 text-uppercase">{{ record.worker.name }}</h2>
|
||||
<p class="text-muted small mb-0">{% if is_advance %}Advance{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}</p>
|
||||
<p class="text-muted small mb-0">{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
||||
<h3 class="fw-bold text-uppercase text-secondary mb-1">{% if is_advance %}Advance{% endif %} Payslip</h3>
|
||||
<h3 class="fw-bold text-uppercase text-secondary mb-1">{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip</h3>
|
||||
<div class="fw-bold">{{ record.date|date:"F j, Y" }}</div>
|
||||
<div class="text-muted small">Payer: Fox Fitt</div>
|
||||
</div>
|
||||
@ -90,6 +90,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif is_loan %}
|
||||
<!-- === LOAN PAYMENT DETAIL === -->
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Loan Details</h6>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ loan_adj.date|date:"M d, Y" }}</td>
|
||||
<td><span class="badge bg-warning text-dark text-uppercase">Loan Payment</span></td>
|
||||
<td>{{ loan_adj.description|default:"Worker loan" }}</td>
|
||||
<td class="text-end text-success fw-bold">R {{ loan_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- === LOAN TOTAL === -->
|
||||
<div class="row justify-content-end mt-4">
|
||||
<div class="col-md-5">
|
||||
<table class="table table-sm border-0">
|
||||
<tr class="border-top border-dark">
|
||||
<td class="text-end border-0 fw-bold fs-5">Loan Amount:</td>
|
||||
<td class="text-end border-0 fw-bold fs-5">R {{ loan_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- === WORK LOG TABLE — each day worked === -->
|
||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Work Log Details (Attendance)</h6>
|
||||
|
||||
@ -107,7 +107,7 @@
|
||||
<div class="header">
|
||||
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
|
||||
<div class="beneficiary-name">{{ record.worker.name }}</div>
|
||||
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
|
||||
<div class="title">{% if is_advance %}Advance Payslip #{{ record.id }}{% elif is_loan %}Loan Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<!-- Beneficiary details box -->
|
||||
@ -132,6 +132,12 @@
|
||||
<td>Advance Payment: {{ advance_description }}</td>
|
||||
<td class="text-right">R {{ advance_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% elif is_loan %}
|
||||
<!-- Loan Payment — single line item -->
|
||||
<tr>
|
||||
<td>Loan Payment: {{ loan_description }}</td>
|
||||
<td class="text-right">R {{ loan_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Base Pay — number of days worked × day rate -->
|
||||
<tr>
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user