Skip to content
ERPNext Data Model
Esc
navigateopen⌘Jpreview
On this page

Delivery & Shipment Logistics

A design reference for outbound goods movement, cross-border parcel data, and route scheduling in a single-codebase ERP system

1. Requirements

1.1 Functional requirements

  • Record the physical movement of goods out of a warehouse against a sales order, a stock-moving sales invoice, or a hand-picked fulfillment list, producing a Dispatch Record that is the system of record for “what left the building.”
  • Support returns: a Dispatch Record can be flagged as a return, carrying negative quantities, rolling back delivered/returned quantities on whatever it was originally issued against.
  • Track billing progress against a Dispatch Record independently of its stock effect, always knowing what fraction of its value has been billed and what fraction has come back as a return.
  • Optionally capture cross-border / parcel-courier data — parcel dimensions and weight, customs description, an Incoterm, a named carrier and service level, an airway-bill/tracking number, and a pickup/delivery window — as a separate Cross-Border Consignment record.
  • Let a warehouse pre-define reusable Parcel Templates (fixed length/width/height/weight) so staff don’t re-enter box dimensions each time.
  • Let a Cross-Border Consignment reference the Dispatch Record(s) whose value it carries, as an informational link.
  • Group multiple Dispatch Records into a Delivery Run: one driver, one vehicle, one departure time, an ordered list of Route Stops, each pointing at a customer address and optionally a Dispatch Record.
  • Estimate per-stop arrival times and total distance for a Delivery Run via an external route-calculation service, optionally re-ordering stops while honoring stops the dispatcher has manually locked.
  • Notify each stop’s contact by email once a run is scheduled, optionally attaching a printable copy of the Dispatch Record.
  • Allow closing a Dispatch Record early and reopening it later.

1.2 Non-functional requirements

  • Consistency of derived percentages: percent-billed and percent-returned must always reflect the current state of every submitted referencing document — recomputed, not incrementally patched.
  • Idempotent status derivation: status is a pure function of percentages, cancel/submit state, and a manual close flag, never a value that can drift out of sync.
  • Graceful degradation of route estimation: if the external route service is unreachable or its credential is missing, the estimate must fail loudly rather than leave stale or wrong distances.
  • Low coupling between subsystems: creating a Consignment or a Run from a Dispatch Record must not block, delay, or alter the Dispatch Record’s own stock and billing lifecycle.
  • Auditable manual overrides: closing/reopening a record and marking a stop “visited” must be visible operations that don’t fight the automatic status recomputation.

1.3 Constraints

  • A Dispatch Record is a submittable, stock-moving transaction — its quantity and value effects flow through the same posting funnel used by every other stock movement, and it participates in the same Cost Layer Queue / Moving Average valuation branching as any other outbound movement. This document does not re-derive that mechanism, only what the dispatch lifecycle feeds into it.
  • The three subsystems here — Dispatch Record, Cross-Border Consignment, Delivery Run — live in three separate, only loosely-connected areas of the codebase. A warehouse can use any one, two, or all three independently.
  • Route calculation depends on a third-party mapping/directions service reachable only with a per-instance API credential; there is no offline or self-hosted fallback in this codebase.

2. High-Level Design

2.1 Component diagram

2.2 How the three subsystems actually relate

This is the load-bearing finding of this document: the Dispatch Record, the Cross-Border Consignment, and the Delivery Run are not one integrated pipeline. Each is a separate document type with its own lifecycle, connected only by thin, mostly one-directional convenience links, not a shared state machine.

  • Dispatch Record to Consignment: a one-shot button copies the grand total, pickup company/address, and customer/contact into a brand-new Consignment, plus one link row (name + value) on its line table. After that the two are independent — changing the Consignment’s tracking status, carrier, or AWB has no effect back on the Dispatch Record, which has no field that even names its Consignment.
  • Dispatch Record to Delivery Run: also a one-shot document copy (“Create > Delivery Trip”), creating one Route Stop per Dispatch Record. Unlike the Consignment case, this is two-way at the field level: the Dispatch Record carries a direct reference to its Delivery Run, and the Run’s save/submit/cancel logic writes the driver, vehicle registration, a received-by-reference number (the Run’s own name), and a reference date back onto every referenced Dispatch Record — cleared again on cancel/delete. Real, live, but narrowly scoped: it never touches quantities, valuation, billing percentages, or the dispatch’s own status.
  • Consignment to Delivery Run: no code path connects these two at all.

In short: two pairings are single-shot field copies; the third adds a narrow, reversible field back-write. None shares a status enum, a percentage, or a stock/value effect.


3. Deep Dive

3.1 Data model

Dispatch Record (the delivery-note transaction) Header: customer, company, shared selling-document currency fields, an optional Delivery Run link, transporter/driver/vehicle/received-by-reference/reference-date fields (manual or Run write-back), is_return, and return_against on return records. Lines: item, quantity, stock quantity, warehouse, rate/amount, links back to the originating sales-order/sales-invoice line and to a pick-list line if fulfillment was planned first, an installed-quantity counter, a billed-amount counter, and — on return lines only — a link to the original line being reversed.

Cross-Border Consignment Unrelated in schema to the Dispatch Record beyond the link rows above. Header: pickup/delivery party (company, customer, or supplier) with address/contact for each, a shipment type (Goods or Documents), a pallet flag, an Incoterm, a free-text customs description, a value-of-goods figure, a pickup date/time window, and a “shipment information” group — service-provider name, external shipment ID, shipment amount, tracking URL, carrier name, carrier service level, airway-bill number, and tracking status (blank / In Progress / Delivered / Returned / Lost). All carrier/tracking fields are plain free-text or select fields with no call-out code here populating or validating them against a real carrier — shaped for an external courier integration that does not exist in the read scope. It computes only two things itself: total weight (each parcel’s weight times count, summed) and value of goods (sum of every linked Dispatch Record’s grand_total, or a manual figure if none are linked).

Parcel Template / Parcel line A Parcel Template is a tiny reusable named record: length, width, height (cm), weight (kg). A Consignment’s parcel line holds the same four measurements plus a count; a template pre-fills a new line but is not a live link.

Delivery Run Header: driver, vehicle, departure time, an ordering employee, a total-estimated-distance figure with its unit, and a table of Route Stops. Status is a small closed enum (Draft / Scheduled / In Transit / Completed / Cancelled) computed from docstatus plus whether all, some, or none of its stops are visited — a hand-written equivalent of the status-map pattern used elsewhere.

Route Stop A child row: customer, address and a resolved display copy of it, a lock flag (the optimizer may not reorder past it), a visited flag (settable post-submission, driving the parent’s status), an optional Dispatch Record reference and its grand total, estimated latitude/longitude, distance from the previous stop, an estimated arrival timestamp, and an email-sent-to marker.

3.2 Status lifecycle — Dispatch Record

Status is never stored as an independent decision; it is recomputed on every save from percent-billed, percent-returned, is_return, docstatus, and a manual Closed flag, using a rule list where later-listed rules outrank earlier ones when more than one matches — Cancelled and Closed both outrank any percentage-driven state:

Two percentages drive this. Percent billed is recomputed across every line whenever a referencing invoice line (or the dispatch’s own billed-amount column) is submitted or cancelled — billed amount over line amount, clamped and summed. A dispatch billed directly against its sales order (rather than against itself) has that amount distributed across dispatches first-submitted-first-billed. Percent returned only exists on a non-return record, populated once a return record referencing it is submitted: returned quantity per line sums the absolute value of every submitted return line pointing at it, and the header percentage is that sum over the original total stock quantity.

A Closed dispatch is a manual override, not a percentage threshold, reversible via Reopen, which recomputes status fresh from current percentages.

3.3 Delivery Run: routing and estimated arrival

  1. A dispatcher creates a Delivery Run — driver, vehicle, departure time — and adds one Route Stop per delivery, by hand or via the one-shot “Create > Delivery Trip” copy from a submitted Dispatch Record. A stop missing a resolved address gets one filled in from the linked address record.
  2. Submitting the Run requires a driver, and requires none of its referenced Dispatch Records still be in Draft.
  3. On submit/save/cancel, the Run’s status is recomputed from docstatus and stop visited-flags, and every referenced Dispatch Record has its driver, vehicle, run reference, and reference date pushed onto it (or cleared, on cancel/delete).
  4. Optionally, a dispatcher triggers arrival-time estimation: stops are split into address sequences at each locked stop (the optimizer cannot reorder across a lock), each sequence is sent to an external route-calculation service from the driver’s home address, and the response’s leg distances/durations are written back per stop — distance in the configured unit, latitude/longitude, and an estimated arrival by walking the departure time forward through each leg plus a configurable inter-stop delay. An optimize request also returns a reordered sequence, applied before arrival times are computed.
  5. Once a stop’s contact has an email address, a dispatch-notification email can be sent per stop, optionally attaching a printed Dispatch Record; the run and each stop record that it went out.

External route-calculation service. The call above goes through a general-purpose mapping/directions client library, authenticated with an API-key credential read from a global settings singleton (systemwide, not scoped per company or run). If the credential is absent, the call is refused up front; its value is never logged or displayed. No offline, cached, or self-hosted routing fallback exists — an unreachable service simply fails the estimate.

3.4 Error handling

  • Warehouse validation: a Dispatch Record line for a stock-tracked item without a resolved warehouse is rejected before submission; if the source sales-order line has a stock reservation, the warehouse must match one of the reserved warehouses, or submission is rejected naming the mismatch.
  • Submitting a Run ahead of its deliveries: rejected, listing which referenced Dispatch Records are still Draft.
  • Reordering past a lock, or no driver-home address: each locked Route Stop is a hard boundary the optimizer never reorders across; without a driver home address, arrival-time estimation is rejected explicitly rather than guessing an origin.
  • Consignment submission guards: cannot submit with no parcel lines, a zero/negative parcel weight, a pickup-to time earlier than pickup-from, or zero value-of-goods.
  • Route-service failures: any exception from the directions call aborts the whole save/estimate action; stops already updated by an earlier successful leg are not rolled back, so a retry after a mid-route failure should treat updated stops as provisional.

4. Scale and Reliability

  • Load pattern: Dispatch Record volume tracks order volume and is the busy path — one record, one posting-funnel call, per outbound shipment. Consignments and Runs are comparatively rare, dispatcher-driven documents.
  • Percentage recomputation cost: percent-billed/returned are recomputed by re-scanning every child row of every affected parent on each referencing submit/cancel, not maintained incrementally — cheap per dispatch, but a sales order billed across many small dispatches triggers a full rescan of each one per invoice submission.
  • Route-service calls are synchronous and blocking: one external HTTP call per address sequence in-line with the dispatcher’s action, no retry/backoff, no background queue.
  • Loose coupling avoids cross-document coordination: the Consignment and Run are only thinly linked to the Dispatch Record, so none needs distributed locking; but the Run’s write-back is per-dispatch, not batched — scheduling a run with many stops issues one separate load-and-save per referenced Dispatch Record rather than one bulk update.

5. Trade-off Analysis

Decision Trade-off
Three separate document types instead of one integrated logistics object Each subsystem is adopted independently and evolves on its own schedule, at the cost of no single place to see “where is this shipment, physically, right now” — assembled by hand across three documents.
Consignment links to the Dispatch Record only via a one-shot copy + a value-only reference row Simple, no ongoing sync burden, survives a later dispatch close/reopen — but a value change on the dispatch after the copy (discount, return) never reflects back, so the two can silently drift apart.
Delivery Run write-back limited to a handful of dispatch fields Narrow and fully reversible on cancel/delete, never risks corrupting the dispatch’s own status or valuation — but billing/return state still lives only on the dispatch, not the run.
Percent-billed/returned recomputed by full rescan, not incremental counters Immune to drift from a missed incremental update, at the cost of rescanning every child row on every referencing submit/cancel rather than maintaining a running total.
Status derived from a fixed, reverse-priority rule list rather than an explicit state machine Compact and reused across many document types — but “last matching rule wins” is easy to get wrong when adding a new status, since ordering rather than explicit precedence decides ties.
Route calculation delegated to one external mapping service with no fallback, and Consignment carrier/tracking fields left as free text with no in-tree integration code Both avoid building or maintaining bespoke logistics infrastructure in-house — but a missing route-service credential means no arrival estimates at all, and free-text tracking fields are only ever as current as whoever last updated them by hand, with no live carrier feed behind either one in this codebase.

6. What to Revisit as the System Grows

  • A real cross-subsystem view: answering “what is the physical status of shipment X” as a single query needs either a genuine Dispatch-to-Consignment link (today only discoverable from the Consignment’s side) or a reporting layer joining all three documents — neither exists today.
  • Batching the Delivery Run write-back and making percentages incremental: replace the per-dispatch load-and-save loop with one bulk update once run sizes grow, and revisit full-rescan percent-billed/returned recompute once a sales order routinely spans enough dispatches/invoices for it to be measurable.
  • Route-service resilience: add retry/backoff and mark a mid-route failure’s already-updated stops as explicitly provisional, since today some stops end up updated and others not, with no marker distinguishing the two.
  • Actual courier integration: the Consignment’s carrier/tracking fields are shaped for one but have no code behind them here; if added elsewhere, this document’s account of “free text, no live call-out” will need updating.
  • This document sits toward the lower end of its calibration band by design: the honest finding is three thin, independently-evolving document types rather than one deep pipeline, and padding the write-up to look more integrated than the code is would misrepresent it.

Was this page helpful?