feat: pay_type UI (form/list/detail), Salary modal+entry, badge, clean Salary payslip
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6f12e7dd1
commit
862766f9b5
@ -255,7 +255,7 @@ class WorkerForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Worker
|
||||
fields = [
|
||||
'name', 'id_number', 'phone_number', 'monthly_salary',
|
||||
'name', 'id_number', 'phone_number', 'monthly_salary', 'pay_type',
|
||||
'tax_number', 'uif_number', 'bank_name', 'bank_account_number',
|
||||
'employment_date', 'active', 'notes',
|
||||
'shoe_size', 'overall_top_size', 'pants_size', 'tshirt_size',
|
||||
@ -267,6 +267,10 @@ class WorkerForm(forms.ModelForm):
|
||||
'id_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'phone_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '+27...'}),
|
||||
'monthly_salary': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||
# pay_type: 'daily' = normal field worker (paid per logged work day);
|
||||
# 'fixed' = manager / salaried staff (paid a monthly Salary
|
||||
# adjustment, never logged on attendance).
|
||||
'pay_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
# Banking & Tax
|
||||
'tax_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'uif_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
|
||||
@ -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 }}{% elif is_loan %}Loan 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 }}{% elif is_salary %}Salary Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<!-- Beneficiary details -->
|
||||
@ -58,6 +58,11 @@
|
||||
<td>Loan Payment: {{ loan_description }}</td>
|
||||
<td style="text-align: right;">R {{ loan_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% elif is_salary %}
|
||||
<tr>
|
||||
<td>Salary: {{ salary_description }}</td>
|
||||
<td style="text-align: right;">R {{ salary_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Base pay line -->
|
||||
<tr>
|
||||
|
||||
@ -41,6 +41,13 @@
|
||||
<button type="button" class="btn-action-soft btn-action-add" data-bs-toggle="modal" data-bs-target="#addAdjustmentModal">
|
||||
<i class="fas fa-plus"></i> Add Adjustment
|
||||
</button>
|
||||
{# Pay Salary — opens the SAME Add-Adjustment modal pre-set to #}
|
||||
{# type=Salary (project field + Pay-Immediately checkbox shown). #}
|
||||
{# Discoverable shortcut for paying a manager / salaried worker. #}
|
||||
<button type="button" class="btn-action-soft btn-action-add" id="paySalaryBtn"
|
||||
title="Pay a manager / salaried worker their monthly salary">
|
||||
<i class="fas fa-user-tie"></i> Pay Salary
|
||||
</button>
|
||||
<button type="button" class="btn-action-soft btn-action-price" data-bs-toggle="modal" data-bs-target="#priceOvertimeModal">
|
||||
<i class="fas fa-clock"></i> Price Overtime
|
||||
</button>
|
||||
@ -1099,10 +1106,10 @@
|
||||
<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. #}
|
||||
{# Pay Immediately — shown for New Loan AND Salary types. #}
|
||||
{# When checked, the amount is paid to the worker right away #}
|
||||
{# and a payslip is emailed to Spark. When unchecked, it sits #}
|
||||
{# in pending payments and is 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"
|
||||
@ -1928,9 +1935,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
addAdjProject.value = ''; // Clear selection when hidden
|
||||
}
|
||||
}
|
||||
// "Pay Immediately" checkbox — only shown for New Loan type
|
||||
// "Pay Immediately" checkbox — shown for New Loan AND Salary.
|
||||
// Salary mirrors New Loan: ticked = pay the manager now + email
|
||||
// payslip; unticked = leave it pending for the next pay cycle.
|
||||
if (addAdjPayImmediatelyGroup) {
|
||||
addAdjPayImmediatelyGroup.style.display = (addAdjType.value === 'New Loan') ? '' : 'none';
|
||||
var payNowTypes = ['New Loan', 'Salary'];
|
||||
addAdjPayImmediatelyGroup.style.display =
|
||||
(payNowTypes.indexOf(addAdjType.value) !== -1) ? '' : 'none';
|
||||
}
|
||||
}
|
||||
if (addAdjType) {
|
||||
@ -2020,8 +2031,35 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// When the modal is opened from the HEADER button (not quick-adjust),
|
||||
// clear any pre-selected workers and project from a previous quick-adjust.
|
||||
// === PAY SALARY BUTTON ===
|
||||
// Opens the SAME Add-Adjustment modal pre-set to type=Salary, with a
|
||||
// clean worker/project slate. Sets _paySalaryOpen so the modal's
|
||||
// show.bs.modal reset handler (which normally forces type=Bonus)
|
||||
// leaves our Salary selection alone. Mirrors the _quickAdjustOpen
|
||||
// pattern. Distinct id 'paySalaryBtn' — no duplicate-id collision.
|
||||
var _paySalaryOpen = false;
|
||||
var paySalaryBtn = document.getElementById('paySalaryBtn');
|
||||
if (paySalaryBtn) {
|
||||
paySalaryBtn.addEventListener('click', function() {
|
||||
_paySalaryOpen = true;
|
||||
// Clean slate: no pre-selected workers, no pre-selected project
|
||||
addAdjWorkerCheckboxes.forEach(function(cb) { cb.checked = false; });
|
||||
updateWorkerCount();
|
||||
if (addAdjProject) addAdjProject.value = '';
|
||||
// Pre-set Salary and trigger field visibility (shows the
|
||||
// project field + the Pay-Immediately checkbox).
|
||||
if (addAdjType) {
|
||||
addAdjType.value = 'Salary';
|
||||
toggleProjectField();
|
||||
}
|
||||
var modal = new bootstrap.Modal(document.getElementById('addAdjustmentModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// When the modal is opened from the HEADER button (not quick-adjust,
|
||||
// not Pay-Salary), clear any pre-selected workers/project and reset
|
||||
// the type to Bonus.
|
||||
var addAdjModal = document.getElementById('addAdjustmentModal');
|
||||
if (addAdjModal) {
|
||||
addAdjModal.addEventListener('show.bs.modal', function() {
|
||||
@ -2029,6 +2067,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
_quickAdjustOpen = false;
|
||||
return; // Quick-adjust already set the values
|
||||
}
|
||||
if (_paySalaryOpen) {
|
||||
_paySalaryOpen = false;
|
||||
return; // Pay-Salary already set type=Salary
|
||||
}
|
||||
// Reset: uncheck all workers, clear project, reset type
|
||||
addAdjWorkerCheckboxes.forEach(function(cb) { cb.checked = false; });
|
||||
updateWorkerCount();
|
||||
|
||||
@ -28,12 +28,12 @@
|
||||
<div class="stat-label mb-1">Payment To Beneficiary:</div>
|
||||
<h2 class="fw-bold mb-0 text-uppercase">{{ record.worker.name }}</h2>
|
||||
<p class="mb-0" style="color: var(--text-tertiary); font-size: 0.85rem;">
|
||||
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip No. #{{ record.id|stringformat:"06d" }}
|
||||
{% if is_advance %}Advance{% elif is_loan %}Loan{% elif is_salary %}Salary{% 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" style="color: var(--text-secondary);">
|
||||
{% if is_advance %}Advance{% elif is_loan %}Loan{% endif %} Payslip
|
||||
{% if is_advance %}Advance{% elif is_loan %}Loan{% elif is_salary %}Salary{% endif %} Payslip
|
||||
</h3>
|
||||
<div class="fw-bold">{{ record.date|date:"F j, Y" }}</div>
|
||||
<div style="color: var(--text-tertiary); font-size: 0.85rem;">Payer: Fox Fitt</div>
|
||||
@ -127,6 +127,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% elif is_salary %}
|
||||
{# === SALARY PAYMENT DETAIL === #}
|
||||
{# Manager / fixed-salary monthly pay. Clean single-line #}
|
||||
{# layout — no empty work-log table, no R 0.00 base-pay row. #}
|
||||
<div class="stat-label mb-3">Salary Details</div>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ salary_adj.date|date:"M d, Y" }}</td>
|
||||
<td><span class="badge-type-salary">SALARY</span></td>
|
||||
<td>{{ salary_adj.description|default:"Monthly salary" }}</td>
|
||||
<td class="text-end fw-bold" style="color: var(--color-success);">R {{ salary_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row justify-content-end mt-4">
|
||||
<div class="col-md-5">
|
||||
<table class="table table-sm border-0">
|
||||
<tr style="border-top: 2px solid var(--text-primary);">
|
||||
<td class="text-end border-0 fw-bold fs-5">Salary Amount:</td>
|
||||
<td class="text-end border-0 fw-bold fs-5">R {{ salary_adj.amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- === WORK LOG TABLE === -->
|
||||
<div class="stat-label mb-3">Work Log Details (Attendance)</div>
|
||||
|
||||
@ -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 }}{% elif is_loan %}Loan 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 }}{% elif is_salary %}Salary Payslip #{{ record.id }}{% else %}Payslip #{{ record.id }}{% endif %}</div>
|
||||
</div>
|
||||
|
||||
<!-- Beneficiary details box -->
|
||||
@ -138,6 +138,12 @@
|
||||
<td>Loan Payment: {{ loan_description }}</td>
|
||||
<td class="text-right">R {{ loan_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% elif is_salary %}
|
||||
<!-- Salary — single line item (manager / fixed monthly pay) -->
|
||||
<tr>
|
||||
<td>Salary: {{ salary_description }}</td>
|
||||
<td class="text-right">R {{ salary_amount|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<!-- Base Pay — number of days worked × day rate -->
|
||||
<tr>
|
||||
|
||||
@ -62,6 +62,16 @@
|
||||
<div class="card-header"><h6 class="m-0 fw-bold">Personal & Pay</h6></div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0" style="font-size: 0.9rem;">
|
||||
<dt class="col-sm-5">Pay Type</dt>
|
||||
<dd class="col-sm-7">
|
||||
{# Manager/salaried staff are paid a monthly Salary, #}
|
||||
{# not per logged work day. #}
|
||||
{% if worker.is_salaried %}
|
||||
<span class="badge-type-salary">Manager / Salaried</span>
|
||||
{% else %}
|
||||
<span class="badge" style="background: rgba(148, 163, 184, 0.15); color: var(--text-secondary);">Daily</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt class="col-sm-5">Monthly Salary</dt><dd class="col-sm-7 fw-semibold">R {{ worker.monthly_salary|money }}</dd>
|
||||
<dt class="col-sm-5">Daily Rate</dt> <dd class="col-sm-7">R {{ worker.daily_rate|money }}</dd>
|
||||
<dt class="col-sm-5">Employment Date</dt><dd class="col-sm-7">{{ worker.employment_date|date:"d M Y" }}</dd>
|
||||
|
||||
@ -65,6 +65,19 @@
|
||||
{{ form.monthly_salary }}
|
||||
{% if form.monthly_salary.errors %}<div class="invalid-feedback d-block">{{ form.monthly_salary.errors|first }}</div>{% endif %}
|
||||
</div>
|
||||
{# Pay Type — Daily-rated field worker vs Fixed-salary manager. #}
|
||||
{# A "Fixed salary" worker is paid a monthly Salary adjustment #}
|
||||
{# and is never logged on attendance / absences. #}
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">
|
||||
Pay Type
|
||||
<i class="fas fa-info-circle text-muted ms-1" style="font-size: 0.8em;"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Daily-rated = normal field worker (paid per logged work day). Fixed salary = manager / salaried staff (paid a monthly Salary, never logged on attendance)."></i>
|
||||
</label>
|
||||
{{ form.pay_type }}
|
||||
{% if form.pay_type.errors %}<div class="invalid-feedback d-block">{{ form.pay_type.errors|first }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Employment Date</label>
|
||||
{{ form.employment_date }}
|
||||
|
||||
@ -75,6 +75,7 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>ID Number</th>
|
||||
<th>Type</th>
|
||||
<th>Phone</th>
|
||||
<th class="text-end">Salary</th>
|
||||
<th class="text-end">Days Worked</th>
|
||||
@ -91,6 +92,15 @@
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); font-size: 0.85rem;">{{ w.id_number }}</td>
|
||||
<td>
|
||||
{# Pay-type indicator — managers/salaried staff are #}
|
||||
{# paid a monthly Salary, not per logged work day. #}
|
||||
{% if w.is_salaried %}
|
||||
<span class="badge-type-salary">Manager / Salaried</span>
|
||||
{% else %}
|
||||
<span class="badge" style="background: rgba(148, 163, 184, 0.15); color: var(--text-secondary);">Daily</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="color: var(--text-secondary); font-size: 0.85rem;">{{ w.phone_number|default:'—' }}</td>
|
||||
<td class="text-end fw-semibold">R {{ w.monthly_salary|money }}</td>
|
||||
<td class="text-end">{{ w.days_worked }}</td>
|
||||
|
||||
@ -3710,3 +3710,100 @@ class ManagerSalariedPayReportTests(TestCase):
|
||||
self.assertIsNotNone(sal.payroll_record)
|
||||
self.assertEqual(sal.payroll_record, ded.payroll_record) # same PayrollRecord
|
||||
self.assertEqual(sal.payroll_record.amount_paid, Decimal('39000.00')) # 40000 - 1000
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === TASK 7 — MANAGER / SALARIED PAY: UI SURFACE + CLEAN SALARY PAYSLIP ===
|
||||
# Server-side, light (no browser). Proves:
|
||||
# - WorkerForm exposes the pay_type field (Part A) and the worker-create
|
||||
# page renders it (Part B).
|
||||
# - The friendly worker list labels a fixed-salary worker as a
|
||||
# "Manager / Salaried" type (Part C).
|
||||
# - An immediate Salary payment's payslip uses the clean single-line
|
||||
# layout — NO "0 days worked" / "Base Pay Subtotal" / "No work logs"
|
||||
# generic-branch artefacts (Part F, absorbed from Task 5 review #2).
|
||||
# =============================================================================
|
||||
|
||||
class ManagerSalariedPayUITests(TestCase):
|
||||
def setUp(self):
|
||||
self.admin = User.objects.create_user('msu_admin', password='x', is_staff=True)
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
def test_workerform_has_pay_type(self):
|
||||
from core.forms import WorkerForm
|
||||
self.assertIn('pay_type', WorkerForm().fields)
|
||||
|
||||
def test_worker_new_page_renders_pay_type(self):
|
||||
# URL name 'worker_new' → /workers/new/ (core/urls.py).
|
||||
resp = self.client.get(reverse('worker_new'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'pay_type')
|
||||
|
||||
def test_worker_list_shows_manager_label_for_fixed(self):
|
||||
Worker.objects.create(
|
||||
name='UI Mgr', id_number='MSU-M', monthly_salary=Decimal('40000'),
|
||||
pay_type='fixed')
|
||||
# worker_list status filter param confirmed in CLAUDE.md routes table.
|
||||
resp = self.client.get(reverse('worker_list') + '?status=all')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Manager')
|
||||
|
||||
def test_salary_immediate_payslip_has_no_zero_days_line(self):
|
||||
# Absorbed Task-5 review #2: an immediate Salary payment's payslip
|
||||
# must use the clean single-line layout — NOT the generic
|
||||
# work-log/base-pay branch (which for log_count==0 renders an
|
||||
# empty "Work Log Details" table with "No work logs in this
|
||||
# period." + a "Base Pay Subtotal" of R 0.00). The PDF/email
|
||||
# templates phrase this as "Base Pay (0 days worked)".
|
||||
proj = Project.objects.create(name='UI Sal Proj')
|
||||
mgr = Worker.objects.create(
|
||||
name='UI Sal Mgr', id_number='MSU-S', monthly_salary=Decimal('40000'),
|
||||
pay_type='fixed')
|
||||
self.client.post(reverse('add_adjustment'), {
|
||||
'workers': [mgr.id], 'type': 'Salary', 'amount': '40000.00',
|
||||
'description': 'Salary - May 2026',
|
||||
'date': _date.today().strftime('%Y-%m-%d'),
|
||||
'project': proj.id, 'pay_immediately': '1',
|
||||
})
|
||||
pr = PayrollRecord.objects.get(worker=mgr)
|
||||
resp = self.client.get(reverse('payslip_detail', args=[pr.id]))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
body = resp.content.decode()
|
||||
# The clean Salary layout shows the Salary detail; it must NOT
|
||||
# fall through to the empty generic work-log/base-pay branch.
|
||||
self.assertIn('Salary', body)
|
||||
self.assertNotIn('No work logs in this period.', body)
|
||||
self.assertNotIn('Base Pay Subtotal', body)
|
||||
|
||||
def test_salary_immediate_payslip_pdf_context_is_clean(self):
|
||||
# Directly prove the PDF/email context flag: a lone Salary
|
||||
# adjustment with log_count==0 must NOT render the generic
|
||||
# "Base Pay (N days worked)" line in payslip_pdf.html.
|
||||
from django.template.loader import render_to_string
|
||||
proj = Project.objects.create(name='UI Sal Proj2')
|
||||
mgr = Worker.objects.create(
|
||||
name='UI Sal Mgr2', id_number='MSU-S2', monthly_salary=Decimal('40000'),
|
||||
pay_type='fixed')
|
||||
self.client.post(reverse('add_adjustment'), {
|
||||
'workers': [mgr.id], 'type': 'Salary', 'amount': '40000.00',
|
||||
'description': 'Salary - May 2026',
|
||||
'date': _date.today().strftime('%Y-%m-%d'),
|
||||
'project': proj.id, 'pay_immediately': '1',
|
||||
})
|
||||
pr = PayrollRecord.objects.get(worker=mgr)
|
||||
adjs = list(pr.adjustments.all())
|
||||
self.assertEqual(len(adjs), 1)
|
||||
self.assertEqual(adjs[0].type, 'Salary')
|
||||
sal = adjs[0]
|
||||
ctx = {
|
||||
'record': pr, 'logs_count': 0, 'logs_amount': Decimal('0.00'),
|
||||
'adjustments': pr.adjustments.all(),
|
||||
'deductive_types': ['Deduction', 'Loan Repayment', 'Advance Repayment'],
|
||||
'is_advance': False, 'advance_amount': None, 'advance_description': '',
|
||||
'is_loan': False, 'loan_amount': None, 'loan_description': '',
|
||||
'is_salary': True, 'salary_amount': sal.amount,
|
||||
'salary_description': sal.description,
|
||||
}
|
||||
html = render_to_string('core/pdf/payslip_pdf.html', ctx)
|
||||
self.assertNotIn('days worked', html) # generic Base Pay line absent
|
||||
self.assertIn('Salary', html)
|
||||
|
||||
@ -3878,10 +3878,12 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
|
||||
total_amount = payroll_record.amount_paid
|
||||
|
||||
# === 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-only, Loan-only or Salary-only payments use a cleaner
|
||||
# payslip layout showing just the amount instead of the generic
|
||||
# "Base Pay (0 days worked) — R 0.00" line followed by the adjustment.
|
||||
advance_adj = None
|
||||
loan_adj = None
|
||||
salary_adj = None
|
||||
if log_count == 0:
|
||||
adjs_list = list(payroll_record.adjustments.all())
|
||||
if len(adjs_list) == 1:
|
||||
@ -3889,13 +3891,18 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
|
||||
advance_adj = adjs_list[0]
|
||||
elif adjs_list[0].type == 'New Loan':
|
||||
loan_adj = adjs_list[0]
|
||||
elif adjs_list[0].type == 'Salary':
|
||||
salary_adj = adjs_list[0]
|
||||
|
||||
is_advance = advance_adj is not None
|
||||
is_loan = loan_adj is not None
|
||||
is_salary = salary_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}"
|
||||
elif is_salary:
|
||||
subject = f"Salary Payslip for {worker.name} - {payroll_record.date}"
|
||||
else:
|
||||
subject = f"Payslip for {worker.name} - {payroll_record.date}"
|
||||
|
||||
@ -3912,6 +3919,9 @@ def _send_payslip_email(request, worker, payroll_record, log_count, logs_amount,
|
||||
'is_loan': is_loan,
|
||||
'loan_amount': loan_adj.amount if loan_adj else None,
|
||||
'loan_description': loan_adj.description if loan_adj else '',
|
||||
'is_salary': is_salary,
|
||||
'salary_amount': salary_adj.amount if salary_adj else None,
|
||||
'salary_description': salary_adj.description if salary_adj else '',
|
||||
}
|
||||
|
||||
# 1. Render HTML email body
|
||||
@ -5172,15 +5182,20 @@ def payslip_detail(request, pk):
|
||||
adjustments_net = record.amount_paid - base_pay
|
||||
|
||||
# === DETECT STANDALONE PAYMENT (no work logs, single adjustment) ===
|
||||
# Advance-only or Loan-only payments use a cleaner layout.
|
||||
# Advance-only, Loan-only or Salary-only payments use a cleaner
|
||||
# layout (one detail line + total) instead of an empty work-log
|
||||
# table with a "Base Pay Subtotal — R 0.00" footer row.
|
||||
adjs_list = list(adjustments)
|
||||
advance_adj = None
|
||||
loan_adj = None
|
||||
salary_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]
|
||||
elif adjs_list[0].type == 'Salary':
|
||||
salary_adj = adjs_list[0]
|
||||
|
||||
context = {
|
||||
'record': record,
|
||||
@ -5194,6 +5209,8 @@ def payslip_detail(request, pk):
|
||||
'advance_adj': advance_adj,
|
||||
'is_loan': loan_adj is not None,
|
||||
'loan_adj': loan_adj,
|
||||
'is_salary': salary_adj is not None,
|
||||
'salary_adj': salary_adj,
|
||||
}
|
||||
return render(request, 'core/payslip.html', context)
|
||||
|
||||
|
||||
@ -89,6 +89,10 @@
|
||||
--badge-loan-rep-bg: #b48a1a; --badge-loan-rep-fg: #fef4d1;
|
||||
--badge-advance-bg: #3e5c7b; --badge-advance-fg: #d7e5f2;
|
||||
--badge-advance-rep-bg: #2f679a; --badge-advance-rep-fg: #d7e5f2;
|
||||
/* Salary (manager / fixed monthly pay) — a distinct teal, not used by
|
||||
any other type; reads as "regular salary/pay", separate from the
|
||||
forest-green Bonus and the slate-blue Advance. */
|
||||
--badge-salary-bg: #1f7a70; --badge-salary-fg: #d6f1ec;
|
||||
|
||||
/* === PAYROLL DASHBOARD action-button tokens (dark theme) ===
|
||||
Soft-fill pastels for the 4 action buttons at the top of /payroll/
|
||||
@ -162,6 +166,8 @@
|
||||
--badge-loan-rep-bg: #f7d873; --badge-loan-rep-fg: #5a4418;
|
||||
--badge-advance-bg: #bccee0; --badge-advance-fg: #243b56;
|
||||
--badge-advance-rep-bg: #9ec1dd; --badge-advance-rep-fg: #1d3550;
|
||||
/* Salary (light theme) — pale teal fill, deep-teal text for contrast. */
|
||||
--badge-salary-bg: #c3e8e2; --badge-salary-fg: #18534b;
|
||||
|
||||
/* === PAYROLL DASHBOARD action-button tokens (light theme) === */
|
||||
--btn-action-lookup-bg: #c7d9e8; --btn-action-lookup-fg: #243b56;
|
||||
@ -1935,7 +1941,8 @@ body, .card, .modal-content, .form-control, .form-select,
|
||||
.badge-type-new-loan,
|
||||
.badge-type-loan-repayment,
|
||||
.badge-type-advance-payment,
|
||||
.badge-type-advance-repayment {
|
||||
.badge-type-advance-repayment,
|
||||
.badge-type-salary {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
@ -1952,6 +1959,7 @@ body, .card, .modal-content, .form-control, .form-select,
|
||||
.badge-type-loan-repayment { background: var(--badge-loan-rep-bg); color: var(--badge-loan-rep-fg); }
|
||||
.badge-type-advance-payment { background: var(--badge-advance-bg); color: var(--badge-advance-fg); }
|
||||
.badge-type-advance-repayment { background: var(--badge-advance-rep-bg); color: var(--badge-advance-rep-fg); }
|
||||
.badge-type-salary { background: var(--badge-salary-bg); color: var(--badge-salary-fg); }
|
||||
|
||||
/* --- Status flags that borrow a type's colour for semantic consistency.
|
||||
The Pending tab's "Loan" worker flag (Has-an-active-loan-or-advance),
|
||||
@ -2075,6 +2083,7 @@ body, .card, .modal-content, .form-control, .form-select,
|
||||
.adj-group-header[data-type="Loan Repayment"] { border-left: 4px solid var(--badge-loan-rep-bg); }
|
||||
.adj-group-header[data-type="Advance Payment"] { border-left: 4px solid var(--badge-advance-bg); }
|
||||
.adj-group-header[data-type="Advance Repayment"] { border-left: 4px solid var(--badge-advance-rep-bg); }
|
||||
.adj-group-header[data-type="Salary"] { border-left: 4px solid var(--badge-salary-bg); }
|
||||
|
||||
/* --- Chevron rotates to indicate collapsed / expanded state.
|
||||
Bootstrap sets aria-expanded="false" on the toggle when collapsed;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user