Skip to main content
Version: 3.0

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

ModelPurpose
ProductA 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.

FieldTypeNotes
facilityFK → facility.Facility (PROTECT)Required at storage, but not a spec field — the viewset sets it from the URL, never the request body.
product_knowledgeFK → 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_definitionFK → 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

FieldTypeRequiredNotes
statusCharField(255)yes (spec)The spec restricts it to ProductStatusOptions: active / inactive / entered_in_error.
product_typeCharField(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

FieldTypeRequiredDefaultNotes
batchJSONField (nullable)nodict / NoneThe column holds an opaque dict; the spec narrows it to ProductBatch, a single optional { lot_number: str | None }.
expiration_dateDateTimeField (nullable)noNoneBatch expiry. Spec type datetime | None.
standard_pack_sizeIntegerField (nullable)noNoneUnits per standard pack. Spec type int | None.
purchase_priceDecimalField(max_digits=20, decimal_places=6) (nullable)noNoneAcquisition cost for this batch. Spec type Decimal | None, held to max_digits=20, decimal_places=6.
extensionsJSONFieldyes (spec)dictOpen 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.

ValueMeaning
activeProduct is in use
inactiveProduct is no longer in use but retained
entered_in_errorRecord 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).

FieldTypeRequiredDefaultNotes
lot_numberstr | NonenoNoneThe 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 classRoleFields beyond the baseBehaviour
BaseProductSpecsharedid, 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.
ProductWriteSpecwrite · createadds 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=...).
ProductUpdateSpecwrite · updateadds 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.
ProductReadSpecread · list & detailadds product_knowledge: dict, charge_item_definition: dict | Noneperform_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_type and facility never appear in the request or response. facility comes from the route; product_type comes from ProductKnowledge.
  • product_knowledge and charge_item_definition go out as slug strings and come back as fully serialized nested objects.
  • extensions is validated on write against the JSON schemas registered for ExtensionResource.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; Product does not track status changes.
  • The base de_serialize dumps with exclude_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 / ProductUpdateSpecde_serializeperform_extra_deserialization (FK slug resolution) → obj.save().
  • Read: Product row → ProductReadSpec.serializeperform_extra_serialization (inline product_knowledge and charge_item_definition) → JSON.
  • PROTECT on every FK blocks deletion of a referenced Facility, ProductKnowledge, or ChargeItemDefinition while products still reference it.

API integration notes

  • A Product carries batch data only. Pull name, codes, dosage form, and product_type from the linked ProductKnowledge — they aren't duplicated here.
  • Send product_knowledge and charge_item_definition as slugs on write; both return as full nested objects on read.
  • Set charge_item_definition to wire the product into billing — charge items are created automatically when it's billed.
  • batch is a structured { lot_number }, not a free-form dict, even though the column is a JSONField.
  • Use extensions for deployment-specific key-value data without a schema migration. Values are checked against the schemas registered for the product extension resource.