This commit is contained in:
gsinghpal
2026-05-18 22:33:23 -04:00
parent 25f568f225
commit 091f98e1f9
76 changed files with 4521 additions and 220 deletions

View File

@@ -342,6 +342,34 @@ class FedexRequest:
res.append({'number': partner.parent_id.vat, 'tinType': 'BUSINESS_NATIONAL'})
return res
def _strip_customs_for_domestic(self, request_data):
"""Remove customsClearanceDetail when shipper + recipient are in
the same country and the service isn't FEDEX_REGIONAL_ECONOMY.
FedEx rejects domestic Ground/Express requests that carry a
customs block (TOTALCUSTOMSVALUE.REQUIRED). The upstream model
always builds the block; we strip it for clearly-domestic cases.
Caller invokes this immediately before _send_fedex_request.
"""
rs = request_data.get('requestedShipment', {})
shipper = rs.get('shipper') or {}
ship_addr = shipper.get('address') or {}
# Recipient lives under 'recipients' (list) for /ship and
# 'recipient' (single) for /rate. Handle both shapes.
rec = rs.get('recipients') or []
if isinstance(rec, list) and rec:
rec_addr = rec[0].get('address') or {}
else:
rec_addr = (rs.get('recipient') or {}).get('address') or {}
ship_country = ship_addr.get('countryCode')
rec_country = rec_addr.get('countryCode')
if (ship_country and rec_country
and ship_country == rec_country
and self.service_type != 'FEDEX_REGIONAL_ECONOMY'
# India domestic still uses customs per upstream logic.
and not (ship_country == 'IN' and rec_country == 'IN')):
rs.pop('customsClearanceDetail', None)
def _get_shipping_price(self, ship_from, ship_to, packages, currency):
fedex_currency = _convert_curr_iso_fdx(currency)
request_data = {
@@ -364,6 +392,7 @@ class FedexRequest:
}
}
self._add_extra_data_to_request(request_data, 'rate')
self._strip_customs_for_domestic(request_data)
res = self._send_fedex_request("/rate/v1/rates/quotes", request_data)
try:
rate = next(filter(lambda d: d['currency'] == fedex_currency, res['rateReplyDetails'][0]['ratedShipmentDetails']), {})
@@ -474,6 +503,7 @@ class FedexRequest:
request_data['requestedShipment']['customsClearanceDetail']['customsOption'] = {'type': 'COURTESY_RETURN_LABEL'}
self._add_extra_data_to_request(request_data, 'ship')
self._strip_customs_for_domestic(request_data)
res = self._send_fedex_request("/ship/v1/shipments", request_data)
try:
@@ -561,6 +591,7 @@ class FedexRequest:
}
self._add_extra_data_to_request(request_data, 'return')
self._strip_customs_for_domestic(request_data)
res = self._send_fedex_request("/ship/v1/shipments", request_data)
try:
@@ -597,6 +628,62 @@ class FedexRequest:
return actual['totalNetChargeWithDutiesAndTaxes']
return actual['totalNetCharge']
def track_shipment(self, tracking_nr):
"""Call FedEx /track/v1/trackingnumbers and return the parsed
scan-event list. Returns:
{
'tracking_number': '<str>',
'status': '<str — latest status description>',
'events': [
{
'date_time': '<ISO 8601 str>',
'description': '<str>',
'event_type': '<str — FedEx event code>',
'city': '<str>',
'state_province': '<str>',
'country': '<str>',
'signed_by': '<str — present on delivery events>',
}, ...
]
}
Empty events list when FedEx returns no scans yet (newly-printed
label that hasn't been picked up). Raises ValidationError on
HTTP error.
"""
res = self._send_fedex_request("/track/v1/trackingnumbers", {
'includeDetailedScans': True,
'trackingInfo': [{
'trackingNumberInfo': {
'trackingNumber': tracking_nr,
},
}],
})
out = {'tracking_number': tracking_nr, 'status': '', 'events': []}
try:
results = (res.get('completeTrackResults') or [{}])[0]
track = (results.get('trackResults') or [{}])[0]
except (AttributeError, IndexError):
return out
latest = track.get('latestStatusDetail') or {}
out['status'] = (
latest.get('description')
or latest.get('statusByLocale')
or ''
)
for scan in (track.get('scanEvents') or []):
addr = scan.get('scanLocation') or {}
out['events'].append({
'date_time': scan.get('date') or '',
'description': scan.get('eventDescription') or '',
'event_type': scan.get('eventType') or '',
'city': addr.get('city') or '',
'state_province': addr.get('stateOrProvinceCode') or '',
'country': addr.get('countryCode') or '',
'signed_by': track.get('deliveryDetails', {}).get(
'receivedByName', '') or '',
})
return out
def cancel_shipment(self, tracking_nr):
res = self._send_fedex_request('/ship/v1/shipments/cancel', {
'accountNumber': {'value': self.account_number},