Source code for ab.api.endpoints.jobs.form
"""Job-scoped form generation — swagger tags ``JobForm`` + ``InvoiceJobForm`` + ``UsarJobForm`` (15 routes).
Exposed as ``api.jobs.form``. All methods were moved here from the
legacy ``api.forms`` endpoint group; :class:`~ab.api.endpoints.forms.FormsEndpoint`
remains as a deprecation shim that forwards every call to this class.
Method renames (``get_`` prefix dropped on PDF-returning routes; the JSON
route ``get_shipments`` becomes :meth:`shipments`):
* :meth:`invoice` (was ``get_invoice``)
* :meth:`invoice_editable` (was ``get_invoice_editable``)
* :meth:`bill_of_lading` (was ``get_bill_of_lading``)
* :meth:`packing_slip` (was ``get_packing_slip``)
* :meth:`customer_quote` (was ``get_customer_quote``)
* :meth:`quick_sale` (was ``get_quick_sale``)
* :meth:`operations` (was ``get_operations``)
* :meth:`shipments` (was ``get_shipments``)
* :meth:`address_label` (was ``get_address_label``)
* :meth:`item_labels` (was ``get_item_labels``)
* :meth:`packaging_labels` (was ``get_packaging_labels``)
* :meth:`packaging_specification` (was ``get_packaging_specification``)
* :meth:`credit_card_authorization` (was ``get_credit_card_authorization``)
* :meth:`usar` (was ``get_usar``)
* :meth:`usar_editable` (was ``get_usar_editable``)
Convenience helpers (``bol``, ``hbl``, ``pbl``, ``dbl``, ``ops``) are
retained on this class.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from ab.api.models.forms import FormsShipmentPlan
from ab.api.base import BaseEndpoint
from ab.api.route import Route
_INVOICE = Route("GET", "/job/{jobDisplayId}/form/invoice", params_model="FormTypeParams", response_model="bytes")
_INVOICE_EDITABLE = Route("GET", "/job/{jobDisplayId}/form/invoice/editable", response_model="bytes")
_BOL = Route(
"GET", "/job/{jobDisplayId}/form/bill-of-lading", params_model="BillOfLadingParams", response_model="bytes"
)
_PACKING_SLIP = Route("GET", "/job/{jobDisplayId}/form/packing-slip", response_model="bytes")
_CUSTOMER_QUOTE = Route("GET", "/job/{jobDisplayId}/form/customer-quote", response_model="bytes")
_QUICK_SALE = Route("GET", "/job/{jobDisplayId}/form/quick-sale", response_model="bytes")
_OPERATIONS = Route(
"GET", "/job/{jobDisplayId}/form/operations", params_model="OperationsFormParams", response_model="bytes"
)
_SHIPMENTS = Route("GET", "/job/{jobDisplayId}/form/shipments", response_model="List[FormsShipmentPlan]")
_ADDRESS_LABEL = Route("GET", "/job/{jobDisplayId}/form/address-label", response_model="bytes")
_ITEM_LABELS = Route("GET", "/job/{jobDisplayId}/form/item-labels", response_model="bytes")
_PACKAGING_LABELS = Route(
"GET", "/job/{jobDisplayId}/form/packaging-labels", params_model="PackagingLabelsParams", response_model="bytes"
)
_PACKAGING_SPEC = Route("GET", "/job/{jobDisplayId}/form/packaging-specification", response_model="bytes")
_CC_AUTH = Route("GET", "/job/{jobDisplayId}/form/credit-card-authorization", response_model="bytes")
_USAR = Route("GET", "/job/{jobDisplayId}/form/usar", params_model="FormTypeParams", response_model="bytes")
_USAR_EDITABLE = Route("GET", "/job/{jobDisplayId}/form/usar/editable", response_model="bytes")
[docs]
class JobFormEndpoint(BaseEndpoint):
"""Job-scoped form generation (ACPortal API).
Most methods return ``{filename: bytes}`` (PDF content). Use
:meth:`shipments` to get JSON shipment plan data for BOL selection.
"""
def _pdf(self, route: Route, job_display_id: int, name: str, **kw: Any) -> dict[str, bytes]:
data = self._request(route.bind(jobDisplayId=job_display_id), **kw)
return {f"{name}_{job_display_id}.pdf": data}
# ------------------------------------------------------------------
# JSON route (shipment-plan discovery)
# ------------------------------------------------------------------
[docs]
def shipments(self, job_display_id: int) -> list[FormsShipmentPlan]:
"""List shipment plans for *job_display_id* (``GET /job/{jobDisplayId}/form/shipments``).
Returns ``List[FormsShipmentPlan]`` rather than PDF bytes -- use the
``job_shipment_id`` of a plan to drive the BOL routes below.
Docs: https://ab-sdk.readthedocs.io/en/latest/api/jobs/form.shipments.html
Response model: List[FormsShipmentPlan]
"""
return self._request(_SHIPMENTS.bind(jobDisplayId=job_display_id))
# ------------------------------------------------------------------
# PDF routes
# ------------------------------------------------------------------
[docs]
def invoice(self, job_display_id: int, *, type: Optional[str] = None) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/invoice``"""
return self._pdf(_INVOICE, job_display_id, "invoice", params=dict(type=type))
[docs]
def invoice_editable(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/invoice/editable``"""
return self._pdf(_INVOICE_EDITABLE, job_display_id, "invoice_editable")
[docs]
def bill_of_lading(
self,
job_display_id: int,
*,
shipment_plan_id: Optional[str] = None,
provider_option_index: Optional[int] = None,
) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/bill-of-lading``"""
return self._pdf(
_BOL, job_display_id, "bol",
params=dict(shipment_plan_id=shipment_plan_id, provider_option_index=provider_option_index),
)
[docs]
def packing_slip(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/packing-slip``"""
return self._pdf(_PACKING_SLIP, job_display_id, "packing_slip")
[docs]
def customer_quote(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/customer-quote``"""
return self._pdf(_CUSTOMER_QUOTE, job_display_id, "customer_quote")
[docs]
def quick_sale(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/quick-sale``"""
return self._pdf(_QUICK_SALE, job_display_id, "quick_sale")
[docs]
def operations(self, job_display_id: int, *, ops_type: Optional[str] = None) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/operations``"""
return self._pdf(_OPERATIONS, job_display_id, "operations", params=dict(ops_type=ops_type))
[docs]
def address_label(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/address-label``"""
return self._pdf(_ADDRESS_LABEL, job_display_id, "address_label")
[docs]
def item_labels(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/item-labels``"""
return self._pdf(_ITEM_LABELS, job_display_id, "item_labels")
[docs]
def packaging_labels(
self,
job_display_id: int,
*,
shipment_plan_id: Optional[str] = None,
) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/packaging-labels``"""
return self._pdf(
_PACKAGING_LABELS, job_display_id, "packaging_labels",
params=dict(shipment_plan_id=shipment_plan_id),
)
[docs]
def packaging_specification(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/packaging-specification``"""
return self._pdf(_PACKAGING_SPEC, job_display_id, "packaging_spec")
[docs]
def credit_card_authorization(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/credit-card-authorization``"""
return self._pdf(_CC_AUTH, job_display_id, "cc_auth")
[docs]
def usar(self, job_display_id: int, *, type: Optional[str] = None) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/usar``"""
return self._pdf(_USAR, job_display_id, "usar", params=dict(type=type))
[docs]
def usar_editable(self, job_display_id: int) -> dict[str, bytes]:
"""``GET /job/{jobDisplayId}/form/usar/editable``"""
return self._pdf(_USAR_EDITABLE, job_display_id, "usar_editable")
# ------------------------------------------------------------------
# Convenience helpers (transport-type aware BOL selection)
# ------------------------------------------------------------------
def _find_plan(self, job_display_id: int, *transport_types: str) -> FormsShipmentPlan:
"""Find a shipment plan by transport-type preference order."""
plans = self.shipments(job_display_id)
for tt in transport_types:
for plan in plans:
if plan.transport_type == tt:
return plan
raise ValueError(f"No shipment plan with transportType in {transport_types}")
[docs]
def ops(self, job_display_id: int, *, ops_type: Optional[str] = None) -> dict[str, bytes]:
"""Alias for :meth:`operations` that prefixes the PDF filename with ``ops_``."""
return self._pdf(_OPERATIONS, job_display_id, "ops", params=dict(ops_type=ops_type))
[docs]
def bol(self, job_display_id: int) -> dict[str, bytes]:
"""BOL for the freight leg (LTL preferred, falls back to Delivery)."""
plan = self._find_plan(job_display_id, "LTL", "Delivery")
return self._pdf(_BOL, job_display_id, "bol", params=dict(shipment_plan_id=plan.job_shipment_id))
[docs]
def hbl(self, job_display_id: int) -> dict[str, bytes]:
"""House Bill of Lading."""
plan = self._find_plan(job_display_id, "House")
return self._pdf(_BOL, job_display_id, "hbl", params=dict(shipment_plan_id=plan.job_shipment_id))
[docs]
def pbl(self, job_display_id: int) -> dict[str, bytes]:
"""Pickup Bill of Lading."""
plan = self._find_plan(job_display_id, "PickUp")
return self._pdf(_BOL, job_display_id, "pbl", params=dict(shipment_plan_id=plan.job_shipment_id))
[docs]
def dbl(self, job_display_id: int) -> dict[str, bytes]:
"""Delivery Bill of Lading (only valid when an LTL leg exists)."""
plans = self.shipments(job_display_id)
if not any(p.transport_type == "LTL" for p in plans):
raise ValueError("No LTL shipment plan exists -- Delivery BOL not applicable")
delivery = next((p for p in plans if p.transport_type == "Delivery"), None)
if delivery is None:
raise ValueError("No Delivery shipment plan found")
return self._pdf(_BOL, job_display_id, "dbl", params=dict(shipment_plan_id=delivery.job_shipment_id))