feat: add_adjustment Salary branch (project-required, pay-now/pending)
This commit is contained in:
parent
86b0cb9dd6
commit
255ec82cef
@ -177,7 +177,7 @@ class PayrollAdjustmentForm(forms.ModelForm):
|
||||
project = cleaned_data.get('project')
|
||||
|
||||
# These types must have a project — they're tied to specific work
|
||||
project_required_types = ('Overtime', 'Bonus', 'Deduction', 'Advance Payment')
|
||||
project_required_types = ('Overtime', 'Bonus', 'Deduction', 'Advance Payment', 'Salary')
|
||||
if adj_type in project_required_types and not project:
|
||||
self.add_error('project', 'A project must be selected for this adjustment type.')
|
||||
|
||||
|
||||
@ -3586,3 +3586,58 @@ class ManagerSalariedAbsenceExclusionTests(TestCase):
|
||||
resp2, reverse('absence_list'), fetch_redirect_response=False)
|
||||
self.assertEqual(Absence.objects.filter(worker=self.daily).count(), 1)
|
||||
self.assertEqual(Absence.objects.filter(worker=self.mgr).count(), 0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# === MANAGER / SALARIED PAY — Salary adjustment branch in add_adjustment ===
|
||||
# Logging a fixed-salary manager's monthly pay: project-required, with a
|
||||
# "Pay Immediately" option that mirrors the proven New Loan pay-now pattern
|
||||
# (isolated PayrollRecord). Unpaid path falls through to the generic pending
|
||||
# adjustment creation so it nets with deductions at dashboard Pay time.
|
||||
# =============================================================================
|
||||
|
||||
class ManagerSalariedPaySalaryAdjustmentTests(TestCase):
|
||||
def setUp(self):
|
||||
self.admin = User.objects.create_user('mss_admin', password='x', is_staff=True)
|
||||
self.client.force_login(self.admin)
|
||||
self.proj = Project.objects.create(name='MSS Project')
|
||||
self.mgr = Worker.objects.create(
|
||||
name='Sal Mgr', id_number='MSS-M', monthly_salary=Decimal('40000'),
|
||||
pay_type='fixed')
|
||||
|
||||
def _post(self, **over):
|
||||
data = {
|
||||
'workers': [self.mgr.id], 'type': 'Salary',
|
||||
'amount': '40000.00', 'description': 'Salary - May 2026',
|
||||
'date': _date.today().strftime('%Y-%m-%d'),
|
||||
'project': self.proj.id,
|
||||
}
|
||||
data.update(over)
|
||||
return self.client.post(reverse('add_adjustment'), data)
|
||||
|
||||
def test_salary_requires_project(self):
|
||||
self._post(project='')
|
||||
self.assertFalse(
|
||||
PayrollAdjustment.objects.filter(worker=self.mgr, type='Salary').exists())
|
||||
|
||||
def test_salary_unpaid_creates_pending_adjustment(self):
|
||||
self._post() # no pay_immediately
|
||||
adj = PayrollAdjustment.objects.get(worker=self.mgr, type='Salary')
|
||||
self.assertIsNone(adj.payroll_record)
|
||||
self.assertEqual(adj.project, self.proj)
|
||||
self.assertEqual(adj.amount, Decimal('40000.00'))
|
||||
|
||||
def test_salary_pay_immediately_creates_payroll_record(self):
|
||||
self._post(pay_immediately='1')
|
||||
adj = PayrollAdjustment.objects.get(worker=self.mgr, type='Salary')
|
||||
self.assertIsNotNone(adj.payroll_record)
|
||||
self.assertEqual(adj.payroll_record.amount_paid, Decimal('40000.00'))
|
||||
|
||||
def test_salary_required_in_payrolladjustmentform_clean(self):
|
||||
from core.forms import PayrollAdjustmentForm
|
||||
f = PayrollAdjustmentForm(data={
|
||||
'type': 'Salary', 'worker': self.mgr.id, 'amount': '40000.00',
|
||||
'date': _date.today().strftime('%Y-%m-%d'), 'description': 'x',
|
||||
})
|
||||
self.assertFalse(f.is_valid())
|
||||
self.assertIn('project', f.errors)
|
||||
|
||||
@ -4249,7 +4249,7 @@ def add_adjustment(request):
|
||||
except Project.DoesNotExist:
|
||||
pass
|
||||
|
||||
project_required_types = ('Overtime', 'Bonus', 'Deduction', 'Advance Payment')
|
||||
project_required_types = ('Overtime', 'Bonus', 'Deduction', 'Advance Payment', 'Salary')
|
||||
if adj_type in project_required_types and not project:
|
||||
messages.error(request, 'A project must be selected for this adjustment type.')
|
||||
return redirect('payroll_dashboard')
|
||||
@ -4390,6 +4390,35 @@ def add_adjustment(request):
|
||||
created_count += 1
|
||||
continue # Skip the generic PayrollAdjustment creation below
|
||||
|
||||
# === SALARY — manager / fixed-salary monthly payment ===
|
||||
# Mirrors the New Loan pay-immediately pattern (isolated
|
||||
# PayrollRecord). If "Pay Immediately" is unchecked the row falls
|
||||
# through to the generic pending-adjustment creation below and is
|
||||
# netted with any deductions/loan-repayments by
|
||||
# _process_single_payment when the admin clicks Pay on the dashboard.
|
||||
if adj_type == 'Salary':
|
||||
pay_immediately = request.POST.get('pay_immediately') == '1'
|
||||
if pay_immediately:
|
||||
salary_adj = PayrollAdjustment.objects.create(
|
||||
worker=worker,
|
||||
type='Salary',
|
||||
amount=amount,
|
||||
date=adj_date,
|
||||
description=description,
|
||||
project=project,
|
||||
)
|
||||
payroll_record = PayrollRecord.objects.create(
|
||||
worker=worker,
|
||||
amount_paid=amount,
|
||||
date=adj_date,
|
||||
)
|
||||
salary_adj.payroll_record = payroll_record
|
||||
salary_adj.save()
|
||||
_send_payslip_email(request, worker, payroll_record, 0, Decimal('0.00'))
|
||||
created_count += 1
|
||||
continue # Skip the generic PayrollAdjustment creation below
|
||||
# else: fall through to generic pending creation (keeps project)
|
||||
|
||||
# === ALL OTHER TYPES — create a pending adjustment ===
|
||||
PayrollAdjustment.objects.create(
|
||||
worker=worker,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user