karrio-server-graph 2025.5.5__py3-none-any.whl → 2025.5.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- karrio/server/graph/schemas/base/__init__.py +82 -18
- karrio/server/graph/schemas/base/inputs.py +199 -70
- karrio/server/graph/schemas/base/mutations.py +313 -122
- karrio/server/graph/schemas/base/types.py +139 -22
- karrio/server/graph/serializers.py +115 -41
- karrio/server/graph/tests/test_rate_sheets.py +1950 -176
- {karrio_server_graph-2025.5.5.dist-info → karrio_server_graph-2025.5.7.dist-info}/METADATA +1 -1
- {karrio_server_graph-2025.5.5.dist-info → karrio_server_graph-2025.5.7.dist-info}/RECORD +10 -10
- {karrio_server_graph-2025.5.5.dist-info → karrio_server_graph-2025.5.7.dist-info}/WHEEL +0 -0
- {karrio_server_graph-2025.5.5.dist-info → karrio_server_graph-2025.5.7.dist-info}/top_level.txt +0 -0
|
@@ -1148,15 +1148,18 @@ class ShipmentType:
|
|
|
1148
1148
|
return utils.paginated_connection(queryset, **_filter.pagination())
|
|
1149
1149
|
|
|
1150
1150
|
|
|
1151
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1152
|
+
# SHARED ZONE TYPE (Rate Sheet Level)
|
|
1153
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1154
|
+
|
|
1155
|
+
|
|
1151
1156
|
@strawberry.type
|
|
1152
|
-
class
|
|
1157
|
+
class SharedZoneType:
|
|
1158
|
+
"""Shared zone definition at the RateSheet level."""
|
|
1159
|
+
|
|
1153
1160
|
object_type: str
|
|
1154
|
-
id:
|
|
1161
|
+
id: str
|
|
1155
1162
|
label: typing.Optional[str] = None
|
|
1156
|
-
rate: typing.Optional[float] = None
|
|
1157
|
-
|
|
1158
|
-
min_weight: typing.Optional[float] = None
|
|
1159
|
-
max_weight: typing.Optional[float] = None
|
|
1160
1163
|
|
|
1161
1164
|
transit_days: typing.Optional[int] = None
|
|
1162
1165
|
transit_time: typing.Optional[float] = None
|
|
@@ -1167,24 +1170,98 @@ class ServiceZoneType:
|
|
|
1167
1170
|
|
|
1168
1171
|
cities: typing.Optional[typing.List[str]] = None
|
|
1169
1172
|
postal_codes: typing.Optional[typing.List[str]] = None
|
|
1170
|
-
country_codes: typing.Optional[typing.List[
|
|
1173
|
+
country_codes: typing.Optional[typing.List[str]] = None
|
|
1171
1174
|
|
|
1172
1175
|
@staticmethod
|
|
1173
1176
|
def parse(zone: dict):
|
|
1174
|
-
return
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1177
|
+
return SharedZoneType(
|
|
1178
|
+
object_type="shared_zone",
|
|
1179
|
+
id=zone.get("id", ""),
|
|
1180
|
+
label=zone.get("label"),
|
|
1181
|
+
transit_days=zone.get("transit_days"),
|
|
1182
|
+
transit_time=zone.get("transit_time"),
|
|
1183
|
+
radius=zone.get("radius"),
|
|
1184
|
+
latitude=zone.get("latitude"),
|
|
1185
|
+
longitude=zone.get("longitude"),
|
|
1186
|
+
cities=zone.get("cities"),
|
|
1187
|
+
postal_codes=zone.get("postal_codes"),
|
|
1188
|
+
country_codes=zone.get("country_codes"),
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1193
|
+
# SHARED SURCHARGE TYPE (Rate Sheet Level)
|
|
1194
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@strawberry.type
|
|
1198
|
+
class SharedSurchargeType:
|
|
1199
|
+
"""Shared surcharge definition at the RateSheet level."""
|
|
1200
|
+
|
|
1201
|
+
object_type: str
|
|
1202
|
+
id: str
|
|
1203
|
+
name: str
|
|
1204
|
+
amount: float
|
|
1205
|
+
surcharge_type: str
|
|
1206
|
+
cost: typing.Optional[float] = None
|
|
1207
|
+
active: bool = True
|
|
1208
|
+
|
|
1209
|
+
@staticmethod
|
|
1210
|
+
def parse(surcharge: dict):
|
|
1211
|
+
return SharedSurchargeType(
|
|
1212
|
+
object_type="shared_surcharge",
|
|
1213
|
+
id=surcharge.get("id", ""),
|
|
1214
|
+
name=surcharge.get("name", ""),
|
|
1215
|
+
amount=float(surcharge.get("amount", 0)),
|
|
1216
|
+
surcharge_type=surcharge.get("surcharge_type", "fixed"),
|
|
1217
|
+
cost=surcharge.get("cost"),
|
|
1218
|
+
active=surcharge.get("active", True),
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1223
|
+
# SERVICE RATE TYPE (Service-Zone Rate Mapping)
|
|
1224
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
@strawberry.type
|
|
1228
|
+
class ServiceRateType:
|
|
1229
|
+
"""Service-zone rate mapping."""
|
|
1230
|
+
|
|
1231
|
+
object_type: str
|
|
1232
|
+
service_id: str
|
|
1233
|
+
zone_id: str
|
|
1234
|
+
rate: float
|
|
1235
|
+
cost: typing.Optional[float] = None
|
|
1236
|
+
min_weight: typing.Optional[float] = None
|
|
1237
|
+
max_weight: typing.Optional[float] = None
|
|
1238
|
+
transit_days: typing.Optional[int] = None
|
|
1239
|
+
transit_time: typing.Optional[float] = None
|
|
1240
|
+
|
|
1241
|
+
@staticmethod
|
|
1242
|
+
def parse(rate: dict):
|
|
1243
|
+
return ServiceRateType(
|
|
1244
|
+
object_type="service_rate",
|
|
1245
|
+
service_id=rate.get("service_id", ""),
|
|
1246
|
+
zone_id=rate.get("zone_id", ""),
|
|
1247
|
+
rate=float(rate.get("rate", 0)),
|
|
1248
|
+
cost=rate.get("cost"),
|
|
1249
|
+
min_weight=rate.get("min_weight"),
|
|
1250
|
+
max_weight=rate.get("max_weight"),
|
|
1251
|
+
transit_days=rate.get("transit_days"),
|
|
1252
|
+
transit_time=rate.get("transit_time"),
|
|
1183
1253
|
)
|
|
1184
1254
|
|
|
1185
1255
|
|
|
1186
1256
|
@strawberry.type
|
|
1187
1257
|
class ServiceLevelType:
|
|
1258
|
+
"""
|
|
1259
|
+
Service level definition for rate sheet-based shipping.
|
|
1260
|
+
|
|
1261
|
+
Services reference shared zones and surcharges defined at the RateSheet level
|
|
1262
|
+
via zone_ids and surcharge_ids. Rate values are stored in RateSheet.service_rates.
|
|
1263
|
+
"""
|
|
1264
|
+
|
|
1188
1265
|
id: str
|
|
1189
1266
|
object_type: str
|
|
1190
1267
|
service_name: typing.Optional[str]
|
|
@@ -1202,24 +1279,33 @@ class ServiceLevelType:
|
|
|
1202
1279
|
max_length: typing.Optional[float]
|
|
1203
1280
|
dimension_unit: typing.Optional[utils.DimensionUnitEnum]
|
|
1204
1281
|
|
|
1282
|
+
min_weight: typing.Optional[float]
|
|
1205
1283
|
max_weight: typing.Optional[float]
|
|
1206
1284
|
weight_unit: typing.Optional[utils.WeightUnitEnum]
|
|
1207
1285
|
|
|
1286
|
+
max_volume: typing.Optional[float]
|
|
1287
|
+
cost: typing.Optional[float]
|
|
1288
|
+
|
|
1208
1289
|
domicile: typing.Optional[bool]
|
|
1209
1290
|
international: typing.Optional[bool]
|
|
1210
1291
|
|
|
1211
1292
|
@strawberry.field
|
|
1212
|
-
def
|
|
1213
|
-
# Use computed_zones for backward compatibility with optimized structure
|
|
1214
|
-
return [ServiceZoneType.parse(zone) for zone in self.computed_zones]
|
|
1215
|
-
|
|
1216
|
-
@strawberry.field
|
|
1217
|
-
def metadata(self: providers.RateSheet) -> typing.Optional[utils.JSON]:
|
|
1293
|
+
def metadata(self: providers.ServiceLevel) -> typing.Optional[utils.JSON]:
|
|
1218
1294
|
try:
|
|
1219
1295
|
return lib.to_dict(self.metadata)
|
|
1220
1296
|
except:
|
|
1221
1297
|
return self.metadata
|
|
1222
1298
|
|
|
1299
|
+
@strawberry.field
|
|
1300
|
+
def zone_ids(self: providers.ServiceLevel) -> typing.List[str]:
|
|
1301
|
+
"""List of zone IDs this service applies to (references RateSheet.zones)."""
|
|
1302
|
+
return self.zone_ids or []
|
|
1303
|
+
|
|
1304
|
+
@strawberry.field
|
|
1305
|
+
def surcharge_ids(self: providers.ServiceLevel) -> typing.List[str]:
|
|
1306
|
+
"""List of surcharge IDs to apply (references RateSheet.surcharges)."""
|
|
1307
|
+
return self.surcharge_ids or []
|
|
1308
|
+
|
|
1223
1309
|
|
|
1224
1310
|
@strawberry.type
|
|
1225
1311
|
class LabelTemplateType:
|
|
@@ -1256,6 +1342,37 @@ class RateSheetType:
|
|
|
1256
1342
|
def services(self: providers.RateSheet) -> typing.List[ServiceLevelType]:
|
|
1257
1343
|
return self.services.all()
|
|
1258
1344
|
|
|
1345
|
+
# ─────────────────────────────────────────────────────────────────
|
|
1346
|
+
# NEW: Shared zones at RateSheet level
|
|
1347
|
+
# ─────────────────────────────────────────────────────────────────
|
|
1348
|
+
@strawberry.field
|
|
1349
|
+
def zones(self: providers.RateSheet) -> typing.Optional[typing.List[SharedZoneType]]:
|
|
1350
|
+
"""Shared zone definitions for this rate sheet."""
|
|
1351
|
+
zones_data = self.zones or []
|
|
1352
|
+
return [SharedZoneType.parse(zone) for zone in zones_data]
|
|
1353
|
+
|
|
1354
|
+
# ─────────────────────────────────────────────────────────────────
|
|
1355
|
+
# NEW: Shared surcharges at RateSheet level
|
|
1356
|
+
# ─────────────────────────────────────────────────────────────────
|
|
1357
|
+
@strawberry.field
|
|
1358
|
+
def surcharges(
|
|
1359
|
+
self: providers.RateSheet,
|
|
1360
|
+
) -> typing.Optional[typing.List[SharedSurchargeType]]:
|
|
1361
|
+
"""Shared surcharge definitions for this rate sheet."""
|
|
1362
|
+
surcharges_data = self.surcharges or []
|
|
1363
|
+
return [SharedSurchargeType.parse(surcharge) for surcharge in surcharges_data]
|
|
1364
|
+
|
|
1365
|
+
# ─────────────────────────────────────────────────────────────────
|
|
1366
|
+
# NEW: Service-zone rate mappings
|
|
1367
|
+
# ─────────────────────────────────────────────────────────────────
|
|
1368
|
+
@strawberry.field
|
|
1369
|
+
def service_rates(
|
|
1370
|
+
self: providers.RateSheet,
|
|
1371
|
+
) -> typing.Optional[typing.List[ServiceRateType]]:
|
|
1372
|
+
"""Service-zone rate mappings for this rate sheet."""
|
|
1373
|
+
rates_data = self.service_rates or []
|
|
1374
|
+
return [ServiceRateType.parse(rate) for rate in rates_data]
|
|
1375
|
+
|
|
1259
1376
|
@staticmethod
|
|
1260
1377
|
@utils.authentication_required
|
|
1261
1378
|
def resolve(info, id: str) -> typing.Optional["RateSheetType"]:
|
|
@@ -244,6 +244,13 @@ def ensure_unique_default_related_data(
|
|
|
244
244
|
|
|
245
245
|
@serializers.owned_model_serializer
|
|
246
246
|
class ServiceLevelModelSerializer(serializers.ModelSerializer):
|
|
247
|
+
"""
|
|
248
|
+
Serializer for ServiceLevel model.
|
|
249
|
+
|
|
250
|
+
Services reference shared zones and surcharges at the RateSheet level
|
|
251
|
+
via zone_ids and surcharge_ids.
|
|
252
|
+
"""
|
|
253
|
+
|
|
247
254
|
dimension_unit = serializers.CharField(
|
|
248
255
|
required=False, allow_null=True, allow_blank=True
|
|
249
256
|
)
|
|
@@ -257,46 +264,6 @@ class ServiceLevelModelSerializer(serializers.ModelSerializer):
|
|
|
257
264
|
exclude = ["created_at", "updated_at", "created_by"]
|
|
258
265
|
extra_kwargs = {field: {"read_only": True} for field in ["id"]}
|
|
259
266
|
|
|
260
|
-
def update_zone(self, zone_index: int, zone_data: dict) -> None:
|
|
261
|
-
"""Update a specific zone in the service level."""
|
|
262
|
-
if zone_index >= len(self.instance.zones):
|
|
263
|
-
raise exceptions.ValidationError(
|
|
264
|
-
_(f"Zone index {zone_index} is out of range"),
|
|
265
|
-
code="invalid_zone_index",
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
self.instance.zones[zone_index].update(
|
|
269
|
-
{k: v for k, v in zone_data.items() if v != strawberry.UNSET}
|
|
270
|
-
)
|
|
271
|
-
self.instance.save(update_fields=["zones"])
|
|
272
|
-
|
|
273
|
-
def update(self, instance, validated_data, context=None, **kwargs):
|
|
274
|
-
"""Handle partial updates of service level data including zones."""
|
|
275
|
-
zones_data = validated_data.pop("zones", None)
|
|
276
|
-
instance = super().update(instance, validated_data, context=context)
|
|
277
|
-
|
|
278
|
-
if zones_data is not None:
|
|
279
|
-
# Handle zone updates if provided
|
|
280
|
-
existing_zones = instance.zones or []
|
|
281
|
-
updated_zones = []
|
|
282
|
-
|
|
283
|
-
for idx, zone_data in enumerate(zones_data):
|
|
284
|
-
if idx < len(existing_zones):
|
|
285
|
-
# Update existing zone
|
|
286
|
-
zone = existing_zones[idx].copy()
|
|
287
|
-
zone.update(
|
|
288
|
-
{k: v for k, v in zone_data.items() if v != strawberry.UNSET}
|
|
289
|
-
)
|
|
290
|
-
updated_zones.append(zone)
|
|
291
|
-
else:
|
|
292
|
-
# Add new zone
|
|
293
|
-
updated_zones.append(zone_data)
|
|
294
|
-
|
|
295
|
-
instance.zones = updated_zones
|
|
296
|
-
instance.save(update_fields=["zones"])
|
|
297
|
-
|
|
298
|
-
return instance
|
|
299
|
-
|
|
300
267
|
|
|
301
268
|
@serializers.owned_model_serializer
|
|
302
269
|
class LabelTemplateModelSerializer(serializers.ModelSerializer):
|
|
@@ -367,14 +334,110 @@ class RateSheetModelSerializer(serializers.ModelSerializer):
|
|
|
367
334
|
carrier.rate_sheet = self.instance if carrier.id in carriers else None
|
|
368
335
|
carrier.save(update_fields=["rate_sheet"])
|
|
369
336
|
|
|
337
|
+
def process_zones(self, zones_data: list, remove_missing: bool = False) -> None:
|
|
338
|
+
"""Process zones for the rate sheet.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
zones_data: List of zone dicts with id, label, country_codes, etc.
|
|
342
|
+
remove_missing: If True, remove zones not present in zones_data.
|
|
343
|
+
"""
|
|
344
|
+
existing_zone_ids = {z["id"] for z in (self.instance.zones or [])}
|
|
345
|
+
incoming_zone_ids = set()
|
|
346
|
+
|
|
347
|
+
for zone_data in zones_data:
|
|
348
|
+
zone_dict = {k: v for k, v in zone_data.items() if v is not None}
|
|
349
|
+
zone_id = zone_dict.get("id")
|
|
350
|
+
|
|
351
|
+
if zone_id:
|
|
352
|
+
incoming_zone_ids.add(zone_id)
|
|
353
|
+
|
|
354
|
+
if zone_id and zone_id in existing_zone_ids:
|
|
355
|
+
self.instance.update_zone(zone_id, zone_dict)
|
|
356
|
+
else:
|
|
357
|
+
self.instance.add_zone(zone_dict)
|
|
358
|
+
|
|
359
|
+
# Remove zones not in incoming data
|
|
360
|
+
if remove_missing:
|
|
361
|
+
zones_to_remove = existing_zone_ids - incoming_zone_ids
|
|
362
|
+
for zone_id in zones_to_remove:
|
|
363
|
+
self.instance.remove_zone(zone_id)
|
|
364
|
+
|
|
365
|
+
def process_surcharges(self, surcharges_data: list, remove_missing: bool = False) -> None:
|
|
366
|
+
"""Process surcharges for the rate sheet.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
surcharges_data: List of surcharge dicts with id, name, amount, etc.
|
|
370
|
+
remove_missing: If True, remove surcharges not present in surcharges_data.
|
|
371
|
+
"""
|
|
372
|
+
existing_surcharge_ids = {s["id"] for s in (self.instance.surcharges or [])}
|
|
373
|
+
incoming_surcharge_ids = set()
|
|
374
|
+
|
|
375
|
+
for surcharge_data in surcharges_data:
|
|
376
|
+
surcharge_dict = {k: v for k, v in surcharge_data.items() if v is not None}
|
|
377
|
+
surcharge_id = surcharge_dict.get("id")
|
|
378
|
+
|
|
379
|
+
if surcharge_id:
|
|
380
|
+
incoming_surcharge_ids.add(surcharge_id)
|
|
381
|
+
|
|
382
|
+
if surcharge_id and surcharge_id in existing_surcharge_ids:
|
|
383
|
+
self.instance.update_surcharge(surcharge_id, surcharge_dict)
|
|
384
|
+
else:
|
|
385
|
+
self.instance.add_surcharge(surcharge_dict)
|
|
386
|
+
|
|
387
|
+
# Remove surcharges not in incoming data
|
|
388
|
+
if remove_missing:
|
|
389
|
+
surcharges_to_remove = existing_surcharge_ids - incoming_surcharge_ids
|
|
390
|
+
for surcharge_id in surcharges_to_remove:
|
|
391
|
+
self.instance.remove_surcharge(surcharge_id)
|
|
392
|
+
|
|
393
|
+
def process_service_rates(
|
|
394
|
+
self, service_rates_data: list, temp_to_real_id_map: dict = None
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Process service rates for the rate sheet.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
service_rates_data: List of service rate dicts with service_id, zone_id, rate, etc.
|
|
400
|
+
temp_to_real_id_map: Optional mapping of temp-{idx} IDs to real service IDs.
|
|
401
|
+
"""
|
|
402
|
+
temp_to_real_id_map = temp_to_real_id_map or {}
|
|
403
|
+
|
|
404
|
+
for rate_data in service_rates_data:
|
|
405
|
+
rate_dict = {k: v for k, v in rate_data.items() if v is not None}
|
|
406
|
+
service_id = rate_dict.pop("service_id", None)
|
|
407
|
+
zone_id = rate_dict.pop("zone_id", None)
|
|
408
|
+
|
|
409
|
+
# Map temp service ID to real ID if needed
|
|
410
|
+
if service_id and str(service_id).startswith("temp-"):
|
|
411
|
+
service_id = temp_to_real_id_map.get(service_id, service_id)
|
|
412
|
+
|
|
413
|
+
if service_id and zone_id:
|
|
414
|
+
self.instance.update_service_rate(service_id, zone_id, rate_dict)
|
|
415
|
+
|
|
416
|
+
def build_temp_to_real_service_map(self, services_data: list) -> dict:
|
|
417
|
+
"""Build mapping from temp-{idx} IDs to real service IDs by service_code."""
|
|
418
|
+
created_services = {s.service_code: s.id for s in self.instance.services.all()}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
f"temp-{idx}": created_services[svc.get("service_code")]
|
|
422
|
+
for idx, svc in enumerate(services_data)
|
|
423
|
+
if svc.get("service_code") in created_services
|
|
424
|
+
}
|
|
425
|
+
|
|
370
426
|
def update(self, instance, validated_data, **kwargs):
|
|
371
|
-
"""Handle updates of rate sheet data including services and carriers.
|
|
427
|
+
"""Handle updates of rate sheet data including services and carriers.
|
|
428
|
+
|
|
429
|
+
When zones or surcharges data is provided, it's treated as a full replacement -
|
|
430
|
+
zones/surcharges not in the incoming data will be removed.
|
|
431
|
+
"""
|
|
372
432
|
services_data = validated_data.pop("services", None)
|
|
373
433
|
carriers = (
|
|
374
434
|
validated_data.pop("carriers", None)
|
|
375
435
|
if "carriers" in validated_data
|
|
376
436
|
else None
|
|
377
437
|
)
|
|
438
|
+
zones_data = validated_data.pop("zones", None)
|
|
439
|
+
surcharges_data = validated_data.pop("surcharges", None)
|
|
440
|
+
service_rates_data = validated_data.pop("service_rates", None)
|
|
378
441
|
remove_missing_services = validated_data.pop("remove_missing_services", False)
|
|
379
442
|
|
|
380
443
|
instance = super().update(instance, validated_data)
|
|
@@ -385,4 +448,15 @@ class RateSheetModelSerializer(serializers.ModelSerializer):
|
|
|
385
448
|
if carriers is not None:
|
|
386
449
|
self.update_carriers(carriers)
|
|
387
450
|
|
|
451
|
+
if zones_data:
|
|
452
|
+
# Full update: remove zones not present in incoming data
|
|
453
|
+
self.process_zones(zones_data, remove_missing=True)
|
|
454
|
+
|
|
455
|
+
if surcharges_data:
|
|
456
|
+
# Full update: remove surcharges not present in incoming data
|
|
457
|
+
self.process_surcharges(surcharges_data, remove_missing=True)
|
|
458
|
+
|
|
459
|
+
if service_rates_data:
|
|
460
|
+
self.process_service_rates(service_rates_data)
|
|
461
|
+
|
|
388
462
|
return instance
|