Data Model
Complete database schema for the Lab Forms module — Process, SubProcess, Question, QuestionValue, and all related entities
Data Model & Schema
DB Schema Diagram: Lab Forms DB Schema
Process
Table: Process
File: admin/account/models/coc/process.py
The root configuration entity. A Process defines a top-level form section scoped to a lab and form type.
| Field | Type | Description |
|---|---|---|
name | varchar(100) | Display name |
description | varchar(250) | Optional description |
icon | varchar(250) | Icon identifier |
lab | FK → Labs | Owning lab (NULL for presets) |
process_type | varchar(30) | One of: Preset, Start Up, Close Down, Other, Patient, Bill, Abort, Test, Sample, Profile, Promotion, Store, Parameter |
form_type | varchar(30) | One of the 9 supported form types |
linked_model | varchar(30) | The Django model class name the process links to (Home Collection, Store, AllTest, etc.) |
instance_id | int | Legacy: single linked instance ID (being phased out in favour of ProcessLinkedInstances) |
is_disabled | bool | Soft-disable flag |
requires_pdf_iterations | bool | Whether PDF generation should produce multiple iterations |
process_code | varchar(30) | Unique code identifier |
category | varchar(50) | Preset category grouping |
response_preference | smallint | 0 = capture once for all instances, 1 = capture separately for each |
ttl / ttl_mode | int / varchar(15) | Expiry duration for consent processes (days/months/years) |
linked_subprocesses | M2M → SubProcess | Via LinkedSubProcess through-table |
Index: (lab, form_type) — primary lookup path for fetching all processes for a lab's form type.
Process Types Behaviour
| Process Type | Instance-Linked | Linked Model | Notes |
|---|---|---|---|
Start Up | No | Home Collection | One per lab per form type |
Close Down | No | Home Collection | One per lab per form type |
Patient | No | — | One per form type; supports TTL for consent |
Bill | No | — | Bill-level questions |
Test | Yes | AllTest (isProfile=0) | Linked to specific tests |
Profile | Yes | AllTest (isProfile=1) | Linked to specific profiles |
Sample | Yes | Sample | Linked to specific samples |
Promotion | Yes | PromotionPackage | Linked to CRM promotions |
Store | Yes | Store | Exactly 1 store per process |
Other | No | — | Generic catch-all |
Preset | No | — | Global template; lab_id=NULL |
Abort | No | — | Abort flow |
SubProcess
Table: SubProcess
File: admin/account/models/coc/subprocess.py
A section/group within a Process. Contains ordered questions.
| Field | Type | Description |
|---|---|---|
name | varchar(200) | Section name |
icon | varchar(250) | Icon identifier |
lab | FK → Labs | Owning lab |
is_hidden | bool | Hidden from UI but present in data |
is_disabled | bool | Soft-disable |
is_parkable | bool | Can be "parked" (saved partially) |
is_mandatory | bool | Must be completed (mutually exclusive with is_hidden) |
for_communication | bool | Marks this as a communication-preferences section (consent only) |
sub_process_code | varchar(30) | Code identifier |
Validation Rules:
is_mandatoryandis_hiddencannot both be true.for_communication=Trueis only valid forconsentform type.- Duplicate subprocess names within the same lab + form type are rejected.
LinkedSubProcess
Table: LinkedSubProcess
File: admin/account/models/coc/linked_subprocess.py
Through-table between Process and SubProcess, with ordering.
| Field | Type | Description |
|---|---|---|
process | FK → Process | |
subprocess | FK → SubProcess | |
sequence | smallint | Display order within the process |
Ordering: (subprocess_id, sequence)
Question
Table: Question
File: admin/account/models/coc/question.py
An individual form field belonging to a SubProcess.
| Field | Type | Description |
|---|---|---|
question | text | The label/prompt shown to the user |
question_code | varchar(30) | Machine-readable identifier (must be unique within subprocess) |
subprocess | FK → SubProcess | Parent section |
field_type | varchar(20) | UI input type (see table below) |
is_hidden | bool | Hidden from UI |
is_disabled | bool | Soft-disable |
is_mandatory | bool | Required (mutually exclusive with is_hidden) |
allow_skipping | bool | Enables conditional skip logic |
allow_prefilling | bool | Enables auto-fill from external sources |
sequence | smallint | Display order within subprocess |
Ordering: sequence ASC
Supported Field Types
| Type | Renderer Component | Notes |
|---|---|---|
text | Text | Single-line text |
textarea | TextArea | Multi-line text |
number | Number | Integer input |
float | Number | Decimal input |
email | Email validation | |
pin | Text | PIN entry |
phonenumber | ContactNumber | Phone with country code |
date | Date | Date picker |
time | Time | Time picker |
datetime | DateTime | Combined date+time |
select | Select | Dropdown (requires options) |
checkbox | CheckBox | Single checkbox |
checkbox-group | CheckBoxGroup | Multiple checkboxes (requires options) |
radiobutton | RadioButton | Single radio (requires exactly 1 option) |
radiobutton-group | RadioButtonGroup | Radio group (requires options) |
signature | Signature | Signature capture (file upload) |
image | Upload | Image upload |
file | Upload | File upload |
camera | Upload | Camera capture |
barcode | — | Barcode scanner |
summary | — | Display-only summary |
testlist | — | Test list display |
address | Address | Structured address input |
Reserved Communication Question Codes (Consent Only)
These codes are reserved for communication-preference subprocesses:
sms_communicationfax_communicationcommunication_modeemail_communicationenable_communicationaddress_communicationwhatsApp_communication
Questions in a for_communication=True subprocess must use one of these codes. Questions outside such subprocesses cannot use them.
QuestionAttribute
Table: QuestionAttribute
File: admin/account/models/coc/question_attribute.py
Key-value metadata attached to a Question that controls rendering and validation behaviour.
| Field | Type | Description |
|---|---|---|
question | FK → Question | |
attribute | varchar(50) | Attribute key |
attribute_value | text | Serialized value |
Attribute Categories
| Category | Attributes | Applies To |
|---|---|---|
| Common | icon, show_helptext, custom_helptext, show_error_text, custom_error_text, placeHolder, readOnly, default_value, disabled, validator, skip_to, skip_to_type, skip_to_condition | All |
| Text | max_length | text |
| Number | min_value, max_value, max_decimal_places | number, float |
| Date | dateformat, allow_past_dates, allow_future_dates | date, datetime, time |
| File | min_number_upload_file, max_number_upload_file, file_category, allowed_file_types | file, image, camera, signature |
| Checkbox Group | options (list of {label, value}) | select, checkbox, radiobutton, checkbox-group, radiobutton-group |
| Address | address_line_1, address_line_2, city, state, country, zip_code | address |
Skip Logic
When allow_skipping=True, the question must have skip_to_conditions in its attributes. This is a dict keyed by answer value (or "default") mapping to:
{
"<value>": {
"skip_to_type": "Process | SubProcess | End Process | Abort Process | End Process & Resume | End Process & Restart",
"skip_to": "<id>",
"values_operation": "reset | iterate"
}
}- Process — skip to another process
- SubProcess — skip to another subprocess
- End Process — end current process, move to close-down if available
- Abort Process — abort and move to next instance
- End Process & Resume — end and resume previous process retaining old values
- End Process & Restart — end and restart previous process
Prefilling
When allow_prefilling=True, the question supports auto-population from external data sources:
| Source | Description |
|---|---|
Home Collection | Fields from the home collection record |
Bill | Fields from the billing record |
Patient | Fields from the patient record |
| `` | Values from another question in the same form (by source_question_id) |
Configuration:
{
"prefilling_source": "Bill",
"prefilling_options": {
"source_field": "labBillId",
"default_value": "",
"requires_value_transformation": false
}
}ProcessLinkedInstances
Table: ProcessLinkedInstances
File: admin/account/models/coc/process_linked_instances.py
Links a Process to specific entity instances (tests, profiles, promotions, store).
| Field | Type | Description |
|---|---|---|
instance_id | bigint | PK of the linked entity |
lab | FK → Labs | |
process | FK → Process |
Unique constraint: (lab, process, instance_id)
Validation Rules:
- Only allowed for
aoe,consentform types. - Instance IDs are validated against the corresponding model (via
PROCESS_TYPE_MODEL_META_MAPPER).
LabForm
Table: LabForm
File: admin/account/models/coc/lab_form.py
A runtime form instance tied to a specific patient + bill. Currently used primarily for consent forms but structured to support all form types.
| Field | Type | Description |
|---|---|---|
lab | FK → Labs | |
bill | FK → Billing | |
patient | FK → UserDetails | |
form_type | varchar(30) | |
date_communicated | bigint | Timestamp when email was first sent |
lab_form_slug | slug (UUID) | Public URL identifier |
form_mode | varchar(25) | Online or Offline |
comments | text | |
form_status | varchar(25) | Pending, Received, Revoked |
linked_processes | M2M → Process | Via LabFormLinkedProcesses |
Lifecycle:
- Created via
LabForm.create_consent()— identifies applicable processes, creates form, links processes, generates QR code, sends email. - Processes linked/unlinked via
link_process()/unlink_process(). - Revoked via
revoke_consent()— marks all linked processes as revoked, revokes patient communication consent.
LabFormLinkedProcesses
Table: LabFormLinkedProcesses
File: admin/account/models/coc/lab_form_linked_processes.py
Through-table linking LabForm to Process with status tracking.
| Field | Type | Description |
|---|---|---|
lab_form | FK → LabForm | |
lab | FK → Labs | |
process | FK → Process | |
status | varchar(25) | Pending or Received |
is_revoked | bool | |
comments | text | |
expires_on | bigint | Expiry timestamp (from process TTL) |
QuestionValue
Table: QuestionValue
File: admin/account/models/coc/question_value.py
Captured answer for a single question.
| Field | Type | Description |
|---|---|---|
question | FK → Question | |
process | FK → Process | |
bill | FK → Billing | |
report | FK → LabReportRelation | NULL for bill-level / start-up / close-down processes |
home_collection | FK → HomeCollection | NULL if not home collection |
value | text | Answer value |
iteration | smallint | Iteration number (for processes with requires_pdf_iterations) |
question_sequence | smallint | Copied from Question.sequence |
subprocess_sequence | smallint | Copied from LinkedSubProcess.sequence |
comments | text | Edit comments |
edited_by | FK → LabUser | Last editor |
Ordering: (subprocess_sequence, question_sequence, iteration)
Index: created_at (used for MIS time-range queries)
Value Levels
Question values are organized into levels based on process type:
| Level | Process Types | Has Report FK | Test Info |
|---|---|---|---|
billLevel | Bill | No | No |
storeLevel | Store | No | No |
testLevel | Test, Profile | Yes | Yes |
promotionLevel | Promotion | Yes | Yes |
PatientConsent
Table: PatientConsent
File: admin/account/models/coc/patient_consent.py
Stores per-patient communication preference flags, derived from consent form answers.
| Field | Type | Description |
|---|---|---|
patient | FK → UserDetails | |
is_sms_enabled | bool | |
is_email_enabled | bool | |
is_fax_enabled | bool | |
is_pick_up_enabled | bool | |
is_patient_portal_enabled | bool | |
is_mail_enabled | bool | |
is_whatsapp_enabled | bool | |
*_expires_on | bigint | Per-channel expiry timestamps |
Communication flags are auto-updated when consent QuestionValues are saved (via PatientConsent.prepare_and_save_communication()).
AdditionalPatientInfo
Table: AdditionalPatientInfo
File: patient/models/additional_patient_info.py
Stores captured patient answers for additional_patient_info forms. Unlike QuestionValue (which is bill-scoped), this is patient-scoped — answers are tied to a patient, not a billing record. This is the key architectural difference: patient demographic/clinical info persists across bills.
| Field | Type | Description |
|---|---|---|
id | BigAutoField (PK) | Auto-generated primary key |
process | FK → Process | The form process this value belongs to |
question | FK → Question | The specific question being answered |
patient | FK → UserDetails | The patient this info belongs to |
value | TextField | The answer value (text, file path, etc.) |
comments | TextField (nullable) | Optional comments |
edited_by | FK → LabUser (nullable) | Who last edited this record |
created_at | PositiveBigIntegerField (indexed) | Unix timestamp of creation |
updated_at | PositiveBigIntegerField | Unix timestamp of last update |
Key difference from QuestionValue: There is no bill_id or report_id — data is stored at the patient level, not the bill level.
Key Operations:
| Method | Description |
|---|---|
validate() | Validates question/process/patient are set, process is active and correct form_type, question is active |
get_additional_patient_info() | Simple fetch → {process_id: {question_id: value}} flat dict |
before_save() | Validates process IDs exist and are valid additional_patient_info forms |
bulk_create() | Creates records in a transaction with activity logging (category 977) |
bulk_update() | Updates existing + creates new records, tracks diffs, logs activity (category 978) |
prepare_diff_json() / generate_update_key() | Formats diff data for the activity log |
get_file_type_values_presigned_url() | Generates S3 pre-signed URLs for file/signature/camera field values |
_build_process_config() | Builds process → subprocess → questions structure from queryset |
_get_questions_with_value() | Fetches and serializes questions with optional attributes |
get_additional_patient_info_details() | Full retrieval with form structure, pre-signed URLs, edited_by info |
get_form_values() | MIS export entry point — uses raw SQL + Elasticsearch branch mapping |
AdditionalPatientInfoSettings
Table: AdditionalPatientInfoSettings
File: admin/account/models/additional_patient_info_settings.py
Configuration model that controls which additional_patient_info form processes are shown on which UI pages.
| Field | Type | Description |
|---|---|---|
id | BigAutoField (PK) | Auto-generated primary key |
lab | FK → Labs | The lab this setting belongs to |
page | CharField(50) | Page type: appointment, registration, home_collection, cc_registration |
custom_page | FK → CustomPage (nullable) | For custom registration pages |
process | FK → Process | Which process to display on this page |
is_default | BooleanField | Whether this is the default form for the page |
is_disabled | BooleanField | Soft-disable flag |
created_at | PositiveBigIntegerField | Unix timestamp |
updated_at | PositiveBigIntegerField | Unix timestamp |
DB constraints:
unique_lab_page_process— unique combination of(lab, page, custom_page, process)unique_default_settings— only oneis_default=Trueper(lab, page, custom_page)
Key Operations:
| Method | Description |
|---|---|
to_dict() | Manual serialization to dict |
get_settings() | Fetch settings grouped by page type; handles custom registration pages |
is_duplicate() | Prevents duplicate settings for same lab/page/process |
validate() | Validates page type, lab state, process validity, custom page validity |
before_bulk_create() | Validates process IDs and checks for existing settings |
bulk_create() | Creates settings in bulk with default process handling |
set_as_default() | Sets a process as default (unsets previous default) |
disable_enable() | Toggles is_disabled with validation (default cannot be disabled) |
Additional Patient Info — Entity-Relationship Summary
Labs ──┐
├── Process (form_type="additional_patient_info")
│ ├── LinkedSubProcess (max 1 for this form type)
│ │ └── SubProcess
│ │ └── Question (field_type, sequence)
│ │ └── QuestionAttribute (options, validators, etc.)
│ └── ProcessLinkedInstances (tests linked to the form)
│
├── AdditionalPatientInfoSettings (page config)
│ ├── page (appointment | registration | home_collection | cc_registration)
│ ├── process → Process
│ └── custom_page → CustomPage (optional)
│
└── UserDetails (patient)
└── AdditionalPatientInfo (captured values)
├── process → Process
├── question → Question
└── edited_by → LabUserPromotionLinkedProcesses
Table: PromotionLinkedProcesses
File: crm/promotion/models/promotion_linked_processes.py
Links Processes to CRM Promotions with ordering. This is a CRM-side through-table that complements ProcessLinkedInstances — while ProcessLinkedInstances tracks the form-config-level link (which instances a process applies to), PromotionLinkedProcesses tracks the promotion-side relationship (which processes are shown when a promotion is selected), along with display sequencing.
| Field | Type | Description |
|---|---|---|
lab | FK → Labs | |
process | FK → Process | |
promotion | FK → PromotionPackage | |
sequence | int | Display order within the promotion (≥ 1, unique per promotion) |
created_at | bigint | |
updated_at | bigint |
Unique constraint: (lab, process, promotion)
DB constraints:
UniqueConstraint(promotion, sequence)— no two processes share the same sequence within a promotion.CheckConstraint(sequence >= 1)— sequence must be positive.
Index: (lab, promotion) — fast lookup of all processes for a given lab + promotion.
Key Operations:
| Method | Description |
|---|---|
link_processes_to_promotions(promotion_ids, process_ids, ...) | Bulk links processes to one or more promotions. Validates process IDs (must be active, belong to lab). Auto-increments sequence per promotion. Optionally creates corresponding ProcessLinkedInstances entries for Promotion-type processes. |
bulk_delete_linked_processes(promotion_ids, process_ids, ...) | Removes links. Optionally cascades to delete ProcessLinkedInstances entries and invalidates process cache. |
Interaction with ProcessLinkedInstances:
When create_process_linked_instances=True (default), linking a Promotion-type process also creates ProcessLinkedInstances records so the form config layer can resolve which processes apply to a given promotion. Deletion mirrors this — removing the CRM-side link also cleans up form-config-side links and invalidates Redis cache.
StoreLinkedProcesses
Table: StoreLinkedProcesses
File: crm/store/models/store_linked_processes.py
Links Processes to CRM Stores with ordering. Analogous to PromotionLinkedProcesses but for the Store entity — tracks which form processes are presented when a store is selected.
| Field | Type | Description |
|---|---|---|
lab | FK → Labs | |
process | FK → Process | |
store | FK → Store | |
sequence | int | Display order within the store (≥ 1, unique per store) |
created_at | bigint | |
updated_at | bigint |
Unique constraint: (lab, process, store)
DB constraints:
UniqueConstraint(store, sequence)— no two processes share the same sequence within a store.CheckConstraint(sequence >= 1)— sequence must be positive.
Index: (lab, store) — fast lookup of all processes for a given lab + store.
Key Operations:
| Method | Description |
|---|---|
link_processes_to_store(store_id, process_ids, ...) | Bulk links processes to a store. Validates process IDs against store_supported_process_types (Bill, Store). Auto-increments sequence. Distinguishes new vs updated processes for activity logging. |
bulk_delete_linked_processes(lab_id, process_ids_to_remove, ...) | Removes links by lab + process IDs. |
Scoped Process Types:
Only processes with process_type in store_supported_process_types (Bill, Store) can be linked to a store. This is validated during link_processes_to_store.