Skip to content

[18.0][MIG] ni_patient: Migration to 18.0 - #154

Merged
piruin merged 84 commits into
nirun-life:18.0from
wasud:18.0-mig-p1-patient
Oct 21, 2026
Merged

piruin merged 84 commits into
nirun-life:18.0from
wasud:18.0-mig-p1-patient

Conversation

@wasud

@wasud wasud commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Decisions to review

  • Labels (checklog): Odoo 18 warns when two fields share a label, and checklog-odoo fails CI on warnings. Our own fields get distinct labels:

    • ni.encounter: name keeps "Encounter No." and identifier becomes "Encounter Identifier" (the form header still reads "Encounter No."), location_history_count "Location Moves", participant_id "Current Participant", show_chief_complaint/show_participant "Show …".
    • ni.patient.encounter_id "Current Encounter".
    • res.partner.patient "Is a Patient".
    • ni.encounter.class.sequence_id "Identifier Sequence".
    • ni.location.encounter_active_* "Active Encounter(s)…".

    The Thai translations of these labels are distinct too: รหัสรับบริการ / เลขที่รับบริการ / รหัสรับบริการปัจจุบัน, รายการ/จำนวนรับบริการปัจจุบัน, ผู้ดูแลปัจจุบัน / แสดงผู้ดูแล, แสดงอาการสำคัญ, จำนวนการย้ายสถานที่. The one clash that isn't ours (res.users birthdate from partner_age vs hr's birthday, both "Date of Birth") gets a precise ignore line in checklog-odoo.cfg.

  • Partner search: res.partner search now ORs core's _search_display_name with exact phone/mobile/identification number. Up to 17 it replaced core's partner name search; now email/ref/VAT also match, as [MIG 19.0][P1-2] ni_patient #137 recommends.

What changed

  • ni_patient replayed from the 17.0 PR branch, then [MIG] ni_patient: Migration to 18.0.
  • Migrator: tree → list and view_mode; removed migrations/17.0.1.0.0 (OCA keeps only the current version's scripts).
  • Views:
    • <chatter/> in the patient and encounter forms.
    • Encounter kanban rewritten as an 18 card (highlight_color, menu template with kanban_color_picker, footer).
    • Patient kanban card restructured to aside/main.
    • The kanban button context uses id instead of active_id.
  • Python:
    • _has_cycle() with odoo.exceptions.ValidationError (location, encounter class).
    • has_access("read") in the portal controller.
    • Cron without numbercall/doall.
    • Plain strings in _sql_constraints (_() at import time warns in 18).
    • Lazy logging and f-strings (newer ruff).
  • Computes: stored and non-stored fields split into separate methods (18 warns otherwise): ni.patient encounter count/start, res.partner.patient_id, ni.location active encounters.
  • ni.encounter.display_age: keeps showing the age at encounter start, through its own _compute_display_age (partner_age split that compute in [18.0][MIG] ni_period, ni_identifier, ni_coding, partner_age, partner_gender, partner_religion, uom_alias: Migration to 18.0 #148).
  • ni.encounter._check_birthdate: disabled; birthdate is a read-only related field validated on the patient.
  • JS: /** @odoo-module **/ dropped.
  • Tests: the duplicate-patient test asserts IntegrityError under mute_logger("odoo.sql_db") (its expected SQL error would fail checklog).
  • oe_structure id in the summary report; README and th.po updated.

Why

Part of #137 (P1-2, epic #131).

Migration notes

  • Effort: L.
  • What the migrator did: list views only.
  • Surprises:
    • 18 checklog turns many deprecation/label warnings into CI failures.
    • The kanban card cannot evaluate Python % in t-att-src (it rendered NaN), so it uses t-attf-src.
    • The kanban button active_id fails view validation.

How tested

  • Unit/integration tests: 32 tests pass on 18; the log has no WARNING/ERROR lines (checklog equivalent).
  • Manual check (browser, Odoo 18 with demo data):
    • patient kanban (new card layout), list, and Archive from the list opening the archive wizard;
    • encounter kanban (patient image now loads) and form (chatter, age at encounter);
    • portal /my/home entry and /my/patients.
  • pre-commit run -a clean.

Documentation

  • Module README.md updated (partner search, computed fields)

Notes for reviewer

@wasud wasud mentioned this pull request Oct 12, 2026
5 tasks done
@wasud
wasud force-pushed the 18.0-mig-p1-patient branch from 7ee134d to 021552d Compare October 14, 2026 01:02
@wasud wasud mentioned this pull request Oct 14, 2026
8 tasks done
@wasud
wasud force-pushed the 18.0-mig-p1-patient branch 2 times, most recently from e123b96 to 6053884 Compare October 14, 2026 01:39
@wasud wasud mentioned this pull request Oct 14, 2026
4 tasks done
piruin added 24 commits October 16, 2026 09:25
[FIX] ni_patient: lazy error

[IMP] ni_patient: add 'user_id' for encounter_participant

[IMP] ni_patient: Add chief complaint

[IMP] ni_patient: Remove participant page

[IMP] ni_patient: No duplicate encounter

[IMP] ni_patient: use `color_picker`

[IMP] ni_patient: lock state for CC

[IMP] ni_patient: better CC
[IMP] ni_patient: rename `ni.encounter.cls` to `ni.encounter.class`

[IMP] ni_patient: check datetime instead of date on every 'ni.patient.res'

[IMP] ni_patient: priority value as char and show as badge on list view

[MIG] ni_patient: cascade on delete Encounter

[FIX] ni_patient: migration error

[IMP] ni_patient_birthdate_est: `Medical` category
[IMP] ni_patient: implement create in batch

[IMP] ni_patient: fix type
…d model

-fix typo of priority's routine
-discharge wizard call action_close() on encounter
[IMP] ni_patient: set name attribute for oe_chatter (xml)

[IMP] ni_patient: location history as server action
bestiing and others added 23 commits October 16, 2026 09:25
[IMP] ni_comminity_care: Area Manager
[ADD] ni_community_care_attendance
[IMP] ni_patient: add Patient Archive Wizard
* [IMP] ni_patient,ni_community_care: update ni_patient_view_search

* [IMP] ni_community_care: update filter my_area_employees

* [IMP] ni_community_care: update field my_area
- Fix _safe_get_parent() returning partner_id instead of parent_id
- Fix action_abort() using in operator on string instead of = match
- Fix action_revoked() filtering wrong state (in-progress doesn't exist
  on requests); now correctly targets draft/active records
- Add state field with color decorations to event and request tree views
- Add Timeline menu section under Encounter app (Events + Requests)
- Add tests for workflow event/request creation, replacement tracking,
  action_resource, and garbage_collect behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Took 20 minutes
Add ni.workflow.line (_auto=False SQL UNION view) merging ni.workflow.event
and ni.workflow.request into a single patient timeline. Add kanban + tree +
search views for all three models with consistent badge/avatar styling and
a shared Thai translation update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Took 1 hour 23 minutes

Took 18 minutes

Took 6 minutes
…ife#97)

- detect-secrets hook scans every commit for passwords, tokens, API keys
- .secrets.baseline captures known false positives (CI postgres/test creds)
- CLAUDE.md: explicit rule — no hardcoded credentials, use env vars
…ght cutoff (nirun-life#103)

cron_auto_close() computed the "today" cutoff from fields.Datetime.now(),
which is UTC-naive, so the midnight-offset day boundary was always UTC
midnight regardless of the encounter's company. For companies running
ahead of UTC (e.g. Thailand, UTC+7), an encounter that had already been
open for a full local day could stay open for up to 7 more extra hours,
then close abruptly whenever the UTC date happened to roll over -
explaining reports that the cron "sometimes works, sometimes doesn't".

Derive the day boundary from each encounter's company timezone instead
(res.company.resource_calendar_id.tz, falling back to UTC), grouping
candidates per company since a class isn't necessarily scoped to one.

Fixes nirun-life#102.

Co-authored-by: Nutcha <nutcha.noonmomo@gmail.com>
…'s event/request id

WorkflowMixin._to_workflow() wrote the parent record's own id into
ni.workflow.event.parent_id / ni.workflow.request.parent_id, but those
fields reference other ni.workflow.event/request rows, not the source
model's own id. This either raised a foreign key violation or silently
linked to an unrelated workflow row, depending on whether the id happened
to collide with an existing workflow row.

Surfaced via ni_observation: an observation type with a computed parent
(e.g. Systolic/Diastolic Blood Pressure -> Blood Pressure) hit this as
soon as both child values were saved, since ni.observation.sheet links
the children to the newly computed parent via child_ids, which re-writes
each child's parent_id and re-triggers the workflow mirror write.

Fixes nirun-life#109. Also fixes ni_device's
test_requires_supported_observation_type, which tried to clear a
device's observation_type_ids via its definition + recompute, but
_compute_from_definition only ever fills a device's observation_type_ids
from a non-empty definition and never clears it.
…irun-life#123)

## Why

The departure wizard was reachable from exactly one view: the community
care
"simp" patient form, the only one carrying `js_class="ni_patient_form"`.
Archive from anywhere else — the base form, any patient list — wrote
`active=False` directly and lost the reason, date and note the wizard
exists
to collect.

## What

- `js_class="ni_patient_form"` on the base patient form,
`js_class="ni_patient_list"` on the base tree
- New `PatientListController` so a multi-selection archives through the
wizard too
- Wizard takes `patient_ids` instead of `patient_id` and writes one
reason set to every selected patient
- One patient still renders as the big heading; several render as tags
on a normal field label
- `toggle_active` / `no_wizard` removed — nothing ever read `no_wizard`,
and `write(active=False)` does the same work

## Deploy order

Ships with nirun-life/nirun-dop#13, which imports
`PatientListController`
from this branch. **This repo must go out first** or the community care
patient
list fails to register its view type.

## Testing

- `ni_patient/tests/test_ni_patient_archive_wizard.py` covers both the
single and multi cases
- Full `ni_patient` suite on a scratch DB: `0 failed, 0 error(s) of 20
tests`
- Verified server-side: rendered arch carries both `js_class` values,
`default_patient_ids` normalises to `Command.SET`, and the Thai field
label resolves to `ผู้รับบริการ`
- Not yet clicked through in a browser — worth one pass on the list and
the form

---------

Co-authored-by: Nutcha <nutcha.noonmomo@gmail.com>
…e#124)

`ni.location._name_search` matched on `display_name`, which is neither
stored nor given a search method. The ORM logs `Non-stored field ...
cannot be searched` and quietly turns that leaf into `TRUE`, so every
location autocomplete — and every `('<m2o to a location>', 'ilike',
...)` leaf — returned the whole table. Typing a community name into the
house map's search bar brought back all five communities and, through
them, every house in the database.

### What changes

The override goes away. `_rec_names_search = ["name", "alias"]` states
the same thing as a class attribute, and core's `_name_search`
(`odoo/models.py:1633-1666`) does the rest — the same aggregator on
`NEGATIVE_TERM_OPERATORS`, the same leaf shape, plus dotted paths,
`convert_to_write` with a `ValueError` skip, and a warning rather than a
`[None]` leaf when `_rec_name` is missing.

Net -25 lines.

### Scope

Nothing in the codebase calls `name_search` directly, so the change is
confined to autocompletes and `ilike` leaves, which now filter instead
of returning everything. Opening a dropdown without typing still lists
all locations — core's `not (name == '' and operator in ('like',
'ilike'))` guard skips the domain entirely.

No model inheriting `ni.location` overrides `_name_search`, so nothing
resolves `super()` through the deleted method. `ni.location.commu`
declares its own `_rec_names_search = ["name", "code"]`; it needs
`"alias"` added there if community aliases should stay matchable.

### Tests

`ni_patient/tests/test_ni_location_name_search.py`, two cases: `ilike`
filters instead of matching everything, and `alias` is still matched.
Both scope their `name_search` to the records they create — unscoped,
the pre-fix `TRUE` leaf returns an arbitrary 100 rows that need not
include the others, and the assertion would pass against the bug it
guards.

Verified they fail against the original bug — `_rec_names_search =
["display_name", "alias"]` reproduces it:

```
FAIL: test_ilike_filters_instead_of_matching_everything
AssertionError: ni.location(22, 23, 24) != ni.location(22,)
FAIL: test_alias_is_still_matched
AssertionError: ni.location(22, 23, 24) != ni.location(23,)
```

`ni_patient` suite: 20 tests, 0 failed.

---------

Co-authored-by: Nutcha <nutcha.noonmomo@gmail.com>
ni.encounter previously only received references from other resources'
workflow events, never producing its own. Since its state selection is
incompatible with ni.workflow.event.mixin, write to ni.workflow.event
directly instead of inheriting the mixin, so start/discharge/cancel
transitions show up in the patient timeline.

Took 12 minutes
Encounter form gains a "Workflow Timeline" action-menu entry, alongside
Location/Participant History, opening ni.workflow.line filtered to that
encounter. Also default-groups the generic timeline by date, adds a
company filter to its searchpanel, and drops the unused
static/src/views/*.xml asset glob (no such templates exist).

th.po gains the reference line the new action's name field needs to
pick up the existing translation, and fills the previously blank
encounter start-date validation message.

Took 17 minutes
`default=fields.Datetime.now()` (and, in ni_reception, the stdlib
`fields.datetime.now()`) is evaluated once when the model class is
defined, freezing every record's default to that one moment instead
of the actual creation time. Pass the bare callable so it evaluates
per record.

Affected: ni_observation (observation wizard), ni_document_ref,
ni_medication (suggest wizard), ni_reception, and ni_patient's
ni.workflow.event.mixin (shared by every clinical event model:
procedures, immunizations, medication dispenses, etc).
Odoo's view validator warns on every <i class="fa ..."> and icon button
without a title, since screen readers have nothing to announce. The
titles name what the icon stands for (Occurrence, Patient, Encounter,
Location, ...) so they double as tooltips.

Took 4 minutes
@wasud
wasud force-pushed the 18.0-mig-p1-patient branch from 6053884 to 5878f23 Compare October 16, 2026 02:27
@piruin piruin mentioned this pull request Oct 19, 2026
5 tasks done

@piruin piruin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

No blockers.

Minor

  • ni.encounter.name is now labelled "Encounter Name", but the field holds the encounter number. That label shows in list headers, search and exports. Consider swapping, e.g. keep "Encounter No." on name and give identifier a distinct label.
  • The new English labels are unique, but some of their Thai translations are still identical. The same-label warning only checks English, so Thai users will see two identical entries in the filter, group-by and export menus:
    • "Encounter Name" and "Current Encounter" are both รหัสรับบริการ.
    • "Active Encounters" and "Active Encounter Count" are both ข้อมูลรับบริการ.
    • "Show Participant" and "Current Participant" are both ผู้ดูแล.
  • The PR body says location_history_count is labelled "Location History", but the code says "Location Moves". #155 also lists that rename as a 19.0 change.

Checked, OK

  • _has_cycle() raises odoo.exceptions.ValidationError, has_access("read") is used in the portal controller, and the cron fields are removed.
  • Stored and non-stored computes are split, and the browse(rec._get_encounter_active_ids()) call gets ids, so it's fine.
  • _search_display_name ORs with core's search instead of replacing it.
  • The duplicate-patient test asserts IntegrityError under mute_logger.
  • CI is green, and the commit series ends with [IMP] pre-commit auto fixes + [MIG].

@wasud
wasud force-pushed the 18.0-mig-p1-patient branch from 5878f23 to 1b80cde Compare October 19, 2026 05:09
@wasud

wasud commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Fixed in the [MIG] commit (now 1b80cdea) and replayed in #155:

  • ni.encounter.name is back to "Encounter No.", and identifier is now "Encounter Identifier". The form header uses <label for="identifier" string="Encounter No."/>, so it still reads "Encounter No.".
  • Thai translations of these labels are now distinct:
    • Encounter No. = รหัสรับบริการ, Encounter Identifier = เลขที่รับบริการ, Current Encounter = รหัสรับบริการปัจจุบัน
    • Active Encounters = รายการรับบริการปัจจุบัน, Active Encounter Count = จำนวนรับบริการปัจจุบัน
    • Current Participant = ผู้ดูแลปัจจุบัน, Show Participant = แสดงผู้ดูแล
    • I also fixed two other new labels that clashed within ni.encounter: Show Chief Complaint = แสดงอาการสำคัญ, Location Moves = จำนวนการย้ายสถานที่
    • I only edited the affected th.po blocks, taking the references from a fresh export.
  • PR body: location_history_count is now "Location Moves" there too, and [19.0][MIG] ni_patient: Migration to 19.0 #155 no longer lists that rename as a 19.0 change.
  • The ni_coding default-views test moved to ni_coding/tests/, as in [17.0][MIG] ni_patient: Migration to 17.0 #153.

@piruin
piruin merged commit 9c73338 into nirun-life:18.0 Oct 21, 2026
4 checks passed
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.

5 participants