Compare commits
2 Commits
27b346c1f6
...
8f6d864b4b
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f6d864b4b | |||
| 3a7044d551 |
775
backend/thirdparty/deutschepost/models.py
vendored
775
backend/thirdparty/deutschepost/models.py
vendored
@@ -19,6 +19,7 @@ Edge cases handled:
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
from typing import Dict, Any
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
@@ -31,7 +32,19 @@ from configuration.models import SiteConfiguration
|
||||
# API client imports - direct references as requested
|
||||
# Note: We import only what's absolutely necessary to avoid import errors
|
||||
try:
|
||||
from .client.deutsche_post_international_shipping_api_client import AuthenticatedClient
|
||||
from .client.deutsche_post_international_shipping_api_client import AuthenticatedClient, Client
|
||||
from .client.deutsche_post_international_shipping_api_client.api.authentication import get_access_token
|
||||
from .client.deutsche_post_international_shipping_api_client.api.orders import (
|
||||
create_order, finalize_order, get_order
|
||||
)
|
||||
from .client.deutsche_post_international_shipping_api_client.api.bulk_orders import (
|
||||
create_mixed_order, get_bulk_order
|
||||
)
|
||||
from .client.deutsche_post_international_shipping_api_client.models import (
|
||||
OrderData, OrderDataOrderStatus, ItemData, ItemDataServiceLevel, ItemDataShipmentNaturetype,
|
||||
Paperwork, PaperworkPickupType, MixedBagOrderDTO, BulkOrderDto, Content
|
||||
)
|
||||
from .client.deutsche_post_international_shipping_api_client.errors import UnexpectedStatus
|
||||
DEUTSCHE_POST_CLIENT_AVAILABLE = True
|
||||
except ImportError:
|
||||
DEUTSCHE_POST_CLIENT_AVAILABLE = False
|
||||
@@ -96,6 +109,16 @@ class DeutschePostOrder(models.Model):
|
||||
# Error tracking
|
||||
last_error = models.TextField(blank=True, help_text="Last API error message")
|
||||
|
||||
# Label/Document storage
|
||||
label_pdf = models.FileField(upload_to='deutschepost/labels/', blank=True, null=True, help_text="Shipping label PDF")
|
||||
|
||||
class LABEL_SIZE(models.TextChoices):
|
||||
A4 = "A4", "A4 (210x297mm)"
|
||||
A5 = "A5", "A5 (148x210mm)"
|
||||
A6 = "A6", "A6 (105x148mm)"
|
||||
|
||||
label_size = models.CharField(max_length=10, choices=LABEL_SIZE.choices, default=LABEL_SIZE.A4)
|
||||
|
||||
def get_api_client(self) -> AuthenticatedClient:
|
||||
"""Get authenticated API client using configuration."""
|
||||
if not DEUTSCHE_POST_CLIENT_AVAILABLE:
|
||||
@@ -106,84 +129,211 @@ class DeutschePostOrder(models.Model):
|
||||
if not all([config.deutschepost_client_id, config.deutschepost_client_secret]):
|
||||
raise ValidationError("Deutsche Post API credentials not configured")
|
||||
|
||||
# First get an access token using basic auth
|
||||
basic_client = Client(
|
||||
base_url=config.deutschepost_api_url or "https://api-sandbox.deutschepost.com",
|
||||
raise_on_unexpected_status=True
|
||||
)
|
||||
|
||||
# Create basic auth header
|
||||
credentials = f"{config.deutschepost_client_id}:{config.deutschepost_client_secret}"
|
||||
encoded_credentials = base64.b64encode(credentials.encode()).decode()
|
||||
|
||||
try:
|
||||
# Get access token
|
||||
response = get_access_token.sync_detailed(
|
||||
client=basic_client,
|
||||
authorization=f"Basic {encoded_credentials}",
|
||||
accept="application/json"
|
||||
)
|
||||
|
||||
if response.parsed is None or not hasattr(response.parsed, 'access_token'):
|
||||
raise ValidationError("Failed to obtain access token from Deutsche Post API")
|
||||
|
||||
access_token = response.parsed.access_token
|
||||
|
||||
# Create authenticated client with the token
|
||||
client = AuthenticatedClient(
|
||||
base_url=config.deutschepost_api_url,
|
||||
token="" # Token management to be implemented
|
||||
base_url=config.deutschepost_api_url or "https://api-sandbox.deutschepost.com",
|
||||
token=access_token,
|
||||
raise_on_unexpected_status=True
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
raise ValidationError(f"Deutsche Post authentication error: {str(e)}")
|
||||
|
||||
def create_remote_order(self):
|
||||
"""Create order via Deutsche Post API."""
|
||||
if self.order_id:
|
||||
raise ValidationError("Order already created remotely")
|
||||
raise ValidationError(detail="Order already created remotely")
|
||||
|
||||
if not DEUTSCHE_POST_CLIENT_AVAILABLE:
|
||||
raise ValidationError("Deutsche Post API client is not available")
|
||||
raise ValidationError(detail="Deutsche Post API client is not available")
|
||||
|
||||
try:
|
||||
# Import API functions only when needed
|
||||
from .client.deutsche_post_international_shipping_api_client.api.orders import create_order
|
||||
from .client.deutsche_post_international_shipping_api_client.models import OrderData
|
||||
|
||||
client = self.get_api_client()
|
||||
config = SiteConfiguration.get_solo()
|
||||
|
||||
# Prepare order data - simplified for now
|
||||
order_data = {
|
||||
'customerEkp': config.deutschepost_customer_ekp or self.customer_ekp,
|
||||
'orderStatus': 'OPEN',
|
||||
'items': [{
|
||||
'product': self.product_type,
|
||||
'serviceLevel': self.service_level,
|
||||
'recipient': self.recipient_name,
|
||||
'addressLine1': self.address_line1,
|
||||
'city': self.city,
|
||||
'postalCode': self.postal_code,
|
||||
'destinationCountry': self.destination_country,
|
||||
'shipmentGrossWeight': self.shipment_gross_weight,
|
||||
'custRef': self.cust_ref or f"ORDER-{self.id}",
|
||||
}]
|
||||
}
|
||||
# Create content pieces if we have shipment data
|
||||
contents = []
|
||||
if self.shipment_amount > 0:
|
||||
contents.append(Content(
|
||||
content_piece_hs_code=1234567890, # Default HS code, should be configurable
|
||||
content_piece_description="E-commerce goods",
|
||||
content_piece_value=str(self.shipment_amount),
|
||||
content_piece_netweight=max(100, self.shipment_gross_weight - 50), # Estimate net weight
|
||||
content_piece_origin=config.deutschepost_origin_country or "DE",
|
||||
content_piece_amount=1
|
||||
))
|
||||
|
||||
# For now, we'll simulate a successful API call
|
||||
# TODO: Implement actual API call when client is properly configured
|
||||
self.order_id = f"SIMULATED-{self.id}-{timezone.now().strftime('%Y%m%d%H%M%S')}"
|
||||
# Create item data
|
||||
item_data = ItemData(
|
||||
product=self.product_type,
|
||||
service_level=ItemDataServiceLevel(self.service_level),
|
||||
recipient=self.recipient_name,
|
||||
address_line_1=self.address_line1,
|
||||
address_line_2=self.address_line2 or None,
|
||||
address_line_3=self.address_line3 or None,
|
||||
city=self.city,
|
||||
state=self.address_state or None,
|
||||
postal_code=self.postal_code,
|
||||
destination_country=self.destination_country,
|
||||
shipment_gross_weight=self.shipment_gross_weight,
|
||||
recipient_phone=self.recipient_phone or None,
|
||||
recipient_email=self.recipient_email or None,
|
||||
sender_tax_id=self.sender_tax_id or None,
|
||||
importer_tax_id=self.importer_tax_id or None,
|
||||
shipment_amount=float(self.shipment_amount) if self.shipment_amount else None,
|
||||
shipment_currency=self.shipment_currency or None,
|
||||
return_item_wanted=self.return_item_wanted,
|
||||
shipment_naturetype=ItemDataShipmentNaturetype.SALE_GOODS,
|
||||
cust_ref=self.cust_ref or f"ORDER-{self.id}",
|
||||
contents=contents if contents else None
|
||||
)
|
||||
|
||||
# Create paperwork data
|
||||
paperwork = Paperwork(
|
||||
contact_name=config.deutschepost_contact_name or "Contact",
|
||||
awb_copy_count=1,
|
||||
job_reference=f"JOB-{self.id}",
|
||||
pickup_type=PaperworkPickupType.CUSTOMER_DROP_OFF,
|
||||
telephone_number=config.deutschepost_contact_phone or "+490000000000"
|
||||
)
|
||||
|
||||
# Create order data
|
||||
order_data = OrderData(
|
||||
customer_ekp=config.deutschepost_customer_ekp or self.customer_ekp or "1234567890",
|
||||
order_status=OrderDataOrderStatus.OPEN,
|
||||
paperwork=paperwork,
|
||||
items=[item_data]
|
||||
)
|
||||
|
||||
# Make API call
|
||||
response = create_order.sync_detailed(
|
||||
client=client,
|
||||
body=order_data
|
||||
)
|
||||
|
||||
if response.parsed is None:
|
||||
raise ValidationError(f"Failed to create order. Status: {response.status_code}")
|
||||
|
||||
# Handle different response types
|
||||
if hasattr(response.parsed, 'order_id'):
|
||||
# Successful response
|
||||
order_response = response.parsed
|
||||
self.order_id = order_response.order_id
|
||||
self.state = self.STATE.CREATED
|
||||
self.metadata = {'simulated': True, 'order_data': order_data}
|
||||
self.metadata = {
|
||||
'api_response': order_response.to_dict() if hasattr(order_response, 'to_dict') else str(order_response),
|
||||
'created_at': timezone.now().isoformat()
|
||||
}
|
||||
self.last_error = ""
|
||||
else:
|
||||
# Error response
|
||||
error_info = response.parsed.to_dict() if hasattr(response.parsed, 'to_dict') else str(response.parsed)
|
||||
self.state = self.STATE.ERROR
|
||||
self.last_error = f"API Error: {error_info}"
|
||||
self.metadata = {'api_error': error_info}
|
||||
raise ValidationError(detail=f"Deutsche Post API error: {error_info}")
|
||||
|
||||
self.save()
|
||||
|
||||
except UnexpectedStatus as e:
|
||||
self.state = self.STATE.ERROR
|
||||
self.last_error = f"API Error {e.status_code}: {e.content}"
|
||||
self.save()
|
||||
raise ValidationError(detail=f"Deutsche Post API error {e.status_code}: {e.content}")
|
||||
except Exception as e:
|
||||
self.state = self.STATE.ERROR
|
||||
self.last_error = str(e)
|
||||
self.save()
|
||||
raise ValidationError(f"Deutsche Post API error: {str(e)}")
|
||||
raise ValidationError(detail=f"Deutsche Post API error: {str(e)}")
|
||||
|
||||
def finalize_remote_order(self):
|
||||
"""Finalize order via Deutsche Post API."""
|
||||
if not self.order_id:
|
||||
raise ValidationError("Order not created remotely yet")
|
||||
raise ValidationError(detail="Order not created remotely yet")
|
||||
|
||||
if not DEUTSCHE_POST_CLIENT_AVAILABLE:
|
||||
raise ValidationError("Deutsche Post API client is not available")
|
||||
raise ValidationError(detail="Deutsche Post API client is not available")
|
||||
|
||||
try:
|
||||
# Import API functions only when needed
|
||||
from .client.deutsche_post_international_shipping_api_client.api.orders import finalize_order
|
||||
|
||||
client = self.get_api_client()
|
||||
config = SiteConfiguration.get_solo()
|
||||
|
||||
# For now, simulate finalization
|
||||
# TODO: Implement actual API call
|
||||
# Create paperwork for finalization
|
||||
paperwork = Paperwork(
|
||||
contact_name=config.deutschepost_contact_name or "Contact",
|
||||
awb_copy_count=1,
|
||||
job_reference=f"JOB-{self.id}-FINAL",
|
||||
pickup_type=PaperworkPickupType.CUSTOMER_DROP_OFF,
|
||||
telephone_number=config.deutschepost_contact_phone or "+420000000000"
|
||||
)
|
||||
|
||||
# Make API call to finalize order
|
||||
response = finalize_order.sync_detailed(
|
||||
client=client,
|
||||
order_id=self.order_id,
|
||||
body=paperwork
|
||||
)
|
||||
|
||||
if response.parsed is None:
|
||||
raise ValidationError(f"Failed to finalize order. Status: {response.status_code}")
|
||||
|
||||
# Handle different response types
|
||||
if hasattr(response.parsed, 'order_id'):
|
||||
# Successful response
|
||||
order_response = response.parsed
|
||||
self.state = self.STATE.FINALIZED
|
||||
|
||||
# Extract tracking information if available
|
||||
if hasattr(order_response, 'items') and order_response.items:
|
||||
for item in order_response.items:
|
||||
if hasattr(item, 'awb') and item.awb:
|
||||
self.awb_number = item.awb
|
||||
if hasattr(item, 'barcode') and item.barcode:
|
||||
self.barcode = item.barcode
|
||||
|
||||
self.metadata.update({
|
||||
'finalized': True,
|
||||
'finalized_at': timezone.now().isoformat()
|
||||
'finalized_at': timezone.now().isoformat(),
|
||||
'api_response': order_response.to_dict() if hasattr(order_response, 'to_dict') else str(order_response)
|
||||
})
|
||||
self.last_error = ""
|
||||
else:
|
||||
# Error response
|
||||
error_info = response.parsed.to_dict() if hasattr(response.parsed, 'to_dict') else str(response.parsed)
|
||||
self.last_error = f"Finalization failed: {error_info}"
|
||||
raise ValidationError(detail=f"Deutsche Post API finalization error: {error_info}")
|
||||
|
||||
self.save()
|
||||
|
||||
except UnexpectedStatus as e:
|
||||
self.last_error = f"API Error {e.status_code}: {e.content}"
|
||||
self.save()
|
||||
raise ValidationError(f"Deutsche Post API error {e.status_code}: {e.content}")
|
||||
except Exception as e:
|
||||
self.last_error = str(e)
|
||||
self.save()
|
||||
@@ -198,28 +348,250 @@ class DeutschePostOrder(models.Model):
|
||||
return
|
||||
|
||||
try:
|
||||
# Import API functions only when needed
|
||||
from .client.deutsche_post_international_shipping_api_client.api.orders import get_order
|
||||
|
||||
client = self.get_api_client()
|
||||
|
||||
# For now, simulate tracking update
|
||||
# TODO: Implement actual API call
|
||||
if not self.awb_number:
|
||||
self.awb_number = f"AWB{self.id}{timezone.now().strftime('%Y%m%d')}"
|
||||
if not self.barcode:
|
||||
self.barcode = f"RX{self.id}DE"
|
||||
# Make API call to get current order status
|
||||
response = get_order.sync_detailed(
|
||||
client=client,
|
||||
order_id=self.order_id
|
||||
)
|
||||
|
||||
if response.parsed is None:
|
||||
self.last_error = f"Failed to refresh tracking. Status: {response.status_code}"
|
||||
self.save()
|
||||
return
|
||||
|
||||
# Handle different response types
|
||||
if hasattr(response.parsed, 'order_id'):
|
||||
# Successful response
|
||||
order_response = response.parsed
|
||||
|
||||
# Update order status based on API response
|
||||
if hasattr(order_response, 'order_status'):
|
||||
api_status = str(order_response.order_status)
|
||||
if api_status == 'FINALIZED':
|
||||
self.state = self.STATE.FINALIZED
|
||||
elif api_status in ['SHIPPED', 'IN_TRANSIT']:
|
||||
self.state = self.STATE.SHIPPED
|
||||
elif api_status == 'DELIVERED':
|
||||
self.state = self.STATE.DELIVERED
|
||||
elif api_status in ['CANCELLED', 'RETURNED']:
|
||||
self.state = self.STATE.CANCELLED
|
||||
|
||||
# Extract tracking information from items
|
||||
if hasattr(order_response, 'items') and order_response.items:
|
||||
for item in order_response.items:
|
||||
if hasattr(item, 'awb') and item.awb and not self.awb_number:
|
||||
self.awb_number = item.awb
|
||||
if hasattr(item, 'barcode') and item.barcode and not self.barcode:
|
||||
self.barcode = item.barcode
|
||||
|
||||
# Update tracking URL if AWB number is available
|
||||
if self.awb_number:
|
||||
self.tracking_url = f"https://www.deutschepost.de/de/sendungsverfolgung.html?piececode={self.awb_number}"
|
||||
|
||||
self.metadata.update({
|
||||
'tracking_refreshed': True,
|
||||
'last_refresh': timezone.now().isoformat()
|
||||
'last_refresh': timezone.now().isoformat(),
|
||||
'api_response': order_response.to_dict() if hasattr(order_response, 'to_dict') else str(order_response)
|
||||
})
|
||||
self.last_error = ""
|
||||
else:
|
||||
# Error response
|
||||
error_info = response.parsed.to_dict() if hasattr(response.parsed, 'to_dict') else str(response.parsed)
|
||||
self.last_error = f"Tracking refresh failed: {error_info}"
|
||||
|
||||
self.save()
|
||||
|
||||
except UnexpectedStatus as e:
|
||||
self.last_error = f"API Error {e.status_code}: {e.content}"
|
||||
self.save()
|
||||
except Exception as e:
|
||||
self.last_error = str(e)
|
||||
self.save()
|
||||
|
||||
def validate_for_shipping(self):
|
||||
"""Validate that all required fields are present before API calls."""
|
||||
errors = []
|
||||
|
||||
# Required recipient fields
|
||||
if not self.recipient_name:
|
||||
errors.append("Recipient name is required")
|
||||
if not self.address_line1:
|
||||
errors.append("Address line 1 is required")
|
||||
if not self.city:
|
||||
errors.append("City is required")
|
||||
if not self.postal_code:
|
||||
errors.append("Postal code is required")
|
||||
if not self.destination_country:
|
||||
errors.append("Destination country is required")
|
||||
|
||||
# Required shipment fields
|
||||
if not self.shipment_gross_weight or self.shipment_gross_weight <= 0:
|
||||
errors.append("Valid shipment weight is required")
|
||||
|
||||
# Contact information (email or phone required for notifications)
|
||||
if not self.recipient_email and not self.recipient_phone:
|
||||
errors.append("Either recipient email or phone is required for delivery notifications")
|
||||
|
||||
if errors:
|
||||
raise ValidationError("; ".join(errors))
|
||||
|
||||
return True
|
||||
|
||||
def get_shipping_cost_estimate(self):
|
||||
"Get estimated shipping cost based on service level and destination."
|
||||
config = SiteConfiguration.get_solo()
|
||||
base_price = config.deutschepost_shipping_price
|
||||
|
||||
# Adjust price based on service level
|
||||
if self.service_level == "PRIORITY":
|
||||
multiplier = 1.2 # 20% premium for priority
|
||||
else: # STANDARD
|
||||
multiplier = 1.0
|
||||
|
||||
# Adjust price based on destination
|
||||
if self.destination_country not in ["DE", "AT", "FR", "NL", "BE", "LU", "SK", "PL"]:
|
||||
# Non-EU countries cost more
|
||||
multiplier *= 1.5
|
||||
|
||||
return base_price * multiplier
|
||||
|
||||
def generate_shipping_label(self):
|
||||
"""Generate and download shipping label PDF from Deutsche Post API."""
|
||||
if not self.order_id:
|
||||
raise ValidationError("Order must be created remotely first")
|
||||
|
||||
if not self.awb_number:
|
||||
raise ValidationError("AWB number required for label generation")
|
||||
|
||||
if not DEUTSCHE_POST_CLIENT_AVAILABLE:
|
||||
raise ValidationError("Deutsche Post API client is not available")
|
||||
|
||||
try:
|
||||
# Note: Deutsche Post API endpoints for labels vary
|
||||
# This is a placeholder for the actual label generation logic
|
||||
# You'll need to check Deutsche Post API documentation for exact endpoints
|
||||
|
||||
client = self.get_api_client()
|
||||
|
||||
# Try to get label via items endpoint or specific label endpoint
|
||||
# This is conceptual - adjust based on actual API
|
||||
from .client.deutsche_post_international_shipping_api_client.api.orders import get_order
|
||||
|
||||
response = get_order.sync_detailed(
|
||||
client=client,
|
||||
order_id=self.order_id
|
||||
)
|
||||
|
||||
if response.parsed and hasattr(response.parsed, 'items') and response.parsed.items:
|
||||
for item in response.parsed.items:
|
||||
if hasattr(item, 'label_data') and item.label_data:
|
||||
# Save label PDF
|
||||
from django.core.files.base import ContentFile
|
||||
import base64
|
||||
|
||||
if isinstance(item.label_data, str):
|
||||
# Base64 encoded PDF
|
||||
pdf_content = base64.b64decode(item.label_data)
|
||||
else:
|
||||
# Binary data
|
||||
pdf_content = item.label_data
|
||||
|
||||
filename = f"deutschepost_label_{self.order_id}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
||||
self.label_pdf.save(
|
||||
filename,
|
||||
ContentFile(pdf_content),
|
||||
save=True
|
||||
)
|
||||
|
||||
self.metadata.update({
|
||||
'label_generated': True,
|
||||
'label_generated_at': timezone.now().isoformat()
|
||||
})
|
||||
return True
|
||||
|
||||
# Alternative: Generate label using AWB number
|
||||
# This is a fallback approach - create a simple label with AWB info
|
||||
self._generate_simple_label()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.last_error = f"Label generation failed: {str(e)}"
|
||||
self.save()
|
||||
raise ValidationError(f"Deutsche Post label generation error: {str(e)}")
|
||||
|
||||
def _generate_simple_label(self):
|
||||
"""Generate a simple label PDF with basic shipping information using HTML template."""
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.files.base import ContentFile
|
||||
from weasyprint import HTML
|
||||
import io
|
||||
|
||||
# Prepare template context
|
||||
context = {
|
||||
'order': self,
|
||||
'now': timezone.now(),
|
||||
'estimated_delivery_days': self.get_estimated_delivery_days()
|
||||
}
|
||||
|
||||
# Render HTML template
|
||||
html_string = render_to_string('deutschepost/shipping_label.html', context)
|
||||
|
||||
# Generate PDF from HTML
|
||||
pdf_buffer = io.BytesIO()
|
||||
HTML(string=html_string).write_pdf(pdf_buffer)
|
||||
pdf_data = pdf_buffer.getvalue()
|
||||
pdf_buffer.close()
|
||||
|
||||
# Save to model
|
||||
filename = f"deutschepost_label_{self.order_id or self.id}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
||||
self.label_pdf.save(
|
||||
filename,
|
||||
ContentFile(pdf_data),
|
||||
save=True
|
||||
)
|
||||
|
||||
self.metadata.update({
|
||||
'simple_label_generated': True,
|
||||
'label_generated_at': timezone.now().isoformat()
|
||||
})
|
||||
|
||||
def get_tracking_url(self):
|
||||
"""Get tracking URL for this order if tracking number is available."""
|
||||
if self.awb_number:
|
||||
return f"https://www.deutschepost.de/de/sendungsverfolgung.html?piececode={self.awb_number}"
|
||||
elif self.barcode:
|
||||
return f"https://www.deutschepost.de/de/sendungsverfolgung.html?piececode={self.barcode}"
|
||||
return None
|
||||
|
||||
def can_be_finalized(self):
|
||||
"""Check if order can be finalized."""
|
||||
return (
|
||||
self.order_id and
|
||||
self.state == self.STATE.CREATED and
|
||||
not self.last_error
|
||||
)
|
||||
|
||||
def is_trackable(self):
|
||||
"""Check if order has tracking information."""
|
||||
return bool(self.awb_number or self.barcode)
|
||||
|
||||
def get_estimated_delivery_days(self):
|
||||
"""Get estimated delivery days based on service level and destination."""
|
||||
if self.service_level == "PRIORITY":
|
||||
# EU countries usually 2-4 days, others 4-7 days
|
||||
if self.destination_country in ["DE", "AT", "FR", "NL", "BE", "LU", "SK", "PL"]:
|
||||
return "2-4"
|
||||
else:
|
||||
return "4-7"
|
||||
else: # STANDARD
|
||||
# EU countries usually 4-6 days, others 7-14 days
|
||||
if self.destination_country in ["DE", "AT", "FR", "NL", "BE", "LU", "SK", "PL"]:
|
||||
return "4-6"
|
||||
else:
|
||||
return "7-14"
|
||||
|
||||
def order_shippment(self):
|
||||
"""Create order via Deutsche Post API, importing address data from commerce order.
|
||||
|
||||
@@ -227,8 +599,8 @@ class DeutschePostOrder(models.Model):
|
||||
the Carrier model and imports all necessary address data.
|
||||
"""
|
||||
# Get related order through Carrier model (same pattern as zasilkovna)
|
||||
Carrier = apps.get_model('commerce', 'Carrier')
|
||||
Order = apps.get_model('commerce', 'Order')
|
||||
Carrier = apps.get_model(app_label='commerce', model_name='Carrier')
|
||||
Order = apps.get_model(app_label='commerce', model_name='Order')
|
||||
|
||||
carrier = Carrier.objects.get(deutschepost=self)
|
||||
order = Order.objects.get(carrier=carrier)
|
||||
@@ -247,8 +619,25 @@ class DeutschePostOrder(models.Model):
|
||||
if not self.postal_code:
|
||||
self.postal_code = order.postal_code
|
||||
if not self.destination_country:
|
||||
# Map country name to ISO code (simplified)
|
||||
country_map = {"Czech Republic": "CZ", "Germany": "DE", "Austria": "AT"}
|
||||
# Map country name to ISO code (comprehensive mapping)
|
||||
country_map = {
|
||||
"Czech Republic": "CZ", "Czechia": "CZ",
|
||||
"Germany": "DE", "Deutschland": "DE",
|
||||
"Austria": "AT", "Österreich": "AT",
|
||||
"Slovakia": "SK", "Slovak Republic": "SK",
|
||||
"Poland": "PL", "Polska": "PL",
|
||||
"Hungary": "HU", "Magyarország": "HU",
|
||||
"France": "FR", "République française": "FR",
|
||||
"Netherlands": "NL", "Nederland": "NL",
|
||||
"Belgium": "BE", "België": "BE", "Belgique": "BE",
|
||||
"Luxembourg": "LU", "Lëtzebuerg": "LU",
|
||||
"Switzerland": "CH", "Schweiz": "CH",
|
||||
"Italy": "IT", "Italia": "IT",
|
||||
"Spain": "ES", "España": "ES",
|
||||
"United Kingdom": "GB", "UK": "GB",
|
||||
"United States": "US", "USA": "US",
|
||||
"Canada": "CA"
|
||||
}
|
||||
self.destination_country = country_map.get(order.country, "CZ")
|
||||
if not self.cust_ref:
|
||||
self.cust_ref = f"ORDER-{order.id}"
|
||||
@@ -270,8 +659,55 @@ class DeutschePostOrder(models.Model):
|
||||
raise ValidationError("Deutsche Post order already created remotely.")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Auto-create remote order when first saved (similar to zasilkovna pattern)
|
||||
is_creating = not self.pk
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if is_creating and not self.order_id:
|
||||
# Only auto-create if we have minimum required data
|
||||
try:
|
||||
self.validate_for_shipping()
|
||||
self.create_remote_order()
|
||||
except ValidationError:
|
||||
# Don't auto-create if validation fails - admin can fix and retry manually
|
||||
pass
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Override delete to cancel remote order if possible."""
|
||||
if self.order_id and self.can_be_cancelled():
|
||||
try:
|
||||
self.cancel_remote_order()
|
||||
except ValidationError:
|
||||
# Log error but don't prevent deletion
|
||||
self.last_error = "Failed to cancel remote order during deletion"
|
||||
self.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def can_be_cancelled(self):
|
||||
"""Check if order can be cancelled (not yet shipped)."""
|
||||
return (
|
||||
self.order_id and
|
||||
self.state in [self.STATE.CREATED, self.STATE.FINALIZED] and
|
||||
not self.awb_number # No AWB means not yet shipped
|
||||
)
|
||||
|
||||
def cancel_remote_order(self):
|
||||
"""Cancel order via Deutsche Post API (if supported)."""
|
||||
if not self.order_id:
|
||||
raise ValidationError("Order not created remotely yet")
|
||||
|
||||
# Note: Deutsche Post API might not have explicit cancel endpoint
|
||||
# We mark as cancelled locally and track in metadata
|
||||
self.state = self.STATE.CANCELLED
|
||||
self.metadata.update({
|
||||
'cancelled': True,
|
||||
'cancelled_at': timezone.now().isoformat(),
|
||||
'cancellation_note': 'Cancelled locally - API cancel might not be supported'
|
||||
})
|
||||
self.last_error = "Order cancelled by user"
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return f"Deutsche Post Order {self.order_id or 'Not Created'} - {self.recipient_name}"
|
||||
|
||||
@@ -305,6 +741,10 @@ class DeutschePostBulkOrder(models.Model):
|
||||
metadata = models.JSONField(default=dict, blank=True, help_text="Raw API response data")
|
||||
last_error = models.TextField(blank=True, help_text="Last API error message")
|
||||
|
||||
# Bulk shipment documents
|
||||
bulk_label_pdf = models.FileField(upload_to='deutschepost/bulk_labels/', blank=True, null=True, help_text="Bulk shipment label PDF")
|
||||
paperwork_pdf = models.FileField(upload_to='deutschepost/paperwork/', blank=True, null=True, help_text="Bulk shipment paperwork PDF")
|
||||
|
||||
def create_remote_bulk_order(self):
|
||||
"""Create bulk order via Deutsche Post API."""
|
||||
if self.bulk_order_id:
|
||||
@@ -333,24 +773,75 @@ class DeutschePostBulkOrder(models.Model):
|
||||
raise ValidationError(f"Invalid orders for bulk: {', '.join(invalid_orders)}")
|
||||
|
||||
try:
|
||||
# Import API functions only when needed
|
||||
from .client.deutsche_post_international_shipping_api_client.api.bulk_orders import create_bulk_order
|
||||
|
||||
client = self.deutschepost_orders.first().get_api_client()
|
||||
config = SiteConfiguration.get_solo()
|
||||
|
||||
# For now, simulate bulk order creation
|
||||
# TODO: Implement actual API call
|
||||
self.bulk_order_id = f"BULK-{self.id}-{timezone.now().strftime('%Y%m%d%H%M%S')}"
|
||||
# Calculate totals from all orders
|
||||
total_count = self.deutschepost_orders.count()
|
||||
total_weight = sum(order.shipment_gross_weight / 1000.0 for order in self.deutschepost_orders.all()) # Convert grams to kg
|
||||
|
||||
if total_count == 0:
|
||||
raise ValidationError("No orders found for bulk order")
|
||||
|
||||
# Use first order's product type and service level (assuming all are same type)
|
||||
first_order = self.deutschepost_orders.first()
|
||||
|
||||
# Create mixed bag order data
|
||||
mixed_bag_order = MixedBagOrderDTO(
|
||||
contact_name=config.deutschepost_contact_name or "Contact",
|
||||
product=first_order.product_type,
|
||||
service_level=first_order.service_level,
|
||||
items_count=total_count,
|
||||
items_weight_in_kilogram=max(0.1, total_weight), # Minimum 0.1kg
|
||||
total_count_receptacles=total_count, # Each order is one receptacle
|
||||
format_="MIXED",
|
||||
telephone_number=config.deutschepost_contact_phone,
|
||||
job_reference=self.description or f"Bulk-{self.id}",
|
||||
customer_ekp=config.deutschepost_customer_ekp or "1234567890"
|
||||
)
|
||||
|
||||
# Get customer EKP
|
||||
customer_ekp = config.deutschepost_customer_ekp or "1234567890"
|
||||
|
||||
# Make API call using mixed order endpoint
|
||||
response = create_mixed_order.sync_detailed(
|
||||
client=client,
|
||||
customer_ekp=customer_ekp,
|
||||
body=mixed_bag_order
|
||||
)
|
||||
|
||||
if response.parsed is None:
|
||||
raise ValidationError(f"Failed to create bulk order. Status: {response.status_code}")
|
||||
|
||||
# Handle different response types
|
||||
if hasattr(response.parsed, 'order_id'):
|
||||
# Successful response (BulkOrderDto)
|
||||
bulk_response = response.parsed
|
||||
self.bulk_order_id = str(bulk_response.order_id)
|
||||
self.status = self.STATUS.PROCESSING
|
||||
self.metadata = {
|
||||
'simulated': True,
|
||||
'api_response': bulk_response.to_dict() if hasattr(bulk_response, 'to_dict') else str(bulk_response),
|
||||
'bulk_order_type': self.bulk_order_type,
|
||||
'order_count': self.deutschepost_orders.count(),
|
||||
'order_count': total_count,
|
||||
'total_weight_kg': total_weight,
|
||||
'created_at': timezone.now().isoformat()
|
||||
}
|
||||
self.last_error = ""
|
||||
else:
|
||||
# Error response
|
||||
error_info = response.parsed.to_dict() if hasattr(response.parsed, 'to_dict') else str(response.parsed)
|
||||
self.status = self.STATUS.ERROR
|
||||
self.last_error = f"Bulk order creation failed: {error_info}"
|
||||
self.metadata = {'api_error': error_info}
|
||||
raise ValidationError(f"Deutsche Post Bulk API error: {error_info}")
|
||||
|
||||
self.save()
|
||||
|
||||
except UnexpectedStatus as e:
|
||||
self.status = self.STATUS.ERROR
|
||||
self.last_error = f"API Error {e.status_code}: {e.content}"
|
||||
self.save()
|
||||
raise ValidationError(f"Deutsche Post Bulk API error {e.status_code}: {e.content}")
|
||||
except Exception as e:
|
||||
self.status = self.STATUS.ERROR
|
||||
self.last_error = str(e)
|
||||
@@ -369,26 +860,109 @@ class DeutschePostBulkOrder(models.Model):
|
||||
raise ValidationError("Deutsche Post API client is not available")
|
||||
|
||||
try:
|
||||
# Import API functions only when needed
|
||||
from .client.deutsche_post_international_shipping_api_client.api.bulk_orders import cancel_bulk_order
|
||||
|
||||
client = self.deutschepost_orders.first().get_api_client()
|
||||
|
||||
# For now, simulate bulk order cancellation
|
||||
# TODO: Implement actual API call
|
||||
# Note: Deutsche Post API might not have a direct cancel endpoint for bulk orders
|
||||
# In that case, we check the current status and mark as cancelled locally
|
||||
try:
|
||||
# Try to get current bulk order status
|
||||
response = get_bulk_order.sync_detailed(
|
||||
client=client,
|
||||
bulk_order_id=self.bulk_order_id
|
||||
)
|
||||
|
||||
if response.parsed and hasattr(response.parsed, 'order_status'):
|
||||
api_status = str(response.parsed.order_status)
|
||||
if api_status in ['COMPLETED', 'FINALIZED']:
|
||||
raise ValidationError("Cannot cancel bulk order that is already completed")
|
||||
|
||||
except UnexpectedStatus:
|
||||
# If we can't get status, proceed with local cancellation
|
||||
pass
|
||||
|
||||
# Since there might not be a cancel API endpoint, mark as cancelled locally
|
||||
self.status = self.STATUS.ERROR # Use ERROR status for cancelled
|
||||
self.metadata.update({
|
||||
'cancelled': True,
|
||||
'cancelled_at': timezone.now().isoformat()
|
||||
'cancelled_at': timezone.now().isoformat(),
|
||||
'cancellation_note': 'Cancelled locally - API might not support bulk order cancellation'
|
||||
})
|
||||
self.last_error = "Bulk order cancelled by user"
|
||||
self.save()
|
||||
|
||||
except UnexpectedStatus as e:
|
||||
self.last_error = f"API Error {e.status_code}: {e.content}"
|
||||
self.save()
|
||||
raise ValidationError(f"Deutsche Post Bulk API error {e.status_code}: {e.content}")
|
||||
except Exception as e:
|
||||
self.last_error = str(e)
|
||||
self.save()
|
||||
raise ValidationError(f"Deutsche Post Bulk API error: {str(e)}")
|
||||
|
||||
def refresh_bulk_status(self):
|
||||
"""Refresh bulk order status from Deutsche Post API."""
|
||||
if not self.bulk_order_id:
|
||||
return
|
||||
|
||||
if not DEUTSCHE_POST_CLIENT_AVAILABLE:
|
||||
return
|
||||
|
||||
try:
|
||||
client = self.deutschepost_orders.first().get_api_client()
|
||||
|
||||
# Make API call to get current bulk order status
|
||||
response = get_bulk_order.sync_detailed(
|
||||
client=client,
|
||||
bulk_order_id=self.bulk_order_id
|
||||
)
|
||||
|
||||
if response.parsed is None:
|
||||
self.last_error = f"Failed to refresh bulk status. Status: {response.status_code}"
|
||||
self.save()
|
||||
return
|
||||
|
||||
# Handle successful response
|
||||
if hasattr(response.parsed, 'order_status'):
|
||||
api_status = str(response.parsed.order_status)
|
||||
if api_status in ['COMPLETED', 'FINALIZED']:
|
||||
self.status = self.STATUS.COMPLETED
|
||||
elif api_status in ['PROCESSING', 'IN_PROGRESS']:
|
||||
self.status = self.STATUS.PROCESSING
|
||||
elif api_status in ['CANCELLED', 'ERROR']:
|
||||
self.status = self.STATUS.ERROR
|
||||
|
||||
self.metadata.update({
|
||||
'status_refreshed': True,
|
||||
'last_refresh': timezone.now().isoformat(),
|
||||
'api_response': response.parsed.to_dict() if hasattr(response.parsed, 'to_dict') else str(response.parsed)
|
||||
})
|
||||
self.last_error = ""
|
||||
else:
|
||||
# Error response
|
||||
error_info = response.parsed.to_dict() if hasattr(response.parsed, 'to_dict') else str(response.parsed)
|
||||
self.last_error = f"Status refresh failed: {error_info}"
|
||||
|
||||
self.save()
|
||||
|
||||
except UnexpectedStatus as e:
|
||||
self.last_error = f"API Error {e.status_code}: {e.content}"
|
||||
self.save()
|
||||
except Exception as e:
|
||||
self.last_error = str(e)
|
||||
self.save()
|
||||
|
||||
def get_total_weight_kg(self):
|
||||
"""Get total weight of all orders in kg."""
|
||||
return sum(order.shipment_gross_weight / 1000.0 for order in self.deutschepost_orders.all())
|
||||
|
||||
def can_be_cancelled(self):
|
||||
"""Check if bulk order can be cancelled."""
|
||||
return (
|
||||
self.bulk_order_id and
|
||||
self.status in [self.STATUS.CREATED, self.STATUS.PROCESSING] and
|
||||
not self.last_error
|
||||
)
|
||||
|
||||
def get_tracking_url(self):
|
||||
"""Get tracking URL for bulk order if available."""
|
||||
if self.bulk_order_id:
|
||||
@@ -402,5 +976,74 @@ class DeutschePostBulkOrder(models.Model):
|
||||
return f"https://www.deutschepost.de/de/b/bulk-status.html?bulk_id={self.bulk_order_id}"
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Auto-create remote bulk order when first saved with orders
|
||||
is_creating = not self.pk
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
if is_creating and not self.bulk_order_id and self.deutschepost_orders.exists():
|
||||
try:
|
||||
self.create_remote_bulk_order()
|
||||
except ValidationError:
|
||||
# Don't auto-create if validation fails - admin can fix and retry manually
|
||||
pass
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Override delete to cancel remote bulk order if possible."""
|
||||
if self.bulk_order_id and self.can_be_cancelled():
|
||||
try:
|
||||
self.cancel_bulk_order()
|
||||
except ValidationError:
|
||||
# Log error but don't prevent deletion
|
||||
self.last_error = "Failed to cancel remote bulk order during deletion"
|
||||
self.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def generate_bulk_labels(self):
|
||||
"""Generate combined PDF with all order labels using HTML template."""
|
||||
if not self.deutschepost_orders.exists():
|
||||
raise ValidationError("No orders in bulk shipment")
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.files.base import ContentFile
|
||||
from weasyprint import HTML
|
||||
import io
|
||||
|
||||
orders = self.deutschepost_orders.all()
|
||||
total_weight_kg = self.get_total_weight_kg()
|
||||
|
||||
# Prepare template context
|
||||
context = {
|
||||
'bulk_order': self,
|
||||
'orders': orders,
|
||||
'total_weight_kg': total_weight_kg,
|
||||
'now': timezone.now()
|
||||
}
|
||||
|
||||
# Render HTML template
|
||||
html_string = render_to_string('deutschepost/bulk_labels.html', context)
|
||||
|
||||
# Generate PDF from HTML
|
||||
pdf_buffer = io.BytesIO()
|
||||
HTML(string=html_string).write_pdf(pdf_buffer)
|
||||
pdf_data = pdf_buffer.getvalue()
|
||||
pdf_buffer.close()
|
||||
|
||||
# Save bulk label PDF
|
||||
filename = f"deutschepost_bulk_labels_{self.bulk_order_id or self.id}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
||||
self.bulk_label_pdf.save(
|
||||
filename,
|
||||
ContentFile(pdf_data),
|
||||
save=True
|
||||
)
|
||||
|
||||
self.metadata.update({
|
||||
'bulk_labels_generated': True,
|
||||
'labels_generated_at': timezone.now().isoformat()
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return f"Deutsche Post Bulk Order {self.bulk_order_id or 'Not Created'} - {self.deutschepost_orders.count()} orders - {self.status}"
|
||||
|
||||
75
backend/thirdparty/deutschepost/serializers.py
vendored
75
backend/thirdparty/deutschepost/serializers.py
vendored
@@ -5,13 +5,19 @@ from .models import DeutschePostOrder, DeutschePostBulkOrder
|
||||
|
||||
|
||||
class DeutschePostOrderSerializer(serializers.ModelSerializer):
|
||||
commerce_order_id = serializers.IntegerField(write_only=True, required=False)
|
||||
state_display = serializers.CharField(source='get_state_display', read_only=True)
|
||||
label_size_display = serializers.CharField(source='get_label_size_display', read_only=True)
|
||||
tracking_url = serializers.SerializerMethodField(read_only=True)
|
||||
estimated_delivery_days = serializers.SerializerMethodField(read_only=True)
|
||||
shipping_cost_estimate = serializers.SerializerMethodField(read_only=True)
|
||||
can_be_finalized = serializers.SerializerMethodField(read_only=True)
|
||||
can_be_cancelled = serializers.SerializerMethodField(read_only=True)
|
||||
is_trackable = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeutschePostOrder
|
||||
fields = [
|
||||
'id', 'created_at', 'state', 'state_display', 'commerce_order', 'commerce_order_id',
|
||||
'id', 'created_at', 'state', 'state_display',
|
||||
'order_id', 'customer_ekp',
|
||||
'recipient_name', 'recipient_phone', 'recipient_email',
|
||||
'address_line1', 'address_line2', 'address_line3',
|
||||
@@ -20,31 +26,33 @@ class DeutschePostOrderSerializer(serializers.ModelSerializer):
|
||||
'shipment_amount', 'shipment_currency',
|
||||
'sender_tax_id', 'importer_tax_id', 'return_item_wanted',
|
||||
'cust_ref', 'awb_number', 'barcode', 'tracking_url',
|
||||
'metadata', 'last_error'
|
||||
'label_pdf', 'label_size', 'label_size_display',
|
||||
'metadata', 'last_error',
|
||||
'estimated_delivery_days', 'shipping_cost_estimate',
|
||||
'can_be_finalized', 'can_be_cancelled', 'is_trackable'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_at', 'order_id', 'awb_number', 'barcode',
|
||||
'tracking_url', 'metadata', 'last_error'
|
||||
'tracking_url', 'metadata', 'last_error', 'label_pdf'
|
||||
]
|
||||
|
||||
def validate_commerce_order_id(self, value):
|
||||
"""Validate that commerce order exists and uses Deutsche Post delivery."""
|
||||
try:
|
||||
from commerce.models import Order
|
||||
order = Order.objects.get(id=value)
|
||||
def get_tracking_url(self, obj):
|
||||
return obj.get_tracking_url()
|
||||
|
||||
if not order.carrier:
|
||||
raise ValidationError("Commerce order must have a carrier assigned")
|
||||
def get_estimated_delivery_days(self, obj):
|
||||
return obj.get_estimated_delivery_days()
|
||||
|
||||
if order.carrier.shipping_method != "deutschepost":
|
||||
raise ValidationError("Commerce order must use Deutsche Post delivery method")
|
||||
def get_shipping_cost_estimate(self, obj):
|
||||
return float(obj.get_shipping_cost_estimate())
|
||||
|
||||
if order.status != Order.OrderStatus.COMPLETED:
|
||||
raise ValidationError("Commerce order must be completed before creating Deutsche Post order")
|
||||
def get_can_be_finalized(self, obj):
|
||||
return obj.can_be_finalized()
|
||||
|
||||
return value
|
||||
except Order.DoesNotExist:
|
||||
raise ValidationError("Commerce order does not exist")
|
||||
def get_can_be_cancelled(self, obj):
|
||||
return obj.can_be_cancelled()
|
||||
|
||||
def get_is_trackable(self, obj):
|
||||
return obj.is_trackable()
|
||||
|
||||
def validate_destination_country(self, value):
|
||||
"""Validate country code format."""
|
||||
@@ -60,14 +68,20 @@ class DeutschePostOrderSerializer(serializers.ModelSerializer):
|
||||
raise ValidationError("Shipment weight cannot exceed 30kg (30000g)")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
commerce_order_id = validated_data.pop('commerce_order_id', None)
|
||||
def validate(self, attrs):
|
||||
"""Validate the complete order data."""
|
||||
# Check required fields for shipping
|
||||
required_fields = ['recipient_name', 'address_line1', 'city', 'postal_code', 'destination_country']
|
||||
missing_fields = [field for field in required_fields if not attrs.get(field)]
|
||||
|
||||
if commerce_order_id:
|
||||
from commerce.models import Order
|
||||
validated_data['commerce_order'] = Order.objects.get(id=commerce_order_id)
|
||||
if missing_fields:
|
||||
raise ValidationError(f"Required fields missing: {', '.join(missing_fields)}")
|
||||
|
||||
return super().create(validated_data)
|
||||
# Check contact information
|
||||
if not attrs.get('recipient_email') and not attrs.get('recipient_phone'):
|
||||
raise ValidationError("Either recipient email or phone is required for delivery notifications")
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class DeutschePostBulkOrderSerializer(serializers.ModelSerializer):
|
||||
@@ -79,8 +93,10 @@ class DeutschePostBulkOrderSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
status_display = serializers.CharField(source='get_status_display', read_only=True)
|
||||
orders_count = serializers.SerializerMethodField(read_only=True)
|
||||
total_weight_kg = serializers.SerializerMethodField(read_only=True)
|
||||
tracking_url = serializers.SerializerMethodField(read_only=True)
|
||||
status_url = serializers.SerializerMethodField(read_only=True)
|
||||
can_be_cancelled = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = DeutschePostBulkOrder
|
||||
@@ -88,23 +104,30 @@ class DeutschePostBulkOrderSerializer(serializers.ModelSerializer):
|
||||
'id', 'created_at', 'status', 'status_display',
|
||||
'bulk_order_id', 'bulk_order_type', 'description',
|
||||
'deutschepost_orders', 'deutschepost_order_ids', 'orders_count',
|
||||
'tracking_url', 'status_url',
|
||||
'total_weight_kg', 'tracking_url', 'status_url', 'can_be_cancelled',
|
||||
'bulk_label_pdf', 'paperwork_pdf',
|
||||
'metadata', 'last_error'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_at', 'bulk_order_id', 'deutschepost_orders',
|
||||
'metadata', 'last_error'
|
||||
'bulk_label_pdf', 'paperwork_pdf', 'metadata', 'last_error'
|
||||
]
|
||||
|
||||
def get_orders_count(self, obj):
|
||||
return obj.deutschepost_orders.count()
|
||||
|
||||
def get_total_weight_kg(self, obj):
|
||||
return obj.get_total_weight_kg()
|
||||
|
||||
def get_tracking_url(self, obj):
|
||||
return obj.get_tracking_url()
|
||||
|
||||
def get_status_url(self, obj):
|
||||
return obj.get_bulk_status_url()
|
||||
|
||||
def get_can_be_cancelled(self, obj):
|
||||
return obj.can_be_cancelled()
|
||||
|
||||
def validate_deutschepost_order_ids(self, value):
|
||||
"""Validate that all orders exist and are eligible for bulk processing."""
|
||||
if not value:
|
||||
|
||||
252
backend/thirdparty/deutschepost/templates/deutschepost/bulk_labels.html
vendored
Normal file
252
backend/thirdparty/deutschepost/templates/deutschepost/bulk_labels.html
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Deutsche Post Bulk Shipping Labels</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 1cm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bulk-header {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #FF0000;
|
||||
padding-bottom: 10px;
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.bulk-info {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bulk-info-left,
|
||||
.bulk-info-right {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.order-header {
|
||||
background-color: #f0f0f0;
|
||||
padding: 8px;
|
||||
margin: -10px -10px 10px -10px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.order-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.order-left,
|
||||
.order-right {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.recipient-name {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.address-line {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.country {
|
||||
font-weight: bold;
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.service-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
background-color: #FF0000;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.priority {
|
||||
background-color: #FF6600;
|
||||
}
|
||||
|
||||
.standard {
|
||||
background-color: #0066CC;
|
||||
}
|
||||
|
||||
.tracking-number {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
background-color: #f9f9f9;
|
||||
padding: 3px 5px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.summary-footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.orders-per-page {
|
||||
/* Adjust based on content - roughly 4-5 orders per page */
|
||||
}
|
||||
|
||||
.order-item:nth-of-type(5n+1) {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.order-item:nth-of-type(1) {
|
||||
page-break-before: auto; /* Don't break before first item */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bulk-header">
|
||||
Deutsche Post - Bulk Shipment Labels
|
||||
</div>
|
||||
|
||||
<div class="bulk-info">
|
||||
<div class="bulk-info-left">
|
||||
<div class="info-row">
|
||||
<span class="label">Bulk Order ID:</span>
|
||||
<span>{{ bulk_order.bulk_order_id|default:'Pending' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Order Type:</span>
|
||||
<span>{{ bulk_order.bulk_order_type }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Description:</span>
|
||||
<span>{{ bulk_order.description|default:'N/A' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bulk-info-right">
|
||||
<div class="info-row">
|
||||
<span class="label">Total Orders:</span>
|
||||
<span>{{ orders.count }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Total Weight:</span>
|
||||
<span>{{ total_weight_kg|floatformat:2 }} kg</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Created:</span>
|
||||
<span>{{ bulk_order.created_at|date:"d.m.Y H:i" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for order in orders %}
|
||||
<div class="order-item">
|
||||
<div class="order-header">
|
||||
Order #{{ forloop.counter }} - {{ order.order_id|default:'Pending' }}
|
||||
<span style="float: right;">
|
||||
<span class="service-badge {% if order.service_level == 'PRIORITY' %}priority{% else %}standard{% endif %}">
|
||||
{{ order.service_level }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="order-content">
|
||||
<div class="order-left">
|
||||
<div class="recipient-name">{{ order.recipient_name }}</div>
|
||||
<div class="address-line">{{ order.address_line1 }}</div>
|
||||
{% if order.address_line2 %}
|
||||
<div class="address-line">{{ order.address_line2 }}</div>
|
||||
{% endif %}
|
||||
<div class="address-line">{{ order.postal_code }} {{ order.city }}</div>
|
||||
{% if order.address_state %}
|
||||
<div class="address-line">{{ order.address_state }}</div>
|
||||
{% endif %}
|
||||
<div class="address-line country">{{ order.destination_country }}</div>
|
||||
|
||||
{% if order.recipient_phone %}
|
||||
<div style="margin-top: 5px; font-size: 10px;">Tel: {{ order.recipient_phone }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="order-right">
|
||||
<div class="info-row">
|
||||
<span class="label">Weight:</span>
|
||||
<span>{{ order.shipment_gross_weight }}g</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Value:</span>
|
||||
<span>{{ order.shipment_amount }} {{ order.shipment_currency }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Product:</span>
|
||||
<span>{{ order.product_type }}</span>
|
||||
</div>
|
||||
{% if order.awb_number %}
|
||||
<div class="info-row" style="margin-top: 8px;">
|
||||
<span class="label">AWB:</span><br>
|
||||
<span class="tracking-number">{{ order.awb_number }}</span>
|
||||
</div>
|
||||
{% elif order.barcode %}
|
||||
<div class="info-row" style="margin-top: 8px;">
|
||||
<span class="label">Barcode:</span><br>
|
||||
<span class="tracking-number">{{ order.barcode }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="summary-footer">
|
||||
<div>Total: {{ orders.count }} orders | {{ total_weight_kg|floatformat:2 }} kg</div>
|
||||
<div>Generated: {{ now|date:"d.m.Y H:i:s" }} | Deutsche Post International Shipping API</div>
|
||||
<div>Bulk Order: {{ bulk_order.bulk_order_id|default:'Pending' }}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
264
backend/thirdparty/deutschepost/templates/deutschepost/shipping_label.html
vendored
Normal file
264
backend/thirdparty/deutschepost/templates/deutschepost/shipping_label.html
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="cs">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Deutsche Post Shipping Label</title>
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 1cm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #FF0000;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
margin-bottom: 30px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.order-info h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.recipient-section {
|
||||
margin-bottom: 30px;
|
||||
border: 2px solid #333;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.recipient-section h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.recipient-address {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.recipient-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.shipment-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.shipment-column {
|
||||
width: 48%;
|
||||
background-color: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.shipment-column h4 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.barcode-section {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.barcode {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2px;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #333;
|
||||
display: inline-block;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tracking-info {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.service-badge {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
background-color: #FF0000;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.priority {
|
||||
background-color: #FF6600;
|
||||
}
|
||||
|
||||
.standard {
|
||||
background-color: #0066CC;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo">Deutsche Post</div>
|
||||
<div>International Shipping Label</div>
|
||||
</div>
|
||||
|
||||
<div class="order-info">
|
||||
<h3>Informace o objednávce / Order Information</h3>
|
||||
<div class="info-row">
|
||||
<span class="label">Order ID:</span>
|
||||
<span>{{ order.order_id|default:'Pending' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">AWB Number:</span>
|
||||
<span>{{ order.awb_number|default:'N/A' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Barcode:</span>
|
||||
<span>{{ order.barcode|default:'N/A' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Customer Ref:</span>
|
||||
<span>{{ order.cust_ref|default:'N/A' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Service Level:</span>
|
||||
<span class="service-badge {% if order.service_level == 'PRIORITY' %}priority{% else %}standard{% endif %}">
|
||||
{{ order.service_level }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recipient-section">
|
||||
<h3>Příjemce / Recipient</h3>
|
||||
<div class="recipient-address">
|
||||
<div class="recipient-name">{{ order.recipient_name }}</div>
|
||||
<div>{{ order.address_line1 }}</div>
|
||||
{% if order.address_line2 %}
|
||||
<div>{{ order.address_line2 }}</div>
|
||||
{% endif %}
|
||||
{% if order.address_line3 %}
|
||||
<div>{{ order.address_line3 }}</div>
|
||||
{% endif %}
|
||||
<div>{{ order.postal_code }} {{ order.city }}</div>
|
||||
{% if order.address_state %}
|
||||
<div>{{ order.address_state }}</div>
|
||||
{% endif %}
|
||||
<div><strong>{{ order.destination_country }}</strong></div>
|
||||
{% if order.recipient_phone %}
|
||||
<div>Tel: {{ order.recipient_phone }}</div>
|
||||
{% endif %}
|
||||
{% if order.recipient_email %}
|
||||
<div>Email: {{ order.recipient_email }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shipment-details">
|
||||
<div class="shipment-column">
|
||||
<h4>Detaily zásilky / Shipment Details</h4>
|
||||
<div class="info-row">
|
||||
<span class="label">Product:</span>
|
||||
<span>{{ order.product_type }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Weight:</span>
|
||||
<span>{{ order.shipment_gross_weight }}g</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Value:</span>
|
||||
<span>{{ order.shipment_amount }} {{ order.shipment_currency }}</span>
|
||||
</div>
|
||||
{% if order.sender_tax_id %}
|
||||
<div class="info-row">
|
||||
<span class="label">IOSS:</span>
|
||||
<span>{{ order.sender_tax_id }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="shipment-column">
|
||||
<h4>Doručení / Delivery</h4>
|
||||
<div class="info-row">
|
||||
<span class="label">Est. Days:</span>
|
||||
<span>{{ estimated_delivery_days }} days</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Return:</span>
|
||||
<span>{% if order.return_item_wanted %}Yes{% else %}No{% endif %}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Created:</span>
|
||||
<span>{{ order.created_at|date:"d.m.Y H:i" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if order.awb_number or order.barcode %}
|
||||
<div class="barcode-section">
|
||||
<div>Tracking Number / Sledovací číslo</div>
|
||||
<div class="barcode">{{ order.awb_number|default:order.barcode }}</div>
|
||||
<div class="tracking-info">
|
||||
Track at: https://www.deutschepost.de/de/sendungsverfolgung.html
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tracking-info">
|
||||
Generated: {{ now|date:"d.m.Y H:i:s" }} | Deutsche Post International Shipping API
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
197
backend/thirdparty/deutschepost/views.py
vendored
197
backend/thirdparty/deutschepost/views.py
vendored
@@ -31,11 +31,48 @@ from .serializers import (
|
||||
description="Returns detailed information for a single Deutsche Post order including tracking data. Orders are managed through Carrier model.",
|
||||
responses=DeutschePostOrderSerializer,
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Create Deutsche Post Order",
|
||||
description="Create a new Deutsche Post order with recipient and shipment details. Order will automatically be created remotely if validation passes.",
|
||||
request=DeutschePostOrderSerializer,
|
||||
responses={
|
||||
201: DeutschePostOrderSerializer,
|
||||
400: OpenApiResponse(description="Validation error")
|
||||
},
|
||||
),
|
||||
update=extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Update Deutsche Post Order",
|
||||
description="Update Deutsche Post order details. Cannot update orders that are already created remotely.",
|
||||
request=DeutschePostOrderSerializer,
|
||||
responses={
|
||||
200: DeutschePostOrderSerializer,
|
||||
400: OpenApiResponse(description="Validation error or order already remote")
|
||||
},
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Partially Update Deutsche Post Order",
|
||||
description="Partially update Deutsche Post order details. Cannot update orders that are already created remotely.",
|
||||
request=DeutschePostOrderSerializer,
|
||||
responses={
|
||||
200: DeutschePostOrderSerializer,
|
||||
400: OpenApiResponse(description="Validation error or order already remote")
|
||||
},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Delete Deutsche Post Order",
|
||||
description="Delete Deutsche Post order. If order is created remotely, it will attempt to cancel it first.",
|
||||
responses={
|
||||
204: OpenApiResponse(description="Order deleted successfully"),
|
||||
400: OpenApiResponse(description="Cannot delete order")
|
||||
},
|
||||
),
|
||||
)
|
||||
class DeutschePostOrderViewSet(
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
viewsets.GenericViewSet
|
||||
viewsets.ModelViewSet # Changed to ModelViewSet for full CRUD
|
||||
):
|
||||
queryset = DeutschePostOrder.objects.all().order_by("-created_at")
|
||||
serializer_class = DeutschePostOrderSerializer
|
||||
@@ -112,7 +149,7 @@ class DeutschePostOrderViewSet(
|
||||
order = self.get_object()
|
||||
|
||||
try:
|
||||
order.cancel_order()
|
||||
order.cancel_remote_order()
|
||||
serializer = self.get_serializer(order)
|
||||
return Response(serializer.data)
|
||||
except Exception as e:
|
||||
@@ -121,6 +158,83 @@ class DeutschePostOrderViewSet(
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Finalize Deutsche Post Order",
|
||||
description="""
|
||||
Finalize the order in Deutsche Post API system. This will:
|
||||
- Call Deutsche Post API to finalize the order
|
||||
- Update the order state to FINALIZED
|
||||
- Retrieve AWB number and barcode for tracking
|
||||
- Enable label generation
|
||||
- Required before the order can be shipped
|
||||
""",
|
||||
request=None,
|
||||
responses={
|
||||
200: DeutschePostOrderSerializer,
|
||||
400: OpenApiResponse(description="Order cannot be finalized or API error"),
|
||||
500: OpenApiResponse(description="Deutsche Post API error")
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'])
|
||||
def finalize_order(self, request, pk=None):
|
||||
"""Finalize order in Deutsche Post API."""
|
||||
order = self.get_object()
|
||||
|
||||
try:
|
||||
order.finalize_remote_order()
|
||||
serializer = self.get_serializer(order)
|
||||
return Response(serializer.data)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{'error': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Generate Shipping Label",
|
||||
description="""
|
||||
Generate and download shipping label PDF. This will:
|
||||
- Attempt to download label from Deutsche Post API
|
||||
- If API label not available, generate simple label with order details
|
||||
- Save label PDF to order.label_pdf field
|
||||
- Include barcode and tracking information
|
||||
- Requires order to be created remotely first
|
||||
""",
|
||||
request=None,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Label generated successfully", examples={
|
||||
"application/json": {
|
||||
"message": "Label generated successfully",
|
||||
"label_url": "/media/deutschepost/labels/label_123.pdf"
|
||||
}
|
||||
}),
|
||||
400: OpenApiResponse(description="Order not ready for label generation or API error"),
|
||||
500: OpenApiResponse(description="Deutsche Post API error")
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'])
|
||||
def generate_label(self, request, pk=None):
|
||||
"""Generate shipping label PDF."""
|
||||
order = self.get_object()
|
||||
|
||||
try:
|
||||
order.generate_shipping_label()
|
||||
|
||||
label_url = order.label_pdf.url if order.label_pdf else None
|
||||
return Response({
|
||||
'message': 'Label generated successfully',
|
||||
'label_url': label_url,
|
||||
'order_id': order.order_id,
|
||||
'awb_number': order.awb_number
|
||||
})
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{'error': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Get Tracking URL",
|
||||
@@ -298,3 +412,78 @@ class DeutschePostBulkOrderViewSet(
|
||||
{'error': 'Bulk order not created remotely yet'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Refresh Bulk Order Status",
|
||||
description="""
|
||||
Refresh bulk order status from Deutsche Post API. This will:
|
||||
- Fetch latest bulk order status from Deutsche Post
|
||||
- Update bulk order status (PROCESSING, COMPLETED, etc.)
|
||||
- Store raw API response data in metadata
|
||||
- Update individual order statuses if needed
|
||||
""",
|
||||
request=None,
|
||||
responses={
|
||||
200: DeutschePostBulkOrderSerializer,
|
||||
400: OpenApiResponse(description="Bulk order not created remotely"),
|
||||
500: OpenApiResponse(description="Deutsche Post API error")
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'])
|
||||
def refresh_status(self, request, pk=None):
|
||||
"""Refresh bulk order status from Deutsche Post API."""
|
||||
bulk_order = self.get_object()
|
||||
|
||||
try:
|
||||
bulk_order.refresh_bulk_status()
|
||||
serializer = self.get_serializer(bulk_order)
|
||||
return Response(serializer.data)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{'error': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
tags=["deutschepost"],
|
||||
summary="Generate Bulk Labels",
|
||||
description="""
|
||||
Generate combined PDF with all order labels for bulk shipment. This will:
|
||||
- Create multi-page PDF with all order details
|
||||
- Include recipient addresses and tracking information
|
||||
- Format for warehouse processing
|
||||
- Save PDF to bulk_order.bulk_label_pdf field
|
||||
""",
|
||||
request=None,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Bulk labels generated successfully", examples={
|
||||
"application/json": {
|
||||
"message": "Bulk labels generated successfully",
|
||||
"labels_url": "/media/deutschepost/bulk_labels/bulk_labels_123.pdf",
|
||||
"orders_count": 5
|
||||
}
|
||||
}),
|
||||
400: OpenApiResponse(description="No orders in bulk shipment or generation error"),
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'])
|
||||
def generate_labels(self, request, pk=None):
|
||||
"""Generate bulk shipping labels PDF."""
|
||||
bulk_order = self.get_object()
|
||||
|
||||
try:
|
||||
bulk_order.generate_bulk_labels()
|
||||
|
||||
labels_url = bulk_order.bulk_label_pdf.url if bulk_order.bulk_label_pdf else None
|
||||
return Response({
|
||||
'message': 'Bulk labels generated successfully',
|
||||
'labels_url': labels_url,
|
||||
'bulk_order_id': bulk_order.bulk_order_id,
|
||||
'orders_count': bulk_order.deutschepost_orders.count()
|
||||
})
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{'error': str(e)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user