Product
A Product is a concrete batch of stock at a facility — a single lot of a medication, nutritional product, or consumable, with its own expiry, lot number, and purchase price. It instantiates a catalogue entry: everything generic about the item (name, codes, dosage form, product_type) lives on the linked ProductKnowledge, and a Product adds only what's true of one batch.
The Django model is storage; the shape you send and receive is defined by the Pydantic resource specs, which set the enums, narrow the model's opaque JSONFields, enforce validation, and split read from write. The fields below note where the spec diverges from the column; Resource specs (API schema) covers the full read/write split.
Source:
Models
| Model | Purpose |
|---|---|
Product | A concrete, batch-level instantiation of a ProductKnowledge definition at a facility (medication, nutritional product, or consumable) |
Product extends EMRBaseModel, which supplies external_id, audit fields, and soft-delete semantics.
Product fields
Relationships
All three foreign keys are PROTECT, so a referenced row can't be deleted while a product points at it.
| Field | Type | Notes |
|---|---|---|
facility | FK → facility.Facility (PROTECT) | Required at storage, but not a spec field — the viewset sets it from the URL, never the request body. |
product_knowledge | FK → emr.ProductKnowledge (PROTECT) | The catalogue definition this batch instantiates. Write takes a slug string and resolves it; read returns the nested ProductKnowledge object. |
charge_item_definition | FK → emr.ChargeItemDefinition (PROTECT, nullable) | Drives charge-item creation when the product is billed. Write takes a slug string; read returns the nested ChargeItemDefinition object. |
Classification & status
| Field | Type | Required | Notes |
|---|---|---|---|
status | CharField(255) | yes (spec) | The spec restricts it to ProductStatusOptions: active / inactive / entered_in_error. |
product_type | CharField(255) | — | A storage column the Product spec does not expose. The authoritative product_type lives on the linked ProductKnowledge (ProductTypeOptions: medication / nutritional_product / consumable). |
Batch & pricing
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
batch | JSONField (nullable) | no | dict / None | The column holds an opaque dict; the spec narrows it to ProductBatch, a single optional { lot_number: str | None }. |
expiration_date | DateTimeField (nullable) | no | None | Batch expiry. Spec type datetime | None. |
standard_pack_size | IntegerField (nullable) | no | None | Units per standard pack. Spec type int | None. |
purchase_price | DecimalField(max_digits=20, decimal_places=6) (nullable) | no | None | Acquisition cost for this batch. Spec type Decimal | None, held to max_digits=20, decimal_places=6. |
extensions | JSONField | yes (spec) | dict | Open extension bag. On write it runs through ExtensionValidator against the schemas registered for ExtensionResource.product — unknown keys are dropped, invalid data raises. |
Enums
ProductStatusOptions values
Defined in spec.py and bound to the model status field.
| Value | Meaning |
|---|---|
active | Product is in use |
inactive | Product is no longer in use but retained |
entered_in_error | Record created in error |
ProductTypeOptions values
Not a field on the Product spec. It's defined in product_knowledge/spec.py and carried on the linked ProductKnowledge; it appears here only because the model keeps a product_type storage column.
| Value |
|---|
medication |
nutritional_product |
consumable |
Nested JSON shapes
ProductBatch shape
Structure of the batch JSONField (care/emr/resources/inventory/product/spec.py).
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
lot_number | str | None | no | None | The only field in the batch shape; omit it and batch stays None. |
Resource specs (API schema)
Every spec derives from EMRResource (care/emr/resources/base.py), which supplies serialize (DB → pydantic, via the perform_extra_serialization hook) and de_serialize (pydantic → DB, via the perform_extra_deserialization hook).
| Spec class | Role | Fields beyond the base | Behaviour |
|---|---|---|---|
BaseProductSpec | shared | id, status, batch, expiration_date, extensions, standard_pack_size, purchase_price | __model__ = Product. __exclude__ = ["product_knowledge", "charge_item_definition"] — these FKs are resolved by the hooks, not by automatic field mapping. ___extension_resource_type__ = ExtensionResource.product. |
ProductWriteSpec | write · create | adds product_knowledge: str (slug), charge_item_definition: str | None (slug) | Mixes in ExtensionValidator. perform_extra_deserialization resolves product_knowledge via get_object_or_404(ProductKnowledge, slug=...) and, when present, charge_item_definition via get_object_or_404(ChargeItemDefinition, slug=...). |
ProductUpdateSpec | write · update | adds charge_item_definition: str | None (slug) | Mixes in ExtensionValidator. perform_extra_deserialization resolves charge_item_definition via ChargeItemDefinition.objects.get(slug=...) when supplied. product_knowledge is immutable after create, so it is not re-bound. |
ProductReadSpec | read · list & detail | adds product_knowledge: dict, charge_item_definition: dict | None | perform_extra_serialization sets id = external_id and inlines product_knowledge (via ProductKnowledgeReadSpec) and, when set, charge_item_definition (via ChargeItemDefinitionReadSpec). One spec serves both list and detail. |
Consequences for an integrator:
product_typeandfacilitynever appear in the request or response.facilitycomes from the route;product_typecomes fromProductKnowledge.product_knowledgeandcharge_item_definitiongo out as slug strings and come back as fully serialized nested objects.extensionsis validated on write against the JSON schemas registered forExtensionResource.product. Keys with no registered handler are silently dropped — current behaviour, with a TODO to make it an error.- There is no server-side
status_history;Productdoes not track status changes. - The base
de_serializedumps withexclude_defaults=True, so unset optional fields never reach the model.
Methods & save behaviour
Product adds no methods of its own. Persistence, external_id, audit fields, and soft delete all come from EMRBaseModel.
- Write: request body →
ProductWriteSpec/ProductUpdateSpec→de_serialize→perform_extra_deserialization(FK slug resolution) →obj.save(). - Read:
Productrow →ProductReadSpec.serialize→perform_extra_serialization(inlineproduct_knowledgeandcharge_item_definition) → JSON. PROTECTon every FK blocks deletion of a referencedFacility,ProductKnowledge, orChargeItemDefinitionwhile products still reference it.
API integration notes
- A
Productcarries batch data only. Pull name, codes, dosage form, andproduct_typefrom the linkedProductKnowledge— they aren't duplicated here. - Send
product_knowledgeandcharge_item_definitionas slugs on write; both return as full nested objects on read. - Set
charge_item_definitionto wire the product into billing — charge items are created automatically when it's billed. batchis a structured{ lot_number }, not a free-form dict, even though the column is aJSONField.- Use
extensionsfor deployment-specific key-value data without a schema migration. Values are checked against the schemas registered for theproductextension resource.
Related
- Reference: Product knowledge
- Reference: Inventory item
- Reference: Supply request
- Reference: Supply delivery
- Reference: Charge item definition
- Reference: Facility
- Reference: Base model
- Source: model
product.py - Source: spec
inventory/product/spec.py - Source: base
resources/base.py