ข้ามไปที่เนื้อหา

การพัฒนาโมดูล

หน้านี้เป็นคู่มือเชิงปฏิบัติสำหรับการสร้าง โมดูลใหม่ ที่ ขยาย HEMMS — ไม่ว่าจะเป็นการร่วมพัฒนาให้ชุมชนใน roots_hemms_* หรือส่วนขยายส่วนตัวของลูกค้าที่อยู่ในรีโพของคุณเอง โดยถือว่า คุณได้อ่าน สถาปัตยกรรม แล้วและเข้าใจ โมเดลสามชั้น

กฎ 3 ชั้น (สิ่งสำคัญที่สุดในหน้านี้)

โค้ดของคุณอยู่ใน Layer 3 ไม่ใช่ Layer 1 (Odoo base) ไม่ใช่ Layer 2 (OCA building blocks)

ชั้น สิ่งที่อยู่ที่นั่น เมื่อใดที่คุณแตะต้องมัน
Layer 1 — Odoo base maintenance.request, res.partner, hr.employee, … ไม่แตะโดยตรงเด็ดขาด ให้ _inherit จาก Layer 3
Layer 2 — OCA-vendored (addons/oca/) maintenance_plan, maintenance_stock, sign_oca, … ไม่แตะโดยตรงเด็ดขาด ตรึงเวอร์ชันเมื่อ vendor แก้บั๊กที่ upstream ที่ OCA
Layer 3roots_hemms_* (addons/roots/) การกำกับดูแล การปฏิบัติตามข้อกำหนด ขั้นตอนการทำงานของไทยที่เฉพาะโรงพยาบาล นี่คือที่ที่โค้ดของคุณอยู่

ทำไม? เพราะ Layer 1 และ Layer 2 เป็น upstream ที่คุณไม่ได้เป็นเจ้าของ ทุก การเปลี่ยนแปลงที่คุณทำที่นั่นคือการเปลี่ยนแปลงที่คุณต้องดูแลไปตลอดทุก ครั้งที่ upstream เหล่านั้นปล่อยเวอร์ชัน Layer 3 เป็นของคุณ — ของ Trinity Roots หรือ ของลูกค้าคุณ — และผ่านการอัปเกรดได้อย่างสะอาด

โมดูลใหม่อยู่ที่ไหน

addons/
├── oca/                  # vendored OCA — don't put your code here
└── roots/
    ├── roots_hemms_workflow/
    ├── roots_hemms_criticality/
    ├── ...
    └── roots_<your_module>/    # ← your code goes here

ชื่อโฟลเดอร์ คือ ชื่อทางเทคนิค (__manifest__.py ถูก อ่านตามชื่อไดเรกทอรี) สำหรับการร่วมพัฒนา HEMMS ให้ใช้คำนำหน้า roots_hemms_ สำหรับส่วนขยายส่วนตัวของลูกค้า ให้ใช้ roots_<customer>_ เพื่อให้เห็นได้ชัดในแวบเดียวว่าโมดูลนี้ไม่ได้เป็นส่วนหนึ่ง ของชุดสาธารณะ

เทมเพลต manifest

ใช้สิ่งนี้เป็นจุดเริ่มต้น — คัดลอกจากโมดูล roots_hemms_* ที่มีอยู่ แทนที่จะคิดค้นคีย์ขึ้นมาเอง:

addons/roots/roots_hemms_yourmodule/__manifest__.py
{
    "name": "Roots HEMMS — Your Module",
    "version": "18.0.1.0.0",
    "category": "Maintenance",
    "summary": "One-sentence description — what it adds, not how.",
    "author": "Trinity Roots Co., Ltd.",
    "website": "https://roots.tech",
    "license": "LGPL-3",
    "development_status": "Alpha",
    "maintainers": ["yourname"],
    "depends": [
        # Depend on the most specific HEMMS module that gives you what
        # you need, NOT on `maintenance` directly. See "Dependency hygiene"
        # below.
        "roots_hemms_pm",
    ],
    "data": [
        "security/ir.model.access.csv",
        "views/yourmodel_views.xml",
    ],
    "installable": True,
    "application": False,
}

คีย์ที่เราตั้งเสมอ:

  • version: เป็น 18.0.<major>.<minor>.<patch> เสมอ (5 ส่วน — ตาม ธรรมเนียมของ Odoo)
  • license: LGPL-3 สำหรับโมดูลสาธารณะ AGPL-3 เฉพาะเมื่อพึ่งพา โมดูล OCA ที่ใช้ใบอนุญาต AGPL และคุณยอมรับนัยที่ตามมา
  • development_status: Alpha จนกว่าจะเสถียร; Beta หลังจากมีลูกค้า จริงใช้งานอยู่; Stable หลังจากอยู่ใน production หนึ่งปี; Mature หลังจากสองปี ดูเกณฑ์ได้ในสกิล oca-coding-standard
  • maintainers: GitHub handle ของคุณ
  • author: "Trinity Roots Co., Ltd." สำหรับการร่วมพัฒนา HEMMS-core

สุขอนามัยของ dependency

พึ่งพาโมดูล HEMMS ที่เฉพาะเจาะจงที่สุด ที่ให้สิ่งที่คุณ ต้องการ

คุณต้องการ... พึ่งพา
เครื่องมือแพทย์พร้อม brand/model + condition + criticality roots_hemms_asset_master และ roots_hemms_criticality
ขั้นตอนการทำงาน 5 ขั้น + SLA + audit roots_hemms_workflow
ชนิด PM + ขั้น PM Scheduled roots_hemms_pm
บรรทัดอะไหล่ roots_hemms_spare_parts
การค้นหาความครอบคลุมของสัญญา roots_hemms_service_contracts
การลงนามผ่าน sign_oca roots_hemms_signature
MoPH device-class + EM-score waterfall roots_hemms_ha_report

อย่าพึ่งพา maintenance ของ Odoo โดยตรง

หากมีโมดูล HEMMS ที่ขยาย maintenance สำหรับ กรณีใช้งานของคุณอยู่แล้ว ให้พึ่งพามัน การพึ่งพา maintenance โดยตรง เป็นการสร้าง hospital lens ที่ขนานกันขึ้นมาใหม่ ซึ่งเป็นการเบี่ยงเบนแบบ เดียวกับที่กฎ 3 ชั้นมีไว้เพื่อป้องกัน

หลักการตั้งชื่อฟิลด์

ฟิลด์เฉพาะของโรงพยาบาลบนโมเดล Odoo / OCA ที่ สืบทอด มาใช้คำนำหน้า roots_ ส่วนโมเดลใหม่ที่นิยามโดย HEMMS ใช้คำนำหน้า _name ว่า hemms. ฟิลด์ของตัวเองไม่จำเป็นต้องมีคำนำหน้าซ้ำซ้อน

Correct — extending an inherited model
class MaintenanceEquipment(models.Model):
    _inherit = "maintenance.equipment"

    roots_floor_number = fields.Integer()        # ✅ prefixed
    roots_room_code = fields.Char()              # ✅ prefixed
Correct — new model owned by HEMMS
class HemmsCalibrationCert(models.Model):
    _name = "hemms.calibration.cert"             # ✅ hemms. prefix
    _description = "Equipment Calibration Certificate"

    name = fields.Char()                          # ✅ no redundant prefix
    expiry_date = fields.Date()                   # ✅ no redundant prefix
    equipment_id = fields.Many2one("maintenance.equipment")   # ✅ standard Odoo suffix

กฎ suffix ชื่อฟิลด์ (ตามมาตรฐานการเขียนโค้ดของ Odoo):

  • _id สำหรับ Many2one
  • _ids สำหรับ One2many และ Many2many
  • _count สำหรับ compute ที่เก็บค่าและคืนค่า Integer แบบนับจำนวน
  • _date สำหรับ Date / Datetime

อ่านผลลัพธ์ของสกิล odoo-coding-standard สำหรับชุดเต็ม

ลำดับ attribute ของโมเดล

ภายในคลาสของโมเดล attribute เรียงตามลำดับนี้ (สะท้อนโค้ด HEMMS ที่มีอยู่):

class HemmsExample(models.Model):
    # 1. Model attributes (_name, _description, _inherit, _order, _rec_name)
    _name = "hemms.example"
    _description = "Example Model"
    _inherit = ["mail.thread"]
    _order = "name"

    # 2. SQL constraints
    _sql_constraints = [
        ("name_uniq", "unique(name)", "Name must be unique."),
    ]

    # 3. Fields, grouped by type with a comment header
    # ── Char / Text ──────────────────────────────────────────────────
    name = fields.Char(required=True)

    # ── Many2one ─────────────────────────────────────────────────────
    partner_id = fields.Many2one("res.partner")

    # 4. Compute methods (in field-declaration order)
    @api.depends("name")
    def _compute_display_name(self):
        ...

    # 5. CRUD lifecycle (create / write / unlink) overrides
    @api.model_create_multi
    def create(self, vals_list):
        ...

    # 6. Constraints
    @api.constrains("name")
    def _check_name(self):
        ...

    # 7. Public action methods (button handlers, named `action_*`)
    def action_do_the_thing(self):
        ...

    # 8. Private helpers (named `_helper_*` or just `_*`)
    def _helper_internal(self):
        ...

การเขียนการทดสอบ

การทดสอบอยู่ใต้ tests/ ในแต่ละโมดูลและทำตามรูปแบบ TransactionCase ของ Odoo การทดสอบที่มีอยู่เป็นตัวอย่างอ้างอิงที่ดีที่สุด — อ่าน addons/roots/roots_hemms_service_contracts/tests/test_contract_unit.py สำหรับตัวอย่างเทสต์หน่วย และ test_contract_integration.py สำหรับตัวอย่าง เทสต์การรวมระบบ

addons/roots/roots_hemms_yourmodule/tests/test_yourfeature.py
from odoo.tests import TransactionCase, tagged


@tagged("post_install", "-at_install")
class TestYourFeature(TransactionCase):
    """Unit tests for roots_hemms_yourmodule.YourFeature."""

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.equipment = cls.env["maintenance.equipment"].create({
            "name": "Test Equipment",
            "roots_value_tier": "high",
            "roots_risk_tier": "high",
        })

    def test_u01_red_criticality_propagates(self):
        """U1: high value + high risk → criticality_color == 'red'."""
        self.assertEqual(self.equipment.roots_criticality_color, "red")

หลักการติดแท็ก:

  • post_install — การทดสอบส่วนใหญ่ ทำให้แน่ใจว่าโมดูลอื่นถูกโหลดแล้ว
  • at_install เฉพาะสำหรับค่าคงที่ ณ เวลาโหลดโมดูล
  • -at_install ลบล้างค่าเริ่มต้นเพื่อไม่ให้การทดสอบรันสองครั้ง

หลักการตั้งชื่อ:

  • เทสต์หน่วย: test_uNN_<short_description> (ตรงกับรายการ unit ใน §7 ของแผน)
  • เทสต์การรวมระบบ: test_iNN_<short_description>

อย่า import คลาสของโมเดล — ผ่าน self.env["..."] เสมอ

การรันการทดสอบบนเครื่อง

# Run all tests for one module:
make test-module MODULE=roots_hemms_yourmodule

# Or, manually:
odoo --test-enable \
     --test-tags=/roots_hemms_yourmodule \
     --stop-after-init \
     -i roots_hemms_yourmodule \
     -d test_yourmodule

--test-tags=/<module> จำกัดขอบเขตให้โมดูลของคุณเพื่อให้การทดสอบของโมดูลอื่น ไม่รัน

การเชื่อมต่อ CI

CI รันทุกการทดสอบบนทุก PR ผ่าน leaf module ทั้งสี่ใน .github/workflows/tests.yml หากโมดูลของคุณอยู่ในกราฟ dependency ของ leaf เหล่านั้น (ส่วนใหญ่อยู่) คุณจะได้ความครอบคลุมจาก CI ฟรี — ไม่ต้องทำอะไร หากคุณเพิ่มโมดูล HEMMS ที่อยู่ยอดสุดของต้นไม้ (ไม่มีอะไรพึ่งพามัน) ให้เพิ่มเข้าไปใน MODULES_TO_TEST ใน tests.yml และ HEMMS_MODULES ใน Makefile ให้สอง ที่นี้สอดคล้องกันเสมอ

ดู CI/CD สำหรับรายละเอียดขั้นตอนการทำงานทั้งหมด

การตั้งค่า Pre-commit

ทุก commit จะรัน toolchain pre-commit ของ OCA ติดตั้งครั้งเดียวหลังจาก clone:

make docs-install        # gets the venv set up
pip install pre-commit
pre-commit install       # installs the .git/hooks/pre-commit shim

จากนั้น ทุก git commit จะรัน:

  • ruff — lint + จัดรูปแบบ Python
  • pylint_odoo — lint เฉพาะของ Odoo (ส่วนใหญ่เป็น UX/style)
  • oca-checks-odoo-module — ตรวจ manifest + โครงสร้าง
  • สุขอนามัย XML / YAML — ช่องว่างท้ายบรรทัด, newline ท้ายไฟล์ ฯลฯ

หาก hook ใดล้มเหลว commit จะถูกบล็อก แก้ไขแล้ว re-stage; อย่า --no-verify ข้าม hook ที่ล้มเหลว

ในการรัน hook เดียวกันบนทั้งต้นไม้:

make lint       # what CI runs, in 1 command

อ่าน .pre-commit-config.yaml สำหรับสามการเบี่ยงเบนโดยตั้งใจจากค่าเริ่มต้นของ OCA (ข้าม oca-gen-addon-readme, ข้าม oca-fix-manifest-website, comment prettier-xml + eslint ออก)

มาตรฐานการเขียนโค้ด (อ้างอิงสกิล)

ไฟล์สกิลสองไฟล์ในรีโพนี้เข้ารหัสมาตรฐานหลัก เมื่อ สงสัย ให้เรียกใช้พวกมัน:

สกิล ครอบคลุม
odoo-coding-standard ธรรมเนียมทางการของ Odoo — ลำดับ attribute ของโมเดล, การเรียงลำดับชนิดฟิลด์, การเรียงลำดับเมท็อด, กฎ suffix ของฟิลด์, 5 idiom การเขียนโปรแกรมของ Odoo (ส่งต่อ context, คิดแบบขยายได้, อย่า commit transaction เอง, เลี่ยงการดักจับ exception, ใช้ _() อย่างถูกต้อง), รูปแบบข้อความ commit
oca-coding-standard ชั้นบังคับใช้ที่เข้มงวดกว่าของ OCA ที่อยู่ด้านบน — pylint_odoo, oca-checks-odoo-module, ส่วนเสริมของ manifest (development_status, maintainers), วงจรชีวิตความสมบูรณ์ของโมดูล, การสร้าง README จากชิ้นส่วน readme/

รูปแบบข้อความ commit

ทำตามธรรมเนียม OCA ที่ใช้ทั่วทั้งรีโพ:

[TAG](module): one-line summary [QN.M]

Optional longer body explaining the why.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

[TAG] เป็นหนึ่งใน feat, fix, test, docs, chore, style, refactor, build (module) คือโมดูลที่ได้รับผลกระทบ (รูปแบบสั้นก็ ใช้ได้: feat(spare) แทน feat(roots_hemms_spare_parts)) ส่วน suffix [QN.M] อ้างถึง task ในแผน — ดูตัวอย่าง commit ที่มีอยู่ใน git log

ตัวอย่างที่ทำเสร็จแล้ว: การเพิ่มโมดูล calibration-cert

ตัวอย่างเล็ก ๆ ที่ทำเสร็จแล้วแสดงทุกชิ้นส่วนรวมกัน

1. สร้างโครงโมดูล:

mkdir -p addons/roots/roots_hemms_calibration/{models,views,security,data,tests}
touch addons/roots/roots_hemms_calibration/{__init__.py,__manifest__.py}
touch addons/roots/roots_hemms_calibration/models/__init__.py
touch addons/roots/roots_hemms_calibration/tests/__init__.py

2. Manifest:

__manifest__.py
{
    "name": "Roots HEMMS — Calibration Certificates",
    "version": "18.0.1.0.0",
    "category": "Maintenance",
    "summary": "Track calibration certificates per equipment with vendor sign-off.",
    "author": "Trinity Roots Co., Ltd.",
    "website": "https://roots.tech",
    "license": "LGPL-3",
    "development_status": "Alpha",
    "maintainers": ["yourname"],
    "depends": [
        "roots_hemms_asset_master",       # depend on the specific HEMMS module
        "roots_hemms_signature",          # for sign_oca bridge later
    ],
    "data": [
        "security/ir.model.access.csv",
        "views/hemms_calibration_cert_views.xml",
    ],
    "installable": True,
    "application": False,
}

3. โมเดล:

models/hemms_calibration_cert.py
from odoo import fields, models


class HemmsCalibrationCert(models.Model):
    _name = "hemms.calibration.cert"
    _description = "Equipment Calibration Certificate"
    _order = "expiry_date asc, id desc"

    # ── Char / Text ──────────────────────────────────────────────────
    name = fields.Char(required=True)

    # ── Date ─────────────────────────────────────────────────────────
    issued_date = fields.Date(required=True)
    expiry_date = fields.Date(required=True)

    # ── Many2one ─────────────────────────────────────────────────────
    equipment_id = fields.Many2one(
        "maintenance.equipment",
        required=True,
        ondelete="cascade",
        index=True,
    )
    calibration_body_id = fields.Many2one("res.partner")

    # ── Binary ───────────────────────────────────────────────────────
    pdf_file = fields.Binary(attachment=True)
    pdf_filename = fields.Char()

4. การทดสอบ:

tests/test_calibration.py
from odoo.tests import TransactionCase, tagged


@tagged("post_install", "-at_install")
class TestCalibration(TransactionCase):
    def test_u01_create_minimal_cert(self):
        equipment = self.env["maintenance.equipment"].create({"name": "Eq"})
        cert = self.env["hemms.calibration.cert"].create({
            "name": "CAL-001",
            "issued_date": "2026-01-01",
            "expiry_date": "2027-01-01",
            "equipment_id": equipment.id,
        })
        self.assertEqual(cert.equipment_id, equipment)

5. รัน CI บนเครื่อง:

make lint
make test-module MODULE=roots_hemms_calibration

6. Commit:

git add addons/roots/roots_hemms_calibration/
git commit -m "feat(calibration): add calibration-cert module skeleton"

นั่นคือลูปทั้งหมด ทำซ้ำกับฟีเจอร์ต่าง ๆ ภายใต้ด่าน lint + test เดียวกัน

  • สถาปัตยกรรม — โมเดล 3 ชั้นที่โมดูลเหล่านี้อยู่ภายใน
  • API Reference — public surface ของทุกโมดูล HEMMS ที่มีอยู่
  • CI/CD — สิ่งที่ขั้นตอนการทำงาน lint และ test บังคับใช้
  • การร่วมพัฒนา — ขั้นตอนการทำงานของเอกสาร / commit / PR
  • Q-plan ใต้ docs/developer/q*-*-plan.md — ตัวอย่างที่ทำเสร็จแล้วของวิธี กำหนดขอบเขตและส่งมอบไตรมาสหนึ่ง