BWH Shipping

BWH Shipping

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Carrier reference

Every carrier is a Single DocType whose controller extends both Document and ShippingProviderBase:

from frappe.model.document import Document

from bwh_shipping.base_class import ShippingProviderBase


class AcmeCourierShippingSettings(Document, ShippingProviderBase): ...
Method Required Called when
get_rates ✅ Checkout asks for delivery options
create_shipment ✅ A Shipping Request is booked
cancel_shipment ✅ Staff click Cancel With Carrier
get_tracking ✅ Someone clicks Sync Status
handle_webhook ✅ The carrier posts to the webhook endpoint
schedule_pickup Staff click Schedule Pickup
generate_manifest Staff click Generate Manifest
resume_booking Staff click Resume Booking after a partial booking
get_service_choices Your code lists the carrier's services

Rules for every method

  • Weights are in kilograms, dimensions in centimetres, and money in major units of the currency named alongside it. Convert to whatever the carrier wants with bwh_shipping.units.
  • Statuses are ladder statuses, already mapped from the carrier's wording: Draft, Ready To Ship, Pickup Scheduled, In Transit, Out For Delivery, Undelivered, RTO, Delivered, Cancelled, Lost.
  • To report failure, throw with frappe.throw. get_rates is the exception: return [] there.

get_rates

def get_rates(
	self,
	origin: dict,
	destination: dict,
	parcels: list[dict],
	cod: bool = False,
	declared_value: float = 0.0,
) -> list[dict]

Quotes every service that can carry the consignment. It's called once per carrier at checkout, and only when the carrier has an origin (its Pickup Address, unless the checkout passes one).

Parameters

Name Type Description
origin, destination dict See Address below
parcels list[dict] See Parcel below. Passed as the checkout gave them, so normalise them first.
cod bool Whether the shopper chose cash on delivery
declared_value float The cart's value, in major units

Returns a list of:

Key Description
service_code The carrier's id for the service, as a string. It must match a Shipping Service's Service Code, and it's passed back unchanged at booking.
amount The price, in major units
currency The price's currency
service_name, carrier, transit_days, cod_available For display

Return [] when you can't quote, for example when the carrier doesn't serve the route. Checkout then prices the service from its Shipping Rule or Backup Charge. If you raise instead, the error is logged and checkout still renders.


create_shipment

def create_shipment(self, shipment: dict) -> dict

Books one consignment and buys its label. Called by ShippingRequest.book().

The shipment dict

Key Description
reference The Shipping Request name, for example SHIP-26-00012
order_reference The document the shipment is for (ref_docname)
service_code From the Shipping Service
origin, destination Address dicts
parcels Parcel dicts
declared_value, currency Declared value, in major units
cod, cod_amount Cash on delivery
customer {name, phone, email}

Returns

Key Required Description
awb ✅ The tracking number. Webhooks find the shipment by it.
order_ref, shipment_ref ✅ Your handles for cancel, pickup and manifest. They're stored as they are.
label_url Opened by Print Label
carrier The actual courier's name
cost_amount, cost_currency What the carrier charged you
status The carrier's own status, stored as provider_status
tracking_ref A separate tracking handle, if the carrier has one

After it returns, the request's status becomes Ready To Ship.

Throw when the carrier refuses the booking. If part of the booking already exists at the carrier, raise PartialBookingError instead (see Helpers).


cancel_shipment

def cancel_shipment(self, order_ref: str, shipment_ref: str | None = None, awb: str | None = None) -> dict

Cancels a booking. It's only offered while the request is Draft, Ready To Ship or Pickup Scheduled.

Returns {"status", "message"}. status is the carrier's own wording. The request becomes Cancelled either way, so throw if the carrier refuses.


get_tracking

def get_tracking(self, awb: str, shipment_ref: str | None = None, tracking_ref: str | None = None) -> dict

Reads the current status from the carrier. Called by ShippingRequest.sync_status().

Returns

Key Description
status A ladder status, or None to leave it unchanged (scans are still added)
provider_status The carrier's own wording
events Scans: {timestamp, status, location, message}, plus an optional event_id

The status only changes if the new one is higher on the ladder. Scans are added whether or not the status moves. Repeats are skipped by event_id, or by timestamp, status and message.


handle_webhook

def handle_webhook(self, payload: bytes, headers: dict) -> dict

Verifies a delivery to the webhook endpoint and says what it means.

Returns

Return Meaning HTTP
(throws) Verification failed 400
{} Genuine, but nothing to act on 200
{"awb", "status", "event_id", "events", "provider_status"} Apply to the Shipping Request with this AWB 200

See Webhooks and security.


Optional methods

Implement only what the carrier really supports. supports(capability) returns True when your class overrides the method. Desk actions for a capability you don't support fail with a clear message.

Method Capability Returns
schedule_pickup(shipment_ref, pickup_date=None) pickup {pickup_ref, scheduled_date, message}. The status moves to Pickup Scheduled.
generate_manifest(shipment_refs: list[str]) manifest {manifest_url}
resume_booking(order_ref, shipment_ref, service_code=None) resume The same shape as create_shipment
get_service_choices() service_choices {provider, accounts: [{carrier, description, services: [{service_code, service_name}]}]}

get_provider_name() is optional too. It returns a display name, and defaults to the class name.


Address

{
	"contact_name": "Ada Lovelace",
	"company_name": "Lovelace Labs",  # the Address title
	"line1": "1 MG Road",
	"line2": None,
	"city": "Bengaluru",
	"state": "Karnataka",
	"pincode": "560001",
	"country": "India",  # the Frappe Country name, not an ISO code
	"phone": "+919800000000",
	"email": "ada@example.com",
}

Convert country to an ISO code yourself if the carrier needs one. Build this dict from an ERPNext Address with get_address_payload(address_name) from bwh_shipping.bwh_shipping.utils.

Parcel

{"length": 30, "width": 20, "height": 10, "weight": 1.2, "count": 1}  # cm, cm, cm, kg

Helpers

bwh_shipping.units

Helper Example
normalise_parcels(parcels) Rounds and validates a list of parcels. Throws on an empty list.
billable_weight(parcels, divisor=5000) A 50 × 40 × 30 cm, 2 kg parcel → 12.0
volumetric_weight(parcel, divisor=5000) The same parcel → 12.0
total_weight(parcels) The actual weight of every parcel × count
enclosing_dimensions(parcels) One box that holds them all: lengths added, widest width and height
to_kg(value, unit), to_cm(value, unit) to_kg(500, "g") → 0.5
to_system_datetime(value) Converts a timestamp with a timezone offset for storage

bwh_shipping.status: SHIPMENT_STATUSES, can_advance(current, new), is_terminal(status).

bwh_shipping.exceptions.PartialBookingError(message, booking=None): raise it when a staged booking fails part-way, with booking={"order_ref": ..., "shipment_ref": ...}. BWH Shipping saves those references, and the Book Shipment button becomes Resume Booking, which calls your resume_booking.

Last updated 3 weeks ago
Was this helpful?
Thanks!