---
title: "Ports & Shipping Lines"
space: "Wiki"
url: "https://jmtradelink.com/docs/setup/ports-shipping-lines"
updated: "2026-06-22"
---

# Ports & Shipping Lines

**Section:** Setup

---

## Overview

Two thin reference doctypes underpin all routing in the system: **Port** and **Shipping Line**. Neither carries business logic — both controller classes are bare `Document` subclasses with no overrides. Their value is as shared lookup targets: the same `Port` record is linked from costings, RFQs, combinations, raw rates, and shipments, so a port rename or country correction propagates everywhere without data-patching.

---

## Port

**DocType:** `Port`
**Module:** Export Management
**Naming:** by `port_name` field (unique, auto-named)
**Quick Entry:** enabled

### Fields

| Field | Fieldname | Type | Required | Notes |
|---|---|---|---|---|
| Port Name | `port_name` | Data | Yes | The display name and document key. Must be unique. |
| Port Code | `port_code` | Data | No | UN/LOCODE or internal shorthand (e.g. `BJANB` for Cotonou). Not validated by the system — informational only. |
| Country | `country` | Link → Country | No | Ties the port to Frappe's built-in Country master. |

### Where it appears

| Document | Field(s) |
|---|---|
| Costing Template | `port_of_discharge` |
| Export Costing | `port_of_loading`, `port_of_discharge` |
| Request for Quotation | `em_port_of_loading`, `em_port_of_discharge` (custom fields) |
| Supplier Quotation Item | `em_port_of_loading` (custom field, per-row) |
| Export Costing Raw Rate | `port_of_loading`, `port_of_discharge` |
| Export Cost Combination | `port_of_loading`, `port_of_discharge` |
| Export Cost Combination Line | `port_of_loading`, `port_of_discharge` |
| Export Shipment | `port_of_loading`, `port_of_discharge` |

---

## Shipping Line

**DocType:** `Shipping Line`
**Module:** Export Management
**Naming:** by `line_name` field (unique, auto-named)
**Quick Entry:** enabled

### Fields

| Field | Fieldname | Type | Required | Notes |
|---|---|---|---|---|
| Shipping Line Name | `line_name` | Data | Yes | The display name and document key. Must be unique. |
| Website | `website` | Data (URL) | No | Carrier booking or tracking portal URL. |

### Where it appears

| Document | Field |
|---|---|
| Export Shipment | `shipping_line` |

Shipping Line does **not** appear on costings or combinations. It is recorded on the Shipment only after a carrier is confirmed — it plays no role in the combination engine's rate selection.

---

## Why the loading port is not fixed by the costing template

The Costing Template pins `port_of_discharge` (the buyer's destination port is known from the contract) but deliberately omits `port_of_loading`. The reason is that at the time a template is created, the cheapest origin port is unknown — that decision depends on which vendor offers the best freight rate out of which port on which day.

The combination engine resolves this at rate-comparison time. It collects every distinct `port_of_loading` value from the costing's raw rates (via `origin_ports()` in `combination_engine.py`) and runs the full landed-cost calculation once per port, producing a separate combination family for each origin. The user then selects whichever combination is cheapest across both supplier and port.

The costing's own `port_of_loading` field therefore acts as a **seed / fallback**: it is included in the candidate port list in case no raw rate carries an explicit port, but it does not restrict which ports the engine considers.

```python
# combination_engine.py — origin_ports()
def origin_ports(self) -> list:
    ports = sorted({row.port_of_loading for row in self.raw_rates if row.port_of_loading})
    if self.port_of_loading and self.port_of_loading not in ports:
        ports.append(self.port_of_loading)
    # every rate is port-agnostic — the single combination space uses the parent port
    return ports or [self.port_of_loading]
```

---

## The blank-port wildcard rule

A vendor rate with a blank `port_of_loading` is treated as an ex-works / any-port offer — it is compatible with every origin port the engine considers. The rule is encoded in two places:

**`matches_route` — the general predicate:**

```python
# combination_engine.py
def matches_route(rate_value: str | None, costing_value: str | None) -> bool:
    # blank-is-wildcard: an unset value on either side matches anything
    return not rate_value or not costing_value or rate_value == costing_value
```

A blank on *either* side returns `True`. This is applied to `port_of_discharge` and `container_type` during `group_rates_by_head()` — if the rate carries no discharge port it participates in any discharge-port costing; likewise for container type.

**`compatible_rates_for_port` — the per-port filter applied during combination building:**

```python
# combination_engine.py
def compatible_rates_for_port(port, variable_heads, rates_by_head) -> list | None:
    rates_per_head = []
    for head in variable_heads:
        compatible = [
            rate for rate in rates_by_head[head]
            if not rate.port_of_loading or rate.port_of_loading == port
        ]
        if not compatible:
            return None
        rates_per_head.append(compatible)
    return rates_per_head
```

A rate row with `port_of_loading = ""` passes the filter for every port. A rate scoped to `Nhava Sheva` will only appear in the `Nhava Sheva` combination family.

### What is NOT a wildcard

The symmetry breaks for discharge port and container type. Those two dimensions are **fixed per costing** — there is only one destination and one container size per deal. A rate that names a different discharge port or container type is excluded entirely in `group_rates_by_head()` before combination building even starts:

```python
# combination_engine.py — group_rates_by_head()
if not matches_route(row.port_of_discharge, self.port_of_discharge):
    continue
if not matches_route(row.container_type, self.container_type):
    continue
```

> **Gotcha:** a vendor who quotes a rate for 20' containers only will never appear in a 40' costing, even if their freight price is lower. Suppliers must quote the correct container type, or leave it blank to signal "any container."

---

## How port flows from RFQ to raw rate

When the RFQ is sent, it carries `em_port_of_loading` and `em_port_of_discharge` as custom fields. The combination engine reads these during `pull_vendor_rates()` to stamp each raw rate row via `route_source_for()` and `resolve_row_port()`.

**Route source resolution (`route_source_for`):** If the Supplier Quotation has any of `em_port_of_loading`, `em_port_of_discharge`, or `em_container_type` set on its header, it is used as the route source — the SQ's own route takes precedence over the originating RFQ. Otherwise the engine falls back to the RFQ linked via the item row's `request_for_quotation` field (or the SQ itself if there is no RFQ link).

**Per-row port resolution (`resolve_row_port`):** Once a route source is established, the per-row port is resolved in priority order:

1. If the individual Supplier Quotation item row carries `em_port_of_loading`, that value is used.
2. If the SQ has *other* rows that carry `em_port_of_loading` (making it a row-level-port quotation), a blank row is stored as `None` — a true wildcard — rather than inheriting from the SQ header or RFQ. This prevents falsely scoping what the supplier intended as an any-port offer.
3. If the SQ uses no row-level ports at all, the engine falls back to the route source's `em_port_of_loading`.

---

## Summary

- Create one **Port** record per physical port. The name is the link key used everywhere; the code field is a convenience label only.
- Create one **Shipping Line** record per carrier. It is referenced only on the Shipment, not during costing or quoting.
- The loading port is intentionally deferred: the combination engine shops across every port present in the raw rates, so a single costing can compare Nhava Sheva vs. Mundra freight in the same run without duplicating the document.
- A vendor rate with no `port_of_loading` automatically participates in every port family — use this for door-to-port or ex-works offers where the origin is genuinely flexible.
- Discharge port and container type on a raw rate are hard filters, not wildcards from the costing's perspective: a rate scoped to a different discharge port or container type is silently excluded from all combinations.
