Refactor order creation and add configuration endpoints
Refactored order creation logic to use new serializers and transaction handling, improving validation and modularity. Introduced admin and public endpoints for shop configuration with sensitive fields protected. Enhanced Zásilkovna (Packeta) integration, including packet widget template, new API fields, and improved error handling. Added django-silk for profiling, updated requirements and settings, and improved frontend Orval config for API client generation.
This commit is contained in:
7
backend/thirdparty/zasilkovna/client.py
vendored
7
backend/thirdparty/zasilkovna/client.py
vendored
@@ -15,11 +15,14 @@ zeepZasClient = Client(wsdl=WSDL_URL)
|
||||
|
||||
|
||||
class PacketaAPI:
|
||||
#TODO: zeptat se jestli nepřidat další checkovací parametry ohledně zásilkovny např: blokování podle nastavení webu
|
||||
#TODO: zeptat se jestli nepřidat další checkovací parametry ohledně zásilkovny např: blokování podle configurace webu
|
||||
# popřemýšlet, jestli api klíče nenastavit přes configurator webu
|
||||
def __getattribute__(self):
|
||||
if PACKETA_API_PASSWORD is None:
|
||||
if PACKETA_API_PASSWORD in [None, ""]:
|
||||
raise Exception("Packeta API password is not set in environment variables.")
|
||||
|
||||
elif zeepZasClient is None:
|
||||
raise Exception("Packeta SOAP client is not initialized.")
|
||||
|
||||
# ---------- CREATE PACKET METHODS ----------
|
||||
|
||||
|
||||
40
backend/thirdparty/zasilkovna/models.py
vendored
40
backend/thirdparty/zasilkovna/models.py
vendored
@@ -34,23 +34,23 @@ class ZasilkovnaPacket(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class STATE(models.TextChoices):
|
||||
PENDING = "PENDING", "Podáno"
|
||||
SENDED = "SENDED", "Odesláno"
|
||||
ARRIVED = "ARRIVED", "Doručeno"
|
||||
CANCELED = "CANCELED", "Zrušeno"
|
||||
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", "Posláno zpátky"
|
||||
RETURNED = "RETURNED", "Vráceno"
|
||||
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 -------
|
||||
#TODO: změnit na nastavení adresy eshopu/obchodu z modelu konfigurace
|
||||
# https://client.packeta.com/cs/senders (admin rozhraní)
|
||||
|
||||
addressId = models.IntegerField(help_text="ID adresy, v Widgetu zásilkovny který si vybere uživatel.")
|
||||
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(help_text="Číslo zásilky v Packetě (api)")
|
||||
barcode = models.CharField(max_length=64, help_text="Čárový kód zásilky v Packetě")
|
||||
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,
|
||||
@@ -61,6 +61,7 @@ class ZasilkovnaPacket(models.Model):
|
||||
return_routing = models.JSONField(
|
||||
default=list,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Seznam 2 routing stringů pro vrácení zásilky"
|
||||
)
|
||||
|
||||
@@ -73,7 +74,13 @@ class ZasilkovnaPacket(models.Model):
|
||||
size_of_pdf = models.CharField(max_length=20, choices=PDF_SIZE.choices, default=PDF_SIZE.A6_ON_A6)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# workaroud to avoid circular import
|
||||
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')
|
||||
|
||||
@@ -84,26 +91,27 @@ class ZasilkovnaPacket(models.Model):
|
||||
|
||||
if not self.packet_id:
|
||||
response = packeta_client.create_packet(
|
||||
address_id=self.addressId,
|
||||
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,
|
||||
addressId=ShopConfiguration.get_solo().zasilkovna_address_id,
|
||||
|
||||
cod=order.total_price if cash_on_delivery else 0, # dobírka
|
||||
value=order.total_price,
|
||||
|
||||
currency=ShopConfiguration.get_solo().currency,
|
||||
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 super().save(args, **kwargs)
|
||||
|
||||
return self.save()
|
||||
|
||||
def cancel_packet(self):
|
||||
"""Cancel this packet via the Packeta API."""
|
||||
packeta_client.cancel_packet(self.packet_id)
|
||||
|
||||
24
backend/thirdparty/zasilkovna/serializers.py
vendored
24
backend/thirdparty/zasilkovna/serializers.py
vendored
@@ -15,16 +15,27 @@ class ZasilkovnaPacketSerializer(serializers.ModelSerializer):
|
||||
"weight",
|
||||
"return_routing",
|
||||
]
|
||||
read_only_fields = fields
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"barcode",
|
||||
"state",
|
||||
"weight",
|
||||
"return_routing",
|
||||
]
|
||||
|
||||
|
||||
#Just for tracking URL of packet
|
||||
class TrackingURLSerializer(serializers.Serializer):
|
||||
barcode = serializers.CharField(read_only=True)
|
||||
tracking_url = serializers.URLField(read_only=True)
|
||||
|
||||
|
||||
|
||||
# -- SHIPMENT --
|
||||
|
||||
class ZasilkovnaShipmentSerializer(serializers.ModelSerializer):
|
||||
packets = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
|
||||
packets = serializers.PrimaryKeyRelatedField(many=True)
|
||||
|
||||
class Meta:
|
||||
model = ZasilkovnaShipment
|
||||
@@ -33,6 +44,11 @@ class ZasilkovnaShipmentSerializer(serializers.ModelSerializer):
|
||||
"created_at",
|
||||
"shipment_id",
|
||||
"barcode",
|
||||
"packets",
|
||||
"packets",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"shipment_id",
|
||||
"barcode",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
47
backend/thirdparty/zasilkovna/templates/zasilkovna/pickup_point_widget.html
vendored
Normal file
47
backend/thirdparty/zasilkovna/templates/zasilkovna/pickup_point_widget.html
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
{% load static %}
|
||||
<script src="https://widget.packeta.com/v6/www/js/library.js"></script>
|
||||
|
||||
<input type="hidden" id="packetaApiKey" value="{{ packeta_Api_Key }}">
|
||||
|
||||
<script>
|
||||
const packetaApiKey = document.querySelector('#packetaApiKey').value;
|
||||
|
||||
const packetaOptions = {
|
||||
country: "cz,sk",
|
||||
language: "cs",
|
||||
valueFormat: "\"Packeta\",id,carrierId,carrierPickupPointId,name,city,street",
|
||||
view: "modal",
|
||||
vendors: [
|
||||
{
|
||||
country: "cz",
|
||||
group: "zbox",
|
||||
price: 45,
|
||||
selected: true
|
||||
},
|
||||
{
|
||||
country: "cz",
|
||||
price: 45,
|
||||
selected: true
|
||||
}
|
||||
],
|
||||
defaultCurrency: "CZK",
|
||||
defaultPrice: "45"
|
||||
};
|
||||
|
||||
|
||||
|
||||
function showSelectedPickupPoint(point) {
|
||||
const saveElement = document.querySelector(".packeta-selector-value");
|
||||
// Add here an action on pickup point selection
|
||||
saveElement.innerText = '';
|
||||
if (point) {
|
||||
console.log("Selected point", point);
|
||||
saveElement.innerText = "point: " + JSON.stringify(point, null, 4); // DŮLEŽITÉ PRO DALŠÍ ZPRACOVÁNÍ (jenom potřebuji ID)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<button class="packeta-selector-open"
|
||||
onclick="Packeta.Widget.pick(packetaApiKey, showSelectedPickupPoint, packetaOptions)">Select pick-up point</button>
|
||||
<div class="packeta-selector-value"></div>
|
||||
108
backend/thirdparty/zasilkovna/views.py
vendored
108
backend/thirdparty/zasilkovna/views.py
vendored
@@ -1,8 +1,11 @@
|
||||
from rest_framework import viewsets, mixins, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.template import loader
|
||||
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, OpenApiParameter, OpenApiTypes
|
||||
|
||||
from backend.configuration.models import ShopConfiguration
|
||||
|
||||
from .models import ZasilkovnaShipment, ZasilkovnaPacket
|
||||
from .serializers import (
|
||||
@@ -11,32 +14,37 @@ from .serializers import (
|
||||
TrackingURLSerializer,
|
||||
)
|
||||
|
||||
# -- SHIPMENT --
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Zásilkovna"],
|
||||
summary="List shipments",
|
||||
tags=["Packeta-Shipment"],
|
||||
summary="Hromadný shipment",
|
||||
description="Returns a paginated list of Packeta (Zásilkovna) shipments.",
|
||||
responses={200: ZasilkovnaShipmentSerializer},
|
||||
responses=ZasilkovnaShipmentSerializer,
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Zásilkovna"],
|
||||
summary="Retrieve a shipment",
|
||||
tags=["Packeta-Shipment"],
|
||||
summary="Detail hromadné zásilky",
|
||||
description="Returns detail for a single shipment.",
|
||||
responses={200: ZasilkovnaShipmentSerializer},
|
||||
responses=ZasilkovnaShipmentSerializer,
|
||||
),
|
||||
)
|
||||
class ZasilkovnaShipmentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class ZasilkovnaShipmentViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet,):
|
||||
queryset = ZasilkovnaShipment.objects.all().order_by("-created_at")
|
||||
serializer_class = ZasilkovnaShipmentSerializer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# -- PACKET --
|
||||
|
||||
@extend_schema_view(
|
||||
retrieve=extend_schema(
|
||||
tags=["Zásilkovna"],
|
||||
summary="Retrieve a packet",
|
||||
description="Returns detail for a single packet.",
|
||||
tags=["Packet"],
|
||||
summary="Packet",
|
||||
description="#TODO: Popis endpointu",
|
||||
responses={200: ZasilkovnaPacketSerializer},
|
||||
)
|
||||
)
|
||||
@@ -45,12 +53,14 @@ class ZasilkovnaPacketViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
serializer_class = ZasilkovnaPacketSerializer
|
||||
|
||||
@extend_schema(
|
||||
tags=["Zásilkovna"],
|
||||
tags=["Packet"],
|
||||
summary="Get public tracking URL",
|
||||
description=(
|
||||
"Returns the public Zásilkovna tracking URL derived from the packet's barcode."
|
||||
),
|
||||
responses={200: OpenApiResponse(response=TrackingURLSerializer)},
|
||||
description="Returns the public Zásilkovna tracking URL derived from the packet's barcode.",
|
||||
responses=OpenApiResponse(response=TrackingURLSerializer),
|
||||
parameters=[
|
||||
OpenApiParameter(name="pk", location=OpenApiParameter.PATH, description="Packet ID", required=True, type=int),
|
||||
],
|
||||
request=None,
|
||||
)
|
||||
@action(detail=True, methods=["get"], url_path="tracking-url")
|
||||
def tracking_url(self, request, pk=None):
|
||||
@@ -60,21 +70,73 @@ class ZasilkovnaPacketViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
"tracking_url": packet.get_tracking_url(),
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
|
||||
|
||||
#HOTOVO
|
||||
@extend_schema(
|
||||
tags=["Zásilkovna"],
|
||||
tags=["Packet"],
|
||||
summary="Order shipping",
|
||||
description=(
|
||||
"Objedná přepravu přes API Packety,"
|
||||
"podle existujicího objektu, kde je od uživatele uložený id od místa poslání."
|
||||
),
|
||||
request=None,
|
||||
responses=OpenApiResponse(response=ZasilkovnaPacketSerializer),
|
||||
parameters=[
|
||||
OpenApiParameter(name="pk", location=OpenApiParameter.PATH, description="Packet ID", required=True, type=int),
|
||||
],
|
||||
)
|
||||
@action(detail=True, methods=["patch"], url_path="order-shipping")
|
||||
def order_shipping(self, request, pk=None):
|
||||
packet: ZasilkovnaPacket = self.get_object()
|
||||
packet.order_shipping()
|
||||
serializer = self.get_serializer(packet)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
#HOTOVO
|
||||
@extend_schema(
|
||||
tags=["Packet"],
|
||||
summary="Cancel packet",
|
||||
description=(
|
||||
"Cancels the packet through the Packeta API and updates its state to CANCELED. "
|
||||
"No request body is required."
|
||||
),
|
||||
request=None,
|
||||
responses={200: OpenApiResponse(response=ZasilkovnaPacketSerializer)},
|
||||
responses=OpenApiResponse(response=ZasilkovnaPacketSerializer),
|
||||
parameters=[
|
||||
OpenApiParameter(name="pk", location=OpenApiParameter.PATH, description="Packet ID", required=True, type=int),
|
||||
],
|
||||
)
|
||||
@action(detail=True, methods=["patch"], url_path="cancel")
|
||||
def cancel(self, request, pk=None):
|
||||
packet: ZasilkovnaPacket = self.get_object()
|
||||
packet.cancel_packet()
|
||||
serializer = self.get_serializer(packet)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
packet: ZasilkovnaPacket = self.get_object()
|
||||
packet.cancel_packet()
|
||||
serializer = self.get_serializer(packet)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
#TODO: dodělat/domluvit se
|
||||
@extend_schema(
|
||||
tags=["Packet"],
|
||||
summary="Get widget for user, to select pickup point.",
|
||||
description=(
|
||||
"Returns HTML widget for user to select pickup point. "
|
||||
"No request body is required."
|
||||
),
|
||||
request=None,
|
||||
responses={200: OpenApiResponse(response=TrackingURLSerializer)},
|
||||
)
|
||||
@action(detail=True, methods=["get"], url_path="pickup-point-widget")
|
||||
def pickup_point_widget(self, request):
|
||||
|
||||
#https://configurator.widget.packeta.com/cs
|
||||
|
||||
widget_html = loader.render_to_string(
|
||||
"zasilkovna/pickup_point_widget.html",
|
||||
{
|
||||
"api_key": ShopConfiguration.get_solo().zasilkovna_widget_api_key,
|
||||
}
|
||||
)
|
||||
|
||||
return Response({"widget_html": widget_html})
|
||||
Reference in New Issue
Block a user