Skip to content

fix(payroll): resolve every salary slip field in component formulas - #5146

Open
logarithmone1128 wants to merge 2 commits into
frappe:developfrom
logarithmone1128:fix/salary-slip-field-eval-context
Open

fix(payroll): resolve every salary slip field in component formulas#5146
logarithmone1128 wants to merge 2 commits into
frappe:developfrom
logarithmone1128:fix/salary-slip-field-eval-context

Conversation

@logarithmone1128

Copy link
Copy Markdown

Fixes #5057. Also reported as #5141.

A salary component formula may reference any Salary Slip field, but the evaluation
context seeded only 21 hard-coded totals. Referencing any other slip field fails at
salary slip creation with:

Error while evaluating the Salary Structure <name> at row N.
Error: name 'custom_worked_hours' is not defined
Hint: This error can be due to missing or deleted field.

The hint is misleading — the field exists, and so does its column.

Details

SALARY_SLIP_EVAL_DEFAULTS in hrms/payroll/utils.py listed 21 fieldnames, all
totals. It became load-bearing when component evaluation moved to a pre-pass on the
Salary Structure Assignment, which runs before any slip exists — so the pre-pass had
to hand-seed seven more fields (start_date, end_date, payment_days,
total_working_days, leave_without_pay, absent_days, unmarked_days) for
exactly this reason. Its comment named the hazard but only for the fields it needed.

That leaves two incomplete lists and 34 of the doctype's own value-bearing fields
uncovered. It is not only custom fields: base_gross_pay, exchange_rate,
base_net_pay and the loan fields this app installs itself
(total_principal_amount, via get_salary_slip_loan_fields) all raise the same
NameError today.

The intended contract is already written down in this repo, in
test_get_evaluated_components_resolves_salary_slip_fields:

SSA evaluation has no slip, so it must mirror a full-cycle preview slip's field
set; otherwise the formula raises a NameError on the missing slip field.

A literal list cannot mirror a field set that includes custom fields.

Change

Replace the literal with get_salary_slip_eval_defaults(), which derives the
defaults from frappe.get_meta("Salary Slip"): 0 for numeric fieldtypes, ""
otherwise, skipping layout and table fieldtypes. frappe.get_meta is already cached
and invalidated on custom field changes, so no separate cache is introduced.

The SSA pre-pass keeps seeding the period values — those are needed for correct
full-cycle proration, not just to avoid a NameError — and its comment is updated to
say so.

Compatibility

The derived map is a strict superset of the previous literal: all 21 fieldnames are
real Salary Slip fields of type Currency or Float, so each still defaults to 0.
Precedence in get_component_eval_context is untouched — assignment fields, then
employee fields, override these defaults, as do the pre-pass period values and the
slip's own as_dict() overlay. So a formula that resolved before resolves to the
same value; only formulas that previously raised now evaluate.

Verification

  • New test_get_evaluated_components_resolves_slip_fields_outside_hardcoded_set
    covers a slip field outside both lists (base_gross_pay), which is the case that
    fails on develop.
  • New test_salary_slip_eval_defaults_cover_every_slip_field asserts every numeric
    slip field is seeded and that table fieldtypes are excluded, so the coverage
    cannot silently regress to a literal list again.
  • Verified offline against the real doctype metadata: 64 fields seeded vs 21; all 21
    previous entries preserved as 0; layout and table fieldtypes excluded; the
    reported custom_worked_hours * 100 evaluates; and assignment, employee, and
    explicit period values still win over the defaults.
  • ruff check and ruff format --check clean on all three files.

I could not run the integration suite locally (no bench), so the two new tests have
not been executed — happy to adjust if CI disagrees.

One note: #5105 also edits get_component_eval_context, one line below this change.
It touches the employee lookup rather than the defaults, so there is no overlap in
intent, but whichever lands second will need a trivial rebase.

@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported Long Int failure is fixed, and no blocking failure remains.

Reviews (3): Last reviewed commit: "fix(payroll): classify slip fieldtypes w..." | Re-trigger Greptile

Comment thread hrms/payroll/utils.py Outdated
@logarithmone1128

Copy link
Copy Markdown
Author

Update on the verification caveat in the description: the two new tests have now been run on a real bench, so that gap is closed.

Environment: frappe and erpnext on version-16, hrms at 51a82ff from this branch, MariaDB 10.8.

With the change — the whole module passes:

Ran 10 tests in 4.756s

OK

Reverting only get_salary_slip_eval_defaults() back to the hard-coded list, on the same bench, both new tests fail — and the integration one fails with exactly the error from #5057:

frappe.exceptions.ValidationError: Error while evaluating the Salary Structure
SSA Test Unseeded Field Structure at row 1.
Error: name 'base_gross_pay' is not defined
Hint: This error can be due to missing or deleted field.

Ran 1 test in 0.522s
FAILED (errors=1)
AssertionError: 'exchange_rate' not found in {'gross_pay': 0, 'net_pay': 0, ... 'total_income_tax': 0}

Ran 1 test in 0.007s
FAILED (failures=1)

So the tests do fail against the current behaviour rather than passing either way, and base_gross_pay and exchange_rate are both standard Salary Slip fields — the gap is not limited to custom fields.

Note the CI, Linters, Patch and Documentation Required workflows on this PR are all sitting at action_required, since first-time contributor runs need maintainer approval. Happy to rebase whenever suits you.

A salary component formula may reference any Salary Slip field, but the
evaluation context seeded only 21 hard-coded totals. Any other slip field
raised "name is not defined" with a hint pointing at a missing or deleted
field, even though the field exists.

This surfaced when component evaluation moved to a pre-pass on the Salary
Structure Assignment, which runs before a slip exists: the pre-pass had to
hand-seed seven more period fields for the same reason, leaving two
incomplete lists and no coverage for custom fields or for standard fields
such as base_gross_pay.

Derive the defaults from the Salary Slip meta instead. The result is a
superset of the previous list, so every field that resolved before still
resolves to the same 0, and precedence is unchanged: assignment fields,
employee fields, the pre-pass period values, and the slip's own values all
still override these defaults.
The local numeric set omitted Long Int, so a custom Long Int field was
seeded as a string and broke arithmetic formulas -- the same failure mode
as the hard-coded fieldname list this fixes.

Use frappe.model.numeric_fieldtypes and no_value_fields instead of
maintaining either set here. The no-value set also gains Attachment
Gallery, which is present upstream on develop but was missing locally.
@logarithmone1128
logarithmone1128 force-pushed the fix/salary-slip-field-eval-context branch from 51a82ff to c173ff9 Compare August 27, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Salary structure formulas referencing a Salary Slip field break on 16.14+ with NameError

1 participant