karrio-server-core 2025.5rc31__py3-none-any.whl → 2026.1.1__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/core/authentication.py +38 -20
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +26 -7
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +284 -11
- karrio/server/core/logging.py +403 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/models/base.py +34 -1
- karrio/server/core/oauth_validators.py +2 -3
- karrio/server/core/permissions.py +1 -2
- karrio/server/core/serializers.py +154 -7
- karrio/server/core/signals.py +22 -28
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests/__init__.py +27 -0
- karrio/server/core/{tests.py → tests/base.py} +6 -7
- karrio/server/core/tests/test_exception_level.py +159 -0
- karrio/server/core/tests/test_resource_token.py +593 -0
- karrio/server/core/utils.py +688 -38
- karrio/server/core/validators.py +144 -222
- karrio/server/core/views/oauth.py +13 -12
- karrio/server/core/views/references.py +2 -2
- karrio/server/iam/apps.py +1 -4
- karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
- karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
- karrio/server/iam/permissions.py +7 -134
- karrio/server/iam/serializers.py +9 -3
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
- karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
- karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
- karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
- karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
- karrio/server/providers/models/carrier.py +101 -29
- karrio/server/providers/models/service.py +182 -125
- karrio/server/providers/models/sheet.py +342 -198
- karrio/server/providers/serializers/base.py +263 -2
- karrio/server/providers/signals.py +2 -4
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/views/carriers.py +1 -3
- karrio/server/providers/views/connections.py +322 -2
- karrio/server/serializers/abstract.py +112 -21
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/models.py +36 -34
- karrio/server/user/serializers.py +1 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
|
@@ -10,6 +10,16 @@ _ = translation.gettext_lazy
|
|
|
10
10
|
|
|
11
11
|
@core.register_model
|
|
12
12
|
class RateSheet(core.OwnedEntity):
|
|
13
|
+
"""
|
|
14
|
+
Rate sheet with shared zones, surcharges, and service-zone rate mappings.
|
|
15
|
+
|
|
16
|
+
Structure:
|
|
17
|
+
- zones: Shared geographic zone definitions
|
|
18
|
+
- surcharges: Shared surcharge definitions (fuel, handling, etc.)
|
|
19
|
+
- service_rates: Service-zone rate mappings with weights and transit times
|
|
20
|
+
- services: Service level definitions that reference zones/surcharges by ID
|
|
21
|
+
"""
|
|
22
|
+
|
|
13
23
|
class Meta:
|
|
14
24
|
db_table = "rate-sheet"
|
|
15
25
|
verbose_name = "Rate Sheet"
|
|
@@ -29,22 +39,40 @@ class RateSheet(core.OwnedEntity):
|
|
|
29
39
|
services = models.ManyToManyField(
|
|
30
40
|
"ServiceLevel", blank=True, related_name="service_sheet"
|
|
31
41
|
)
|
|
32
|
-
|
|
33
|
-
#
|
|
42
|
+
|
|
43
|
+
# ─────────────────────────────────────────────────────────────────
|
|
44
|
+
# SHARED ZONE DEFINITIONS
|
|
45
|
+
# Structure: [{'id': 'zone_1', 'label': 'Zone 1', 'country_codes': [...], 'cities': [...], 'postal_codes': [...], 'transit_days': int}]
|
|
46
|
+
# ─────────────────────────────────────────────────────────────────
|
|
34
47
|
zones = models.JSONField(
|
|
35
48
|
blank=True,
|
|
36
49
|
null=True,
|
|
37
50
|
default=core.field_default([]),
|
|
38
|
-
help_text="Shared zone definitions: [{'id': 'zone_1', 'label': 'Zone 1', 'cities': [...], 'country_codes': [...]}]"
|
|
51
|
+
help_text="Shared zone definitions: [{'id': 'zone_1', 'label': 'Zone 1', 'cities': [...], 'country_codes': [...]}]",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────
|
|
55
|
+
# SHARED SURCHARGE DEFINITIONS
|
|
56
|
+
# Structure: [{'id': 'surch_1', 'name': 'Fuel', 'amount': 8.5, 'surcharge_type': 'percentage', 'cost': 5.2, 'active': true}]
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────
|
|
58
|
+
surcharges = models.JSONField(
|
|
59
|
+
blank=True,
|
|
60
|
+
null=True,
|
|
61
|
+
default=core.field_default([]),
|
|
62
|
+
help_text="Shared surcharge definitions: [{'id': 'surch_1', 'name': 'Fuel', 'amount': 8.5, 'surcharge_type': 'percentage'}]",
|
|
39
63
|
)
|
|
64
|
+
|
|
65
|
+
# ─────────────────────────────────────────────────────────────────
|
|
66
|
+
# SERVICE-ZONE RATE MAPPING
|
|
67
|
+
# Structure: [{'service_id': 'svc_1', 'zone_id': 'zone_1', 'rate': 10.50, 'cost': 8.00, 'min_weight': 0, 'max_weight': 5}]
|
|
68
|
+
# ─────────────────────────────────────────────────────────────────
|
|
40
69
|
service_rates = models.JSONField(
|
|
41
70
|
blank=True,
|
|
42
71
|
null=True,
|
|
43
72
|
default=core.field_default([]),
|
|
44
|
-
help_text="Service-zone rate mapping: [{'service_id': 'svc_1', 'zone_id': 'zone_1', 'rate': 10.50}]"
|
|
73
|
+
help_text="Service-zone rate mapping: [{'service_id': 'svc_1', 'zone_id': 'zone_1', 'rate': 10.50}]",
|
|
45
74
|
)
|
|
46
|
-
|
|
47
|
-
# Keep old structure for backward compatibility during migration
|
|
75
|
+
|
|
48
76
|
metadata = models.JSONField(
|
|
49
77
|
blank=True,
|
|
50
78
|
null=True,
|
|
@@ -74,214 +102,330 @@ class RateSheet(core.OwnedEntity):
|
|
|
74
102
|
return providers.Carrier.objects.filter(
|
|
75
103
|
carrier_code=self.carrier_name, rate_sheet__id=self.id
|
|
76
104
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
|
|
106
|
+
# ─────────────────────────────────────────────────────────────────
|
|
107
|
+
# ZONE MANAGEMENT
|
|
108
|
+
# ─────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def add_zone(self, zone_data: dict) -> str:
|
|
111
|
+
"""Add a new shared zone definition."""
|
|
112
|
+
zones = list(self.zones or [])
|
|
113
|
+
|
|
114
|
+
# Generate zone ID if not provided
|
|
115
|
+
if not zone_data.get("id"):
|
|
116
|
+
zone_data["id"] = f"zone_{len(zones) + 1}"
|
|
117
|
+
|
|
118
|
+
zones.append(zone_data)
|
|
119
|
+
self.zones = zones
|
|
120
|
+
self.save(update_fields=["zones"])
|
|
121
|
+
return zone_data["id"]
|
|
122
|
+
|
|
123
|
+
def update_zone(self, zone_id: str, zone_data: dict) -> dict:
|
|
124
|
+
"""Update a shared zone definition."""
|
|
125
|
+
zones = list(self.zones or [])
|
|
126
|
+
|
|
127
|
+
for i, zone in enumerate(zones):
|
|
128
|
+
if zone.get("id") == zone_id:
|
|
129
|
+
zones[i] = {"id": zone_id, **{k: v for k, v in zone_data.items() if k != "id"}}
|
|
130
|
+
self.zones = zones
|
|
131
|
+
self.save(update_fields=["zones"])
|
|
132
|
+
return zones[i]
|
|
133
|
+
|
|
134
|
+
raise ValueError(f"Zone {zone_id} not found")
|
|
135
|
+
|
|
136
|
+
def remove_zone(self, zone_id: str):
|
|
137
|
+
"""Remove a zone and all its associated rates."""
|
|
138
|
+
zones = [z for z in (self.zones or []) if z.get("id") != zone_id]
|
|
139
|
+
self.zones = zones
|
|
140
|
+
|
|
141
|
+
# Remove all rates for this zone
|
|
142
|
+
service_rates = [sr for sr in (self.service_rates or []) if sr.get("zone_id") != zone_id]
|
|
143
|
+
self.service_rates = service_rates
|
|
144
|
+
|
|
145
|
+
# Remove zone_id from services
|
|
146
|
+
for service in self.services.all():
|
|
147
|
+
if zone_id in (service.zone_ids or []):
|
|
148
|
+
service.zone_ids = [zid for zid in service.zone_ids if zid != zone_id]
|
|
149
|
+
service.save(update_fields=["zone_ids"])
|
|
150
|
+
|
|
151
|
+
self.save(update_fields=["zones", "service_rates"])
|
|
152
|
+
|
|
153
|
+
def get_zone(self, zone_id: str) -> dict:
|
|
154
|
+
"""Get a zone by ID."""
|
|
155
|
+
for zone in self.zones or []:
|
|
156
|
+
if zone.get("id") == zone_id:
|
|
157
|
+
return zone
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
# ─────────────────────────────────────────────────────────────────
|
|
161
|
+
# SURCHARGE MANAGEMENT
|
|
162
|
+
# ─────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
def add_surcharge(self, surcharge_data: dict) -> str:
|
|
165
|
+
"""Add a new shared surcharge definition."""
|
|
166
|
+
surcharges = list(self.surcharges or [])
|
|
167
|
+
|
|
168
|
+
if not surcharge_data.get("id"):
|
|
169
|
+
surcharge_data["id"] = f"surch_{len(surcharges) + 1}"
|
|
170
|
+
|
|
171
|
+
surcharge_data.setdefault("active", True)
|
|
172
|
+
surcharge_data.setdefault("surcharge_type", "fixed")
|
|
173
|
+
|
|
174
|
+
surcharges.append(surcharge_data)
|
|
175
|
+
self.surcharges = surcharges
|
|
176
|
+
self.save(update_fields=["surcharges"])
|
|
177
|
+
return surcharge_data["id"]
|
|
178
|
+
|
|
179
|
+
def update_surcharge(self, surcharge_id: str, surcharge_data: dict) -> dict:
|
|
180
|
+
"""Update a shared surcharge definition."""
|
|
181
|
+
surcharges = list(self.surcharges or [])
|
|
182
|
+
|
|
183
|
+
for i, surcharge in enumerate(surcharges):
|
|
184
|
+
if surcharge.get("id") == surcharge_id:
|
|
185
|
+
surcharges[i] = {"id": surcharge_id, **{k: v for k, v in surcharge_data.items() if k != "id"}}
|
|
186
|
+
self.surcharges = surcharges
|
|
187
|
+
self.save(update_fields=["surcharges"])
|
|
188
|
+
return surcharges[i]
|
|
189
|
+
|
|
190
|
+
raise ValueError(f"Surcharge {surcharge_id} not found")
|
|
191
|
+
|
|
192
|
+
def remove_surcharge(self, surcharge_id: str):
|
|
193
|
+
"""Remove a surcharge definition and its references from services."""
|
|
194
|
+
surcharges = [s for s in (self.surcharges or []) if s.get("id") != surcharge_id]
|
|
195
|
+
self.surcharges = surcharges
|
|
196
|
+
|
|
197
|
+
# Remove surcharge_id from services
|
|
198
|
+
for service in self.services.all():
|
|
199
|
+
if surcharge_id in (service.surcharge_ids or []):
|
|
200
|
+
service.surcharge_ids = [sid for sid in service.surcharge_ids if sid != surcharge_id]
|
|
201
|
+
service.save(update_fields=["surcharge_ids"])
|
|
202
|
+
|
|
203
|
+
self.save(update_fields=["surcharges"])
|
|
204
|
+
|
|
205
|
+
def batch_update_surcharges(self, updates: list):
|
|
206
|
+
"""Batch update multiple surcharges."""
|
|
207
|
+
surcharges = list(self.surcharges or [])
|
|
208
|
+
surcharge_map = {s.get("id"): i for i, s in enumerate(surcharges)}
|
|
209
|
+
|
|
210
|
+
for update in updates:
|
|
211
|
+
surcharge_id = update.get("id")
|
|
212
|
+
if not surcharge_id:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if surcharge_id in surcharge_map:
|
|
216
|
+
idx = surcharge_map[surcharge_id]
|
|
217
|
+
surcharges[idx] = {**surcharges[idx], **update}
|
|
218
|
+
else:
|
|
219
|
+
update.setdefault("active", True)
|
|
220
|
+
update.setdefault("surcharge_type", "fixed")
|
|
221
|
+
surcharges.append(update)
|
|
222
|
+
surcharge_map[surcharge_id] = len(surcharges) - 1
|
|
223
|
+
|
|
224
|
+
self.surcharges = surcharges
|
|
225
|
+
self.save(update_fields=["surcharges"])
|
|
226
|
+
|
|
227
|
+
def get_surcharge(self, surcharge_id: str) -> dict:
|
|
228
|
+
"""Get a surcharge by ID."""
|
|
229
|
+
for surcharge in self.surcharges or []:
|
|
230
|
+
if surcharge.get("id") == surcharge_id:
|
|
231
|
+
return surcharge
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# ─────────────────────────────────────────────────────────────────
|
|
235
|
+
# SERVICE RATE MANAGEMENT
|
|
236
|
+
# ─────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
def get_service_rate(self, service_id: str, zone_id: str) -> dict:
|
|
239
|
+
"""Get a service rate by service_id and zone_id."""
|
|
240
|
+
for rate in self.service_rates or []:
|
|
241
|
+
if rate.get("service_id") == service_id and rate.get("zone_id") == zone_id:
|
|
242
|
+
return rate
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def update_service_rate(self, service_id: str, zone_id: str, rate_data: dict) -> dict:
|
|
246
|
+
"""Update or create a service-zone rate mapping."""
|
|
132
247
|
service_rates = list(self.service_rates or [])
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
'zone_id': zone_id,
|
|
145
|
-
field: value
|
|
146
|
-
})
|
|
147
|
-
|
|
248
|
+
|
|
249
|
+
for i, rate in enumerate(service_rates):
|
|
250
|
+
if rate.get("service_id") == service_id and rate.get("zone_id") == zone_id:
|
|
251
|
+
service_rates[i] = {"service_id": service_id, "zone_id": zone_id, **rate_data}
|
|
252
|
+
self.service_rates = service_rates
|
|
253
|
+
self.save(update_fields=["service_rates"])
|
|
254
|
+
return service_rates[i]
|
|
255
|
+
|
|
256
|
+
# Create new rate record
|
|
257
|
+
new_rate = {"service_id": service_id, "zone_id": zone_id, **rate_data}
|
|
258
|
+
service_rates.append(new_rate)
|
|
148
259
|
self.service_rates = service_rates
|
|
149
|
-
self.save(update_fields=[
|
|
150
|
-
|
|
151
|
-
|
|
260
|
+
self.save(update_fields=["service_rates"])
|
|
261
|
+
return new_rate
|
|
262
|
+
|
|
263
|
+
def batch_update_service_rates(self, updates: list):
|
|
152
264
|
"""
|
|
153
|
-
Batch update service rates
|
|
154
|
-
updates format: [{'service_id': str, 'zone_id': str, '
|
|
265
|
+
Batch update service rates.
|
|
266
|
+
updates format: [{'service_id': str, 'zone_id': str, 'rate': float, 'cost': float, ...}, ...]
|
|
155
267
|
"""
|
|
156
|
-
allowed_fields = {
|
|
157
|
-
'rate': float,
|
|
158
|
-
'min_weight': float,
|
|
159
|
-
'max_weight': float,
|
|
160
|
-
'transit_days': int,
|
|
161
|
-
'transit_time': float,
|
|
162
|
-
}
|
|
163
|
-
|
|
164
268
|
service_rates = list(self.service_rates or [])
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
# Create lookup map for existing rates
|
|
269
|
+
rate_map = {}
|
|
270
|
+
|
|
168
271
|
for i, rate in enumerate(service_rates):
|
|
169
272
|
key = f"{rate.get('service_id')}:{rate.get('zone_id')}"
|
|
170
|
-
|
|
171
|
-
|
|
273
|
+
rate_map[key] = i
|
|
274
|
+
|
|
172
275
|
for update in updates:
|
|
173
|
-
service_id = update.get(
|
|
174
|
-
zone_id = update.get(
|
|
175
|
-
|
|
176
|
-
value = update.get('value')
|
|
177
|
-
|
|
178
|
-
if field not in allowed_fields:
|
|
179
|
-
continue
|
|
180
|
-
|
|
181
|
-
# Validate value
|
|
182
|
-
try:
|
|
183
|
-
if value is not None and value != '':
|
|
184
|
-
value = allowed_fields[field](value)
|
|
185
|
-
except (ValueError, TypeError):
|
|
276
|
+
service_id = update.get("service_id")
|
|
277
|
+
zone_id = update.get("zone_id")
|
|
278
|
+
if not service_id or not zone_id:
|
|
186
279
|
continue
|
|
187
|
-
|
|
280
|
+
|
|
188
281
|
key = f"{service_id}:{zone_id}"
|
|
189
|
-
|
|
190
|
-
if key in
|
|
191
|
-
|
|
192
|
-
service_rates[service_rate_map[key]][field] = value
|
|
282
|
+
|
|
283
|
+
if key in rate_map:
|
|
284
|
+
service_rates[rate_map[key]] = {**service_rates[rate_map[key]], **update}
|
|
193
285
|
else:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
'zone_id': zone_id,
|
|
198
|
-
field: value
|
|
199
|
-
}
|
|
200
|
-
service_rates.append(new_rate)
|
|
201
|
-
service_rate_map[key] = len(service_rates) - 1
|
|
202
|
-
|
|
286
|
+
service_rates.append(update)
|
|
287
|
+
rate_map[key] = len(service_rates) - 1
|
|
288
|
+
|
|
203
289
|
self.service_rates = service_rates
|
|
204
|
-
self.save(update_fields=[
|
|
205
|
-
|
|
206
|
-
def
|
|
290
|
+
self.save(update_fields=["service_rates"])
|
|
291
|
+
|
|
292
|
+
def remove_service_rate(self, service_id: str, zone_id: str):
|
|
293
|
+
"""Remove a service-zone rate mapping."""
|
|
294
|
+
service_rates = [
|
|
295
|
+
sr
|
|
296
|
+
for sr in (self.service_rates or [])
|
|
297
|
+
if not (sr.get("service_id") == service_id and sr.get("zone_id") == zone_id)
|
|
298
|
+
]
|
|
299
|
+
self.service_rates = service_rates
|
|
300
|
+
self.save(update_fields=["service_rates"])
|
|
301
|
+
|
|
302
|
+
# ─────────────────────────────────────────────────────────────────
|
|
303
|
+
# RATE CALCULATION
|
|
304
|
+
# ─────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
def apply_surcharges_to_rate(self, base_rate: float, surcharge_ids: list) -> tuple:
|
|
207
307
|
"""
|
|
208
|
-
|
|
308
|
+
Apply surcharges to a base rate.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
base_rate: The base rate before surcharges
|
|
312
|
+
surcharge_ids: List of surcharge IDs to apply
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
tuple: (final_rate, breakdown)
|
|
209
316
|
"""
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
317
|
+
total_surcharge = 0.0
|
|
318
|
+
breakdown = []
|
|
319
|
+
|
|
320
|
+
for surcharge_id in surcharge_ids:
|
|
321
|
+
surcharge = self.get_surcharge(surcharge_id)
|
|
322
|
+
if not surcharge or not surcharge.get("active", True):
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
name = surcharge.get("name", "Surcharge")
|
|
326
|
+
amount = float(surcharge.get("amount", 0))
|
|
327
|
+
surcharge_type = surcharge.get("surcharge_type", "fixed")
|
|
328
|
+
|
|
329
|
+
applied_amount = (base_rate * amount / 100) if surcharge_type == "percentage" else amount
|
|
330
|
+
total_surcharge += applied_amount
|
|
331
|
+
breakdown.append(
|
|
332
|
+
{
|
|
333
|
+
"id": surcharge_id,
|
|
334
|
+
"name": name,
|
|
335
|
+
"amount": applied_amount,
|
|
336
|
+
"surcharge_type": surcharge_type,
|
|
337
|
+
"original_value": amount,
|
|
338
|
+
"cost": surcharge.get("cost"),
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return base_rate + total_surcharge, breakdown
|
|
343
|
+
|
|
344
|
+
def calculate_rate(self, service_id: str, zone_id: str) -> tuple:
|
|
222
345
|
"""
|
|
223
|
-
|
|
346
|
+
Calculate the total rate for a service-zone combination including surcharges.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
tuple: (total_rate, breakdown)
|
|
224
350
|
"""
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
351
|
+
rate_data = self.get_service_rate(service_id, zone_id)
|
|
352
|
+
base_rate = float(rate_data.get("rate", 0)) if rate_data else 0
|
|
353
|
+
|
|
354
|
+
# Get service surcharge_ids
|
|
355
|
+
service = self.services.filter(id=service_id).first()
|
|
356
|
+
surcharge_ids = service.surcharge_ids if service else []
|
|
357
|
+
|
|
358
|
+
total_rate, surcharge_breakdown = self.apply_surcharges_to_rate(base_rate, surcharge_ids or [])
|
|
359
|
+
|
|
360
|
+
return total_rate, {
|
|
361
|
+
"base_rate": base_rate,
|
|
362
|
+
"base_cost": rate_data.get("cost") if rate_data else None,
|
|
363
|
+
"surcharges": surcharge_breakdown,
|
|
364
|
+
"total": total_rate,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
def get_service_zones_for_rating(self, service_id: str) -> list:
|
|
236
368
|
"""
|
|
237
|
-
|
|
369
|
+
Get zones with rates for a service, formatted for SDK rate calculation.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
list: Zone dicts with rate data merged from service_rates
|
|
238
373
|
"""
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
374
|
+
zones = self.zones or []
|
|
375
|
+
service_rates = self.service_rates or []
|
|
376
|
+
|
|
377
|
+
# Build rate lookup
|
|
378
|
+
rate_map = {
|
|
379
|
+
sr.get("zone_id"): sr
|
|
380
|
+
for sr in service_rates
|
|
381
|
+
if sr.get("service_id") == service_id
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# Merge zone definitions with rates
|
|
385
|
+
result = []
|
|
386
|
+
for zone in zones:
|
|
387
|
+
zone_id = zone.get("id")
|
|
388
|
+
rate_data = rate_map.get(zone_id, {})
|
|
389
|
+
|
|
390
|
+
result.append({
|
|
391
|
+
"id": zone_id,
|
|
392
|
+
"label": zone.get("label"),
|
|
393
|
+
"rate": rate_data.get("rate", 0),
|
|
394
|
+
"cost": rate_data.get("cost"),
|
|
395
|
+
"min_weight": rate_data.get("min_weight"),
|
|
396
|
+
"max_weight": rate_data.get("max_weight"),
|
|
397
|
+
"transit_days": rate_data.get("transit_days") or zone.get("transit_days"),
|
|
398
|
+
"transit_time": rate_data.get("transit_time") or zone.get("transit_time"),
|
|
399
|
+
"radius": zone.get("radius"),
|
|
400
|
+
"latitude": zone.get("latitude"),
|
|
401
|
+
"longitude": zone.get("longitude"),
|
|
402
|
+
"cities": zone.get("cities", []),
|
|
403
|
+
"postal_codes": zone.get("postal_codes", []),
|
|
404
|
+
"country_codes": zone.get("country_codes", []),
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
def get_surcharges_for_rating(self, surcharge_ids: list) -> list:
|
|
410
|
+
"""
|
|
411
|
+
Get surcharges formatted for SDK rate calculation.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
surcharge_ids: List of surcharge IDs to include
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
list: Surcharge dicts for active surcharges
|
|
418
|
+
"""
|
|
419
|
+
result = []
|
|
420
|
+
for surcharge_id in surcharge_ids or []:
|
|
421
|
+
surcharge = self.get_surcharge(surcharge_id)
|
|
422
|
+
if surcharge and surcharge.get("active", True):
|
|
423
|
+
result.append({
|
|
424
|
+
"id": surcharge.get("id"),
|
|
425
|
+
"name": surcharge.get("name"),
|
|
426
|
+
"amount": surcharge.get("amount", 0),
|
|
427
|
+
"surcharge_type": surcharge.get("surcharge_type", "fixed"),
|
|
428
|
+
"cost": surcharge.get("cost"),
|
|
429
|
+
"active": surcharge.get("active", True),
|
|
282
430
|
})
|
|
283
|
-
|
|
284
|
-
# Save optimized structure
|
|
285
|
-
self.zones = list(all_zones.values())
|
|
286
|
-
self.service_rates = service_rates
|
|
287
|
-
self.save(update_fields=['zones', 'service_rates'])
|
|
431
|
+
return result
|