Skip to content

[17.0][MIG] ni_patient: Migration to 17.0 - #153

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

piruin merged 82 commits into
nirun-life:17.0from
wasud:17.0-mig-p1-patient

Conversation

@wasud

@wasud wasud commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Decisions to review

  • D2, data: OpenUpgrade 17 turns private partners into contacts and overwrites their address, phone, email, VAT, job position, website and notes with ***** (and clears country/state); the originals survive only in its ou_res_partner_private table. migrations/17.0.1.0.0/post-migration.py restores every masked column of the patient/encounter "other addresses" (street, street2, city, zip, state_id, country_id, phone, mobile, email, vat, function, website, comment) from that copy and sets them to type='other'. Checked by hand on a real OpenUpgrade 16→17 run (the patient's address comes back complete, unrelated private partners stay masked), plus a seeded check in a rolled-back transaction for vat/function/website/comment, since that run had no data in them. The migration snapshot ([16.0][ADD] tools: migration data snapshot script #143) does not hash other_address_id fields, so it would not catch this.
  • D2, UI: a compact address form ni_patient.res_partner_view_form_address replaces base.res_partner_view_form_private (deprecated in 17, removed in 19).
  • D7, extension seam: the JS files are renamed *.esm.js to match the OCA eslint config, so downstream imports change to @ni_patient/views/list_view.esm.

What changed

  • ni_patient history replayed, then [IMP] pre-commit and [MIG] ni_patient: Migration to 17.0.
  • D1:
    • ni.patient._rec_name = "name", so patients display by name, not HN.
    • _compute_display_name with depends_context on patient, encounter and location; _name_get() kept as the extension seam for ni_observation.
    • _rec_names_search on patient and encounter; res.partner._name_search uses the new signature.
  • D2: type='other' domains and form context, compact address form, post-migration.
  • D7:
    • getStaticActionMenuItems() replaces getActionMenuItems().
    • @odoo/owl import and _t import.
  • Encounter lock: field states= moved into the form as readonly="state ..." (25 fields); ni.workflow intent/priority lose states= (documented for downstream forms).
  • Other 17 changes:
    • Discharge wizard: the onchange no longer returns a domain; uses a computed discharge_disposition_domain instead.
    • Calendar quick_create.
    • file_path; from odoo import Command.
    • Field domains no longer hold lambdas.
    • No write() inside an onchange.
    • Named translation placeholders and lazy logging (pylint).
  • New tests:
  • th.po regenerated; translations dropped by the export restored from 16.0.

Why

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

Migration notes

  • Effort: L.
  • What the migrator did: attrs in 16 view files.
  • Manual work:
    • All states= (fields, groups).
    • name_get/_name_search, private partners, JS.
    • The discharge onchange domain.
    • Calendar attribute.
    • Lint backlog (~25 items the template's pylint/ruff now flag).
  • Surprises:

How tested

  • Unit/integration tests: 32 tests pass on 17 (24 existing + 8 new).
  • Manual check (browser, Odoo 17 with demo data):
    • patient list with search panel;
    • Archive from the list and from the form opens the archive wizard, which shows the patient name;
    • "Other address" create opens the compact form and saves type='other';
    • encounter kanban, calendar and form, with in-progress fields locked as expected;
    • discharge wizard domain changes with the status (shell Form);
    • portal /my/home and /my/patients.
    • No JS console errors.
  • OpenUpgrade 16→17 on a 16 DB without demo: the D2 post-migration restores the patient address.
  • pre-commit clean on ni_patient.

Documentation

  • Module README.md updated (display names, addresses, encounter locking, JS seam, tests)

Notes for reviewer

piruin added 23 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
piruin and others added 8 commits October 16, 2026 09:25
…'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 17.0-mig-p1-patient branch from 0af76bd to 8b7a78f Compare October 16, 2026 02:27
@wasud wasud mentioned this pull request Oct 16, 2026
2 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

Fix before merge: the private-address restore misses some masked columns.

OpenUpgrade 17 (base/17.0.1.3/pre-migration.py, _handle_partner_private_type) overwrites these columns with *****: street, street2, city, zip, vat, function, phone, mobile, email, website, comment. It also clears country_id and state_id.

migrations/17.0.1.0.0/post-migration.py doesn't restore vat, function, website or comment, so those stay ***** on the restored patient addresses. comment (the address notes) is the one most likely to hold real data. Please add the four columns to the SET clause.

The migration snapshot (#143) doesn't hash other_address_id fields, so it would not catch this. The "verified" claim rests on a manual check only.

Minor

  • tests/test_ni_coding_default_views.py lives in ni_patient but tests ni_coding behaviour.

Checked, OK

  • Each of the 25 field-level states= locks from 16.0 that is still on the encounter form now has an equivalent readonly= there. The other three (name, location_history_ids, participant_ids) aren't on any form.
  • The private page name on the patient form is kept, so the ni_related_person xpath still matches.
  • _compute_display_name has the right depends_context keys, _rec_names_search is set, and res.partner._name_search uses the 17 signature.
  • The discharge wizard uses a computed domain field instead of returning a domain from the onchange.
  • CI is green, and the commit series ends with [IMP] pre-commit auto fixes + [MIG].

Note: #154 and #155 replay this branch, so any code change here needs to be replayed forward (except the 17.0 migration script, which exists only on 17.0).

@wasud
wasud force-pushed the 17.0-mig-p1-patient branch from 8b7a78f to 44b8bc1 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 44b8bc17):

  • post-migration.py now also restores vat, function, website and comment, so it covers every column OpenUpgrade masks, plus state_id/country_id.
  • Verified on mig17_d2 (a real OpenUpgrade 16→17 run) inside a rolled-back transaction, because that run has no data in these four columns: I seeded ***** on the other-address partners and the real values in ou_res_partner_private, then ran the script, and all four values came back. The PR body now says the check is manual plus this seeded one, and that the [16.0][ADD] tools: migration data snapshot script #143 snapshot does not cover other_address_id.
  • Minor: the default-views test moved to ni_coding/tests/ and now tests the abstract ni.coding directly, with no ni_patient models. The same move is replayed in [18.0][MIG] ni_patient: Migration to 18.0 #154 and [19.0][MIG] ni_patient: Migration to 19.0 #155.

The 17.0 observation and questionnaire stacks were rebased on top; their content is unchanged.

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