"""Models for integration with Packeta (Zásilkovna) API. These models wrap calls to the SOAP API via the immutable client defined in `client.py` (PacketaAPI). DO NOT modify the client; we only consume it here. Workflow: - Create a `PacketaPacket` instance locally with required recipient data. - On first save (when `packet_id` is empty) call the remote API to create the packet and persist the returned identifier + metadata. - Use helper methods to refresh remote info, fetch label PDF, cancel packet. - Group packets into a `PacketaShipment` and create shipment remotely. Edge cases handled: - API faults raise exceptions (to be surfaced by serializer validation). - Missing remote fields are stored in `metadata` JSON. - Label PDF stored as binary field; can be re-fetched if empty. """ import json from django.db import models from django.utils import timezone from django.core.validators import RegexValidator from django.core.files.base import ContentFile from django.apps import apps from rest_framework.exceptions import ValidationError from .client import PacketaAPI from configuration.models import ShopConfiguration packeta_client = PacketaAPI() # single reusable instance class ZasilkovnaPacket(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATE(models.TextChoices): WAITING_FOR_ORDER = "WAITING_FOR_ORDERING_SHIPMENT", "cz#Čeká na objednání zásilkovny" PENDING = "PENDING", "cz#Podáno" SENDED = "SENDED", "cz#Odesláno" ARRIVED = "ARRIVED", "cz#Doručeno" CANCELED = "CANCELED", "cz#Zrušeno" RETURNING = "RETURNING", "cz#Posláno zpátky" RETURNED = "RETURNED", "cz#Vráceno" state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.PENDING) # ------- API ------- # https://client.packeta.com/cs/senders (admin rozhraní) addressId = models.IntegerField(null=True, blank=True, help_text="ID adresy/pointu, ve Widgetu zásilkovny který si vybere uživatel.") packet_id = models.IntegerField(null=True, blank=True, help_text="Číslo zásilky v Packetě (vraceno od API od Packety)") barcode = models.CharField(null=True, blank=True, max_length=64, help_text="Čárový kód zásilky od Packety") weight = models.IntegerField( default=0, help_text="Hmotnost zásilky v gramech" ) # 🚚 návratové směrovací kódy (pro vrácení zásilky) return_routing = models.JSONField( default=list, blank=True, null=True, help_text="Seznam 2 routing stringů pro vrácení zásilky" ) class PDF_SIZE(models.TextChoices): A6_ON_A6 = ("A6 on A6", "105x148 mm (A6) label on a page of the same size") A7_ON_A7 = ("A7 on A7", "105x74 mm (A7) label on a page of the same size") A6_ON_A4 = ("A6 on A4", "105x148 mm (A6) label on a page of size 210x297 mm (A4)") A7_ON_A4 = ("A7 on A4", "105x74 mm (A7) label on a page of size 210x297 mm (A4)") A8_ON_A8 = ("A8 on A8", "50x74 mm (A8) label on a page of the same size") size_of_pdf = models.CharField(max_length=20, choices=PDF_SIZE.choices, default=PDF_SIZE.A6_ON_A6) def save(self, *args, **kwargs): return super().save(args, **kwargs) def order_shippment(self): if self.addressId is None: raise ValidationError("AddressId must be set to order shipping.") Carrier = apps.get_model('commerce', 'Carrier') Order = apps.get_model('commerce', 'Order') carrier = Carrier.objects.get(zasilkovna=self) order = Order.objects.get(carrier=carrier) cash_on_delivery = order.payment.payment_method == order.payment.PAYMENT.CASH_ON_DELIVERY if not self.packet_id: response = packeta_client.create_packet( addressId=self.addressId, # ID z widgetu weight=self.weight, number=order.id, name=order.first_name, surname=order.last_name, company=order.company, email=order.email, cod=order.total_price if cash_on_delivery else 0, # dobírka value=order.total_price, currency=ShopConfiguration.get_solo().currency, #CZK eshop= ShopConfiguration.get_solo().name, ) self.packet_id = response['packet_id'] self.barcode = response['barcode'] else: raise ValidationError("Přeprava už byla objednana!!!.") return self.save() def cancel_packet(self): """Cancel this packet via the Packeta API.""" packeta_client.cancel_packet(self.packet_id) self.state = self.STATE.CANCELED self.save() # persist state change def get_tracking_url(self): """Vrátí veřejnou URL pro sledování zásilky (Zásilkovna.cz).""" base_url = "https://www.zasilkovna.cz/vyhledavani?query=" return f"{base_url}{self.barcode}" def returning_packet(self): """Mark this packet as returning via the Packeta API.""" response = packeta_client.get_return_routing(self.packet_id) self.return_routing = json.loads(response) self.state = self.STATE.RETURNING self.save() # persist state change class ZasilkovnaShipment(models.Model): created_at = models.DateTimeField(auto_now_add=True, editable=False) shipment_id = models.CharField(max_length=255, unique=True, help_text="ID zásilky v Packetě", editable=False) barcode = models.CharField( max_length=64, help_text="Čárový kód zásilky v Packetě (format: )", validators=[ RegexValidator(r'D-***-XM-', message="Neplatný formát čárového kódu.") ] ) packets = models.ManyToManyField(ZasilkovnaPacket, related_name="shipments", help_text="Seznam zásilek v této zásilce (packet_id)") def save(self, *args, **kwargs): if not self.shipment_id: response = packeta_client.create_shipment( packet_ids=[packet.packet_id for packet in self.packets.all()] ) self.shipment_id = response['shipment_id'] self.barcode = response['barcode'] return super().save(args, **kwargs)