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="header">
|
||||||
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
|
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
|
||||||
<div class="beneficiary-name">{{ record.worker.name }}</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>
|
</div>
|
||||||
|
|
||||||
<!-- Beneficiary details -->
|
<!-- Beneficiary details -->
|
||||||
@ -53,6 +53,11 @@
|
|||||||
<td>Advance Payment: {{ advance_description }}</td>
|
<td>Advance Payment: {{ advance_description }}</td>
|
||||||
<td style="text-align: right;">R {{ advance_amount|floatformat:2 }}</td>
|
<td style="text-align: right;">R {{ advance_amount|floatformat:2 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% elif is_loan %}
|
||||||
|
<tr>
|
||||||
|
<td>Loan Payment: {{ loan_description }}</td>
|
||||||
|
<td style="text-align: right;">R {{ loan_amount|floatformat:2 }}</td>
|
||||||
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Base pay line -->
|
<!-- Base pay line -->
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -545,6 +545,22 @@
|
|||||||
<label class="form-label">Description</label>
|
<label class="form-label">Description</label>
|
||||||
<textarea name="description" class="form-control" rows="2" placeholder="Reason for this adjustment..."></textarea>
|
<textarea name="description" class="form-control" rows="2" placeholder="Reason for this adjustment..."></textarea>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@ -1295,9 +1311,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const adjSelectedCount = document.getElementById('adjSelectedCount');
|
const adjSelectedCount = document.getElementById('adjSelectedCount');
|
||||||
const addAdjWorkerError = document.getElementById('addAdjWorkerError');
|
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
|
// Also toggles the HTML "required" attribute so browser validation
|
||||||
// only enforces the project when the type actually needs one.
|
// only enforces the project when the type actually needs one.
|
||||||
|
var addAdjPayImmediatelyGroup = document.getElementById('addAdjPayImmediatelyGroup');
|
||||||
function toggleProjectField() {
|
function toggleProjectField() {
|
||||||
if (!addAdjType || !addAdjProjectGroup) return;
|
if (!addAdjType || !addAdjProjectGroup) return;
|
||||||
// Loan types and repayments don't need a project.
|
// Loan types and repayments don't need a project.
|
||||||
@ -1313,6 +1330,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
addAdjProject.value = ''; // Clear selection when hidden
|
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) {
|
if (addAdjType) {
|
||||||
addAdjType.addEventListener('change', toggleProjectField);
|
addAdjType.addEventListener('change', toggleProjectField);
|
||||||
|
|||||||
@ -28,10 +28,10 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h6 class="text-uppercase text-muted fw-bold small mb-1">Payment To Beneficiary:</h6>
|
<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>
|
<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>
|
||||||
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
<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="fw-bold">{{ record.date|date:"F j, Y" }}</div>
|
||||||
<div class="text-muted small">Payer: Fox Fitt</div>
|
<div class="text-muted small">Payer: Fox Fitt</div>
|
||||||
</div>
|
</div>
|
||||||
@ -90,6 +90,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% else %}
|
||||||
<!-- === WORK LOG TABLE — each day worked === -->
|
<!-- === WORK LOG TABLE — each day worked === -->
|
||||||
<h6 class="text-uppercase text-muted fw-bold small mb-3">Work Log Details (Attendance)</h6>
|
<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="header">
|
||||||
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
|
<div class="sub-header">PAYMENT TO BENEFICIARY</div>
|
||||||
<div class="beneficiary-name">{{ record.worker.name }}</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>
|
</div>
|
||||||
|
|
||||||
<!-- Beneficiary details box -->
|
<!-- Beneficiary details box -->
|
||||||
@ -132,6 +132,12 @@
|
|||||||
<td>Advance Payment: {{ advance_description }}</td>
|
<td>Advance Payment: {{ advance_description }}</td>
|
||||||
<td class="text-right">R {{ advance_amount|floatformat:2 }}</td>
|
<td class="text-right">R {{ advance_amount|floatformat:2 }}</td>
|
||||||
</tr>
|
</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 %}
|
{% else %}
|
||||||
<!-- Base Pay — number of days worked × day rate -->
|
<!-- Base Pay — number of days worked × day rate -->
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -1338,18 +1338,27 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
|
|||||||
|
|
||||||
total_amount = payroll_record.amount_paid
|
total_amount = payroll_record.amount_paid
|
||||||
|
|
||||||
# === DETECT ADVANCE-ONLY PAYMENT ===
|
# === DETECT STANDALONE PAYMENT (no work logs, single adjustment) ===
|
||||||
# If the payment has 0 work logs and consists of only an Advance Payment
|
# Advance-only or Loan-only payments use a cleaner payslip layout
|
||||||
# adjustment, use the special advance payslip layout (shows the advance
|
# showing just the amount instead of "0 days worked + adjustment".
|
||||||
# as a positive amount instead of the confusing "0 days + deduction" format).
|
|
||||||
advance_adj = None
|
advance_adj = None
|
||||||
|
loan_adj = None
|
||||||
if log_count == 0:
|
if log_count == 0:
|
||||||
adjs_list = list(payroll_record.adjustments.all())
|
adjs_list = list(payroll_record.adjustments.all())
|
||||||
if len(adjs_list) == 1 and adjs_list[0].type == 'Advance Payment':
|
if len(adjs_list) == 1:
|
||||||
advance_adj = adjs_list[0]
|
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
|
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
|
# Context for both the HTML email body and the PDF attachment
|
||||||
email_context = {
|
email_context = {
|
||||||
@ -1361,6 +1370,9 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
|
|||||||
'is_advance': is_advance,
|
'is_advance': is_advance,
|
||||||
'advance_amount': advance_adj.amount if advance_adj else None,
|
'advance_amount': advance_adj.amount if advance_adj else None,
|
||||||
'advance_description': advance_adj.description if advance_adj else '',
|
'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
|
# 1. Render HTML email body
|
||||||
@ -1760,6 +1772,10 @@ def add_adjustment(request):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# === NEW LOAN — create a Loan record (loan_type='loan') ===
|
# === 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':
|
if adj_type == 'New Loan':
|
||||||
loan = Loan.objects.create(
|
loan = Loan.objects.create(
|
||||||
worker=worker,
|
worker=worker,
|
||||||
@ -1770,6 +1786,30 @@ def add_adjustment(request):
|
|||||||
reason=description,
|
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 ===
|
# === ADVANCE PAYMENT — immediate payment + auto-repayment ===
|
||||||
# An advance is a salary prepayment — worker gets money now, and
|
# An advance is a salary prepayment — worker gets money now, and
|
||||||
# the full amount is automatically deducted from their next salary.
|
# 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)
|
# Calculate net adjustment amount (additive minus deductive)
|
||||||
adjustments_net = record.amount_paid - base_pay
|
adjustments_net = record.amount_paid - base_pay
|
||||||
|
|
||||||
# === DETECT ADVANCE-ONLY PAYMENT ===
|
# === DETECT STANDALONE PAYMENT (no work logs, single adjustment) ===
|
||||||
# If payment has 0 work logs and a single Advance Payment adjustment,
|
# Advance-only or Loan-only payments use a cleaner layout.
|
||||||
# show a cleaner "advance payslip" layout instead of "0 days worked".
|
|
||||||
adjs_list = list(adjustments)
|
adjs_list = list(adjustments)
|
||||||
advance_adj = None
|
advance_adj = None
|
||||||
if logs.count() == 0 and len(adjs_list) == 1 and adjs_list[0].type == 'Advance Payment':
|
loan_adj = None
|
||||||
advance_adj = adjs_list[0]
|
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 = {
|
context = {
|
||||||
'record': record,
|
'record': record,
|
||||||
@ -2214,6 +2257,8 @@ def payslip_detail(request, pk):
|
|||||||
'deductive_types': DEDUCTIVE_TYPES,
|
'deductive_types': DEDUCTIVE_TYPES,
|
||||||
'is_advance': advance_adj is not None,
|
'is_advance': advance_adj is not None,
|
||||||
'advance_adj': advance_adj,
|
'advance_adj': advance_adj,
|
||||||
|
'is_loan': loan_adj is not None,
|
||||||
|
'loan_adj': loan_adj,
|
||||||
}
|
}
|
||||||
return render(request, 'core/payslip.html', context)
|
return render(request, 'core/payslip.html', context)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user