Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
76de148
[MIG] ni_patient: version 16.0
piruin Apr 18, 2023
cf5538c
[RMV] ni_patient_address: merge into `ni_patient`
piruin Apr 21, 2023
6a36af6
[IMP] ni_patient: Add History Page
piruin Apr 21, 2023
6b921c5
[FIX] ni_patient: unit test
piruin Apr 27, 2023
b3395cb
[IMP] ni_patient: add state for workflow.mixin
piruin Apr 28, 2023
76414c7
[IMP] ni_patient: Add other address field
piruin May 15, 2023
4834258
[i18n] th.po: 'ผู้สูงอายุ' -> 'ผู้ป่วย'
piruin May 24, 2023
c63d3d0
[IMP] ni_patient: add `hpi`, `ros` and `pe` field for encounter
piruin Jun 2, 2023
5b0b769
[FIX] ni_patient: error on create ni. workflow.request.mixin inherite…
piruin Jun 8, 2023
a0c71a8
[IMP] ni_patient: other_address_id use `ref` of partner instead of `p…
piruin Jun 9, 2023
1ea5a16
[IMP] ni_patient: set notebook page icon
piruin Jun 21, 2023
d042079
[IMP] ni_patient: add discharge status
piruin Jun 28, 2023
5be749b
[IMP] ni_patient: remove patient.code
piruin Jul 4, 2023
7c38725
[IMP] ni_patient: remove private page from encounter
piruin Jul 7, 2023
2f93bc4
[IMP] ni_patient: unaccent=False for parent_path
piruin Jul 11, 2023
44c46c8
[IMP] ni_patient: remove tracking property
piruin Jul 11, 2023
fdb9395
[FIX] ni_patient: code_uniq to identifier_uniq
piruin Jul 11, 2023
5a2a0a2
[IMP] ni_patient: Add company for multi-field index of ni.patient.res
piruin Jul 15, 2023
e8b08ce
[IMP] ni_patient: add religion
piruin Jul 25, 2023
0bd2a07
[IMP] ni_patient: improve ni.location and search panel
piruin Jul 27, 2023
bd60909
[FIX] ni_patient: patient kanban not show avatar
piruin Jul 29, 2023
5513ad7
[FIX] ni_patient: user can't access encounter location
piruin Aug 7, 2023
d380827
[i18n] ni_patient: th translate
piruin Aug 10, 2023
675c8fe
[I18N] ni_observation: th translation
piruin Aug 11, 2023
029c61f
[IMP] ni_reception: update decoration
piruin Aug 13, 2023
d9c5ba2
[IMP] ni_patient: link identification_id of patent and partner
piruin Oct 10, 2023
0d63c3b
[IMP] ni_patient: Can set default encounter's class_id of each company
piruin Oct 10, 2023
8527a93
[IMP] ni_patient: change group of ref-fields in res_partner to `viewer`
piruin Nov 1, 2023
8af084b
[IMP] ni_patient: add 'state' for encounter search panel
piruin Nov 14, 2023
38ef8bc
[IMP] ni_patient: configable encounter form by class
piruin Dec 20, 2023
d60eadf
[FIX] ni_patient: location history
piruin Jan 2, 2024
75bce6a
[IMP] ni_patient: show/hide laboratory and questionnaire page
piruin Jan 2, 2024
f0b2726
[FIX] ni_encounter: error when location_id have no value
piruin Jan 4, 2024
85c29da
[IMP] ni_patient: not use Optional[] typing
piruin Jan 4, 2024
c2b9b66
[IMP] ni_patient: refactor ni.patient.res._check_period_start
piruin Jan 4, 2024
2219e9a
[IMP] ni_patient: Introduce ni.export.data.logger
piruin Jan 4, 2024
3f3bc29
[I18N] ni_*: "ผุ้ป่วย" -> "ผู้รับบริการ"
piruin May 20, 2024
8b03a30
[FIX] ni_patient: participate error when user have no employee
piruin May 24, 2024
a487419
[IMP] ni_patient: service notebook
piruin May 24, 2024
56403d4
[IMP] ni_patient: add PMH field
piruin May 28, 2024
aca8c90
[IMP] ni_patient: hide partner_id from user
piruin Jun 7, 2024
ece600f
[IMP] ni_patient: no create & edit encounter class on company form
piruin Jun 20, 2024
ec5bc83
[IMP] ni_patient: update
piruin Aug 1, 2024
19722cd
[IMP] ni_patient: add icon on ni.patient notebook page
poplacto Aug 2, 2024
666965a
[FIX] ni_patient: error when insert request without parent_id field
piruin Aug 22, 2024
fd2f1f4
[IMP] ni_patient: improve menu, kanban view and seq
piruin Aug 27, 2024
4b98879
[FIX] ni_patient: warning <i> must have title
piruin Sep 5, 2024
b673a4e
[IMP] ni_patient: add order
piruin Sep 6, 2024
452b889
[IMP] ni_patient: move demo data to /demo
piruin Sep 9, 2024
c0e22b0
[IMP] ni_patient: encounter compute patient age by period_start
piruin Sep 26, 2024
0224845
[IMP] ni_patient: add summary report
poplacto Sep 26, 2024
b10b7c9
[FIX] ni_patient: company show only Identifier
piruin Nov 12, 2024
c7cd3dc
[FIX] ni_patient: participant_title warning by remove it
piruin Nov 26, 2024
0cb5c2a
[FIX] ni_patient: error on demo data
piruin Dec 11, 2024
90054da
[FIX] ni_patient: no participant_title on generate summary report
piruin Dec 16, 2024
8edc0d0
[FIX] ni_patient: can't set default nationality
piruin Jan 20, 2025
693b77b
[IMP] ni_patient: participant feature
poplacto Feb 15, 2025
b13275a
[IMP] ni_patient: onchange title
poplacto Mar 1, 2025
cee04d6
[IMP] ni_patient: accessible on portal
bestiing Mar 26, 2025
e0c2270
[IMP] ni_patient: patient user not implied as partner manager
piruin Apr 4, 2025
987757c
[IMP] ni_patient,ni_allergy,ni_condition,ni_service: update patient p…
bestiing Apr 6, 2025
a5a0abc
[IMP] ni_comminity_care: Area Manager
bestiing Sep 1, 2025
b08bd56
[IMP] ni_community_care: better search
bestiing Dec 4, 2025
121b6eb
[IMP] ni_community_care: update approval form, patient kanban
Noonmomo Feb 27, 2026
fed29db
[FIX] ni_careplan: resource created by careplan not check period_start
piruin Mar 11, 2026
36b6a92
[FIX] ni_patient: fix ni.workflow mixin bugs and polish timeline
piruin May 9, 2026
b30a14a
[FIX] ni_patient: not found workflow action
piruin May 9, 2026
0939c93
[FIX] ni_patient: Thai translate of 'State' from จังหวัด to สถานะ
piruin May 9, 2026
fd8329d
[ADD] ni_patient: unified workflow timeline via ni.workflow.line
piruin May 10, 2026
c736b6c
[IMP] core: Add summary field to all module manifests
piruin May 26, 2026
533ffd9
[IMP] pre-commit: detect-secrets to prevent credential leaks (#97)
piruin May 30, 2026
4e1206b
[IMP] docs: add ODOX module READMEs
piruin Jun 10, 2026
1df028e
[FIX] ni_patient: use company timezone for encounter auto-close midni…
bestiing Jul 2, 2026
c2381c7
[FIX] ni_patient: workflow mirror parent_id must reference the parent…
piruin Jul 9, 2026
b58f83a
[IMP] ni_patient: archive patients through the wizard from any view (…
bestiing Aug 14, 2026
098a513
[FIX] ni_patient: match locations on their own name fields (#124)
bestiing Aug 18, 2026
b7ff08c
[ADD] ni_patient: log workflow event on encounter state changes
piruin Aug 29, 2026
a900bb1
[ADD] ni_patient: encounter workflow timeline action and th translation
piruin Aug 29, 2026
1fd0daf
[FIX] various: stop freezing Datetime.now() record-creation defaults
piruin Sep 2, 2026
bba5707
[IMP] various: give icon-only elements a title
piruin Sep 13, 2026
966dac1
[IMP] ni_patient: pre-commit auto fixes
wasud Oct 12, 2026
44b8bc1
[MIG] ni_patient: Migration to 17.0
wasud Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ni_coding/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (c) 2026 NSTDA

from . import test_ni_coding_default_views
24 changes: 24 additions & 0 deletions ni_coding/tests/test_ni_coding_default_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright (c) 2026 NSTDA
"""ni.coding models get the default views from ni_coding (#136, #137).

ni_coding reads its default views from files by view type, so a view type rename
in a new Odoo version (tree -> list in 18.0) would silently stop matching.
"""

from odoo.tests import TransactionCase


class TestCodingDefaultViews(TransactionCase):
def test_default_views(self):
list_type = (
"list"
if "list" in dict(self.env["ir.ui.view"]._fields["type"].selection)
else "tree"
)
for view_type in (list_type, "kanban", "form"):
arch = self.env["ni.coding"].get_view(view_type=view_type)["arch"]
self.assertTrue(
arch.lstrip().startswith(f"<{view_type}"),
f"{view_type}: {arch[:60]}",
)
self.assertIn('name="code"', arch, view_type)
128 changes: 128 additions & 0 deletions ni_patient/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Patient (`ni_patient`)

Odoo 17.0 module that provides the core patient and encounter model for Nirun. It
anchors the clinical data hierarchy, patient/encounter workflow state, and shared
timeline records used by the rest of the healthcare modules.

## Purpose

`ni_patient` is the central clinical module in the repository. It owns the patient
record, encounter lifecycle, and the read-only workflow timeline that combines events
and requests into a single patient history.

## Main Models

| Model | Role |
| -------------------------------------------------- | ------------------------------------------------------------------- |
| `ni.patient` | Central patient record, `_inherits` `res.partner` |
| `ni.encounter` | Patient visit/admission, `_inherits` `ni.patient` |
| `ni.patient.res` | Abstract mixin for patient-linked clinical records |
| `ni.workflow` | Shared workflow base used by event and request records |
| `ni.workflow.event` | Stored timeline mirror for completed events |
| `ni.workflow.request` | Stored timeline mirror for requested items |
| `ni.workflow.line` | Read-only union view over events and requests |
| `ni.location` and related encounter support models | Location, class, participant, admit, discharge, and reason metadata |

## Workflow and Views

- `ni.workflow.event.mixin` and `ni.workflow.request.mixin` power the state transitions
used by downstream clinical modules.
- `ni.encounter` does not inherit the event mixin (its own `state` selection is
incompatible with the mixin's). Instead, `Encounter._log_workflow_event()`
(`models/ni_encounter.py`) creates a new `ni.workflow.event` row on every state
transition (`write()` when `state` changes), so encounter start/discharge/cancel/etc.
show up in the patient timeline alongside other clinical events.
- `views/ni_workflow_event_views.xml`, `views/ni_workflow_request_views.xml`, and
`views/ni_workflow_line_views.xml` expose the timeline and state history.
- The encounter form's action menu includes "Workflow Timeline"
(`Encounter.action_encounter_workflow_timeline()`, bound via
`ni_encounter_action_server_workflow_timeline` in `views/ni_encounter_views.xml`),
which opens `ni_workflow_line_action` filtered to that encounter's `ni.workflow.line`
rows — the same pattern used by the "Location History" and "Participant History"
action-menu entries.
- `views/ni_patient_views.xml`, `views/ni_encounter_views.xml`, and the related support
views define the core patient and encounter UI.
- Portal templates in `views/ni_patient_portal.xml` and
`views/ni_patient_portal_templates.xml` expose patient-facing entry points.

## Display Names and Search

- `ni.patient` sets `_rec_name = "name"`: patients display by name, not by the
identifier (HN) that `ni.identifier.mixin` would use. `_compute_display_name` (via the
`_name_get()` extension seam, extended by `ni_observation`) adds details for the
context keys `show_identifier`, `show_identification_id`, `show_gender_age` and
`show_age`. Name search covers `name`, `identifier` and `identification_id`
(`_rec_names_search`).
- `ni.encounter` display name honours `show_class`, `show_patient`/`show_patient_name`,
`show_state`, `show_location` and `show_period`; name search covers the encounter
number and the patient.
- `ni.location` display name honours `location_display="short"`, `show_parent` and
`show_alias`; `full_name` is the parent path with alias.
- `res.partner` name search matches the name, or exactly the phone, mobile or
identification number (`_name_search` override).

## Addresses

- Patient and encounter "other addresses" are `res.partner` records of type `other` with
`ref` set to the patient identifier, edited in the compact form
`res_partner_view_form_address`. Up to 16.0 they were `private` partners, a type Odoo
17 removed.
- Upgrade from 16.0: `migrations/17.0.1.0.0/post-migration.py` restores those addresses
from OpenUpgrade's `ou_res_partner_private` copy (OpenUpgrade masks private addresses)
and sets them to `other`.

## Encounter Locking

- Encounter fields are read-only in the form once the encounter is finished, cancelled
or entered in error, and the patient, number and start are editable only in draft.
Since 17.0 this lives in `views/ni_encounter_views.xml` (`readonly="state ..."`), not
in field `states=`. Forms of downstream modules that edit `ni.workflow` `intent`/
`priority` must add their own `readonly` expression for completed/revoked requests.

## Frontend View Registrations

- `static/src/views/form_view.esm.js` and `static/src/views/list_view.esm.js` register
the `ni_patient_form` and `ni_patient_list` view types; the base patient form and tree
in `views/ni_patient_views.xml` carry the matching `js_class`.
- Both controllers replace the standard Archive action menu item through the shared
`static/src/views/archive_patient_hook.esm.js`, which opens
`ni.patient.archive.wizard` with `default_patient_ids` (one id from the form, the
whole selection from the list).
- Invariant: archiving a patient must go through that wizard — it is the only path that
records state reason, date, and note. A plain `write({"active": False})` bypasses it
and loses that data.
- `PatientListController` is an extension seam imported by downstream repositories
(since 17.0 from `@ni_patient/views/list_view.esm`); renaming it is a breaking change
outside this module. The controllers override `getStaticActionMenuItems()`.

## Security and Support Files

- `security/ni_patient_group.xml` defines the patient access group.
- `security/ni_patient_rules.xml` constrains record access.
- `security/ir.model.access.csv` grants model-level permissions.
- `data/ir_sequence_data.xml` and the other `data/*.xml` files seed encounter classes,
discharge data, participant types, and state reasons.
- `wizard/ni_encounter_discharge_wizard_views.xml` and
`wizard/ni_patient_archive_wizard_views.xml` support guided encounter and archive
actions.

## Dependencies

- `ni_coding`
- `ni_identifier`
- `ni_period`
- `hr`
- `mail`
- `partner_age`
- `partner_gender`
- `partner_religion`
- `portal`

## Verification

- `tests/test_ni_patient_display_name.py` (D1) and `tests/test_ni_patient_address.py`
(address type). The ni_coding default views are tested in `ni_coding`.
- Review `ni_patient/tests/test_ni_patient.py` and
`ni_patient/tests/test_ni_workflow.py` after any workflow, security, or portal change.
- Re-check patient, encounter, workflow timeline, and portal flows after structural
changes to the core models.
5 changes: 5 additions & 0 deletions ni_patient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright (c) 2023 NSTDA

from . import models
from . import wizard
from . import controller
76 changes: 76 additions & 0 deletions ni_patient/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) 2021-2023 NSTDA

{
"name": "Patient",
"summary": "Core patient data model, encounters, and encounter management",
"version": "17.0.1.0.0",
"development_status": "Alpha",
"category": "Medical",
"author": "NSTDA, Piruin P.",
"website": "https://nirun.life/",
"license": "LGPL-3",
"maintainers": ["piruin"],
"depends": [
"ni_coding",
"ni_identifier",
"ni_period",
"hr",
"mail",
"partner_age",
"partner_gender",
"partner_religion",
"portal",
],
"data": [
"security/ni_patient_group.xml",
"security/ni_patient_rules.xml",
"security/ir.model.access.csv",
"data/ir_cron_data.xml",
"data/ir_sequence_data.xml",
"data/ni_location_type_data.xml",
"data/ni_encounter_admit_data.xml",
"data/ni_encounter_arrangement_data.xml",
"data/ni_encounter_class_data.xml",
"data/ni_encounter_courtesy_data.xml",
"data/ni_encounter_diet_data.xml",
"data/ni_participant_type_data.xml",
"data/ni_encounter_discharge_disposition_data.xml",
"data/ni_encounter_discharge_status_data.xml",
"data/ni_patient_state_reason_data.xml",
"wizard/ni_encounter_discharge_wizard_views.xml",
"wizard/ni_patient_archive_wizard_views.xml",
"views/ni_location_views.xml",
"views/ni_location_type_views.xml",
"views/ni_encounter_location_views.xml",
"views/ni_encounter_participant_views.xml",
"views/ni_encounter_reason_views.xml",
"views/ni_encounter_admit_views.xml",
"views/ni_encounter_arrangement_views.xml",
"views/ni_encounter_class_views.xml",
"views/ni_encounter_courtesy_views.xml",
"views/ni_encounter_diet_views.xml",
"views/ni_encounter_discharge_views.xml",
"views/ni_encounter_views.xml",
"views/ni_participant_type_views.xml",
"views/res_partner_views.xml",
"views/res_company_views.xml",
"views/ni_patient_views.xml",
"views/ni_workflow_event_views.xml",
"views/ni_workflow_request_views.xml",
"views/ni_workflow_line_views.xml",
"views/ni_patient_portal.xml",
"views/ni_patient_portal_templates.xml",
"report/summary_report_templates.xml",
"report/summary_report.xml",
"views/ni_patient_menu.xml",
],
"assets": {
"web.assets_backend": [
"ni_patient/static/src/views/*.js",
]
},
"demo": ["demo/ni_patient_demo.xml", "demo/ni_location_demo.xml"],
"application": True,
"auto_install": False,
"installable": True,
}
3 changes: 3 additions & 0 deletions ni_patient/controller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (c) 2023 NSTDA

from . import patient
91 changes: 91 additions & 0 deletions ni_patient/controller/patient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request

from odoo.addons.portal.controllers.portal import CustomerPortal


class PatientPortal(CustomerPortal):
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if "patient_count" in counters:
values["patient_count"] = (
request.env["ni.patient"].search_count(
[("user_ids", "=", request.env.user.id)]
)
if request.env["ni.patient"].check_access_rights(
"read", raise_exception=False
)
else 0
)

# print("PatientPortal Values:", values) # Debug

return values

@http.route(["/my/patients"], type="http", auth="user", website=True)
def portal_my_patients(
self, page=1, date_begin=None, date_end=None, sortby=None, **kw
):
values = self._prepare_portal_layout_values()
Patient = request.env["ni.patient"]
domain = [("user_ids", "=", request.env.user.id)]

patients = Patient.search(domain, limit=self._items_per_page)
values.update(
{
"patients": patients,
"page_name": "patient",
"default_url": "/my/patients",
}
)
# print("PatientPortal Values:", values) # Debug

return request.render("ni_patient.portal_my_patients", values)

@http.route(
["/my/patients/<int:patient_id>"], type="http", auth="user", website=True
)
def portal_my_patient(self, patient_id=None, **kw):
values = self._prepare_portal_layout_values()
Encounters = request.env["ni.encounter"]
domain = [("patient_id", "=", int(patient_id))]

encounters = Encounters.search(domain, limit=self._items_per_page)
values.update(
{
"patient": encounters.patient_id,
"encounters": encounters,
"page_name": "encounters",
"default_url": "/my/patients",
"user": request.env.user,
}
)

# print("portal_my_patient Values:", values) # Debug
return request.render("ni_patient.portal_my_patient", values)

@http.route(
["/my/patients/<int:patient_id>/<int:encounter_id>"],
type="http",
auth="user",
website=True,
)
def portal_my_patient_encounter(self, encounter_id=None, **kw):
values = self._prepare_portal_layout_values()
Encounters = request.env["ni.encounter"]
domain = [("id", "=", int(encounter_id))]

encounters = Encounters.search(domain)
values.update(
{
"encounter": encounters[0],
"patient": encounters[0].patient_id,
"page_name": "encounter",
"default_url": "/my/patients",
"user": request.env.user,
}
)

# print("portal_my_patient_encounter Values:", values) # Debug
return request.render("ni_patient.portal_my_patient_encounter", values)
17 changes: 17 additions & 0 deletions ni_patient/data/ir_cron_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright (c) 2021-2023 NSTDA
-->
<odoo noupdate="1">
<record id="ir_cron_ni_encounter_auto_close" model="ir.cron">
<field name="name">Encounter class: Auto-close</field>
<field name="model_id" ref="ni_patient.model_ni_encounter_class" />
<field name="state">code</field>
<field name="code">model.cron_auto_close()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">12</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
</record>
</odoo>
30 changes: 30 additions & 0 deletions ni_patient/data/ir_sequence_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
~ Copyright (c) 2021-2023 NSTDA
-->
<odoo noupdate="1">
<record id="seq_patient" model="ir.sequence">
<field name="name">Patient Sequence</field>
<field name="code">ni.patient</field>
<field name="prefix">HN-%(range_year)s</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
<field name="use_date_range" eval="True" />
</record>
<record id="seq_encounter" model="ir.sequence">
<field name="name">Encounter Sequence</field>
<field name="code">ni.encounter</field>
<field name="prefix">EN-%(range_year)s</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
<field name="use_date_range" eval="True" />
</record>
<record id="seq_company" model="ir.sequence">
<field name="name">Provider Sequence</field>
<field name="code">res.company</field>
<field name="prefix">H</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
<field name="use_date_range" eval="True" />
</record>
</odoo>
Loading
Loading