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:
- Model:
care/emr/models/inventory_item.py - Spec:
care/emr/resources/inventory/inventory_item/spec.py - Helpers:
create_inventory_item.py,sync_inventory_item.py
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
| Model | Purpose |
|---|---|
InventoryItem | Stock 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
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
location | FK → FacilityLocation | yes | — | on_delete=PROTECT. The location holding the stock (central store, ward pharmacy, and so on). |
product | FK → Product | yes | — | on_delete=PROTECT. The product being tracked. |
status | CharField(255) | yes | — | The model field is an unconstrained string; the spec restricts it to InventoryItemStatusOptions (see below). |
net_content | DecimalField | no | Decimal(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:
| Value | Meaning |
|---|---|
active | Live and dispensable. Set when the server auto-creates a row. |
inactive | No longer dispensed — damaged stock, or another hold. |
entered_in_error | Record created in error. |
Related models
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 class | Role | Fields exposed | Notes |
|---|---|---|---|
BaseInventoryItemSpec | shared | id, status | __model__ = InventoryItem, __exclude__ = []. status typed as InventoryItemStatusOptions. |
InventoryItemWriteSpec | write | id, status | Inherits BaseInventoryItemSpec unchanged. Only status is client-writable; product, location, and net_content are server-maintained. |
InventoryItemReadSpec | read · list | id, status, net_content, product, location | See serialization below. |
InventoryItemRetrieveSpec | read · detail | same as InventoryItemReadSpec | A pass subclass — identical shape to the list spec. |
Read serialization (InventoryItemReadSpec.perform_extra_serialization)
| Field | Serialized shape | Source |
|---|---|---|
id | UUID4 | obj.external_id — the public id, not the internal pk. |
net_content | Decimal (max_digits=20, decimal_places=0 on the read field) | model net_content. |
product | nested dict | ProductReadSpec.serialize(obj.product).to_json() — carries nested product_knowledge and optional charge_item_definition. See Product. |
location | nested dict | FacilityLocationListSpec.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 databaseIntegrityError, 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_contentis the aggregate sum ofsupplied_item_quantity/ dispensequantity, 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 isstatus(for example, settinginactive). - One row exists per
(location, product)pair.net_contentis adjusted on that row, not by inserting duplicates. - Don't patch
net_contentdirectly —sync_inventory_itemrecomputes it under anInventoryLock, and the next sync overwrites your value. - Read responses inline the full
product(withproduct_knowledge) andlocationobjects, not just their ids. locationandproductarePROTECTforeign keys, so referenced products and locations can't be hard-deleted while stock rows exist.
Related
- Reference: Product
- Reference: Supply Request
- Reference: Supply Delivery
- Reference: Medication Dispense
- Reference: Location
- Reference: Base model
- Source: inventory_item.py on GitHub
- Source: inventory_item spec on GitHub