Skip to content

API Reference

This page documents the public surface of each roots_hemms_* module — the models, fields, and methods that other modules (yours included) are expected to inherit, call, or reference. It is intentionally selective: internal helpers and compute methods that aren't extension points are omitted; read the source for those.

Conventions used on this page

  • Brand-new models declared by HEMMS use a hemms. _name prefix.
  • Hospital-specific fields added to inherited (Odoo / OCA) models use a roots_ prefix.
  • "Extension point" calls out methods designed to be overridden via super() in downstream code.

For the bigger picture, read Architecture first. For how to add a new module that extends these, read Module Development.


Equipment & criticality

These modules form the asset master + risk-classification core that every other HEMMS module depends on.

roots_hemms_criticality

Adds the 2D Value × Risk → traffic-light criticality matrix on top of maintenance.equipment.

Model: maintenance.equipment (_inherit)

Field Type Notes
roots_value_tier Selection (low / medium / high) Required, tracked, default medium
roots_risk_tier Selection (low / medium / high) Required, tracked, default medium
roots_criticality_color Selection (green / yellow / red) Stored compute from the 2D matrix; indexed
roots_criticality_label Char Non-stored bilingual label that follows the user's lang

The matrix itself is CRITICALITY_MATRIX in models/maintenance_equipment.py — a (value_tier, risk_tier) → color dict. Override the compute method _compute_roots_criticality_color if you need a different mapping.

roots_hemms_asset_master

Brand / Model master data and government Ref. No. uniqueness.

Models:

  • hemms.equipment.brand — Brand master (_inherit = ["mail.thread"]). Action methods action_model_list() and action_equipment_list() for stat buttons.
  • hemms.equipment.model — Model master, M2O to brand. Domain [('brand_id', '=', brand_id)] is enforced on maintenance.equipment.equipment_model_id.

Model: maintenance.equipment (_inherit)

Field Type Notes
brand_id M2O hemms.equipment.brand Tracked, indexed
equipment_model_id M2O hemms.equipment.model Domain-filtered by brand
current_condition Selection in_use / broken / deteriorated / lost / unused. Bilingual labels; indexed; tracked

SQL constraint: ref (Odoo's existing field, added by OCA maintenance_equipment_ref) is enforced unique here — the official รหัสครุภัณฑ์ identifier cannot collide across equipment records.


Workflow & PM

roots_hemms_workflow

The 5-stage Thai BME repair workflow with SLA tracking and an immutable audit trail.

Model: maintenance.stage (_inherit)

Field Type Notes
roots_description_th Text (translate) Thai-language description
roots_sla_hours Float Max hours a request should spend in this stage
roots_is_done_stage Boolean Marks terminal stages
roots_responsible_role Selection ward_staff / bme_manager / bme_technician / bme_manager_signoff

Model: maintenance.request (_inherit)

Field Type Notes
roots_stage_entered_at Datetime Set on stage change; default now; copy=False
roots_time_in_current_stage_hours Float Non-stored compute
roots_is_sla_breached Boolean Non-stored compute + custom search (_search_roots_is_sla_breached) that pushes the inequality into SQL
roots_equipment_criticality_color Selection (related, stored) Mirror of equipment's criticality for kanban colouring
roots_current_responsible_user_id M2O res.users Stored compute; does NOT fall back to env.user
roots_stage_transition_log_ids One2many Audit-log entries

Extension points on maintenance.request:

  • create() — logs an initial transition row from False → stage_id.
  • write() — when stage_id is being changed, captures from_stage, to_stage, time-in-previous-stage, and posts a chatter note. Runs super first so access errors abort before orphan logs.

Model: hemms.stage.transition.log — append-only audit row. Stored on maintenance.request.roots_stage_transition_log_ids. Each row records from_stage_id, to_stage_id, transitioned_at, transitioned_by_id, roots_time_in_previous_stage_hours.

Searching for SLA breaches

breached = env["maintenance.request"].search([
    ("roots_is_sla_breached", "=", True),
    ("equipment_id.roots_criticality_color", "=", "red"),
])

roots_hemms_pm

Hospital PM kinds (bilingual) and PM-aware stage routing.

Model: maintenance.kind (_inherit) — adds roots_name_th and roots_description.

Model: maintenance.stage (_inherit) — adds roots_is_pm_initial_stage (Boolean) with a SQL exclusion constraint so only one stage can be the PM initial.

Model: maintenance.request (_inherit)

The create() override routes any request that arrives with maintenance_plan_id to the PM-initial stage (unless the caller specifies stage_id explicitly). The stage_id default is overridden via _roots_default_stage_excluding_pm() — corrective requests skip PM stages on creation.

If you need to add a new PM kind, declare it in your own module's data/maintenance_kind_data.xml:

<record id="kind_calibration" model="maintenance.kind">
    <field name="name">Calibration</field>
    <field name="roots_name_th">การสอบเทียบ</field>
    <field name="roots_description">Verify accuracy against a traceable reference.</field>
</record>

Annual asset inspection (ปจป.)

roots_hemms_asset_inspection

Model: hemms.asset.inspection — one record per (department_id, fiscal_year_be). The _sql_constraints block this combo from duplicating.

Key fields:

Field Type Notes
name Char Running ref AI/YYYY/NNNNN via ir.sequence
fiscal_year_be Selection Thai BE fiscal year; window ±10 years
department_id M2O hr.department Required, tracked
chair_id, committee_member_1_id, committee_member_2_id M2O hr.employee Distinct-people constraint
state Selection draftin_progresssubmitted
line_ids O2M One row per equipment under inspection
count_* Integer Stored computes: broken / deteriorated / lost / unused / in_use / unmarked / total

Public action methods (button handlers):

  • action_open_add_equipment_wizard() — Draft-only.
  • action_start_inspection()draftin_progress. Refuses if no line_ids.
  • action_submit()in_progresssubmitted. Refuses if any line has no condition set. Calls _propagate_condition_to_master().
  • action_reset_to_draft() — Maintenance Manager only.
  • action_print_report() — Renders the ปจป. PDF.

_propagate_condition_to_master() writes each line's final condition back onto maintenance.equipment.current_condition in bulk per condition — one UPDATE per distinct value.

Model: hemms.asset.inspection.line — equipment × inspection line with its observed condition.


Spare parts

roots_hemms_spare_parts

Model: hemms.equipment.spare.line — per-equipment spare-parts template.

Field Type Notes
equipment_id M2O maintenance.equipment Required, ondelete='cascade', indexed
product_id M2O product.product Domain [('type', 'in', ['product', 'consu'])]
default_qty Float UoM digits
uom_id Related (read-only) From product
kind_ids M2M maintenance.kind Restrict to specific PM kinds; empty = all
roots_is_critical Boolean Stored compute: product flagged critical OR equipment is red
qty_available Related (read-only) From product

Cron: _cron_check_critical_low_stock — daily; raises a Warning activity on equipment for any critical spare with on-hand ≤ 0. De-duplicates against existing activities matching the product name.

Model: product.template (_inherit) — adds roots_is_critical_spare and roots_name_th.

Model: maintenance.request (_inherit) — auto-creates a draft Consumption picking on PM-cron requests via _roots_auto_create_spare_picking(). The picking filters spare lines by kind_ids (lines with no kind restriction are always included).

Also adds roots_open_picking_count (non-stored compute) and action_view_pending_stock_pickings() for the stat button.

Model: stock.picking (_inherit) — running Stock Request number via the hemms.stock.request ir.sequence on Consumption-type pickings.


Service contracts

roots_hemms_service_contracts

Model: hemms.contract.type — vocabulary (Warranty, Extended, MA, Spare-only, Labor-only, ...). Bilingual name + roots_name_th, unique code, ordered.

Model: hemms.service.contract — the contract header.

Key fields:

Field Type Notes
name, roots_name_th Char Bilingual; display_name is [REF] EN / TH
reference Char Running ref via hemms.service.contract sequence (SC/YYYY/NNNN); unique
partner_id M2O res.partner Vendor; supplier-flag filter deferred (see I1 in Release Notes)
contract_type_id M2O hemms.contract.type ondelete='restrict'
equipment_ids M2M maintenance.equipment hemms_contract_equipment_rel
coverage_line_ids O2M hemms.service.contract.coverage.line Per-kind coverage
date_start, date_end Date CHECK constraint: start ≤ end
state Selection (stored compute) draft / active / expired / cancelled — cancelled is sticky
roots_sla_tier Selection Bronze / Silver / Gold / Platinum (display-only in Q3)
roots_renewal_alert_days Integer Default 60
roots_renewal_alerted Boolean Set by cron to avoid duplicates

Public actions: action_set_cancelled(), action_set_draft(), action_view_equipment(), action_view_requests().

Cron: _cron_update_state_and_alert — daily; promotes draft → active and active → expired by date, then fires a mail.activity on the responsible user when roots_renewal_alert_date has been reached. Cancelled stays cancelled.

Bilingual search: _name_search() is overridden to match name or roots_name_th or reference.

Model: hemms.service.contract.coverage.line — per-kind matrix.

Field Type Notes
kind_id M2O maintenance.kind Required
included_in_contract Boolean Default True
billable_to_hospital Boolean Whose budget pays

Model: maintenance.equipment (_inherit) — adds roots_active_contract_id (stored compute) and roots_active_contract_type_code (related). Useful for auto-linking new requests:

equipment.roots_active_contract_id   # the contract covering this equipment right now

Model: maintenance.request (_inherit)

Field Type Notes
roots_service_contract_id M2O (stored compute, writable) Auto-filled when blank — manual override sticks (plan I8)
roots_is_billable_to_hospital Boolean (stored compute) True unless covered AND not hospital-billable
roots_contract_warning Char (compute) Banner copy
roots_contract_warning_level Selection (compute) success / info / warning for the alert-class on the form

Creating a contract programmatically

env["hemms.service.contract"].create({
    "name": "MRI 1.5T - 5yr MA",
    "partner_id": vendor.id,
    "contract_type_id": env.ref(
        "roots_hemms_service_contracts.contract_type_ma"
    ).id,
    "date_start": "2026-01-01",
    "date_end": "2031-12-31",
    "equipment_ids": [(6, 0, [mri.id])],
    "coverage_line_ids": [
        (0, 0, {
            "kind_id": env.ref("roots_hemms_pm.kind_calibration").id,
            "included_in_contract": True,
            "billable_to_hospital": False,
        }),
    ],
})

e-Signature

roots_hemms_signature

A pure bridge module — adds no new models, only _inherits sign.oca.request, hemms.asset.inspection, and hemms.service.contract.

Model: sign.oca.request (_inherit)

Adds stored M2O computes from the polymorphic record_ref:

  • hemms_asset_inspection_id
  • hemms_service_contract_id

These let the bridged hospital models declare One2many inverses, since Reference fields can't be inverse_name directly.

Extension point: _check_signed() — overridden. After super()._check_signed() may have transitioned state to 2_signed, the override looks at record_ref and, when it points to a bridged hospital model, calls _roots_attach_signed_pdf_to_parent(). That writes the final composited PDF to the parent's roots_signed_pdf / roots_signed_pdf_filename and posts it as a chatter attachment.

No state advance on the parent. Signing is a parallel facet — inspection stays submitted, contract stays in its own lifecycle state. (Plan §8 OQ3 + OQ2.)

Model: hemms.asset.inspection (_inherit) — adds sign_request_ids (O2M), sign_request_count (stored compute), roots_signed_pdf (Binary, attachment=True), roots_signed_pdf_filename (Char), and:

  • action_send_for_signature() — renders the ปจป. PDF, creates a sign.oca.request with 3 signers (chair + 2 members). Refuses if the inspection is not submitted or any committee member is unresolvable (no linked Odoo user).
  • action_view_sign_requests() — opens the related list.

Model: hemms.service.contract (_inherit) — same bridge pattern: sign_request_ids, sign_request_count, roots_signed_pdf*, action_send_for_signature(), action_view_sign_requests().


HA Annual PM Report

roots_hemms_ha_report

Model: hemms.ha.device.class — MoPH 82-device classification. Seeded as noupdate="1" so hospital edits survive upgrades. Fields: name_th, name_en, risk_tier, pm_freq_months, pm_duration_hours, display_name (compute).

Model: maintenance.equipment (_inherit) — risk-tier waterfall fields:

Field Type Notes
device_class_id M2O hemms.ha.device.class ondelete='restrict'
roots_ha_risk_tier_override Selection Engineer override wins the waterfall
roots_ha_pm_freq_months_override Integer Override default 0 = inherit
roots_em_function, roots_em_application, roots_em_maintenance, roots_em_history Integer WHO Fennigkoh-Smith sliders
roots_ha_risk_tier_effective Selection (compute) Resolved through the waterfall
roots_ha_pm_freq_months_effective Integer (compute) Resolved through the waterfall
roots_ha_risk_tier_source Selection Provenance: override / device_class / em_score

Model: hemms.ha.report — one per (department_id, fiscal_year_be). State machine: draft → in_progress → submitted → archived. Signing is parallel (no separate signed state — see Q4 OQ3).

Public action methods:

  • action_start()draftin_progress.
  • action_submit()in_progresssubmitted. Builds the immutable snapshot via hemms.ha.report.snapshot._build_snapshot_payload(), pins it via snapshot_id. From this point, line writes raise UserError (see line model _ensure_unlocked).
  • action_archive_report()submittedarchived.
  • action_send_for_signature() — 3 signers (BME Head, Engineering Head, Director). Refuses if any signer slot is empty or has no linked partner.
  • action_reset_to_draft() — Manager only; clears the snapshot.

Model: hemms.ha.report.line — per-equipment detail. Locked after parent submission. Helpers _ensure_unlocked() in write/unlink raise UserError once the parent is submitted.

Model: hemms.ha.report.snapshot — append-only JSON snapshot. _build_snapshot_payload() and _build_paperformat_hash() are the two helpers QWeb (Q5.8) reads from. The template renders from the snapshot for any submitted/archived report so reprints stay byte-identical.

Extension point: sign.oca.request._check_signed() — extended a second time here to handle hemms.ha.report parents (in addition to the inspection / contract handled by Q4).

Generating an HA report

wizard = env["hemms.ha.report.generate"].create({
    "fiscal_year_be": "2569",
    "department_id": dept.id,
})
action = wizard.action_generate()    # opens the draft report
report = env["hemms.ha.report"].browse(action["res_id"])
report.action_start()
# ... engineer fills exception reasons on report.line_ids ...
report.action_submit()               # builds snapshot, locks lines
report.action_send_for_signature()   # spawns sign.oca.request

Mass import

roots_hemms_mass_import

Model: hemms.import.batch — one record per XLSX upload. States: draft → previewed → committed → (archived | deleted).

Key fields: name (running ref via sequence), object_type (equipment / vendor / location), file_data (Binary), file_name (Char), data_json (Text — parsed rows + per-row verdicts), committed_count, error_count, …

Service helpers (importable):

  • services.parser — XLSX → list of dicts.
  • services.validator — per-row business-key checks (MoPH strict match, required fields, …).
  • services.committer — transactional commit; on row failure rolls back the whole batch (plan D14).
  • services.error_export — error-row XLSX export.

Bridge fields on the imported objects:

  • maintenance.equipment.roots_hemms_import_batch_id — M2O hemms.import.batch
  • res.partner.roots_hemms_import_batch_id
  • hr.department.roots_hemms_import_batch_id

These let the batch's _check_delete_blockers confirm whether any tagged record has acquired a downstream Q1 PM event or Q3 service contract before the 7-day delete window closes.

Constants:

  • OBJECT_MODEL{"equipment": "maintenance.equipment", "vendor": "res.partner", "location": "hr.department"}.
  • DELETE_WINDOW_DAYS = 7.

Common ORM recipes

Resolve effective criticality for a request

def is_red_critical(request):
    # `roots_equipment_criticality_color` is a stored related — no SQL roundtrip.
    return request.roots_equipment_criticality_color == "red"

Find all open PM requests against red equipment that are SLA-breached

env["maintenance.request"].search([
    ("maintenance_type", "=", "preventive"),
    ("roots_equipment_criticality_color", "=", "red"),
    ("roots_is_sla_breached", "=", True),
    ("stage_id.roots_is_done_stage", "=", False),
])

Submit an inspection from code (e.g., a script during data migration)

inspection = env["hemms.asset.inspection"].create({
    "department_id": dept.id,
    "fiscal_year_be": "2569",
    "chair_id": chair_emp.id,
    "committee_member_1_id": m1_emp.id,
    "committee_member_2_id": m2_emp.id,
})
# Add lines through the wizard or directly (line_ids o2m).
inspection.action_start_inspection()
# Set each line's condition.
inspection.action_submit()

Check a contract's coverage decision before billing

contract = equipment.roots_active_contract_id
if not contract:
    bill_hospital()
    return
line = contract.coverage_line_ids.filtered(
    lambda L, k=kind: L.kind_id == k
)[:1]
if not line or not line.included_in_contract:
    bill_hospital()
elif line.billable_to_hospital:
    bill_hospital()
else:
    bill_vendor(contract.partner_id)

(In practice you read maintenance_request.roots_is_billable_to_hospital directly — the compute above already runs.)


  • Architecture — the 3-layer model these APIs sit inside.
  • Module Development — how to build a new module that uses these APIs.
  • CI/CD — make sure your changes pass the same gates.