Logistic — Architecture (Multimodal Freight & Forwarding)
Імплементаційна сторона платного плагіна Logistic. Операційну модель (bussiness entities, journals, scenarios) — див. operations.md; короткий огляд плагіна — README.md.
Module: logistic (платний плагін, поза community backend)
Date: 2026-02-24
Status: Architecture approved, implementation in progress
Problem Statement
Support multimodal freight forwarding where a single shipment travels via multiple
transport modes (rail → road → sea), involves multiple subcontractors per leg,
and belongs either to a spot order or a long-term contract. Two canonical scenarios:
Scenario A — Container, Kiev → Toronto
1. Rail: Kyiv-Tovarna → Odessa-Port (contractor: UZ)
2. Road: Odessa station → Port Chornomorsk (own vehicle)
3. Sea: Port Chornomorsk → Toronto (chartered / liner)
Scenario B — Bulk Grain Consolidation, Ukraine → Egypt
- 10–20 elevators across Ukraine ship grain (rail + road) to Odessa port
- All grain consolidated at port into one vessel
- Sea: Odessa → Alexandria/Damietta
- One ConsolidationPlan groups N TransportOrders → one Shipment (sea leg)
Core Entity Model
"Multimodal freight forwarding: transport orders, shipments, consolidation planning, vessel bookings and route templates.",
RouteTemplate ──────────────────────────────────────┐
└── RouteTemplateSegment × N (legs config) │
│
FreightContract (long-term, volume, spot) ───────────┤
└── FreightContractRate × N │
▼
TransportOrder (client request) ──────────► Shipment (execution)
└── ShipmentLeg × N
ConsolidationPlan ──────────────────────────► └── TransportDocument × N
└── ConsolidationCollectionOrder × N
VesselBooking (1 booking → N plans)
Django Models
Master Data (logistic/models/master_data.py)
CargoType
| Field |
Type |
Notes |
| name / name_ua |
CharField |
|
| cargo_class |
choice |
bulk, container, breakbulk, tanker, reefer |
| requires_container |
BooleanField |
|
| hs_code_prefix |
CharField |
ТН ЗЕД prefix |
ContainerType
| Field |
Type |
Notes |
| iso_code |
CharField |
22G1, 45G1… |
| length_ft |
IntegerField |
20, 40, 45 |
| max_payload_kg |
DecimalField |
|
| max_volume_m3 |
DecimalField |
|
LocationPoint (logistic-extended)
| Field |
Type |
Notes |
| location_type |
choice |
city, sea_port, river_port, railway_station, warehouse, elevator, customs, airport |
| country |
FK(Country) |
|
| city |
CharField |
|
| un_locode |
CharField |
UN/LOCODE for ports |
| latitude / longitude |
DecimalField |
|
| timezone |
CharField |
|
FreightContractor
| Field |
Type |
Notes |
| client |
OneToOne(essentials.Client) |
reuses client record |
| contractor_type |
choice |
carrier, agent, stevedore, customs_broker, surveyor |
| handles_road/rail/sea/air |
BooleanField |
|
| scac_code |
CharField |
maritime ID |
RouteTemplate
| Field |
Type |
Notes |
| name / name_ua |
CharField |
e.g. "Ukraine → Toronto (Container 40HC)" |
| origin_country |
FK(Country) |
|
| destination_country |
FK(Country) |
|
| cargo_type |
FK(CargoType) |
|
| is_consolidation_route |
BooleanField |
for grain-type scenarios |
| consolidation_point |
FK(LocationPoint) |
e.g. Odessa port |
| estimated_transit_days |
IntegerField |
|
| _subtables |
['segments'] |
|
RouteTemplateSegment
| Field |
Type |
Notes |
| route_template |
FK |
|
| sequence |
IntegerField |
ordering |
| transport_mode |
choice |
road, rail, sea, air |
| from_location |
FK(LocationPoint) |
|
| to_location |
FK(LocationPoint) |
|
| executor_type |
choice |
own, subcontractor |
| default_contractor |
FK(FreightContractor) |
nullable |
| transit_days_min / max |
IntegerField |
|
| generates_document |
choice |
cmr, railway_bill, bl, awb |
Freight Contract (logistic/models/freight_contract.py)
FreightContract (extends TransactionModel)
| Field |
Type |
Notes |
| client |
FK(Client) |
|
| route_template |
FK(RouteTemplate) |
|
| contract_type |
choice |
spot, period, volume |
| valid_from / valid_to |
DateField |
|
| contracted_volume |
Decimal |
for volume contracts |
| contracted_unit |
FK(Unit) |
tons, TEU, m³ |
| shipped_volume |
Decimal |
auto-updated |
| currency |
FK(Currency) |
|
| _subtables |
['rates'] |
|
FreightContractRate
| Field |
Type |
Notes |
| contract |
FK |
|
| segment |
FK(RouteTemplateSegment) |
null = whole route |
| rate_type |
choice |
per_ton, per_container, lumpsum, per_cbm |
| rate |
Decimal |
|
| currency |
FK(Currency) |
|
| valid_from / valid_to |
DateField |
|
| min_quantity |
Decimal |
volume discount threshold |
Transport Order (logistic/models/transport_order.py)
TransportOrder (extends TransactionModel)
| Field |
Type |
Notes |
| client |
FK(Client) |
|
| contract |
FK(FreightContract) |
nullable for spot |
| route_template |
FK(RouteTemplate) |
nullable, can override |
| origin |
FK(LocationPoint) |
|
| destination |
FK(LocationPoint) |
|
| required_loading_date |
DateField |
|
| required_delivery_date |
DateField |
|
| cargo_type |
FK(CargoType) |
|
| cargo_description |
TextField |
|
| gross_weight_kg |
Decimal |
|
| volume_m3 |
Decimal |
|
| container_count |
IntegerField |
|
| container_type |
FK(ContainerType) |
|
| is_hazardous |
BooleanField |
ADR/IMDG |
| requires_customs |
BooleanField |
|
| temperature_regime |
CharField |
for reefer |
| priority |
choice |
normal, high, urgent |
| consolidation_plan |
FK(ConsolidationPlan) |
assigned after plan created |
| shipment |
FK(Shipment) |
set when shipment created |
| _subtables |
['cargo_items'] |
|
TransportOrderCargoItem
| Field |
Type |
Notes |
| order |
FK |
|
| item |
FK(essentials.Item) |
nullable |
| description |
CharField |
|
| quantity / unit |
Decimal + FK |
|
| weight_kg |
Decimal |
|
| hs_code |
CharField |
ТН ЗЕД |
| country_of_origin |
FK(Country) |
|
Shipment (logistic/models/shipment.py)
Shipment (extends TransactionModel)
Status workflow: draft → confirmed → in_progress → completed | cancelled
| Field |
Type |
Notes |
| route_template |
FK |
nullable |
| consolidation_plan |
FK |
nullable |
| total_weight_kg |
Decimal |
|
| total_volume_m3 |
Decimal |
|
| container_count |
IntegerField |
|
| container_type |
FK(ContainerType) |
|
| total_freight_cost |
Decimal |
auto-sum of legs |
| currency |
FK(Currency) |
|
| planned/actual_departure |
DateField |
|
| planned/actual_arrival |
DateField |
|
| customs_declaration_number |
CharField |
|
| incoterms |
choice |
EXW, FOB, CIF, DAP, DDP |
| _subtables |
['legs', 'documents'] |
|
ShipmentLeg
| Field |
Type |
Notes |
| shipment |
FK |
|
| sequence |
IntegerField |
ordering |
| transport_mode |
choice |
road, rail, sea, air |
| from_location |
FK(LocationPoint) |
|
| to_location |
FK(LocationPoint) |
|
| executor_type |
choice |
own, subcontractor |
| contractor |
FK(FreightContractor) |
null if own |
| vehicle |
FK(fleet.Vehicle) |
null unless own |
| driver |
FK(fleet.Driver) |
null unless own |
| waybill |
FK(fleet.Waybill) |
null unless own |
| vessel_name |
CharField |
sea leg |
| voyage_number |
CharField |
sea leg |
| wagon_numbers |
TextField |
rail leg (comma separated) |
| container_numbers |
TextField |
|
| truck_number |
CharField |
road leg |
| planned/actual_departure |
DateTimeField |
|
| planned/actual_arrival |
DateTimeField |
|
| freight_cost |
Decimal |
|
| currency |
FK(Currency) |
|
| cost_basis |
choice |
per_ton, per_container, lumpsum |
| status |
choice |
planned, confirmed, cargo_ready, departed, arrived, completed, delayed |
| tracking_reference |
CharField |
external tracking number |
| delay_reason |
TextField |
|
| _subtables |
['documents'] |
|
TransportDocument
| Field |
Type |
Notes |
| shipment_leg |
FK(ShipmentLeg) |
nullable |
| shipment |
FK(Shipment) |
nullable (shipment-level docs) |
| doc_type |
choice |
cmr, ttn, railway_bill, bl, awb, customs_decl, phyto, quality_cert |
| document_number |
CharField |
|
| issue_date |
DateField |
|
| issued_by |
CharField |
|
| file |
FileField |
S3 / local |
Consolidation (logistic/models/consolidation.py)
ConsolidationPlan (extends TransactionModel)
| Field |
Type |
Notes |
| route_template |
FK |
|
| consolidation_point |
FK(LocationPoint) |
e.g. Odessa port |
| destination |
FK(LocationPoint) |
e.g. Alexandria |
| vessel_booking |
FK(VesselBooking) |
assigned after booking |
| cargo_type |
FK(CargoType) |
|
| target_quantity |
Decimal |
e.g. 8 000 tons |
| target_unit |
FK(Unit) |
|
| collected_quantity |
Decimal |
auto-updated |
| collection_window_from/to |
DateField |
window for collection legs |
| planned_loading_start/end |
DateField |
port loading dates |
| requires_phyto |
BooleanField |
grain export |
| requires_quality_cert |
BooleanField |
|
| _subtables |
['collection_orders'] |
|
ConsolidationCollectionOrder
| Field |
Type |
Notes |
| plan |
FK |
|
| origin |
FK(LocationPoint) |
elevator / warehouse |
| transport_order |
FK(TransportOrder) |
optional link |
| planned_quantity |
Decimal |
|
| unit |
FK(Unit) |
|
| transport_mode |
choice |
road, rail |
| contractor |
FK(FreightContractor) |
|
| planned_departure |
DateField |
|
| planned_arrival_to_port |
DateField |
|
| actual_quantity |
Decimal |
null until arrived |
| status |
choice |
planned, in_transit, arrived |
Vessel Booking (logistic/models/vessel_booking.py)
VesselBooking (extends TransactionModel)
| Field |
Type |
Notes |
| vessel_name |
CharField |
|
| imo_number |
CharField |
|
| voyage_number |
CharField |
|
| booking_type |
choice |
liner, charter |
| shipping_line |
FK(FreightContractor) |
|
| port_of_loading |
FK(LocationPoint) |
|
| port_of_discharge |
FK(LocationPoint) |
|
| etd / eta |
DateTimeField |
estimated |
| atd / ata |
DateTimeField |
actual |
| booked_capacity |
Decimal |
|
| capacity_unit |
FK(Unit) |
tons / TEU |
| actual_loaded |
Decimal |
|
| freight_rate |
Decimal |
|
| rate_basis |
choice |
per_ton, per_teu, lumpsum |
| total_freight |
Decimal |
|
| currency |
FK(Currency) |
|
| laytime_hours |
Decimal |
charter: allowed port time |
| demurrage_rate |
Decimal |
penalty per day over laytime |
| dispatch_rate |
Decimal |
bonus per day under laytime |
| bill_of_lading_number |
CharField |
|
Status Flow
TransportOrder Shipment ShipmentLeg
─────────────── ────────── ─────────────────────
new draft planned
↓ assign ↓ confirm ↓ contractor confirms
confirmed confirmed confirmed
↓ first leg ↓ cargo ready
in_progress cargo_ready
↓ all legs ↓ vehicle departs
completed departed
↓ arrives at next point
arrived
↓ docs received
completed
↑
delayed (any time, resumes)
Scenario Walkthrough: Kiev → Toronto
RouteTemplate "Ukraine → Canada (Container 40HC)"
Seg 1: RAIL Kyiv-Tovarna → Odessa-Port SUB UZ doc=railway_bill
Seg 2: ROAD Odessa stn → Port Chornomorsk OWN own doc=ttn
Seg 3: SEA Chornomorsk → Toronto SUB MSC Agnt doc=bill_of_lading
FreightContract #FC-001 client=ABC Corp type=period valid 2026-2028
TransportOrder #TO-042 contract=FC-001 cargo=Electronics 18t 1×40HC
CargoItems: [{ item=Electronics, qty=500, weight=18000kg, hs=8471 }]
Shipment #SH-042
Leg 1: RAIL Kyiv→Odessa UZ wagon=984512 ЗН=94821 1200 USD
Leg 2: ROAD Odessa→Port OWN Volvo FH №АА1234ОО WB-156 120 USD
Leg 3: SEA Odessa→Toronto MSC Agnt MSC Alina B/L=MSCU1234 8500 USD
total_freight = 9820 USD
Documents:
railway_bill №94821 leg=1
ttn №0042/26 leg=2
bill_of_lading №MSCU1234 leg=3
customs_decl №UA/2026/.. shipment-level
Scenario Walkthrough: Grain Ukraine → Egypt
ConsolidationPlan #CP-008 route=Ukraine→Egypt point=Odessa Port
target=8000 tons wheat vessel_booking=#VB-021 window=1-28 Feb
CollectionOrders:
Elevator Voznesensk 1200t RAIL UZ
Elevator Mykolaiv 800t RAIL UZ
Elevator Kryvyi Rih 1500t RAIL UZ
Elevator Dnipro 2000t RAIL UZ
Elevator Zaporizhzhia 1000t ROAD carrier X
Elevator Kherson 1500t ROAD carrier Y
VesselBooking #VB-021 MV Grain Star voyage=GS-0312
Port of loading=Odessa Port of discharge=Damietta
ETD=5 Mar ETA=12 Mar 8000t charter demurrage=3500 USD/day
Shipment #SH-043 plan=CP-008 vessel=VB-021
Leg 1: SEA Odessa → Damietta MV Grain Star 120000 USD
Documents: bill_of_lading, phyto, quality_cert
Frontend: Process Components
ShipmentTracker (components/Logistic/ShipmentTracker.tsx)
Visual stepper/timeline for a single Shipment:
- Each ShipmentLeg = one step with icon (🚂 🚛 🚢 ✈️)
- Color-coded by status (grey=planned, blue=in_progress, green=completed, red=delayed)
- Shows: contractor, reference numbers, planned vs actual dates, cost per leg
- Total cost summary at bottom
ConsolidationBoard (components/Logistic/ConsolidationBoard.tsx)
Table view for ConsolidationPlan:
- Each row = one CollectionOrder (elevator)
- Columns: origin, planned qty, actual qty, mode, contractor, ETA to port, status badge
- Progress bar header: collected/target tons + % fill
- Vessel ETD countdown timer
File Structure (implementation)
backend/logistic/models/
├── __init__.py (update exports)
├── logistic_order.py (keep, rename later)
├── route_sheet.py (keep)
├── master_data.py ← NEW
├── freight_contract.py ← NEW
├── transport_order.py ← NEW
├── shipment.py ← NEW
├── consolidation.py ← NEW
└── vessel_booking.py ← NEW
frontend/erp/src/
├── config/logistic.ts ← UPDATED (full multimodal structure)
└── components/Logistic/
├── ShipmentTracker.tsx ← NEW Process component
└── ConsolidationBoard.tsx ← NEW Process component
Key Design Decisions
| Decision |
Rationale |
RouteTemplate separate from Shipment |
Enables recurring shipments; one template → N executions |
executor_type=own/sub on each ShipmentLeg |
Our vehicle on leg 2, subcontractors on legs 1 and 3 |
ConsolidationPlan as first-class entity |
Not just "group of orders" — has its own lifecycle, vessel link, collection window |
FreightContractor wraps essentials.Client |
Reuses client accounting records; adds transport-specific fields |
| Document per leg, not per shipment |
Railway bill belongs to rail leg; B/L belongs to sea leg |
VesselBooking independent of ConsolidationPlan |
One vessel booking may serve multiple consolidation plans (groupage) |
| Cost tracked per leg, aggregated to Shipment |
Enables per-mode cost analytics and contractor invoicing |
Distribution Logistics (Last-Mile Delivery)
Concept
Inverse of consolidation. ConsolidationPlan is N → 1 (many origins → one port).
DistributionPlan is 1 → N (one warehouse → many delivery points).
Scenario: company accepted 100 orders → 87 delivery points in 3 Ukrainian regions
(Kyiv City, Kyiv Oblast, Cherkasy Oblast) → 8 own vehicles → 8 delivery routes.
Warehouse (Boryspil)
├── Region: Kyiv City 4 routes 36 stops own fleet
│ ├── R-01 Volvo FH Petrov Obolon/Podil 12 stops ✅
│ ├── R-02 DAF XF Koval Shevchenkivsky 10 stops 🔵 8/10
│ ├── R-03 MAN TGX Moroz Dniprovskyi 8 stops ✅
│ └── R-04 Mercedes Shevchenko Svyatoshynsky 6 stops ⏳ dispatched
├── Region: Kyiv Oblast 2 routes 28 stops
│ ├── R-05 Renault T Tkach Brovary/Boryspil 15 stops 🔵 12/15
│ └── R-06 Volvo FH Bondar. Vasylkiv/Fastiv 13 stops 🔵 6/13
└── Region: Cherkasy Oblast 2 routes 23 stops
├── R-07 DAF XF Lysenko Cherkasy/Smila 12 stops 🔵 10/12
└── R-08 MAN TGX Prokop. Uman/Khrystynivka 11 stops 🔵 4/11
Django Models (logistic/models/distribution.py)
DistributionPlan (extends TransactionModel)
| Field |
Type |
Notes |
| source_warehouse |
FK(LocationPoint) |
origin warehouse |
| delivery_date |
DateField |
planned delivery day |
| orders_total |
IntegerField |
total client orders grouped |
| stops_total |
IntegerField |
total delivery points |
| vehicles_count |
IntegerField |
computed |
| status |
choice |
planning, dispatched, in_progress, completed |
| _subtables |
['routes'] |
|
DistributionRoute (extends TenantAwareModel)
| Field |
Type |
Notes |
| plan |
FK(DistributionPlan) |
|
| number |
CharField |
R-01, R-02... |
| vehicle |
FK(fleet.Vehicle) |
own fleet only |
| driver |
FK(fleet.Driver) |
|
| region |
CharField |
e.g. "Kyiv City" |
| area |
CharField |
e.g. "Obolon / Podil" |
| planned_departure |
DateTimeField |
|
| actual_departure |
DateTimeField |
null |
| planned_return |
DateTimeField |
|
| actual_return |
DateTimeField |
null |
| stop_count |
IntegerField |
total stops |
| delivered_count |
IntegerField |
auto-updated |
| total_weight_kg |
DecimalField |
|
| waybill |
FK(fleet.Waybill) |
generated on dispatch |
| status |
choice |
planned, dispatched, in_progress, completed |
| _subtables |
['stops'] |
|
DistributionStop (extends TenantAwareModel)
| Field |
Type |
Notes |
| route |
FK(DistributionRoute) |
|
| sequence |
IntegerField |
stop order |
| client |
FK(essentials.Client) |
|
| address |
CharField |
full delivery address |
| district |
CharField |
city district / area |
| orders_count |
IntegerField |
orders grouped at this stop |
| weight_kg |
DecimalField |
|
| planned_arrival |
DateTimeField |
|
| actual_arrival |
DateTimeField |
null |
| status |
choice |
pending, in_transit, delivered, failed |
| failure_reason |
TextField |
blank |
| driver_notes |
TextField |
blank |
Difference vs ConsolidationPlan
| Aspect |
ConsolidationPlan |
DistributionPlan |
| Direction |
N → 1 (collect) |
1 → N (deliver) |
| Transport |
Mixed (rail/road) |
Own fleet (road) |
| Contractors |
External + own |
Own fleet only |
| Key document |
Bill of Lading |
Delivery receipt |
| Vessel link |
VesselBooking |
— |
| Split logic |
By origin point |
By region + route optimization |
| Stop type |
Collection point |
Delivery point |
Frontend: DistributionBoard Process Component
Located at: components/Logistic/DistributionBoard.tsx
3-level drill-down layout:
PlanHeader
├── Overall progress bar (delivered/total stops)
└── Stats: orders, stops, vehicles, in-progress, completed, failed
RegionSection × N (collapsible, open by default)
├── Region header: name, route count, stop count, mini progress bar
└── Routes table (compact rows, clickable to expand)
└── RouteRow (on click → expands StopsTable)
└── StopsTable: seq | client | address | orders | weight | planned | actual | status
UI highlights:
- Progress bars change color: red (<30%) → orange (30-60%) → blue (60-90%) → green (90%+)
- Route R-02 expanded by default (shows mixed statuses: delivered/in_transit/pending)
- Stops show actual arrival time in green when delivered, delay notes in orange
- Region filter dropdown to focus on one oblast
- Driver can see ETD of their own route in header
Status Flow
DistributionPlan DistributionRoute DistributionStop
──────────────── ────────────────── ────────────────
planning planned pending
↓ finalize ↓ vehicle assigned
dispatched dispatched pending
↓ first departure ↓ driver departs ↓ en route to stop
in_progress in_progress in_transit
↓ all routes done ↓ all stops done ↓ arrives
completed completed delivered
└── or: failed