# Manager / Salaried Pay Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (or > superpowers:subagent-driven-development if running in-session) to implement > this plan task-by-task. **Goal:** Let Konrad log fixed monthly pay for managers/salaried staff (people with no daily work logs, e.g. Fitz) so it lands in payroll records + project costs and can carry loans/advances — without changing anything about how daily-rated workers are logged, paid, or costed. **Architecture:** Approach A — a `Worker.pay_type` discriminator (`'daily'` default | `'fixed'`). Managers are excluded *only* from the attendance/absence pickers so they can never reach a `WorkLog` (which makes all WorkLog-derived money math provably unchanged). Their pay is a new additive `Salary` `PayrollAdjustment` type, project-attributed, logged through the existing `add_adjustment` rails (a new branch modelled on the proven `New Loan` branch). Loans/advances/deductions/payslips/history work for them with zero new code because those are all `Worker`-FK'd. **Tech Stack:** Django 5.2.7, Python 3.13, SQLite (local dev) / MySQL (prod), Bootstrap 5, Django test framework. --- ## Context the implementing engineer MUST read first - **Design doc:** `docs/plans/2026-05-15-manager-salaried-pay-design.md` (committed locally as `325c59d` on `ai-dev`, NOT pushed). Read it in full. - **Project guide:** `CLAUDE.md` at repo root. Especially the "UI-vs-DB naming drift" section (Path-A pattern) and the multi-line Django comment gotcha. - **Baseline:** branch `ai-dev`, **173/173 tests passing**. Do not regress. - **Test command (Git Bash):** `USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2` - Run a single test class: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.ManagerSalariedPayModelTests -v 2` - On `cmd.exe` use `set USE_SQLITE=true && set DJANGO_DEBUG=true && python manage.py test core.tests` - **Test gotcha (already bit a prior session):** in `core/tests.py` use `datetime.date.today()` / `datetime.date(...)`, NOT `timezone.now().date()`. `tests.py` imports `datetime` (and a `_date` alias around line 1693); it does NOT import `timezone` in many test classes. Match the surrounding style of the absence test classes. - **Template comment gotcha:** Django `{# ... #}` is SINGLE-LINE only. After ANY template edit run: `grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'` — every match is a broken multi-line comment rendering as visible text. Use `{% comment %}...{% endcomment %}` for multi-line. - **Add new test classes at the END of `core/tests.py`** (after the last existing class) unless told otherwise. Name them as specified per task so the spec reviewer can find them. ## Exact code locations (verified during planning) | What | Location | |---|---| | `Worker` model | `core/models.py:37` (note `daily_rate` property at `:92-95`) | | `PayrollAdjustment.TYPE_CHOICES` | `core/models.py:289` | | `ADDITIVE_TYPES` / `DEDUCTIVE_TYPES` | top of `core/views.py` — locate with `grep -n "^ADDITIVE_TYPES" core/views.py` | | `AttendanceLogForm` worker queryset — supervisor branch | `core/forms.py:106-109` | | `AttendanceLogForm` worker queryset — admin branch | `core/forms.py:114` | | `PayrollAdjustmentForm.clean` `project_required_types` | `core/forms.py:179` (worker queryset `:170` — **must KEEP managers**) | | `WorkerForm` | `core/forms.py:248` (fields `:256-263`, widgets `:264-288`) | | `AbsenceLogForm.workers` queryset — admin default | `core/forms.py:662` | | `AbsenceLogForm.workers` queryset — supervisor branch | `core/forms.py:696-702` | | `AbsenceEditForm.worker` queryset — admin / supervisor | `core/forms.py:842` / `:851` | | `_build_team_workers_map` worker prefetch | `core/views.py:588` | | attendance cost-estimate JS rates loop | `core/views.py:791` | | `add_adjustment` | `core/views.py:4204` (`project_required_types` `:4247`, `New Loan` branch `:4280-4312`, generic fall-through `:4388-4398`) | | `_process_single_payment` | `core/views.py:3696` (NOT modified — regression only) | | `_build_report_context` | `core/views.py:2416` (adjustments filtered by project `:2484-2490`, labour-cost-by-project `~:2502`) | ## CRITICAL invariants (the spec reviewer will check these) 1. The `pay_type='fixed'` exclusion is applied **ONLY** to attendance + absence pickers (Tasks 3 & 4). It must **NEVER** be added to `PayrollAdjustmentForm` (`forms.py:170`), the Team edit form (`forms.py:427`), or any payroll modal worker picker — managers MUST remain selectable there. 2. `_process_single_payment` (`views.py:3696`) logic is **not modified**. The only money-path edits are: adding `'Salary'` to `ADDITIVE_TYPES`, and a new `Salary` branch in `add_adjustment`. 3. The migration defaults `pay_type` to `'daily'` for every existing worker. 4. **Pay-Immediately Salary nuance (intentional):** the immediate path creates an isolated `PayrollRecord` exactly like the existing `New Loan` pay-immediately sub-branch — it does NOT call `_process_single_payment`. The UNPAID path falls through to the generic pending-adjustment creation and is netted by `_process_single_payment` later when the admin clicks Pay on the dashboard. This satisfies the design intent ("pay now → PayrollRecord + payslip"; "unchecked → pending, nets with deductions/loan-repayments at pay time"). Do not "fix" this to route immediate through `_process_single_payment`. 5. No DB rename — table/model stays `Worker`. UI label only ("Manager / Salaried"). This is a Path-A display-only entry. --- ## Task 1: `Worker.pay_type` field + `is_salaried` property + migration **Files:** - Modify: `core/models.py:37` (Worker class; add field near the other simple fields, add property next to `daily_rate` at `:92-95`) - Create: `core/migrations/0016_worker_pay_type.py` (auto-generated; the latest existing migration is `0015_absence_project`) - Test: `core/tests.py` (new class `ManagerSalariedPayModelTests` at end of file) **Step 1: Write the failing tests** ```python class ManagerSalariedPayModelTests(TestCase): """Worker.pay_type discriminator + is_salaried convenience property.""" def test_pay_type_defaults_to_daily(self): w = Worker.objects.create( name='Daily Dan', id_number='PT-D1', monthly_salary=Decimal('6000.00'), ) self.assertEqual(w.pay_type, 'daily') self.assertFalse(w.is_salaried) def test_pay_type_fixed_is_salaried(self): m = Worker.objects.create( name='Manager Fitz', id_number='PT-M1', monthly_salary=Decimal('40000.00'), pay_type='fixed', ) self.assertEqual(m.pay_type, 'fixed') self.assertTrue(m.is_salaried) ``` **Step 2: Run tests to verify they fail** Run: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.ManagerSalariedPayModelTests -v 2` Expected: FAIL — `Worker() got unexpected keyword 'pay_type'` / `AttributeError: is_salaried`. **Step 3: Implement** In `core/models.py`, inside `class Worker`, add (place the choices + field with the other simple fields, e.g. just after `active = models.BooleanField(...)`): ```python # === PAY TYPE === # 'daily' = normal field worker, paid per logged work day (daily_rate). # 'fixed' = manager / salaried staff: paid a fixed monthly amount via a # 'Salary' PayrollAdjustment, never logged on a WorkLog. See # CLAUDE.md "Manager / Salaried pay" + the Path-A naming note. PAY_TYPE_CHOICES = [ ('daily', 'Daily-rated'), ('fixed', 'Fixed salary'), ] pay_type = models.CharField( max_length=10, choices=PAY_TYPE_CHOICES, default='daily', help_text='Daily-rated workers are logged per day. Fixed-salary ' 'staff (managers) are paid a set monthly amount and are ' 'never added to a work log.', ) ``` Add the property next to `daily_rate` (after it, ~line 96): ```python @property def is_salaried(self): # True for managers / fixed-salary staff (pay_type='fixed'). return self.pay_type == 'fixed' ``` **Step 4: Make + run the migration, then run tests** Run: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py makemigrations core` Expected: creates `core/migrations/0016_worker_pay_type.py` adding `pay_type` with `default='daily'`. Open it and confirm `default='daily'` is present. Run: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.ManagerSalariedPayModelTests -v 2` Expected: PASS (2 tests). **Step 5: Commit** ```bash git add core/models.py core/migrations/0016_worker_pay_type.py core/tests.py git commit -m "feat: add Worker.pay_type discriminator + is_salaried property" ``` --- ## Task 2: Register the `Salary` adjustment type as additive **Files:** - Modify: `core/models.py:289` (`PayrollAdjustment.TYPE_CHOICES`) - Modify: `core/views.py` (`ADDITIVE_TYPES` — find with `grep -n "^ADDITIVE_TYPES" core/views.py`) - Create: `core/migrations/0017_alter_payrolladjustment_type.py` (auto-generated; depends on `0016_worker_pay_type`). **Adding a value to `PayrollAdjustment.TYPE_CHOICES` DOES require a migration in this codebase** — Django's migration autodetector tracks `choices` in field state, so it emits a no-op `AlterField` (choices metadata only; no data or schema change). Precedent: `0012_alter_payrolladjustment_type_display_labels.py` was generated for the exact same kind of `TYPE_CHOICES` edit. Skipping it leaves `makemigrations --check` non-clean — a deploy-hygiene defect. - Test: `core/tests.py` (`ManagerSalariedPayTypeRegistrationTests`) **Step 1: Write the failing tests** ```python class ManagerSalariedPayTypeRegistrationTests(TestCase): def test_salary_is_a_valid_adjustment_type(self): from core.models import PayrollAdjustment type_values = [c[0] for c in PayrollAdjustment.TYPE_CHOICES] self.assertIn('Salary', type_values) def test_salary_is_additive(self): from core.views import ADDITIVE_TYPES self.assertIn('Salary', ADDITIVE_TYPES) ``` **Step 2: Run — Expected:** FAIL (`'Salary'` not in list). **Step 3: Implement** `core/models.py` — append to `PayrollAdjustment.TYPE_CHOICES` (DB value == label, so NO UI-vs-DB drift — see CLAUDE.md). Note: "DB value == label" only means the displayed text equals the stored string; it does **not** mean the change is migration-free — see the migration bullet above. ```python ('Salary', 'Salary'), ``` `core/views.py` — add `'Salary'` to the `ADDITIVE_TYPES` list (it increases net pay). Leave `DEDUCTIVE_TYPES` untouched. Add a short inline comment: `# 'Salary' = manager / fixed-salary monthly pay (additive).` Then generate the required choices migration: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py makemigrations core` Expected: creates `core/migrations/0017_alter_payrolladjustment_type.py` — a single `AlterField` on `payrolladjustment.type` that changes only `choices` (depends on `0016_worker_pay_type`; no data migration, no other model touched). **Step 4: Run — Expected:** PASS (2 tests). Then run the FULL suite to confirm no existing test hard-codes the choices length: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2` Expected: still green (175 now; the migration is a no-op for behavior). Finally confirm a clean tree: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py makemigrations --check --dry-run core` must report "No changes detected" (exit 0). **Step 5: Commit** ```bash git add core/models.py core/views.py core/tests.py \ core/migrations/0017_alter_payrolladjustment_type.py git commit -m "feat: register 'Salary' PayrollAdjustment type as additive" ``` --- ## Task 3: Exclude managers from the daily ATTENDANCE pickers **Files:** - Modify: `core/forms.py:106-109` (AttendanceLogForm supervisor branch), `core/forms.py:114` (admin branch) - Modify: `core/views.py:588` (`_build_team_workers_map` prefetch queryset) - Modify: `core/views.py:791` (attendance cost-estimate JS rates loop) - Test: `core/tests.py` (`ManagerSalariedAttendanceExclusionTests`) **Step 1: Write the failing tests** Mirror the existing attendance/team-filter test setup (see `WorkHistoryTeamFilterTests` / `AbsenceLogViewTests` for fixtures patterns — create an admin user, a supervisor user, a Team, a daily worker, a fixed worker). ```python class ManagerSalariedAttendanceExclusionTests(TestCase): def setUp(self): self.admin = User.objects.create_user('msa_admin', password='x', is_staff=True) self.sup = User.objects.create_user('msa_sup', password='x') self.daily = Worker.objects.create( name='Daily Del', id_number='MSA-D', monthly_salary=Decimal('6000')) self.mgr = Worker.objects.create( name='Mgr Mo', id_number='MSA-M', monthly_salary=Decimal('40000'), pay_type='fixed') self.team = Team.objects.create(name='MSA Team', supervisor=self.sup) self.team.workers.add(self.daily, self.mgr) def test_attendance_admin_picker_excludes_fixed(self): from core.forms import AttendanceLogForm qs = AttendanceLogForm(user=self.admin).fields['workers'].queryset self.assertIn(self.daily, qs) self.assertNotIn(self.mgr, qs) def test_attendance_supervisor_picker_excludes_fixed(self): from core.forms import AttendanceLogForm qs = AttendanceLogForm(user=self.sup).fields['workers'].queryset self.assertIn(self.daily, qs) self.assertNotIn(self.mgr, qs) def test_team_workers_map_excludes_fixed(self): from core.views import _build_team_workers_map m = _build_team_workers_map(self.admin) ids = m.get(self.team.id) or m.get(str(self.team.id)) or [] self.assertIn(self.daily.id, ids) self.assertNotIn(self.mgr.id, ids) ``` **Step 2: Run — Expected:** FAIL (`mgr` present in querysets/map). **Step 3: Implement** Add `.exclude(pay_type='fixed')` to each queryset: - `core/forms.py:106-109` (supervisor branch): ```python self.fields['workers'].queryset = Worker.objects.filter( active=True, teams__in=supervised_teams, ).exclude(pay_type='fixed').distinct() ``` - `core/forms.py:114` (admin branch): ```python self.fields['workers'].queryset = Worker.objects.filter( active=True).exclude(pay_type='fixed') ``` - `core/views.py:588` — the `Prefetch('workers', queryset=Worker.objects .filter(active=True), to_attr='active_workers_cached')`: change the inner queryset to `Worker.objects.filter(active=True).exclude(pay_type='fixed')`. Add a comment: `# Managers (pay_type='fixed') never go on a WorkLog.` - `core/views.py:791` — `for w in Worker.objects.filter(active=True):` → `for w in Worker.objects.filter(active=True).exclude(pay_type='fixed'):` **Step 4: Run — Expected:** PASS (3 tests). **Step 5: Commit** ```bash git add core/forms.py core/views.py core/tests.py git commit -m "feat: exclude fixed-salary managers from attendance pickers" ``` --- ## Task 4: Exclude managers from the ABSENCE pickers **Files:** - Modify: `core/forms.py:662` (`AbsenceLogForm.workers` admin default), `core/forms.py:696-702` (supervisor branch) - Modify: `core/forms.py:842` (`AbsenceEditForm.worker` admin), `core/forms.py:851` (supervisor branch) - Test: `core/tests.py` (`ManagerSalariedAbsenceExclusionTests`) **Step 1: Write the failing tests** ```python class ManagerSalariedAbsenceExclusionTests(TestCase): def setUp(self): self.admin = User.objects.create_user('msab_admin', password='x', is_staff=True) self.sup = User.objects.create_user('msab_sup', password='x') self.daily = Worker.objects.create( name='Abs Daily', id_number='MSAB-D', monthly_salary=Decimal('6000')) self.mgr = Worker.objects.create( name='Abs Mgr', id_number='MSAB-M', monthly_salary=Decimal('40000'), pay_type='fixed') self.team = Team.objects.create(name='MSAB Team', supervisor=self.sup, active=True) self.team.workers.add(self.daily, self.mgr) def test_absencelog_admin_excludes_fixed(self): from core.forms import AbsenceLogForm qs = AbsenceLogForm(user=self.admin).fields['workers'].queryset self.assertIn(self.daily, qs) self.assertNotIn(self.mgr, qs) def test_absencelog_supervisor_excludes_fixed(self): from core.forms import AbsenceLogForm qs = AbsenceLogForm(user=self.sup).fields['workers'].queryset self.assertIn(self.daily, qs) self.assertNotIn(self.mgr, qs) def test_absenceedit_admin_excludes_fixed(self): from core.forms import AbsenceEditForm qs = AbsenceEditForm(user=self.admin).fields['worker'].queryset self.assertNotIn(self.mgr, qs) def test_absenceedit_supervisor_excludes_fixed(self): from core.forms import AbsenceEditForm qs = AbsenceEditForm(user=self.sup).fields['worker'].queryset self.assertNotIn(self.mgr, qs) ``` > If `AbsenceEditForm(user=...)` needs an `instance=` to construct, mirror how > the existing absence-edit tests instantiate it (check > `AbsenceEditDeleteTests` in `core/tests.py`) and adjust the two > `AbsenceEditForm` tests accordingly — the assertion (mgr not in queryset) > stays the same. **Step 2: Run — Expected:** FAIL. **Step 3: Implement** — add `.exclude(pay_type='fixed')` to each of the four querysets (`forms.py:662`, `:696-702`, `:842`, `:851`), preserving any existing `.distinct()` (keep `.exclude(pay_type='fixed')` before `.distinct()`). **Step 4: Run — Expected:** PASS (4 tests). **Step 5: Commit** ```bash git add core/forms.py core/tests.py git commit -m "feat: exclude fixed-salary managers from absence pickers" ``` --- ## Task 5: `add_adjustment` Salary branch + project-required validation **Files:** - Modify: `core/views.py:4247` (add `'Salary'` to `project_required_types`) - Modify: `core/views.py` — new `Salary` branch placed AFTER the `Advance Payment` branch (ends ~`:4386`) and BEFORE the generic "ALL OTHER TYPES" block (`:4388`) - Modify: `core/forms.py:179` (`PayrollAdjustmentForm.clean` `project_required_types` tuple — add `'Salary'`) - Test: `core/tests.py` (`ManagerSalariedPaySalaryAdjustmentTests`) **Step 1: Write the failing tests** ```python 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): resp = 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) ``` > `_date` is the `datetime.date` alias already imported in `core/tests.py` > (~line 1693). If your new class is above that import, add > `from datetime import date as _date` locally or use `datetime.date`. **Step 2: Run — Expected:** FAIL (Salary not project-required; no branch). **Step 3: Implement** `core/forms.py:179` — add `'Salary'`: ```python project_required_types = ('Overtime', 'Bonus', 'Deduction', 'Advance Payment', 'Salary') ``` `core/views.py:4247` — add `'Salary'`: ```python project_required_types = ('Overtime', 'Bonus', 'Deduction', 'Advance Payment', 'Salary') ``` `core/views.py` — insert this branch AFTER the Advance Payment block's `continue` (~`:4386`), BEFORE the generic `# === ALL OTHER TYPES ===` block: ```python # === 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) ``` (The generic block at `:4388-4398` already sets `project=project`, so an unpaid Salary needs no extra code there.) **Step 4: Run — Expected:** PASS (4 tests). Then full suite: `USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2` — green. **Step 5: Commit** ```bash git add core/views.py core/forms.py core/tests.py git commit -m "feat: add_adjustment Salary branch (project-required, pay-now/pending)" ``` --- ## Task 6: Report — per-project "Management / Salaried Cost" + regression guard **Files:** - Modify: `core/views.py:2416` (`_build_report_context`) — add a `salaried_cost_by_project` rollup from `Salary`-typed adjustments - Modify: `core/templates/core/report.html` — render the new line in the per-project section (find the existing "Labour Cost by Project" block) - Test: `core/tests.py` (`ManagerSalariedPayReportTests`) **Step 1: Write the failing tests** The regression test is the important one — it proves daily-worker numbers are byte-for-byte unchanged when a manager + Salary adjustment exist. ```python class ManagerSalariedPayReportTests(TestCase): def setUp(self): self.proj = Project.objects.create(name='Rep Proj') self.daily = Worker.objects.create( name='Rep Daily', id_number='RP-D', monthly_salary=Decimal('6000')) self.start = _date(2026, 5, 1) self.end = _date(2026, 5, 31) wl = WorkLog.objects.create(date=_date(2026, 5, 4), project=self.proj) wl.workers.add(self.daily) def _ctx(self): from core.views import _build_report_context return _build_report_context(self.start, self.end) def test_daily_numbers_unchanged_when_manager_added(self): before = self._ctx() mgr = Worker.objects.create( name='Rep Mgr', id_number='RP-M', monthly_salary=Decimal('40000'), pay_type='fixed') PayrollAdjustment.objects.create( worker=mgr, type='Salary', amount=Decimal('40000.00'), date=_date(2026, 5, 25), project=self.proj) after = self._ctx() # WorkLog-derived figures must be identical with a manager present. self.assertEqual( before.get('total_worker_days'), after.get('total_worker_days')) self.assertEqual( str(before.get('labour_cost_by_project')), str(after.get('labour_cost_by_project'))) def test_salaried_cost_by_project_exposed(self): mgr = Worker.objects.create( name='Rep Mgr2', id_number='RP-M2', monthly_salary=Decimal('40000'), pay_type='fixed') PayrollAdjustment.objects.create( worker=mgr, type='Salary', amount=Decimal('40000.00'), date=_date(2026, 5, 25), project=self.proj) ctx = self._ctx() self.assertIn('salaried_cost_by_project', ctx) total = sum( (row.get('total') or Decimal('0')) for row in ctx['salaried_cost_by_project']) self.assertEqual(total, Decimal('40000.00')) ``` > Adjust the exact context-key names in the assertions to match what > `_build_report_context` actually returns for the labour-cost-by-project > structure (inspect `views.py:~2502` and the keys the function returns near > its `return {...}`). Keep the **intent**: (a) WorkLog-derived keys identical > before/after a manager exists; (b) a new `salaried_cost_by_project` key whose > Salary amounts sum to 40000. **Step 2: Run — Expected:** FAIL (`salaried_cost_by_project` missing). **Step 3: Implement** In `_build_report_context`, after the existing adjustments queryset is built (it is already filtered by `project_id` when `project_ids` is set, ~`:2484-2490`) and near the "Labour Cost by Project" computation (~`:2502`), add a salaried rollup that does NOT touch the WorkLog-derived `labour_cost_by_project`: ```python # --- Management / Salaried Cost by Project (selected period) --- # Managers have no WorkLogs, so their pay never appears in the # WorkLog-derived labour cost above. Surface it as a SEPARATE line so # it is visible + attributed but never merged into daily-worker numbers. salaried_rows = ( adjustments.filter(type='Salary') .values('project__id', 'project__name') .annotate(total=Sum('amount')) .order_by('-total') ) salaried_cost_by_project = [ { 'project_id': r['project__id'], 'project': r['project__name'] or 'Unassigned', 'total': r['total'] or Decimal('0.00'), } for r in salaried_rows ] ``` Add `salaried_cost_by_project` to the context dict that the function returns (find its `return {...}` / `context = {...}` and add the key). `core/templates/core/report.html` — in the per-project section that renders "Labour Cost by Project", add a sibling block iterating `salaried_cost_by_project` titled **"Management / Salaried Cost"**, mirroring the existing block's markup/classes. Keep money formatting consistent (`{{ row.total|money }}` — the project uses the `money` filter, see `format_tags.py`). If you write any multi-line explanatory comment in the template use `{% comment %}...{% endcomment %}`, never multi-line `{# #}`. **Step 4: Run — Expected:** PASS (2 tests). Then: - Full suite green. - Template sanity check: `grep -rn "^\s*{#" core/templates/ | awk -F: '$0 !~ /#}/ {print}'` Expected: **no output**. **Step 5: Commit** ```bash git add core/views.py core/templates/core/report.html core/tests.py git commit -m "feat: per-project Management/Salaried Cost line + regression guard" ``` --- ## Task 7: UI — WorkerForm pay_type, Type indicator, Pay-Salary entry, badge CSS **Files:** - Modify: `core/forms.py:248` (`WorkerForm` — add `pay_type` to `fields` + a Select widget) - Modify: worker templates — `core/templates/core/workers/edit.html` (render the `pay_type` field in the "Personal & Pay" section), `core/templates/core/workers/list.html` and `.../detail.html` (a "Type" indicator: "Daily" vs "Manager / Salaried") - Modify: the Add-Adjustment modal template (the payroll dashboard modal — locate with `grep -rn "name=\"pay_immediately\"\|id=\"adjustmentModal\"\|Add Adjustment" core/templates`) — add a "Salary" `