Inventory → Containers
Purpose
Track every physical bottle, vial, or package of a controlled substance from receipt through depletion or disposal. The container is the unit of audit — every transaction in CS Logbook is tied to one container, and every container traces back to a supplier and invoice.
Regulatory basis: DEA rules require receipt and dispensing records to identify the source, accurate and immutable record-keeping, and disposal records.
When to use it
- Receive a new container: every time a shipment of controlled substances arrives
- Open a container: typically auto-handled by the first transaction against an unopened container
- Dispose of a container: end-of-life for a bottle (empty, expired, contaminated, or DEA Form 41 reverse-distribution)
- Migrate paper logs: entering historical containers from before you adopted CS Logbook
- Spot check: verify physical balance matches recorded balance
Container lifecycle
┌──────────┐
receive → │ unopened │ (sealed, never used)
└─────┬────┘
↓ first transaction
┌──────────┐
│ opened │ (active, can draw from)
└─────┬────┘
↓ balance reaches 0
┌──────────┐
│ depleted │ (cannot receive new transactions)
└──────────┘Side branches from any state:
- expired — past expiration date (soft warning, can still complete final waste/disposal)
- disposed — DEA Form 41 submitted; container is permanently inactive
- destroyed — physically destroyed on-site (witnessed)
- transferred — moved to a different clinic / location
Once a container leaves unopened or opened, no new transactions can be added to it — that is one of the few hard blocks in CS Logbook (preserving audit-trail integrity per DEA record-retention rules).
Walkthrough — receiving a container
Step 1 — Open the containers list
Navigate to /home/<your-clinic>/inventory/containers.
The list shows every container with its number, substance, current balance, status, expiration, and lot number.

After clicking Receive Container, the Receive Container page (/inventory/containers/new) opens with the empty form. The receiving type defaults to Standard Purchase:

The Receiving Type select drives the conditional fields. Each receiving type shows different inputs:
Standard Purchase — supplier and invoice combobox pickers (the default):

Transfer from Another Clinic — source clinic name + transfer documentation number:

Migration (Historical Data) — initial status (unopened or active) plus reference number; useful when importing paper-log inventory:

Pick substance, supplier, invoice, lot number, quantity, and dates in Purchase mode:

After saving, the container appears in the list:

Step 2 — Click "Receive container"
You're taken to the Receive Container page (/inventory/containers/new). The form has conditional sections based on receiving type. (For entering many containers at once, use Bulk receive at /inventory/containers/bulk-receive.)
Step 3 — Choose the receiving type
purchase(default) — you bought the container from a DEA-registered supplier with an invoicetransfer_in— another clinic location transferred the container to you (no commercial invoice)paper_migration— entering historical data from paper logs
Each type changes which fields are required.
Step 4 — Fill in the container details
Required for all types:
- Substance — the drug; selecting auto-populates the unit
- Container/bottle number — unique identifier. Leave blank to use the auto-generated number (preview shown after substance selection, e.g.,
2026-KET-001). Override with a custom number if your clinic uses a different scheme. - Lot number — from the bottle label (manufacturer's batch identifier)
- Initial quantity — amount received (numeric, supports decimals like
9.7) - Unit — auto-populated from substance, read-only
- Received date — when the container physically arrived. Cannot be in the future.
- Expiration date — from the bottle label. Must be after the received date. Required for
purchaseandtransfer_in; optional forpaper_migration(which allows past expirations for historical data).
Optional for all types:
- NDC at receipt — bottle's NDC code (good practice to capture)
- Storage location — pick from your configured locations (vault, fridge, etc.)
- Notes — anything else worth recording
For purchase type, additionally required:
- Supplier — select from your suppliers list
- Invoice — filtered by supplier; select the invoice this container is on
For transfer_in type, additionally required:
- Source clinic name — where the container is coming from
- Transfer documentation number — your transfer paperwork's reference number
For paper_migration type, additionally required:
- Initial container status — was the container
unopenedor alreadyopened(active) when you migrated it? - Opened date — if active, when was it first opened (historically)?
- Reference number — your old paper-log page or system identifier
- Supplier + Invoice — required (use placeholder supplier "Unknown - Historical" and invoice "MIGRATION-
" for legacy data)
Click Save. The container appears in the list with the chosen status.
Step 5 — Use the container
The first transaction against an unopened container automatically transitions it to opened. Each subsequent transaction adjusts the balance. When the balance reaches 0, the container moves to depleted.
Walkthrough — disposing of a container
Step 1 — Open the container detail
From the containers list, click into a specific container.
Step 2 — Click "Dispose"
A confirmation flow opens. You'll be asked:
- Disposal type —
DEA Form 41 reverse-distribution,expired,destroyed on-site, etc. - Disposal date — when the disposal occurred (or was scheduled)
- Witness — required for
destroyed on-site; another clinic member who witnessed the destruction - DEA Form 41 reference — if applicable, the form number / submission ID
- Notes — explanation; e.g., "Contents contaminated; reverse-distributed to
"
Step 3 — Confirm
The container's status changes to disposed (or destroyed). A disposal transaction is auto-created with the witness reference. The container is removed from active inventory but remains queryable for audit purposes.
Hard block: After disposal, the system rejects any further transactions on the container with an error message ("Cannot record transactions on a disposed container — this would corrupt the audit trail"). This is one of the few hard blocks in CS Logbook.
Field reference
Receive container form
Required for all types:
| Field | Label | Type | Required | Validation |
|---|---|---|---|---|
receivingType |
Receiving type | select | ✓ | enum: purchase, transfer_in, paper_migration |
substanceId |
Substance | combobox | ✓ | uuid |
containerNumber |
Container/bottle number | text | ✓ | max 50 chars; auto-generated if blank |
lotNumber |
Lot number | text | ✓ | 1-100 chars |
initialQuantity |
Initial quantity | number | ✓ | > 0; max 999999.99 |
unit |
Unit | select (read-only) | ✓ | enum: mg, mL, tablets, capsules, grams, cc, units; auto-populated |
receivedDate |
Received date | date | ✓ | not in future |
expirationDate |
Expiration date | date | ✓ for purchase/transfer; optional for migration | after receivedDate; not past unless migration |
For purchase:
| Field | Label | Type | Required | Validation |
|---|---|---|---|---|
supplierId |
Supplier | combobox | ✓ | uuid |
invoiceId |
Invoice | combobox | ✓ | uuid; filtered by supplier |
purchasePrice |
Purchase price | number | optional | ≥ 0 |
For transfer_in:
| Field | Label | Type | Required | Validation |
|---|---|---|---|---|
transferSource |
Source clinic name | text | ✓ | max 255 |
transferDocNumber |
Transfer documentation # | text | ✓ | max 100 |
For paper_migration:
| Field | Label | Type | Required | Validation |
|---|---|---|---|---|
initialStatus |
Initial container status | select | ✓ | enum: unopened, active |
openedDate |
Opened date (if active) | date | conditional | ≤ today |
referenceNumber |
Reference number | text | optional | max 255 |
supplierId / invoiceId |
Supplier / Invoice | combobox | ✓ | placeholder values acceptable for legacy |
Optional (all types):
| Field | Label | Type | Required | Validation |
|---|---|---|---|---|
ndc |
NDC at receipt | text | optional | max 11 chars |
storageLocation |
Storage location | select | optional | from configured locations |
notes |
Notes | textarea | optional | max 1000 |
Hidden / system fields: accountId, id, current_balance (mirrors initial_quantity on receive), status, created_by, created_at, updated_at, lock_version (optimistic concurrency).
Edge cases
Soft warning: receiving past-expiration in non-migration mode
If you set receivingType = purchase and the expiration date is in the past, the form blocks save (a real-purchase shouldn't have an expired bottle). For paper_migration mode, past expirations are allowed because you're entering historical data.
Concurrent edits: lock_version
Containers use optimistic concurrency via a lock_version field. If two staff members open the same container at the same time and the first one saves, the second one's save will fail with "Container has been modified — please refresh and try again". This prevents lost-update bugs that would corrupt the balance.
Auto-generated container number scheme
The container number is computed from the clinic's container-number template (configured at clinic settings) and the substance's substanceCode. Default template: {YEAR}-{CODE}-{SEQ:3} produces 2026-KET-001. Tokens:
{YEAR}— the receive year{CODE}— the substance'ssubstance_code(e.g.,KET){SEQ}or{SEQ:n}— sequential number, optionally zero-padded tondigits
The system handles uniqueness — auto-generated numbers won't collide.
Hard block: transactions on a disposed container
After a container is disposed, no new transactions can be added. This includes corrections — you cannot retroactively add a transaction to a disposed container. If you missed a transaction, file an adjustment against a different active container with notes documenting the missed entry.
Re-opening a depleted container
You cannot re-open a depleted container. If you find leftover contents (e.g., the meniscus was misread), file an adjustment transaction with positive quantity on the container — the status returns to opened if balance becomes > 0.
Common mistakes
- Logging the receipt as a separate
receivedtransaction. Don't. The Receive container form auto-creates thereceivedtransaction with the right quantity. Filing a manual one double-counts. - Re-using a container number across substances. Container numbers must be unique per clinic. Auto-generation handles this; if you override, make sure your scheme is unique.
- Disposing a container that still has balance without a witness. Disposal transactions require a witness for waste of remaining contents. The form will block save if witness is missing.
- Not setting initial status correctly during paper migration. If a container was already in use when you migrated, set
initialStatus = activeand theopenedDate. Otherwise the audit log will incorrectly show the container as "unopened" before the historical transactions.
Related
- Substances — the drug; container references it
- Suppliers + Invoices — the receipt chain
- Transactions — what changes a container's balance
- Glossary § Container
- Glossary § Container status