Skip to main content
Version: 3.0

Inventory Item

InventoryItem records how much of one Product is on hand at one facility location. Deliveries move product in, dispenses move it out, and the server recomputes the balance into net_content. You never create these rows — the server does, and the only field a client can write is status, such as marking a line inactive when stock is damaged or pulled from circulation.

Source:

The Django model is storage. The Pydantic resource specs (care/emr/resources/inventory/inventory_item/) define the status enum and the read schema, inlining the product and location objects that the model holds only as foreign keys.

Models

ModelPurpose
InventoryItemStock of one product at one location, with its current net quantity

InventoryItem extends EMRBaseModel, the shared Care EMR base providing external_id, audit fields, and soft-delete semantics. See Base model.

InventoryItem fields

FieldTypeRequiredDefaultNotes
locationFK → FacilityLocationyeson_delete=PROTECT. The location holding the stock (central store, ward pharmacy, and so on).
productFK → Productyeson_delete=PROTECT. The product being tracked.
statusCharField(255)yesThe model field is an unconstrained string; the spec restricts it to InventoryItemStatusOptions (see below).
net_contentDecimalFieldnoDecimal(0)max_digits=20, decimal_places=6. On-hand quantity, computed by the server — see sync(). Goes negative when dispenses and outgoing deliveries outrun incoming stock.

The (location, product) pair is unique — one inventory item per product per location — enforced in save() (see Methods & save behaviour).

InventoryItemStatusOptions values

Defined in spec.py. The spec accepts only these three values; the model field itself stays an open CharField:

ValueMeaning
activeLive and dispensable. Set when the server auto-creates a row.
inactiveNo longer dispensed — damaged stock, or another hold.
entered_in_errorRecord created in error.

Two foreign keys anchor the row; two transactional sources set its quantity:

location → FK FacilityLocation (PROTECT)
product → FK Product (PROTECT)

drives net_content (read in sync_inventory_item):
SupplyDelivery (incoming completed, outgoing in-progress/completed)
MedicationDispense (dispensed-out, excluding cancelled statuses)

Both foreign keys are on_delete=PROTECT: a location or product can't be hard-deleted while an inventory row references it. The records that move net_content live in Supply Delivery and Medication Dispense.

Resource specs (API schema)

All specs extend EMRResource (care/emr/resources/base.py), which supplies serialize / de_serialize and the perform_extra_serialization hook that inlines the nested product and location objects.

Spec classRoleFields exposedNotes
BaseInventoryItemSpecsharedid, status__model__ = InventoryItem, __exclude__ = []. status typed as InventoryItemStatusOptions.
InventoryItemWriteSpecwriteid, statusInherits BaseInventoryItemSpec unchanged. Only status is client-writable; product, location, and net_content are server-maintained.
InventoryItemReadSpecread · listid, status, net_content, product, locationSee serialization below.
InventoryItemRetrieveSpecread · detailsame as InventoryItemReadSpecA pass subclass — identical shape to the list spec.

Read serialization (InventoryItemReadSpec.perform_extra_serialization)

FieldSerialized shapeSource
idUUID4obj.external_id — the public id, not the internal pk.
net_contentDecimal (max_digits=20, decimal_places=0 on the read field)model net_content.
productnested dictProductReadSpec.serialize(obj.product).to_json() — carries nested product_knowledge and optional charge_item_definition. See Product.
locationnested dictFacilityLocationListSpec.serialize(obj.location).to_json() — carries parent, mode, has_children, system_availability_status, and optional current_encounter. See Location.

status (typed InventoryItemStatusOptions) carries through from BaseInventoryItemSpec. There is no CreateSpec; rows are never created through a client write path.

Methods & save behaviour

save()

On creation only (when self.id is unset), save() enforces the (location, product) uniqueness:

if creating and InventoryItem with same (location, product) exists:
raise ValueError("Inventory item already exists")
  • The check runs only for new rows; updates to an existing item skip it.
  • A violation raises a plain ValueError, not a database IntegrityError, so callers creating items through the ORM must handle it.

create_inventory_item(product, location)

Helper in create_inventory_item.py. Idempotent get-or-create:

  • Returns the existing (product, location) row if one exists.
  • Otherwise creates one with status=active, net_content=0, and saves it.

sync_inventory_item()

Helper in sync_inventory_item.py, the source of truth for net_content. Holding InventoryLock(product, location), it recomputes the balance from the transactional records and writes it back:

net_content =
Σ incoming completed deliveries (SupplyDelivery.status == completed,
order.destination == location,
supplied_inventory_item.product == product)
- Σ outgoing in-progress + completed deliveries
(SupplyDelivery.order.origin is not null,
supplied_inventory_item == this item,
status in {in_progress, completed})
- Σ dispenses (MedicationDispense.item == this item,
excluding cancelled statuses)
  • If no row exists for (product, location), one is auto-created (status=active, net_content=0) before computing.
  • net_content is the aggregate sum of supplied_item_quantity / dispense quantity, and can be negative.
  • Runs whenever a delivery completes or a dispense changes, so the balance stays current.

API integration notes

  • Rows are server-maintained: they come from create_inventory_item / sync_inventory_item, never a direct client create. The only meaningful client write is status (for example, setting inactive).
  • One row exists per (location, product) pair. net_content is adjusted on that row, not by inserting duplicates.
  • Don't patch net_content directly — sync_inventory_item recomputes it under an InventoryLock, and the next sync overwrites your value.
  • Read responses inline the full product (with product_knowledge) and location objects, not just their ids.
  • location and product are PROTECT foreign keys, so referenced products and locations can't be hard-deleted while stock rows exist.