EVERJUST Payroll & HR — Agent Skill
Operate the Payroll & HR app of a live everjust.app tenant as an agent: read
employees and their contract "versions", pull real attendance (clock in/out), generate
and compute payslips, move a payslip through its state machine, and reconcile the
payslip's Worked Days against actual hours. Everything runs through the platform's
Odoo MCP (search / get / find / count / create / update / call /
describe_model) — see [[everjust-agent-mcp]] for opening the session against the right
tenant DB, and [[everjust-platform]] for the platform invariants (one-DB-per-tenant,
sudo/ACL boundaries, Odoo-19 API surface) you must not break.
This is mostly stock/OCA: core hr + hr_attendance + hr_holidays, and the
OCA payroll module (hr.payslip / hr.salary.rule / hr.payslip.run — NOT the
Odoo Enterprise hr_payroll, which this fork does not have). The everjust twist is a
single small bridge, everjust_payroll_attendance.
Install state — CHECK THIS FIRST (biggest gotcha)
The prompt-era assumption "not installed on either tenant" is stale. Reality (verified
live, 2026-07): install state is uneven across tenants, so before any recipe here,
confirm the module is actually installed on the DB you're on:
# via MCP: find on ir.module.module, or just describe_model('hr.payslip')
find("ir.module.module",
[["name","in",["payroll","hr_attendance","everjust_payroll_attendance"]]],
["name","state"])
Observed:
| Tenant |
hr |
payroll |
hr_attendance |
everjust_payroll_attendance |
Bridge active? |
| headsup |
installed |
installed |
installed |
installed |
YES — full stack |
| burekraft-llc, riftline-labs, trust-works-company, weldon |
installed |
installed |
not installed |
not installed |
no (no attendance) |
| connectdomain |
installed |
not installed |
not installed |
not installed |
no payroll at all |
| tcstartupweek |
not installed |
not installed |
not installed |
not installed |
no HR at all |
Consequences you must respect:
everjust_payroll_attendance is auto_install=True and its depends include
hr_attendance (plus payroll, hr_work_entry). It therefore installs itself
ONLY once hr_attendance is also present. On the four "payroll-but-no-attendance"
tenants the bridge is dormant — payslips there use the stock calendar-based
_compute_worked_days, not attendance.
- On headsup the bridge IS in the live
hr.payslip MRO (verified — the override's
source is the one that runs). But headsup currently has 0 salary structures, 0 salary
rules, 0 attendances, 0 payslips: the app is installed but unconfigured and
un-operated. Computing a payslip with no structure yields an empty payslip (see
Pitfalls). Treat "installed" ≠ "ready to pay".
- If the task targets a tenant where payroll isn't installed, stop — installing a
module is platform-ops (add to the deploy manifest +
-u), not an ORM call you make
from here. See [[everjust-platform]].
Key models (real _names and the fields that matter)
Odoo-19 renamed the contract model: a "contract" is an hr.version, not the old
hr.contract. Every payroll contract_id (on payslip, worked-days line, and elsewhere)
points to hr.version. This is the #1 field-name trap.
| Model |
Role |
Notable fields (introspected live) |
hr.employee |
The person. |
name, user_id(→res.users), company_id(req), department_id, job_id, resource_calendar_id(→resource.calendar, "Working Hours"), attendance_ids(→hr.attendance), hourly_cost(monetary), work_email, active, version_id(req, →hr.version = current contract), current_version_id, version_ids |
hr.version |
The contract (Odoo-19 name). Payroll reads struct_id, schedule_pay, resource_calendar_id, date_start. |
struct_id(→hr.payroll.structure), schedule_pay(monthly/weekly/…), resource_calendar_id, wage, date_start |
hr.attendance |
One clock-in/out span. The bridge's data source. |
employee_id(req), check_in(datetime, req), check_out(datetime), worked_hours(float — computed span the bridge SUMS), in_mode/out_mode(selection: how they clocked), company_id |
hr.payslip |
One employee's slip for one period. |
name, number(ref seq salary.slip), employee_id(req), contract_id(→hr.version), struct_id(→hr.payroll.structure), date_from/date_to(req), state(draft/verify/done/cancel), worked_days_line_ids, line_ids(→hr.payslip.line, computed), input_line_ids, payslip_run_id, credit_note, refunded_id, compute_date, paid |
hr.payslip.worked_days |
The Worked-Days lines — what the bridge writes. |
name, code(req — bridge writes "ATTN"), sequence, number_of_days(float), number_of_hours(float), contract_id(→hr.version), payslip_id |
hr.payslip.line |
Computed money lines (one per salary rule). Read-only; produced by compute_sheet. |
salary_rule_id, code, amount, quantity, rate, total, slip_id, category_id |
hr.payslip.run |
A batch of payslips for a period. |
name, slip_ids(→hr.payslip), date_start/date_end, struct_id, credit_note, state(only draft/close — NOT the payslip's states) |
hr.payroll.structure |
Salary structure = a set of rules. Payslips need one or compute nothing. |
name, code, rule_ids(→hr.salary.rule), parent_id |
hr.salary.rule |
One computation rule (Python/fixed/percentage). |
name, code, category_id, amount_select, amount_python_compute, condition_select |
account.analytic.line |
Timesheets / analytic entries — NOT attendance. Shared with Projects. |
name(req), date(req), unit_amount(float=hours), employee_id, project_id, task_id, company_id(req) |
hr.attendance and account.analytic.line are independent here. This tenant has no
hr_attendance_timesheet-style bridge, so clocking in does not create an analytic
line, and the payroll bridge reads hr.attendance, not timesheets. Analytic lines are
the Projects/timesheet world — see [[everjust-projects]]. Don't reconcile payroll worked
days against account.analytic.line; reconcile against hr.attendance.worked_hours.
How the everjust bridge changes payroll (the one thing that's non-stock)
everjust_payroll_attendance/models/hr_payslip.py inherits hr.payslip and overrides
_compute_worked_days(contract, day_from, day_to). During compute_sheet the OCA
flow calls get_worked_day_lines → _compute_worked_days; the override:
- Searches
hr.attendance for the employee where check_in is within [day_from, day_to] and check_out != False (open/un-clocked-out spans are ignored).
- If no such attendances →
super() → stock calendar-based worked days
(so salaried employees without a clock are unaffected). This fall-through is why the
four attendance-less tenants behave stock even though they run payroll.
- If attendances exist → returns ONE worked-days dict:
name="Attendance", code="ATTN", sequence=1,
number_of_days = count of DISTINCT check-in dates,
number_of_hours = sum(worked_hours), contract_id = contract.id.
So on headsup a computed payslip's worked_days_line_ids will contain an ATTN line
whose hours equal real clocked hours and whose days equal distinct clock-in days — that is
the everjust behavior to expect and to explain. The money lines (line_ids) still come
from the salary structure's rules multiplying against these worked days.
Recipes
Route each through the Odoo MCP against the tenant DB (see [[everjust-agent-mcp]]).
create/update/call are shown as MCP verbs; call invokes a model method.
1. Look up an employee, their contract, and clocked attendance for a period
# Find the employee
emp = find("hr.employee", [["name","ilike","Jordan"]],
["name","work_email","department_id","job_id","hourly_cost",
"version_id","resource_calendar_id"], limit=5)
# The contract is the current hr.version (NOT hr.contract):
get("hr.version", emp[0]["version_id"][0],
["struct_id","schedule_pay","wage","resource_calendar_id","date_start"])
# Real clocked spans in July 2026 (closed spans only — the bridge ignores open ones):
find("hr.attendance",
[["employee_id","=",emp[0]["id"]],
["check_in",">=","2026-07-01 00:00:00"],
["check_in","<=","2026-07-31 23:59:59"],
["check_out","!=",false]],
["check_in","check_out","worked_hours","in_mode"], order="check_in")
# What the payslip WILL show as worked days (mirror the bridge yourself):
# number_of_hours = sum(worked_hours); number_of_days = distinct check_in dates.
2. Generate a payroll batch and compute everyone's slip (the correct path)
Do NOT hand-build payslips row by row. Create a run, then call the OCA
by-employees wizard, which builds each slip via get_payslip_vals (this is where the
attendance bridge fires) and computes them.
# Preconditions: a salary structure with rules must exist, else slips compute empty.
struct = find("hr.payroll.structure", [], ["name"], limit=1) # must be non-empty!
run = create("hr.payslip.run", {
"name": "July 2026 Payroll",
"date_start": "2026-07-01",
"date_end": "2026-07-31",
"struct_id": struct[0]["id"],
})
# Build + compute slips for chosen employees, in the run's context (active_id = run):
emp_ids = [e["id"] for e in find("hr.employee", [["active","=",true]], ["id"])]
wiz = create("hr.payslip.employees", {"employee_ids": [[6,0,emp_ids]]})
call("hr.payslip.employees", "compute_sheet", [[wiz]], {"context": {"active_id": run["id"]}})
# Inspect results — each slip now has worked-days lines (ATTN if attendance existed):
slips = find("hr.payslip", [["payslip_run_id","=",run["id"]]],
["employee_id","state","number","date_from","date_to"])
get("hr.payslip", slips[0]["id"], ["worked_days_line_ids","line_ids"])
find("hr.payslip.worked_days", [["payslip_id","=",slips[0]["id"]]],
["code","name","number_of_days","number_of_hours"]) # look for code == "ATTN"
3. Compute / confirm / cancel a single payslip (its state machine)
hr.payslip.state: draft → verify → done, or → cancel. compute_sheet is
what moves draft → verify AND (re)builds all lines; action_payslip_done re-computes
(unless prevent_compute_on_confirm) then locks to done.
# From a draft slip: compute it (draft -> verify, fills worked_days + money lines)
call("hr.payslip", "compute_sheet", [[slip_id]])
get("hr.payslip", slip_id, ["state","number","line_ids"]) # state == "verify"
# Confirm (verify/draft -> done). Recomputes first unless prevent_compute_on_confirm.
call("hr.payslip", "action_payslip_done", [[slip_id]]) # state == "done"
# Reset a rejected slip back to draft (ONLY works from state == "cancel"):
call("hr.payslip", "action_payslip_draft", [[slip_id]])
# Cancel. A "done" slip can only be cancelled if the payroll.allow_cancel_payslips
# ir.config_parameter is set; otherwise this raises UserError.
call("hr.payslip", "action_payslip_cancel", [[slip_id]])
For bulk state changes across many slips, the OCA wizard hr.payslip.change.state
(change_state_confirm, state ∈ draft/verify/done/cancel, with active_ids in
context) applies the same guarded transitions to a selection.
4. Explain a payslip's Worked Days (attendance vs. calendar reconciliation)
# Pull the worked-days lines and compare to raw attendance for the same window:
wd = find("hr.payslip.worked_days", [["payslip_id","=",slip_id]],
["code","name","number_of_days","number_of_hours"])
slip = get("hr.payslip", slip_id, ["employee_id","date_from","date_to"])
att = find("hr.attendance",
[["employee_id","=",slip["employee_id"][0]],
["check_in",">=", slip["date_from"]+" 00:00:00"],
["check_in","<=", slip["date_to"]+" 23:59:59"],
["check_out","!=",false]],
["check_in","worked_hours"])
# If wd has a code=="ATTN" line: number_of_hours == sum(att.worked_hours) and
# number_of_days == count(distinct check_in dates). That's the bridge working.
# If wd has NO "ATTN" line (only calendar codes like WORK100): the employee had NO
# closed attendance in the window, so the stock calendar fallback ran. That is
# EXPECTED for salaried/un-clocked employees — not a bug.
5. Refund (credit-note) a confirmed payslip
# Creates a negative "credit note" copy of a done slip (for corrections):
call("hr.payslip", "refund_sheet", [[slip_id]])
# The new slip has credit_note=True and refunded_id pointing at the original.
Pitfalls (everjust-specific and Odoo-19-specific)
Check install state before you do anything. Only headsup has the full stack
live; several tenants have payroll but not hr_attendance (bridge dormant → stock
calendar worked days), and some have no payroll/HR at all. Recipes here silently no-op
or error on the wrong tenant. Verify via ir.module.module / describe_model first.
"Contract" = hr.version, not hr.contract. Odoo 19 replaced the contract model.
Every payroll contract_id (payslip, worked-days line) and the employee's
version_id/current_version_id point at hr.version. Searching hr.contract will
fail or return nothing. The employee's active contract is version_id.
No structure → empty payslip. compute_sheet multiplies worked days by the salary
structure's rules. headsup currently has 0 hr.payroll.structure / 0
hr.salary.rule, so computing there yields a slip with worked-days lines but no
money line_ids. Ensure a struct_id with rules exists (on the run and/or employee
contract) before expecting pay amounts.
The bridge only counts CLOSED attendance. _compute_worked_days requires
check_out != False. An employee still clocked in (open span) contributes zero
hours to the payslip. If worked hours look short, check for open hr.attendance rows.
No attendance → stock calendar fallback, and that's intentional. A missing ATTN
line does NOT mean the bridge is broken; it means the employee had no closed attendance
in the period, so OCA's calendar-based computation ran (salaried staff on a fixed
resource_calendar_id). Don't "fix" this by forcing an ATTN line.
hr.attendance ≠ account.analytic.line. Attendance (clock) and analytic
timesheets are separate systems on this fork — clocking in creates no analytic line.
Payroll reads attendance; Projects/timesheets read analytic lines
([[everjust-projects]]). Reconcile payroll against hr.attendance.worked_hours only.
hr.payslip.run.state is draft/close — different axis from the slip. The run
is a container; close/draft is just its own batch status. The pay workflow
(draft→verify→done→cancel) lives on each hr.payslip. Don't confuse them, and
don't look for action_payslip_* methods on the run (there are none) — generate slips
via the hr.payslip.employees wizard.
line_ids and worked_days_line_ids are read-only / computed. Don't write pay
lines by hand; they're rebuilt every compute_sheet (which first unlinks the old
lines). To change pay, change inputs, the contract wage, or the structure — then
recompute.
action_payslip_done recomputes by default. Unless prevent_compute_on_confirm
is set, confirming re-runs compute_sheet — so late attendance edits made after the
first compute WILL be picked up at confirm time. If you need a frozen snapshot, confirm
with the without_compute_sheet context or set prevent_compute_on_confirm.
Everything is per-company_id and role-bounded. Payslips, attendance, and
employees are scoped to the tenant company, and the MCP acts as a specific Odoo user —
a limited role may not see payroll data at all (record rules, not a bug). Confirm the
tenant DB and your acting user before mutating pay (see [[everjust-platform]] /
[[everjust-agent-mcp]]).
everjust_brand_hr_attendance is cosmetic only. The sibling auto-install module
just white-labels the attendance kiosk "Powered by" footer — it changes no data, no
fields, no computation. Never treat it as part of the payroll logic.
1---2name: everjust-payroll-hr3description: EVERJUST Payroll & HR — Agent Skill4---56# EVERJUST Payroll & HR — Agent Skill78Operate the **Payroll & HR** app of a live everjust.app tenant as an agent: read9employees and their contract "versions", pull real attendance (clock in/out), generate10and compute payslips, move a payslip through its state machine, and reconcile the11payslip's **Worked Days** against actual hours. Everything runs through the platform's12Odoo MCP (`search` / `get` / `find` / `count` / `create` / `update` / `call` /13`describe_model`) — see [[everjust-agent-mcp]] for opening the session against the right14tenant DB, and [[everjust-platform]] for the platform invariants (one-DB-per-tenant,15sudo/ACL boundaries, Odoo-19 API surface) you must not break.1617This is mostly **stock/OCA**: core `hr` + `hr_attendance` + `hr_holidays`, and the18**OCA `payroll`** module (`hr.payslip` / `hr.salary.rule` / `hr.payslip.run` — NOT the19Odoo Enterprise `hr_payroll`, which this fork does not have). The **everjust twist** is a20single small bridge, `everjust_payroll_attendance`.2122## Install state — CHECK THIS FIRST (biggest gotcha)2324The prompt-era assumption "not installed on either tenant" is stale. Reality (verified25live, 2026-07): install state is **uneven across tenants**, so before any recipe here,26confirm the module is actually installed on the DB you're on:2728```29# via MCP: find on ir.module.module, or just describe_model('hr.payslip')30find("ir.module.module",31 [["name","in",["payroll","hr_attendance","everjust_payroll_attendance"]]],32 ["name","state"])33```3435Observed:3637| Tenant | `hr` | `payroll` | `hr_attendance` | `everjust_payroll_attendance` | Bridge active? |38|---|---|---|---|---|---|39| **headsup** | installed | installed | installed | **installed** | **YES** — full stack |40| burekraft-llc, riftline-labs, trust-works-company, weldon | installed | installed | *not* installed | not installed | no (no attendance) |41| connectdomain | installed | *not* installed | *not* installed | not installed | no payroll at all |42| tcstartupweek | *not* installed | not installed | not installed | not installed | no HR at all |4344Consequences you must respect:45- **`everjust_payroll_attendance` is `auto_install=True`** and its `depends` include46 **`hr_attendance`** (plus `payroll`, `hr_work_entry`). It therefore installs itself47 ONLY once `hr_attendance` is also present. On the four "payroll-but-no-attendance"48 tenants the bridge is dormant — payslips there use the **stock calendar-based**49 `_compute_worked_days`, not attendance.50- On **headsup** the bridge IS in the live `hr.payslip` MRO (verified — the override's51 source is the one that runs). But headsup currently has **0 salary structures, 0 salary52 rules, 0 attendances, 0 payslips**: the app is installed but *unconfigured and53 un-operated*. Computing a payslip with no structure yields an empty payslip (see54 Pitfalls). Treat "installed" ≠ "ready to pay".55- If the task targets a tenant where payroll isn't installed, **stop** — installing a56 module is platform-ops (add to the deploy manifest + `-u`), not an ORM call you make57 from here. See [[everjust-platform]].5859## Key models (real `_name`s and the fields that matter)6061Odoo-19 renamed the contract model: **a "contract" is an `hr.version`**, not the old62`hr.contract`. Every payroll `contract_id` (on payslip, worked-days line, and elsewhere)63points to `hr.version`. This is the #1 field-name trap.6465| Model | Role | Notable fields (introspected live) |66|---|---|---|67| `hr.employee` | The person. | `name`, `user_id`(→res.users), `company_id`(req), `department_id`, `job_id`, `resource_calendar_id`(→resource.calendar, "Working Hours"), `attendance_ids`(→hr.attendance), `hourly_cost`(monetary), `work_email`, `active`, **`version_id`(req, →hr.version = current contract)**, `current_version_id`, `version_ids` |68| `hr.version` | **The contract** (Odoo-19 name). Payroll reads `struct_id`, `schedule_pay`, `resource_calendar_id`, `date_start`. | `struct_id`(→hr.payroll.structure), `schedule_pay`(monthly/weekly/…), `resource_calendar_id`, `wage`, `date_start` |69| `hr.attendance` | One clock-in/out span. **The bridge's data source.** | `employee_id`(req), `check_in`(datetime, req), `check_out`(datetime), **`worked_hours`(float — computed span the bridge SUMS)**, `in_mode`/`out_mode`(selection: how they clocked), `company_id` |70| `hr.payslip` | One employee's slip for one period. | `name`, `number`(ref seq `salary.slip`), `employee_id`(req), `contract_id`(→hr.version), `struct_id`(→hr.payroll.structure), `date_from`/`date_to`(req), **`state`**(`draft`/`verify`/`done`/`cancel`), `worked_days_line_ids`, `line_ids`(→hr.payslip.line, computed), `input_line_ids`, `payslip_run_id`, `credit_note`, `refunded_id`, `compute_date`, `paid` |71| `hr.payslip.worked_days` | **The Worked-Days lines** — what the bridge writes. | `name`, `code`(req — bridge writes **`"ATTN"`**), `sequence`, `number_of_days`(float), `number_of_hours`(float), `contract_id`(→hr.version), `payslip_id` |72| `hr.payslip.line` | Computed money lines (one per salary rule). Read-only; produced by `compute_sheet`. | `salary_rule_id`, `code`, `amount`, `quantity`, `rate`, `total`, `slip_id`, `category_id` |73| `hr.payslip.run` | A **batch** of payslips for a period. | `name`, `slip_ids`(→hr.payslip), `date_start`/`date_end`, `struct_id`, `credit_note`, **`state`**(only `draft`/`close` — NOT the payslip's states) |74| `hr.payroll.structure` | Salary structure = a set of rules. Payslips need one or compute nothing. | `name`, `code`, `rule_ids`(→hr.salary.rule), `parent_id` |75| `hr.salary.rule` | One computation rule (Python/fixed/percentage). | `name`, `code`, `category_id`, `amount_select`, `amount_python_compute`, `condition_select` |76| `account.analytic.line` | **Timesheets / analytic entries** — NOT attendance. Shared with Projects. | `name`(req), `date`(req), `unit_amount`(float=hours), `employee_id`, `project_id`, `task_id`, `company_id`(req) |7778**`hr.attendance` and `account.analytic.line` are independent here.** This tenant has no79`hr_attendance_timesheet`-style bridge, so clocking in does **not** create an analytic80line, and the payroll bridge reads **`hr.attendance`**, not timesheets. Analytic lines are81the Projects/timesheet world — see [[everjust-projects]]. Don't reconcile payroll worked82days against `account.analytic.line`; reconcile against `hr.attendance.worked_hours`.8384## How the everjust bridge changes payroll (the one thing that's non-stock)8586`everjust_payroll_attendance/models/hr_payslip.py` inherits `hr.payslip` and overrides87**`_compute_worked_days(contract, day_from, day_to)`**. During `compute_sheet` the OCA88flow calls `get_worked_day_lines` → `_compute_worked_days`; the override:89901. Searches `hr.attendance` for the employee where `check_in` is within `[day_from,91 day_to]` **and `check_out != False`** (open/un-clocked-out spans are ignored).922. If **no** such attendances → `super()` → **stock calendar-based** worked days93 (so salaried employees without a clock are unaffected). This fall-through is why the94 four attendance-less tenants behave stock even though they run payroll.953. If attendances exist → returns ONE worked-days dict:96 `name="Attendance"`, `code="ATTN"`, `sequence=1`,97 `number_of_days = count of DISTINCT check-in dates`,98 `number_of_hours = sum(worked_hours)`, `contract_id = contract.id`.99100So on headsup a computed payslip's `worked_days_line_ids` will contain an **`ATTN`** line101whose hours equal real clocked hours and whose days equal distinct clock-in days — that is102the everjust behavior to expect and to explain. The *money* lines (`line_ids`) still come103from the salary structure's rules multiplying against these worked days.104105## Recipes106107Route each through the Odoo MCP against the tenant DB (see [[everjust-agent-mcp]]).108`create`/`update`/`call` are shown as MCP verbs; `call` invokes a model method.109110### 1. Look up an employee, their contract, and clocked attendance for a period111112```113# Find the employee114emp = find("hr.employee", [["name","ilike","Jordan"]],115 ["name","work_email","department_id","job_id","hourly_cost",116 "version_id","resource_calendar_id"], limit=5)117# The contract is the current hr.version (NOT hr.contract):118get("hr.version", emp[0]["version_id"][0],119 ["struct_id","schedule_pay","wage","resource_calendar_id","date_start"])120# Real clocked spans in July 2026 (closed spans only — the bridge ignores open ones):121find("hr.attendance",122 [["employee_id","=",emp[0]["id"]],123 ["check_in",">=","2026-07-01 00:00:00"],124 ["check_in","<=","2026-07-31 23:59:59"],125 ["check_out","!=",false]],126 ["check_in","check_out","worked_hours","in_mode"], order="check_in")127# What the payslip WILL show as worked days (mirror the bridge yourself):128# number_of_hours = sum(worked_hours); number_of_days = distinct check_in dates.129```130131### 2. Generate a payroll batch and compute everyone's slip (the correct path)132133Do NOT hand-build payslips row by row. Create a **run**, then call the OCA134**by-employees wizard**, which builds each slip via `get_payslip_vals` (this is where the135attendance bridge fires) and computes them.136137```138# Preconditions: a salary structure with rules must exist, else slips compute empty.139struct = find("hr.payroll.structure", [], ["name"], limit=1) # must be non-empty!140141run = create("hr.payslip.run", {142 "name": "July 2026 Payroll",143 "date_start": "2026-07-01",144 "date_end": "2026-07-31",145 "struct_id": struct[0]["id"],146})147148# Build + compute slips for chosen employees, in the run's context (active_id = run):149emp_ids = [e["id"] for e in find("hr.employee", [["active","=",true]], ["id"])]150wiz = create("hr.payslip.employees", {"employee_ids": [[6,0,emp_ids]]})151call("hr.payslip.employees", "compute_sheet", [[wiz]], {"context": {"active_id": run["id"]}})152153# Inspect results — each slip now has worked-days lines (ATTN if attendance existed):154slips = find("hr.payslip", [["payslip_run_id","=",run["id"]]],155 ["employee_id","state","number","date_from","date_to"])156get("hr.payslip", slips[0]["id"], ["worked_days_line_ids","line_ids"])157find("hr.payslip.worked_days", [["payslip_id","=",slips[0]["id"]]],158 ["code","name","number_of_days","number_of_hours"]) # look for code == "ATTN"159```160161### 3. Compute / confirm / cancel a single payslip (its state machine)162163`hr.payslip.state`: **`draft` → `verify` → `done`**, or → `cancel`. `compute_sheet` is164what moves `draft → verify` AND (re)builds all lines; `action_payslip_done` re-computes165(unless `prevent_compute_on_confirm`) then locks to `done`.166167```168# From a draft slip: compute it (draft -> verify, fills worked_days + money lines)169call("hr.payslip", "compute_sheet", [[slip_id]])170get("hr.payslip", slip_id, ["state","number","line_ids"]) # state == "verify"171172# Confirm (verify/draft -> done). Recomputes first unless prevent_compute_on_confirm.173call("hr.payslip", "action_payslip_done", [[slip_id]]) # state == "done"174175# Reset a rejected slip back to draft (ONLY works from state == "cancel"):176call("hr.payslip", "action_payslip_draft", [[slip_id]])177178# Cancel. A "done" slip can only be cancelled if the payroll.allow_cancel_payslips179# ir.config_parameter is set; otherwise this raises UserError.180call("hr.payslip", "action_payslip_cancel", [[slip_id]])181```182For bulk state changes across many slips, the OCA wizard `hr.payslip.change.state`183(`change_state_confirm`, `state` ∈ `draft`/`verify`/`done`/`cancel`, with `active_ids` in184context) applies the same guarded transitions to a selection.185186### 4. Explain a payslip's Worked Days (attendance vs. calendar reconciliation)187188```189# Pull the worked-days lines and compare to raw attendance for the same window:190wd = find("hr.payslip.worked_days", [["payslip_id","=",slip_id]],191 ["code","name","number_of_days","number_of_hours"])192slip = get("hr.payslip", slip_id, ["employee_id","date_from","date_to"])193att = find("hr.attendance",194 [["employee_id","=",slip["employee_id"][0]],195 ["check_in",">=", slip["date_from"]+" 00:00:00"],196 ["check_in","<=", slip["date_to"]+" 23:59:59"],197 ["check_out","!=",false]],198 ["check_in","worked_hours"])199# If wd has a code=="ATTN" line: number_of_hours == sum(att.worked_hours) and200# number_of_days == count(distinct check_in dates). That's the bridge working.201# If wd has NO "ATTN" line (only calendar codes like WORK100): the employee had NO202# closed attendance in the window, so the stock calendar fallback ran. That is203# EXPECTED for salaried/un-clocked employees — not a bug.204```205206### 5. Refund (credit-note) a confirmed payslip207208```209# Creates a negative "credit note" copy of a done slip (for corrections):210call("hr.payslip", "refund_sheet", [[slip_id]])211# The new slip has credit_note=True and refunded_id pointing at the original.212```213214## Pitfalls (everjust-specific and Odoo-19-specific)2152161. **Check install state before you do anything.** Only **headsup** has the full stack217 live; several tenants have `payroll` but not `hr_attendance` (bridge dormant → stock218 calendar worked days), and some have no payroll/HR at all. Recipes here silently no-op219 or error on the wrong tenant. Verify via `ir.module.module` / `describe_model` first.2202212. **"Contract" = `hr.version`, not `hr.contract`.** Odoo 19 replaced the contract model.222 Every payroll `contract_id` (payslip, worked-days line) and the employee's223 `version_id`/`current_version_id` point at `hr.version`. Searching `hr.contract` will224 fail or return nothing. The employee's *active* contract is `version_id`.2252263. **No structure → empty payslip.** `compute_sheet` multiplies worked days by the salary227 structure's rules. headsup currently has **0 `hr.payroll.structure` / 0228 `hr.salary.rule`**, so computing there yields a slip with worked-days lines but **no229 money `line_ids`**. Ensure a `struct_id` with rules exists (on the run and/or employee230 contract) before expecting pay amounts.2312324. **The bridge only counts CLOSED attendance.** `_compute_worked_days` requires233 `check_out != False`. An employee still clocked in (open span) contributes **zero**234 hours to the payslip. If worked hours look short, check for open `hr.attendance` rows.2352365. **No attendance → stock calendar fallback, and that's intentional.** A missing `ATTN`237 line does NOT mean the bridge is broken; it means the employee had no closed attendance238 in the period, so OCA's calendar-based computation ran (salaried staff on a fixed239 `resource_calendar_id`). Don't "fix" this by forcing an ATTN line.2402416. **`hr.attendance` ≠ `account.analytic.line`.** Attendance (clock) and analytic242 timesheets are separate systems on this fork — clocking in creates no analytic line.243 Payroll reads attendance; Projects/timesheets read analytic lines244 ([[everjust-projects]]). Reconcile payroll against `hr.attendance.worked_hours` only.2452467. **`hr.payslip.run.state` is `draft`/`close` — different axis from the slip.** The run247 is a container; `close`/`draft` is just its own batch status. The pay workflow248 (`draft`→`verify`→`done`→`cancel`) lives on each `hr.payslip`. Don't confuse them, and249 don't look for `action_payslip_*` methods on the run (there are none) — generate slips250 via the `hr.payslip.employees` wizard.2512528. **`line_ids` and `worked_days_line_ids` are read-only / computed.** Don't write pay253 lines by hand; they're rebuilt every `compute_sheet` (which first `unlink`s the old254 lines). To change pay, change inputs, the contract wage, or the structure — then255 recompute.2562579. **`action_payslip_done` recomputes by default.** Unless `prevent_compute_on_confirm`258 is set, confirming re-runs `compute_sheet` — so late attendance edits made after the259 first compute WILL be picked up at confirm time. If you need a frozen snapshot, confirm260 with the `without_compute_sheet` context or set `prevent_compute_on_confirm`.26126210. **Everything is per-`company_id` and role-bounded.** Payslips, attendance, and263 employees are scoped to the tenant company, and the MCP acts as a specific Odoo user —264 a limited role may not see payroll data at all (record rules, not a bug). Confirm the265 tenant DB and your acting user before mutating pay (see [[everjust-platform]] /266 [[everjust-agent-mcp]]).26726811. **`everjust_brand_hr_attendance` is cosmetic only.** The sibling auto-install module269 just white-labels the attendance kiosk "Powered by" footer — it changes no data, no270 fields, no computation. Never treat it as part of the payroll logic.