Skip to content

[19.0][MIG] ni_patient: Migration to 19.0 - #155

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

piruin merged 87 commits into
nirun-life:19.0from
wasud:19.0-mig-p1-patient

Conversation

@wasud

@wasud wasud commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Decisions to review

What changed

  • ni_patient replayed from the 18.0 PR branch, then [MIG] ni_patient: Migration to 19.0.
  • Migrator: models.Constraint, bypass_search_access, env.cr/env.context, group_ids.
  • D3:
    • title → title_id (onchange, demo, test);
    • ni_partner_mobile dependency (new LGPL module, commit before the [MIG]);
    • phone restore script.
  • D5: privilege and group sequence.
  • Other 19 changes:
    • self.env._(...) with arguments.
    • Tests: name_search(domain=), fields.Date/fields.Datetime; they don't rely on demo data.
    • Inheritable image default.
    • Redundant labels removed (19 pylint).
    • Single-line CHECK constraints.
  • README (titles/mobile, privilege, dependencies) and th.po.

Why

Closes #137 (P1-2, epic #131).

Migration notes

  • Effort: M.
  • What the migrator did: most of the model API changes.
  • Manual work:
    • D3/D5;
    • translations;
    • a larger pylint backlog (19 flags more redundant labels).
  • Surprise: name_search(args=) is gone in 19.

How tested

  • Unit/integration tests: 32 tests pass on 19 without demo data, and the log has no WARNING lines.
  • Real OpenUpgrade 18→19 (OpenUpgrade 19 + upgrade_path/) on an 18 DB with ni_patient and no demo:
    • patients keep their titles (Thai custom and base), phone and mobile, each separate;
    • a nurse stays in ni_patient.group_user;
    • migration_snapshot.py patient/encounter hashes are identical before and after.
  • Manual check: browser smoke on 19 (see Notes).
  • pre-commit run -a clean.

Documentation

  • Module README.md updated

Notes for reviewer

@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

Blocking (mechanical): the branch conflicts with 19.0.

  • upgrade_path/README.md is added on both sides.
  • The branch still carries #149's commits from before that PR was merged, so GitHub shows about 226 changed files instead of about 40.
  • A rebase onto origin/19.0 fixes both.

Non-blocking: a bug carried over from 16.0 in ni_encounter.check_origin_date.

  • raise self.env._("...") raises a plain string, which fails with a TypeError.
  • rec.origin_date > rec.period_start compares a Date with a Datetime, which also raises a TypeError.
  • Per the epic, fix it on 16.0 under P0-4 (#135) and forward-port it with oca-port, rather than inside the [MIG] commit.

Checked, OK

  • post-nirun_partner_mobile.py against OpenUpgrade 19's _migrate_partner_mobile:
    • OpenUpgrade copies phone to the legacy column, then sets phone to phone // mobile, or to just the mobile number when the phone is empty.
    • The WHERE COALESCE(mobile, '') != '' AND phone IS DISTINCT FROM legacy condition undoes exactly those rows. An originally empty phone ends up empty again.
    • Rows where phone equalled mobile are skipped by OpenUpgrade, and the script leaves them alone too.
    • The mobile column is never dropped, so ni_partner_mobile picks it up.
  • The privilege setup (res.groups.privilege plus group sequence), user_ids, bypass_search_access, models.Constraint and self.env._(...) with arguments are all used correctly.
  • The list view reads the selection with model.root.getResIds(true).
  • CI is green.

piruin added 16 commits October 19, 2026 11:56
[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
Noonmomo and others added 22 commits October 19, 2026 11:56
- 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
Odoo 19 removed the partner mobile from base. OCA partner_mobile brings it
back, but under AGPL-3; OPL-1 modules sit on top of ni_patient, so they
cannot depend on AGPL code.

This module brings back the mobile field, its formatting and the partner
form and list fields as they were in Odoo 18.0 (LGPL-3). phone_validation
picks it up as a number field for the phone search and SMS.

@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.

Follow-up review

Two more items, found while reviewing #158:

  • Fix: copyright headers were lost. The 18→19 step removed the <!-- Copyright … NSTDA --> header from views/ni_encounter_location_views.xml, views/ni_encounter_participant_views.xml, views/ni_location_views.xml and views/ni_patient_views.xml. The cause is probably the migrator rewriting the search-view <group> tags with lxml. Please restore the headers.
  • Minor: there is no [IMP] ni_patient: pre-commit auto fixes commit before [MIG] ni_patient: Migration to 19.0, which the epic's commit order asks for.

@wasud
wasud force-pushed the 19.0-mig-p1-patient branch from e430962 to b3f621e Compare October 19, 2026 05:09
@wasud

wasud commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. All fixed:

  • Conflict: rebased onto origin/19.0. [19.0][MIG] ni_period, ni_identifier, ni_coding, partner_age, partner_gender, partner_religion, uom_alias: Migration to 19.0 #149's commits are gone, and so are the already-merged survey_subject/survey_grading commits in the questionnaire stack. upgrade_path/README.md merged cleanly and lists both scripts. The PR now touches only ni_patient, ni_partner_mobile and upgrade_path.
  • Copyright headers: restored in the 4 views. You were right about the cause: odoo-module-migrate's lxml rewrite of the search-view <group> drops the leading comment. 17.0 and 18.0 lost no headers.
  • Commit order: added [IMP] ni_patient: pre-commit auto fixes (odoo-module-migrate + pre-commit output on the 18.0 code) before [MIG] ni_patient: Migration to 19.0. The [MIG] diff now holds only the manual changes.
  • Replayed from [17.0][MIG] ni_patient: Migration to 17.0 #153/[18.0][MIG] ni_patient: Migration to 18.0 #154: the label and Thai fixes, and the move of the default-views test to ni_coding. The PR body no longer lists "Location Moves" as a 19.0 change.
  • check_origin_date: fixed on 16.0 as you asked, with forward-ports for 17/18/19 prepared. That PR is not opened yet.

A fresh 19.0 install with ni_coding and ni_patient tests: 32/32 pass. #158 and #161 were rebased on top and only gain the 4 headers.

@piruin
piruin merged commit 16f8567 into nirun-life:19.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