Data Model

Complete database schema for the Lab Forms module — Process, SubProcess, Question, QuestionValue, and all related entities

👤 Ritu Kataria📅 Updated: Mar 13, 2026🏷️ feature

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.

FieldTypeDescription
namevarchar(100)Display name
descriptionvarchar(250)Optional description
iconvarchar(250)Icon identifier
labFK → LabsOwning lab (NULL for presets)
process_typevarchar(30)One of: Preset, Start Up, Close Down, Other, Patient, Bill, Abort, Test, Sample, Profile, Promotion, Store, Parameter
form_typevarchar(30)One of the 9 supported form types
linked_modelvarchar(30)The Django model class name the process links to (Home Collection, Store, AllTest, etc.)
instance_idintLegacy: single linked instance ID (being phased out in favour of ProcessLinkedInstances)
is_disabledboolSoft-disable flag
requires_pdf_iterationsboolWhether PDF generation should produce multiple iterations
process_codevarchar(30)Unique code identifier
categoryvarchar(50)Preset category grouping
response_preferencesmallint0 = capture once for all instances, 1 = capture separately for each
ttl / ttl_modeint / varchar(15)Expiry duration for consent processes (days/months/years)
linked_subprocessesM2M → SubProcessVia LinkedSubProcess through-table

Index: (lab, form_type) — primary lookup path for fetching all processes for a lab's form type.

Process Types Behaviour

Process TypeInstance-LinkedLinked ModelNotes
Start UpNoHome CollectionOne per lab per form type
Close DownNoHome CollectionOne per lab per form type
PatientNoOne per form type; supports TTL for consent
BillNoBill-level questions
TestYesAllTest (isProfile=0)Linked to specific tests
ProfileYesAllTest (isProfile=1)Linked to specific profiles
SampleYesSampleLinked to specific samples
PromotionYesPromotionPackageLinked to CRM promotions
StoreYesStoreExactly 1 store per process
OtherNoGeneric catch-all
PresetNoGlobal template; lab_id=NULL
AbortNoAbort flow

SubProcess

Table: SubProcess File: admin/account/models/coc/subprocess.py

A section/group within a Process. Contains ordered questions.

FieldTypeDescription
namevarchar(200)Section name
iconvarchar(250)Icon identifier
labFK → LabsOwning lab
is_hiddenboolHidden from UI but present in data
is_disabledboolSoft-disable
is_parkableboolCan be "parked" (saved partially)
is_mandatoryboolMust be completed (mutually exclusive with is_hidden)
for_communicationboolMarks this as a communication-preferences section (consent only)
sub_process_codevarchar(30)Code identifier

Validation Rules:

  • is_mandatory and is_hidden cannot both be true.
  • for_communication=True is only valid for consent form 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.

FieldTypeDescription
processFK → Process
subprocessFK → SubProcess
sequencesmallintDisplay 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.

FieldTypeDescription
questiontextThe label/prompt shown to the user
question_codevarchar(30)Machine-readable identifier (must be unique within subprocess)
subprocessFK → SubProcessParent section
field_typevarchar(20)UI input type (see table below)
is_hiddenboolHidden from UI
is_disabledboolSoft-disable
is_mandatoryboolRequired (mutually exclusive with is_hidden)
allow_skippingboolEnables conditional skip logic
allow_prefillingboolEnables auto-fill from external sources
sequencesmallintDisplay order within subprocess

Ordering: sequence ASC

Supported Field Types

TypeRenderer ComponentNotes
textTextSingle-line text
textareaTextAreaMulti-line text
numberNumberInteger input
floatNumberDecimal input
emailEmailEmail validation
pinTextPIN entry
phonenumberContactNumberPhone with country code
dateDateDate picker
timeTimeTime picker
datetimeDateTimeCombined date+time
selectSelectDropdown (requires options)
checkboxCheckBoxSingle checkbox
checkbox-groupCheckBoxGroupMultiple checkboxes (requires options)
radiobuttonRadioButtonSingle radio (requires exactly 1 option)
radiobutton-groupRadioButtonGroupRadio group (requires options)
signatureSignatureSignature capture (file upload)
imageUploadImage upload
fileUploadFile upload
cameraUploadCamera capture
barcodeBarcode scanner
summaryDisplay-only summary
testlistTest list display
addressAddressStructured address input

These codes are reserved for communication-preference subprocesses:

  • sms_communication
  • fax_communication
  • communication_mode
  • email_communication
  • enable_communication
  • address_communication
  • whatsApp_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.

FieldTypeDescription
questionFK → Question
attributevarchar(50)Attribute key
attribute_valuetextSerialized value

Attribute Categories

CategoryAttributesApplies To
Commonicon, show_helptext, custom_helptext, show_error_text, custom_error_text, placeHolder, readOnly, default_value, disabled, validator, skip_to, skip_to_type, skip_to_conditionAll
Textmax_lengthtext
Numbermin_value, max_value, max_decimal_placesnumber, float
Datedateformat, allow_past_dates, allow_future_datesdate, datetime, time
Filemin_number_upload_file, max_number_upload_file, file_category, allowed_file_typesfile, image, camera, signature
Checkbox Groupoptions (list of {label, value})select, checkbox, radiobutton, checkbox-group, radiobutton-group
Addressaddress_line_1, address_line_2, city, state, country, zip_codeaddress

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:

SourceDescription
Home CollectionFields from the home collection record
BillFields from the billing record
PatientFields 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).

FieldTypeDescription
instance_idbigintPK of the linked entity
labFK → Labs
processFK → Process

Unique constraint: (lab, process, instance_id)

Validation Rules:

  • Only allowed for aoe, consent form 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.

FieldTypeDescription
labFK → Labs
billFK → Billing
patientFK → UserDetails
form_typevarchar(30)
date_communicatedbigintTimestamp when email was first sent
lab_form_slugslug (UUID)Public URL identifier
form_modevarchar(25)Online or Offline
commentstext
form_statusvarchar(25)Pending, Received, Revoked
linked_processesM2M → ProcessVia LabFormLinkedProcesses

Lifecycle:

  1. Created via LabForm.create_consent() — identifies applicable processes, creates form, links processes, generates QR code, sends email.
  2. Processes linked/unlinked via link_process() / unlink_process().
  3. 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.

FieldTypeDescription
lab_formFK → LabForm
labFK → Labs
processFK → Process
statusvarchar(25)Pending or Received
is_revokedbool
commentstext
expires_onbigintExpiry timestamp (from process TTL)

QuestionValue

Table: QuestionValue File: admin/account/models/coc/question_value.py

Captured answer for a single question.

FieldTypeDescription
questionFK → Question
processFK → Process
billFK → Billing
reportFK → LabReportRelationNULL for bill-level / start-up / close-down processes
home_collectionFK → HomeCollectionNULL if not home collection
valuetextAnswer value
iterationsmallintIteration number (for processes with requires_pdf_iterations)
question_sequencesmallintCopied from Question.sequence
subprocess_sequencesmallintCopied from LinkedSubProcess.sequence
commentstextEdit comments
edited_byFK → LabUserLast 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:

LevelProcess TypesHas Report FKTest Info
billLevelBillNoNo
storeLevelStoreNoNo
testLevelTest, ProfileYesYes
promotionLevelPromotionYesYes

PatientConsent

Table: PatientConsent File: admin/account/models/coc/patient_consent.py

Stores per-patient communication preference flags, derived from consent form answers.

FieldTypeDescription
patientFK → UserDetails
is_sms_enabledbool
is_email_enabledbool
is_fax_enabledbool
is_pick_up_enabledbool
is_patient_portal_enabledbool
is_mail_enabledbool
is_whatsapp_enabledbool
*_expires_onbigintPer-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.

FieldTypeDescription
idBigAutoField (PK)Auto-generated primary key
processFK → ProcessThe form process this value belongs to
questionFK → QuestionThe specific question being answered
patientFK → UserDetailsThe patient this info belongs to
valueTextFieldThe answer value (text, file path, etc.)
commentsTextField (nullable)Optional comments
edited_byFK → LabUser (nullable)Who last edited this record
created_atPositiveBigIntegerField (indexed)Unix timestamp of creation
updated_atPositiveBigIntegerFieldUnix 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:

MethodDescription
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.

FieldTypeDescription
idBigAutoField (PK)Auto-generated primary key
labFK → LabsThe lab this setting belongs to
pageCharField(50)Page type: appointment, registration, home_collection, cc_registration
custom_pageFK → CustomPage (nullable)For custom registration pages
processFK → ProcessWhich process to display on this page
is_defaultBooleanFieldWhether this is the default form for the page
is_disabledBooleanFieldSoft-disable flag
created_atPositiveBigIntegerFieldUnix timestamp
updated_atPositiveBigIntegerFieldUnix timestamp

DB constraints:

  1. unique_lab_page_process — unique combination of (lab, page, custom_page, process)
  2. unique_default_settings — only one is_default=True per (lab, page, custom_page)

Key Operations:

MethodDescription
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 → LabUser

PromotionLinkedProcesses

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.

FieldTypeDescription
labFK → Labs
processFK → Process
promotionFK → PromotionPackage
sequenceintDisplay order within the promotion (≥ 1, unique per promotion)
created_atbigint
updated_atbigint

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:

MethodDescription
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.

FieldTypeDescription
labFK → Labs
processFK → Process
storeFK → Store
sequenceintDisplay order within the store (≥ 1, unique per store)
created_atbigint
updated_atbigint

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:

MethodDescription
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.

On this page