Packages
A package is one physical parcel — the unit that actually gets sealed, handed to a carrier, and tracked to delivery. This page tracks every package's journey and lets you record what happens to it after it leaves the warehouse.
Where packages come from
Packages are created as part of picking-wave preparation (packing a wave's stock moves into physical parcels) — there is no create button here. Most packages reach Despatched in bulk, the moment their wave is despatched — see the cascade explained on the Picking Waves page. This page is where you track each package individually from that point on, and seal it beforehand.
Where to find it & who can see it
Open Despatch › Packages in the left navigation. The list is visible to anyone holding package.view (or a super administrator). It is index/view-only — no create or edit form. Each row has a View action showing the package's Contents and its GPS tracking trail (both described below), plus its other row actions.
The package lifecycle
A package can be in one of nine states. Only four transitions are driven from this screen; getting to Despatched normally happens automatically when its wave is despatched.
Columns
| Column | Shows |
|---|---|
package_number | The package's identifying number, shown as a badge. Searchable and sortable. |
| Customer | customer.profile.full_name — who the package is addressed to. |
| Carrier | carrier.name — the delivery carrier handling this package. |
tracking_number | The carrier's tracking reference. Searchable. |
| State | A coloured badge — see the diagram above for the colour key. |
is_cod | A tick/cross icon — whether this is a Cash on Delivery (COD) package (see below). |
cod_received | A tick/cross icon — whether the COD cash-on-delivery amount for this package has been confirmed received. |
The list defaults to newest first (created_at desc).
Filters
| Filter | What it narrows |
|---|---|
| State | All nine PackageState values: Draft, Packing, Sealed, Labeled, Despatched, Delivered, Returned, Lost, Cancelled. Filtering on Sealed is how you find packages that are closed up but whose label has not been recorded yet. |
| is_cod | A three-way yes / no / all toggle to isolate Cash on Delivery packages from ordinary ones. |
Actions
- Seal — Draft or Packing → Sealed Confirmation required
Use this once a package is physically packed and ready to be closed up. Confirming calculates and stores the package's
postage_costautomatically (via the postage calculator, based on weight/carrier/destination) — you do not enter a postage figure yourself. - Mark labelled — Sealed → Labeled Confirmation required
Records that the package's address label has been printed and applied. Only appears on a package currently Sealed. It changes nothing else — no postage is recalculated and no stock is touched.
This step is optional. Despatching a wave sweeps up every one of its packages that has not already been delivered, returned, lost or cancelled — whether or not it was marked labelled. Use it when you want the labelling step on the record, and use the State filter on Sealed to see which packages are still waiting on a label.
- Record delivery — Despatched → Delivered, Returned or Lost
Only appears once a package is Despatched. Unlike most actions in this console, it opens a form directly rather than a plain confirmation dialog:
Field Type Required Notes delivery_stateSelect — Delivered / Returned / Lost Yes The outcome you are recording; the package's state moves to whichever you choose. Choosing Delivered also closes the package's stock moves: any move whose every part has now been delivered moves to Done. A move split across several packages waits until the last of them is delivered. Returned and Lost still leave the stock moves untouched — stock is not automatically put back for a return, and no write-off is raised for a loss; both still need handling by hand. Recording Delivered can also complete the whole sale order — see below. return_reasonTextarea No Free text — typically filled in when recording a Returned outcome, but the form does not enforce this. delivery_notesTextarea No Any additional context about the delivery attempt. - Mark COD received Confirmation required
Only shown when the package is COD and has not yet been marked received. It does not change the package's state — it simply sets
cod_receivedto true, recording that the cash the carrier collected on delivery has come back to AIR (usually because it was reconciled through a payment lot — see below).
When you mark a package Delivered, the portal checks the parent sale order: if that was the last outstanding delivery on it — every delivery move for the order is now either Done or Cancelled — the order moves itself from In Despatch to Completed. Nobody has to close it by hand, and the change is written to the order's own state log with your name against it, exactly as a manual completion would be.
Anything still outstanding holds the order open — a move that is still Reserved, Shipped or Suspended all count as outstanding. So an order with a held part stays In Despatch until that part is either despatched and delivered or cancelled. If an order you expected to close is still sitting in In Despatch, look for its unfinished moves on Stock Moves.
What is inside a package Built
Open a package row's View action and the first tab is Contents — the list of stock moves packed into that parcel. It needs only package.view, the same permission as the list itself. It is read-only: contents are written when the wave's packages are generated (see Picking Waves), never edited here.
| Column | What it means |
|---|---|
| Move | The stock move's reference (MOV-…). Searchable. |
| Product | The product being shipped. Searchable. |
| In this package | How much of that move is in this parcel. |
| Move total | How much the move authorises altogether. When it is larger than In this package, the row is annotated Partially packed — the difference has not been packed yet, and is either in another package or still waiting to be packed. |
| Move state | The stock move's own state badge — see Stock Moves. |
A stock move used to live in exactly one package. It no longer does: a move can be split, so that part of it ships now and the rest ships later, in a different parcel with its own tracking number. That is why In this package and Move total are two separate columns.
In practice, package generation still packs whole moves today, so the two figures will normally match — nothing yet decides to pack a part-quantity automatically. The machinery for a split shipment is in place; what is not in place is anything that chooses to make one on its own.
When a move is split, each parcel decrements only the stock it actually carries at despatch, and the move is only marked Shipped once all of it has gone. The unshipped remainder stays reserved.
Understanding COD
Cash on Delivery (COD) means the customer pays for the goods in cash at the point of delivery — to the postman on an India Post despatch, or to the courier rider — rather than in advance. AIR uses it for certain despatches. A COD package therefore carries a cash liability until that money is actually banked and confirmed — which is why is_cod and cod_received are separate flags, and why Mark COD received exists independently of the delivery outcome itself. The cash from many COD packages is banked together and reconciled in batches — see COD Payment Lots for how that reconciliation works, and note that Mark COD received here can also happen automatically as part of a lot's Reconcile step.
Bulk actions
There are none — packages are actioned one row at a time.
Delivery tracking (GPS) & hand-delivery proof Built
Every despatched package can accumulate a trail of GPS-stamped tracking events — captured by the SE (or hand-delivery carrier) in the field via the Flutter app, not typed in here. The HO console shows this trail read-only: open a package row's View action to see the GPS tracking trail relation manager. It requires delivery_tracking.view — a package.view-only user sees the package but not this tab.
API — recording and reading the trail
| Method & path | Permission | Purpose |
|---|---|---|
POST /api/v1/delivery-tracking-logs |
delivery_tracking.create | Record one GPS event. Body: package_id, event_type (pickup / in_transit / attempted / delivered / returned), latitude (-90..90), longitude (-180..180), optional accuracy_meters, device_info, notes. captured_by and captured_at are set server-side from the authenticated actor and the current time — they cannot be supplied by the client. Returns 201 with the created log. Rejected with 422 if the package is Draft or Cancelled — events can only be logged for a despatchable package. |
GET /api/v1/packages/{package}/tracking-logs |
delivery_tracking.view | Lists a package's tracking logs oldest-first (captured_at ascending), each with the capturing user eager-loaded. This is the same data the HO relation manager renders (sorted the same way). |
The hand-delivery completion rule
delivered_at (whether defaulted to "now" or supplied explicitly on the Record delivery form) can no longer be a future timestamp — the request is rejected with 422 if it is after the current time.
A package attachment can no longer be deleted once its package is Delivered, Returned, or Lost — DELETE /api/v1/packages/{package}/attachments/{attachment} returns 422 for a package in any of these three states. This locks the delivery-challan (and any other) evidence in place permanently once the delivery outcome is settled; attachments can still be freely added or removed while the package is in an earlier state.
Related pages
See Picking Waves for how packages reach Despatched in bulk, COD Payment Lots for how COD cash collected on delivery is banked and reconciled, and Attachments for the delivery-challan / proof-of-delivery upload API referenced above.