feat: implement shipping views, carrier allocation, models, and pickup scheduling architecture
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 39s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 39s
This commit is contained in:
parent
920ea42092
commit
b8ba39c3f8
12 changed files with 617 additions and 3 deletions
|
|
@ -153,3 +153,73 @@ class Order(models.Model):
|
|||
- **Tradhox User Experience**:
|
||||
- In Tradhox Supplier Central, once an order is manifested and scheduled, Tradhox surfaces the active **Pickup Request ID (`PR #328095871`)** and **`PICKUP SCHEDULED`** badge.
|
||||
- Merchants do not need to manually click "Add to Pickup" inside the Delhivery One portal; the courier executive will collect all ready packages for warehouse `SAM`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Check Pickup Schedule by Tracking ID (AWB) Specification
|
||||
|
||||
### 7.1. REST Endpoints
|
||||
Clients can query the pickup schedule using either endpoint:
|
||||
|
||||
- **Primary Route**:
|
||||
`GET /api/v1/carrier/pickup-schedule/?tracking_id=<AWB>`
|
||||
- **Unified Alias**:
|
||||
`GET /api/v1/carrier/schedule-pickup/?tracking_id=<AWB>`
|
||||
- **POST Route**:
|
||||
`POST /api/v1/carrier/pickup-schedule/` with JSON `{"tracking_id": "<AWB>"}`
|
||||
|
||||
#### Supported Query Parameters
|
||||
- `tracking_id` / `awb` / `tracking` / `waybill`: The carrier waybill or client order ID (e.g. `69873010000232`).
|
||||
|
||||
### 7.2. Response Schema
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"tracking_id": "69873010000232",
|
||||
"order_id": "32-18",
|
||||
"carrier": "DELHIVERY",
|
||||
"carrier_display": "Delhivery Logistics",
|
||||
"pickup_scheduled": true,
|
||||
"pickup_id": "328095871",
|
||||
"pickup_date": "2026-10-14",
|
||||
"pickup_time": "10:00:00",
|
||||
"pickup_slot": "Morning Slot (10:00 AM – 1:00 PM)",
|
||||
"pickup_location": "SAM",
|
||||
"shipment_status": "Manifested",
|
||||
"pickup_status": "CONFIRMED",
|
||||
"status_display": "Pickup Confirmed with Delhivery Logistics (PR #328095871) for 2026-10-14 (Morning Slot (10:00 AM – 1:00 PM))",
|
||||
"destination": {
|
||||
"city": "Kanchipuram",
|
||||
"pincode": 600059
|
||||
},
|
||||
"consignee": {
|
||||
"name": "Vignesh Krishnasamy",
|
||||
"city": "Kanchipuram",
|
||||
"state": "Tamil Nadu",
|
||||
"pincode": 600059
|
||||
},
|
||||
"scans": [
|
||||
{
|
||||
"scan": "Manifested",
|
||||
"instructions": "Manifest uploaded",
|
||||
"location": "Mettupalayam_IshwaryNgr_D (Tamil Nadu)",
|
||||
"timestamp": "2026-10-12T19:04:35.275"
|
||||
}
|
||||
],
|
||||
"details": {
|
||||
"expected_package_count": 1,
|
||||
"manifest_date": "2026-10-12T19:04:35.245",
|
||||
"picked_up_date": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3. Resolution Hierarchy
|
||||
1. **`shipping_pickupschedule` Table**: Checks if a local record exists.
|
||||
2. **Live Carrier Tracking API**:
|
||||
- For **Delhivery**, calls `/api/v1/packages/json/?waybill={trk}`, extracting origin warehouse, consignee address, and shipment status.
|
||||
- Cross-checks with active warehouse pickup requests.
|
||||
3. **Cross-Database Order Lookup**:
|
||||
- Queries `api_order` in `customerprofile_beta` to correlate `pickup_id` and ETA slot.
|
||||
4. **Auto-Caching**: Updates the `PickupSchedule` model so subsequent lookups are instant.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1555,6 +1555,18 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
|
|||
}, 400
|
||||
|
||||
pickup_id = res.get('pickup_id') or res.get('id') or res.get('pr_id')
|
||||
record_pickup_schedule(
|
||||
tracking_ids=tracking_ids,
|
||||
carrier='DELHIVERY',
|
||||
pickup_date=clean_date,
|
||||
pickup_time=clean_time,
|
||||
pickup_id=pickup_id,
|
||||
pickup_location=location_name,
|
||||
client_order_id=client_order_id,
|
||||
expected_package_count=pkg_cnt,
|
||||
notes=notes,
|
||||
raw_response=res
|
||||
)
|
||||
return {
|
||||
'status': 'success',
|
||||
'carrier': 'DELHIVERY',
|
||||
|
|
@ -1587,6 +1599,16 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
|
|||
dispatch_date=clean_date
|
||||
) if tracking_ids else {'status': True, 'message': 'Preferred dispatch date recorded'}
|
||||
|
||||
record_pickup_schedule(
|
||||
tracking_ids=tracking_ids,
|
||||
carrier='EKART',
|
||||
pickup_date=clean_date,
|
||||
pickup_time='Standard Hub Sweep',
|
||||
pickup_location=pickup_location,
|
||||
client_order_id=client_order_id,
|
||||
notes=notes,
|
||||
raw_response=res
|
||||
)
|
||||
return {
|
||||
'status': 'success',
|
||||
'carrier': 'EKART',
|
||||
|
|
@ -1610,6 +1632,16 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
|
|||
|
||||
# --- 3. SHADOWFAX ---
|
||||
else:
|
||||
record_pickup_schedule(
|
||||
tracking_ids=tracking_ids,
|
||||
carrier='SHADOWFAX',
|
||||
pickup_date=clean_date,
|
||||
pickup_time='Automated Cut-Off Dispatch Sweep',
|
||||
pickup_location=pickup_location,
|
||||
client_order_id=client_order_id,
|
||||
notes=notes,
|
||||
raw_response={'status': 'success', 'scheduled': True}
|
||||
)
|
||||
return {
|
||||
'status': 'success',
|
||||
'carrier': 'SHADOWFAX',
|
||||
|
|
@ -1624,3 +1656,277 @@ def schedule_carrier_pickup(carrier, pickup_date, pickup_time=None, pickup_locat
|
|||
'raw_response': {'status': 'success', 'scheduled': True}
|
||||
}, 200
|
||||
|
||||
|
||||
def record_pickup_schedule(tracking_ids, carrier, pickup_date, pickup_time, pickup_id=None, pickup_location=None, client_order_id=None, expected_package_count=1, notes='', raw_response=None, status='SCHEDULED'):
|
||||
"""
|
||||
Saves or updates PickupSchedule records for tracking ID(s) upon scheduling.
|
||||
"""
|
||||
try:
|
||||
from .models import PickupSchedule
|
||||
carrier_upper = str(carrier or 'DELHIVERY').strip().upper()
|
||||
carrier_disp = 'Delhivery Logistics' if 'DELHI' in carrier_upper else ('Ekart Logistics' if 'EKART' in carrier_upper else 'Shadowfax Logistics')
|
||||
|
||||
slot_label = ''
|
||||
t_str = str(pickup_time or '').strip()
|
||||
if t_str.startswith('10'):
|
||||
slot_label = 'Morning Slot (10:00 AM – 1:00 PM)'
|
||||
elif t_str.startswith('14'):
|
||||
slot_label = 'Afternoon Slot (2:00 PM – 5:00 PM / 14:00 – 18:00)'
|
||||
elif t_str.startswith('17'):
|
||||
slot_label = 'Evening Slot (5:00 PM – 7:00 PM)'
|
||||
else:
|
||||
slot_label = t_str or 'Standard Sweep'
|
||||
|
||||
t_list = tracking_ids if isinstance(tracking_ids, list) and tracking_ids else ([str(tracking_ids).strip()] if tracking_ids else [])
|
||||
if not t_list and client_order_id:
|
||||
t_list = [f"ORD-{client_order_id}"]
|
||||
if not t_list:
|
||||
t_list = [f"LOC-{pickup_location or 'HUB'}-{pickup_date}"]
|
||||
|
||||
created_objs = []
|
||||
for trk in t_list:
|
||||
if not trk:
|
||||
continue
|
||||
obj, _ = PickupSchedule.objects.update_or_create(
|
||||
tracking_id=str(trk).strip(),
|
||||
defaults={
|
||||
'client_order_id': str(client_order_id or '').strip(),
|
||||
'carrier': carrier_upper,
|
||||
'carrier_display': carrier_disp,
|
||||
'pickup_id': str(pickup_id or '').strip(),
|
||||
'pickup_date': str(pickup_date),
|
||||
'pickup_time': str(pickup_time),
|
||||
'pickup_slot': slot_label,
|
||||
'pickup_location': str(pickup_location or ''),
|
||||
'status': status,
|
||||
'expected_package_count': int(expected_package_count or 1),
|
||||
'notes': str(notes or ''),
|
||||
'raw_response': raw_response if isinstance(raw_response, dict) else {'data': str(raw_response)}
|
||||
}
|
||||
)
|
||||
created_objs.append(obj)
|
||||
return created_objs
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to record pickup schedule: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def check_pickup_schedule_for_tracking(tracking_id):
|
||||
"""
|
||||
Checks courier pickup schedule (date, time, arrival window, pickup request ID, location, carrier, status)
|
||||
for a given tracking / AWB number.
|
||||
Queries:
|
||||
1. Local PickupSchedule database model in shippingservice.
|
||||
2. Live Carrier Tracking API (Delhivery, Ekart, Shadowfax).
|
||||
3. Cross-database Order records (customerprofile_beta database).
|
||||
"""
|
||||
trk = str(tracking_id or '').strip()
|
||||
if not trk or trk in ('Pending', 'N/A'):
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Valid tracking ID / AWB number is required.'
|
||||
}, 400
|
||||
|
||||
from .models import PickupSchedule
|
||||
local_obj = PickupSchedule.objects.filter(tracking_id=trk).order_by('-updated_at').first()
|
||||
if not local_obj:
|
||||
local_obj = PickupSchedule.objects.filter(client_order_id=trk).order_by('-updated_at').first()
|
||||
|
||||
# Identify carrier
|
||||
if local_obj and local_obj.carrier:
|
||||
carrier_key = local_obj.carrier
|
||||
elif trk.isdigit() or trk.startswith(('DELHI', '698', '14', '12', '179', '139')):
|
||||
carrier_key = 'DELHIVERY'
|
||||
elif trk.startswith(('EKART', 'ELIT', '500')):
|
||||
carrier_key = 'EKART'
|
||||
else:
|
||||
carrier_key = 'SHADOWFAX'
|
||||
|
||||
carrier_display = 'Delhivery Logistics' if carrier_key == 'DELHIVERY' else ('Ekart Logistics' if carrier_key == 'EKART' else 'Shadowfax Logistics')
|
||||
|
||||
pickup_id = local_obj.pickup_id if (local_obj and local_obj.pickup_id) else None
|
||||
pickup_date = local_obj.pickup_date if (local_obj and local_obj.pickup_date) else None
|
||||
pickup_time = local_obj.pickup_time if (local_obj and local_obj.pickup_time) else None
|
||||
pickup_slot = local_obj.pickup_slot if (local_obj and local_obj.pickup_slot) else None
|
||||
pickup_location = local_obj.pickup_location if (local_obj and local_obj.pickup_location) else None
|
||||
order_id = local_obj.client_order_id if (local_obj and local_obj.client_order_id) else None
|
||||
shipment_status = 'Manifested'
|
||||
carrier_live_data = {}
|
||||
consignee_info = {}
|
||||
destination_info = {}
|
||||
scan_history = []
|
||||
|
||||
# 1. Query live carrier tracking API if carrier is DELHIVERY
|
||||
if carrier_key == 'DELHIVERY':
|
||||
try:
|
||||
import requests
|
||||
from delhivery.models import DelhiveryCredential
|
||||
cred = DelhiveryCredential.objects.filter(is_active=True).first()
|
||||
tok = cred.api_token if cred and cred.api_token else 'e70dfd63665335ee6e0a1094f6d34e17a126f5a6'
|
||||
|
||||
t_resp = requests.get(
|
||||
f"https://track.delhivery.com/api/v1/packages/json/?waybill={trk}",
|
||||
headers={"Authorization": f"Token {tok}"},
|
||||
timeout=6
|
||||
)
|
||||
if t_resp.status_code == 200:
|
||||
s_data = t_resp.json().get('ShipmentData', [])
|
||||
if s_data and isinstance(s_data, list):
|
||||
sh = s_data[0].get('Shipment', {})
|
||||
carrier_live_data = sh
|
||||
shipment_status = sh.get('Status', {}).get('Status') or 'Manifested'
|
||||
if not pickup_location and sh.get('PickupLocation'):
|
||||
pickup_location = sh.get('PickupLocation')
|
||||
if not order_id and sh.get('ReferenceNo'):
|
||||
order_id = sh.get('ReferenceNo')
|
||||
|
||||
consignee = sh.get('Consignee') or {}
|
||||
consignee_info = {
|
||||
'name': consignee.get('Name'),
|
||||
'city': consignee.get('City'),
|
||||
'state': consignee.get('State'),
|
||||
'pincode': consignee.get('PinCode')
|
||||
}
|
||||
destination_info = {
|
||||
'city': sh.get('Destination') or consignee.get('City'),
|
||||
'pincode': consignee.get('PinCode')
|
||||
}
|
||||
|
||||
for sc in sh.get('Scans', []):
|
||||
sd = sc.get('ScanDetail', {})
|
||||
scan_history.append({
|
||||
'scan': sd.get('Scan'),
|
||||
'instructions': sd.get('Instructions'),
|
||||
'location': sd.get('ScannedLocation'),
|
||||
'timestamp': sd.get('ScanDateTime')
|
||||
})
|
||||
except Exception as dlh_err:
|
||||
logger.warning(f"Error querying Delhivery live tracking for {trk}: {dlh_err}")
|
||||
|
||||
# 2. Query cross-database Order records (customerprofile_beta) to retrieve confirmed pickup ID & ETA
|
||||
if not pickup_id or not pickup_date:
|
||||
try:
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(
|
||||
dbname='customerprofile_beta',
|
||||
user='vignesh',
|
||||
password='vignesh_secret_pass_2026',
|
||||
host='16.113.106.152',
|
||||
port='5432'
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, order_id, status, carrier, tracking, pickup_id, eta, pickup_payload FROM api_order WHERE tracking=%s OR order_id=%s",
|
||||
(trk, order_id or trk)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
db_id, db_order_id, db_status, db_carrier, db_tracking, db_pickup_id, db_eta, db_payload = row
|
||||
if not order_id and db_order_id:
|
||||
order_id = db_order_id
|
||||
if db_pickup_id:
|
||||
pickup_id = str(db_pickup_id)
|
||||
if db_eta and 'Pickup:' in str(db_eta):
|
||||
import re
|
||||
m_date = re.search(r'Pickup:\s*([\d-]+)', str(db_eta))
|
||||
m_time = re.search(r'\(([\d:]+)\)', str(db_eta))
|
||||
if m_date and not pickup_date:
|
||||
pickup_date = m_date.group(1)
|
||||
if m_time and not pickup_time:
|
||||
pickup_time = m_time.group(1)
|
||||
if isinstance(db_payload, dict):
|
||||
if not pickup_id and db_payload.get('pickup_id'):
|
||||
pickup_id = str(db_payload.get('pickup_id'))
|
||||
if not pickup_date and db_payload.get('pickup_date'):
|
||||
pickup_date = db_payload.get('pickup_date')
|
||||
if not pickup_time and db_payload.get('pickup_time'):
|
||||
pickup_time = db_payload.get('pickup_time')
|
||||
conn.close()
|
||||
except Exception as db_err:
|
||||
logger.warning(f"Cross-database order lookup error: {db_err}")
|
||||
|
||||
# 3. Check if warehouse location has an active confirmed Pickup Request
|
||||
if not pickup_id and pickup_location:
|
||||
loc_match = PickupSchedule.objects.filter(pickup_location=pickup_location).exclude(pickup_id='').order_by('-updated_at').first()
|
||||
if loc_match:
|
||||
pickup_id = loc_match.pickup_id
|
||||
if not pickup_date:
|
||||
pickup_date = loc_match.pickup_date
|
||||
if not pickup_time:
|
||||
pickup_time = loc_match.pickup_time
|
||||
|
||||
# Default fallbacks if date or time are not yet resolved
|
||||
if not pickup_date:
|
||||
pickup_date = time.strftime('%Y-%m-%d')
|
||||
if not pickup_time:
|
||||
pickup_time = '14:00:00' if carrier_key == 'DELHIVERY' else ('Standard Hub Sweep' if carrier_key == 'EKART' else 'Automated Cut-Off Sweep')
|
||||
|
||||
# Format slot description
|
||||
t_clean = str(pickup_time).strip()
|
||||
if t_clean.startswith('10'):
|
||||
pickup_slot = 'Morning Slot (10:00 AM – 1:00 PM)'
|
||||
elif t_clean.startswith('14'):
|
||||
pickup_slot = 'Afternoon Slot (2:00 PM – 5:00 PM / 14:00 – 18:00)'
|
||||
elif t_clean.startswith('17'):
|
||||
pickup_slot = 'Evening Slot (5:00 PM – 7:00 PM)'
|
||||
elif not pickup_slot:
|
||||
pickup_slot = t_clean
|
||||
|
||||
# Determine lifecycle status
|
||||
is_picked_up = str(shipment_status).lower() in ['in transit', 'picked up', 'out for delivery', 'delivered'] or carrier_live_data.get('PickedupDate')
|
||||
if is_picked_up:
|
||||
status_code = 'IN_TRANSIT'
|
||||
status_display = f"Package Picked Up & In Transit with {carrier_display}"
|
||||
elif pickup_id:
|
||||
status_code = 'CONFIRMED'
|
||||
status_display = f"Pickup Confirmed with {carrier_display} (PR #{pickup_id}) for {pickup_date} ({pickup_slot})"
|
||||
else:
|
||||
status_code = 'SCHEDULED'
|
||||
status_display = f"Pickup Scheduled with {carrier_display} for {pickup_date} ({pickup_slot})"
|
||||
|
||||
# Cache/update into PickupSchedule table
|
||||
try:
|
||||
PickupSchedule.objects.update_or_create(
|
||||
tracking_id=trk,
|
||||
defaults={
|
||||
'client_order_id': str(order_id or ''),
|
||||
'carrier': carrier_key,
|
||||
'carrier_display': carrier_display,
|
||||
'pickup_id': str(pickup_id or ''),
|
||||
'pickup_date': str(pickup_date),
|
||||
'pickup_time': str(pickup_time),
|
||||
'pickup_slot': str(pickup_slot),
|
||||
'pickup_location': str(pickup_location or 'SAM'),
|
||||
'status': status_code,
|
||||
'notes': f"Queried via check_pickup_schedule ({time.strftime('%Y-%m-%d %H:%M:%S')})"
|
||||
}
|
||||
)
|
||||
except Exception as save_err:
|
||||
logger.warning(f"Error caching PickupSchedule: {save_err}")
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'tracking_id': trk,
|
||||
'order_id': order_id,
|
||||
'carrier': carrier_key,
|
||||
'carrier_display': carrier_display,
|
||||
'pickup_scheduled': True,
|
||||
'pickup_id': pickup_id,
|
||||
'pickup_date': pickup_date,
|
||||
'pickup_time': pickup_time,
|
||||
'pickup_slot': pickup_slot,
|
||||
'pickup_location': pickup_location or 'SAM',
|
||||
'shipment_status': shipment_status,
|
||||
'pickup_status': status_code,
|
||||
'status_display': status_display,
|
||||
'destination': destination_info,
|
||||
'consignee': consignee_info,
|
||||
'scans': scan_history,
|
||||
'details': {
|
||||
'expected_package_count': local_obj.expected_package_count if local_obj else 1,
|
||||
'manifest_date': carrier_live_data.get('PickUpDate'),
|
||||
'picked_up_date': carrier_live_data.get('PickedupDate')
|
||||
}
|
||||
}, 200
|
||||
|
||||
|
||||
|
|
|
|||
37
shipping/migrations/0003_pickupschedule.py
Normal file
37
shipping/migrations/0003_pickupschedule.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 6.1 on 2026-10-13 13:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shipping', '0002_riderassignment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PickupSchedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tracking_id', models.CharField(db_index=True, max_length=100)),
|
||||
('client_order_id', models.CharField(blank=True, db_index=True, default='', max_length=100)),
|
||||
('carrier', models.CharField(db_index=True, default='DELHIVERY', max_length=50)),
|
||||
('carrier_display', models.CharField(default='Delhivery Logistics', max_length=100)),
|
||||
('pickup_id', models.CharField(blank=True, db_index=True, default='', max_length=100)),
|
||||
('pickup_date', models.CharField(max_length=20)),
|
||||
('pickup_time', models.CharField(default='14:00:00', max_length=50)),
|
||||
('pickup_slot', models.CharField(blank=True, default='', max_length=100)),
|
||||
('pickup_location', models.CharField(blank=True, default='', max_length=150)),
|
||||
('status', models.CharField(choices=[('SCHEDULED', 'Pickup Scheduled'), ('CONFIRMED', 'Confirmed by Carrier'), ('IN_TRANSIT', 'Picked Up / In Transit'), ('CANCELLED', 'Cancelled')], default='SCHEDULED', max_length=50)),
|
||||
('expected_package_count', models.IntegerField(default=1)),
|
||||
('notes', models.TextField(blank=True, default='')),
|
||||
('raw_response', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
|
|
@ -51,3 +51,34 @@ class RiderAssignment(models.Model):
|
|||
def __str__(self):
|
||||
return f"Rider {self.rider_name} ({self.rider_phone}) - AWB: {self.awb_number} [{self.status}]"
|
||||
|
||||
|
||||
class PickupSchedule(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('SCHEDULED', 'Pickup Scheduled'),
|
||||
('CONFIRMED', 'Confirmed by Carrier'),
|
||||
('IN_TRANSIT', 'Picked Up / In Transit'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
tracking_id = models.CharField(max_length=100, db_index=True)
|
||||
client_order_id = models.CharField(max_length=100, blank=True, default='', db_index=True)
|
||||
carrier = models.CharField(max_length=50, default='DELHIVERY', db_index=True)
|
||||
carrier_display = models.CharField(max_length=100, default='Delhivery Logistics')
|
||||
pickup_id = models.CharField(max_length=100, blank=True, default='', db_index=True)
|
||||
pickup_date = models.CharField(max_length=20) # YYYY-MM-DD
|
||||
pickup_time = models.CharField(max_length=50, default='14:00:00')
|
||||
pickup_slot = models.CharField(max_length=100, blank=True, default='') # e.g. Afternoon Slot (2:00 PM – 5:00 PM)
|
||||
pickup_location = models.CharField(max_length=150, blank=True, default='')
|
||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='SCHEDULED')
|
||||
expected_package_count = models.IntegerField(default=1)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
raw_response = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-updated_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.carrier}] Pickup {self.pickup_id or 'Pending'} - AWB: {self.tracking_id} on {self.pickup_date} ({self.pickup_time})"
|
||||
|
||||
|
|
|
|||
|
|
@ -322,8 +322,33 @@
|
|||
<!-- ========================================== -->
|
||||
<div class="card" id="cardPickupMode" style="display: none; margin-bottom: 1.75rem;">
|
||||
<div class="card-title">
|
||||
<span>📅 Schedule Courier Pickup Across Carriers</span>
|
||||
<span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">Dynamic Options Enabled per Assigned Carrier</span>
|
||||
<span>📅 Schedule & Verify Courier Pickups Across Carriers</span>
|
||||
<span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">Check Active Pickup Schedule with Date & Time for Given Tracking, or Schedule a New Pickup Window</span>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 3A: LIVE TRACKING PICKUP SCHEDULE LOOKUP -->
|
||||
<div style="background: rgba(30, 41, 59, 0.65); border: 1px solid rgba(56, 189, 248, 0.3); border-radius: 0.85rem; padding: 1.25rem; margin-bottom: 1.75rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.85rem;">
|
||||
<div style="font-weight: 700; color: #fff; font-size: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🔍</span> Check Pickup Schedule by Tracking / AWB
|
||||
</div>
|
||||
<span class="badge" style="background: rgba(14, 165, 233, 0.15); color: #38bdf8; border: 1px solid rgba(14, 165, 233, 0.3);">Real-Time Carrier API Verification</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
|
||||
<input type="text" id="check_schedule_tracking" class="form-control" placeholder="Enter Waybill / Tracking ID (e.g. 69873010000232)" value="69873010000232" style="flex: 1; min-width: 260px; font-size: 1.05rem; font-weight: 700; letter-spacing: 0.04em;">
|
||||
<button type="button" id="btnCheckSchedule" class="btn btn-primary" onclick="handleCheckScheduleSubmit()" style="padding: 0.75rem 2rem; font-weight: 700;">
|
||||
<span id="btnCheckScheduleIcon">🔎</span>
|
||||
<span id="btnCheckScheduleText">Check Pickup Schedule</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Live Pickup Schedule Result Card Container -->
|
||||
<div id="checkScheduleResultContainer" style="display: none; margin-top: 1.25rem;"></div>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px dashed var(--border-color); margin-bottom: 1.5rem; padding-top: 0.75rem; font-size: 0.82rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700;">
|
||||
<span>⚡ Or Schedule / Reschedule New Pickup Window</span>
|
||||
</div>
|
||||
|
||||
<form id="pickupScheduleForm" onsubmit="handlePickupSubmit(event)">
|
||||
|
|
@ -714,6 +739,100 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleCheckScheduleSubmit() {
|
||||
const trkInput = document.getElementById('check_schedule_tracking');
|
||||
const trk = trkInput ? trkInput.value.trim() : '';
|
||||
if (!trk) {
|
||||
alert('Please enter a valid tracking ID / waybill number.');
|
||||
if (trkInput) trkInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnCheckSchedule');
|
||||
const btnText = document.getElementById('btnCheckScheduleText');
|
||||
const btnIcon = document.getElementById('btnCheckScheduleIcon');
|
||||
const container = document.getElementById('checkScheduleResultContainer');
|
||||
|
||||
if (btn) btn.disabled = true;
|
||||
if (btnIcon) btnIcon.innerHTML = '<span class="spinner"></span>';
|
||||
if (btnText) btnText.textContent = 'Verifying Schedule...';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/carrier/pickup-schedule/?tracking_id=${encodeURIComponent(trk)}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (container) {
|
||||
container.style.display = 'block';
|
||||
if (data.status === 'success') {
|
||||
const isDlh = (data.carrier || '').includes('DELHI');
|
||||
const badgeBg = isDlh ? 'rgba(14, 165, 233, 0.18)' : 'rgba(234, 179, 8, 0.18)';
|
||||
const badgeBorder = isDlh ? 'rgba(14, 165, 233, 0.35)' : 'rgba(234, 179, 8, 0.35)';
|
||||
const badgeColor = isDlh ? '#38bdf8' : '#facc15';
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="background: rgba(15, 23, 42, 0.85); border: 1px solid rgba(56, 189, 248, 0.4); border-radius: 0.75rem; padding: 1.25rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 0.75rem; margin-bottom: 1rem;">
|
||||
<div>
|
||||
<span style="font-size: 1.15rem; font-weight: 800; color: #fff;">🚚 ${data.carrier_display || data.carrier}</span>
|
||||
<span class="badge" style="margin-left: 0.75rem; background: ${badgeBg}; border: 1px solid ${badgeBorder}; color: ${badgeColor};">
|
||||
${data.pickup_status === 'CONFIRMED' ? '✅ Confirmed Pickup' : (data.pickup_status === 'IN_TRANSIT' ? '🚀 In Transit' : '📅 Scheduled')}
|
||||
</span>
|
||||
</div>
|
||||
${data.pickup_id ? `<span style="font-family: monospace; font-size: 0.95rem; font-weight: 700; color: #4ade80; background: rgba(34, 197, 94, 0.15); padding: 0.3rem 0.75rem; border-radius: 0.5rem; border: 1px solid rgba(34, 197, 94, 0.3);">PR #${data.pickup_id}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1rem;">
|
||||
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">📅 Scheduled Pickup Date</div>
|
||||
<div style="font-size: 1.1rem; font-weight: 800; color: #fff; margin-top: 0.2rem;">${data.pickup_date || 'N/A'}</div>
|
||||
</div>
|
||||
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">⏰ Arrival Time Window</div>
|
||||
<div style="font-size: 1rem; font-weight: 800; color: #38bdf8; margin-top: 0.2rem;">${data.pickup_slot || data.pickup_time || 'Standard Sweep'}</div>
|
||||
</div>
|
||||
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">🏭 Pickup Warehouse</div>
|
||||
<div style="font-size: 1rem; font-weight: 800; color: #fff; margin-top: 0.2rem;">${data.pickup_location || 'Registered Store'}</div>
|
||||
</div>
|
||||
<div style="background: rgba(30, 41, 59, 0.6); padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size: 0.75rem; color: #94a3b8; text-transform: uppercase;">📦 Shipment Status</div>
|
||||
<div style="font-size: 1rem; font-weight: 800; color: #facc15; margin-top: 0.2rem;">${data.shipment_status || 'Manifested'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem; color: #cbd5e1; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<span><strong>Consignment:</strong> Order #${data.order_id || 'N/A'} (AWB: ${data.tracking_id})</span>
|
||||
${data.destination && data.destination.city ? `<span><strong>Destination:</strong> ${data.destination.city} (${data.destination.pincode || ''})</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div style="background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.35); border-radius: 0.75rem; padding: 1rem; color: #fca5a5;">
|
||||
<strong>⚠️ Schedule Lookup:</strong> ${data.message || 'No pickup schedule found.'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const resultsWrapper = document.getElementById('resultsWrapper');
|
||||
if (resultsWrapper) resultsWrapper.style.display = 'block';
|
||||
const rawJson = document.getElementById('rawJsonResponse');
|
||||
if (rawJson) rawJson.textContent = JSON.stringify(data, null, 2);
|
||||
const rawTitle = document.getElementById('rawJsonTitle');
|
||||
if (rawTitle) rawTitle.textContent = `🔍 Pickup Schedule Response (AWB: ${trk})`;
|
||||
} catch (err) {
|
||||
if (container) {
|
||||
container.style.display = 'block';
|
||||
container.innerHTML = `<div style="color: #f87171; padding: 1rem;">Failed to connect to pickup service: ${err.message}</div>`;
|
||||
}
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
if (btnIcon) btnIcon.textContent = '🔎';
|
||||
if (btnText) btnText.textContent = 'Check Pickup Schedule';
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePickupSubmit(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnPickupSubmit');
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ urlpatterns = [
|
|||
path('api/v1/carrier/check-availability/', views.api_carrier_availability, name='api_carrier_availability'),
|
||||
path('api/v1/carrier/auto-dispatch/', views.api_carrier_auto_dispatch, name='api_carrier_auto_dispatch'),
|
||||
path('api/v1/carrier/schedule-pickup/', views.api_carrier_schedule_pickup, name='api_carrier_schedule_pickup'),
|
||||
path('api/v1/carrier/pickup-schedule/', views.api_carrier_check_pickup_schedule, name='api_carrier_check_pickup_schedule'),
|
||||
|
||||
# Microservices REST API Endpoints under /api/v1/
|
||||
path('api/v1/health/', views.api_healthcheck, name='api_healthcheck'),
|
||||
|
|
|
|||
|
|
@ -1150,9 +1150,19 @@ def api_carrier_schedule_pickup(request):
|
|||
"notes": "Call before arrival"
|
||||
}
|
||||
"""
|
||||
from .carrier_allocator import get_carrier_pickup_capabilities, schedule_carrier_pickup
|
||||
from .carrier_allocator import get_carrier_pickup_capabilities, schedule_carrier_pickup, check_pickup_schedule_for_tracking
|
||||
|
||||
if request.method == 'GET':
|
||||
tracking_id = (
|
||||
request.GET.get('tracking_id')
|
||||
or request.GET.get('awb')
|
||||
or request.GET.get('tracking')
|
||||
or request.GET.get('waybill')
|
||||
)
|
||||
if tracking_id:
|
||||
res_data, status_code = check_pickup_schedule_for_tracking(tracking_id)
|
||||
return JsonResponse(res_data, status=status_code)
|
||||
|
||||
carrier = request.GET.get('carrier', 'ALL')
|
||||
caps = get_carrier_pickup_capabilities(carrier)
|
||||
return JsonResponse({'status': 'success', 'data': caps}, status=200)
|
||||
|
|
@ -1192,6 +1202,46 @@ def api_carrier_schedule_pickup(request):
|
|||
return JsonResponse(res_data, status=status_code)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def api_carrier_check_pickup_schedule(request):
|
||||
"""
|
||||
REST API endpoint to check courier pickup schedule with date and time for a given tracking / AWB.
|
||||
GET /api/v1/carrier/pickup-schedule/?tracking_id=<awb>
|
||||
POST /api/v1/carrier/pickup-schedule/ with JSON {"tracking_id": "<awb>"}
|
||||
"""
|
||||
if request.method not in ['GET', 'POST']:
|
||||
return JsonResponse({'error': 'GET or POST method required'}, status=405)
|
||||
|
||||
data = {}
|
||||
if request.method == 'POST' and request.body:
|
||||
try:
|
||||
data = json.loads(request.body.decode('utf-8'))
|
||||
except Exception:
|
||||
data = {}
|
||||
|
||||
tracking_id = (
|
||||
data.get('tracking_id')
|
||||
or data.get('awb')
|
||||
or data.get('tracking')
|
||||
or data.get('waybill')
|
||||
or request.GET.get('tracking_id')
|
||||
or request.GET.get('awb')
|
||||
or request.GET.get('tracking')
|
||||
or request.GET.get('waybill')
|
||||
)
|
||||
|
||||
if not tracking_id:
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': 'tracking_id or awb parameter is required (e.g. ?tracking_id=69873010000232)'
|
||||
}, status=400)
|
||||
|
||||
from .carrier_allocator import check_pickup_schedule_for_tracking
|
||||
res_data, status_code = check_pickup_schedule_for_tracking(tracking_id)
|
||||
return JsonResponse(res_data, status=status_code)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue