karrio-server-core 2025.5rc12__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 +59 -25
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +53 -22
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +285 -10
- karrio/server/core/logging.py +403 -0
- karrio/server/core/management/commands/runserver.py +5 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
- 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 +183 -10
- 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 +17 -2
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
- karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
- karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
- karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
- 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/__init__.py +1 -2
- karrio/server/providers/models/carrier.py +103 -18
- karrio/server/providers/models/service.py +188 -1
- karrio/server/providers/models/sheet.py +371 -0
- 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/samples.py +1 -1
- karrio/server/serializers/abstract.py +116 -21
- karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
- karrio/server/tracing/models.py +2 -0
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/migrations/0007_user_metadata.py +25 -0
- karrio/server/user/models.py +38 -23
- karrio/server/user/serializers.py +1 -0
- karrio/server/user/templates/registration/registration_confirm_email.html +1 -1
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +67 -86
- karrio/server/providers/extension/__init__.py +0 -1
- karrio/server/providers/extension/models/__init__.py +0 -1
- karrio/server/providers/extension/models/allied_express.py +0 -22
- karrio/server/providers/extension/models/allied_express_local.py +0 -22
- karrio/server/providers/extension/models/amazon_shipping.py +0 -27
- karrio/server/providers/extension/models/aramex.py +0 -25
- karrio/server/providers/extension/models/asendia_us.py +0 -21
- karrio/server/providers/extension/models/australiapost.py +0 -20
- karrio/server/providers/extension/models/boxknight.py +0 -19
- karrio/server/providers/extension/models/bpost.py +0 -21
- karrio/server/providers/extension/models/canadapost.py +0 -21
- karrio/server/providers/extension/models/canpar.py +0 -19
- karrio/server/providers/extension/models/chronopost.py +0 -22
- karrio/server/providers/extension/models/colissimo.py +0 -22
- karrio/server/providers/extension/models/dhl_express.py +0 -23
- karrio/server/providers/extension/models/dhl_parcel_de.py +0 -25
- karrio/server/providers/extension/models/dhl_poland.py +0 -22
- karrio/server/providers/extension/models/dhl_universal.py +0 -19
- karrio/server/providers/extension/models/dicom.py +0 -20
- karrio/server/providers/extension/models/dpd.py +0 -37
- karrio/server/providers/extension/models/dpdhl.py +0 -26
- karrio/server/providers/extension/models/easypost.py +0 -20
- karrio/server/providers/extension/models/eshipper.py +0 -21
- karrio/server/providers/extension/models/fedex.py +0 -25
- karrio/server/providers/extension/models/fedex_ws.py +0 -24
- karrio/server/providers/extension/models/freightcom.py +0 -21
- karrio/server/providers/extension/models/generic.py +0 -35
- karrio/server/providers/extension/models/geodis.py +0 -22
- karrio/server/providers/extension/models/hay_post.py +0 -22
- karrio/server/providers/extension/models/laposte.py +0 -19
- karrio/server/providers/extension/models/locate2u.py +0 -22
- karrio/server/providers/extension/models/nationex.py +0 -22
- karrio/server/providers/extension/models/purolator.py +0 -21
- karrio/server/providers/extension/models/roadie.py +0 -18
- karrio/server/providers/extension/models/royalmail.py +0 -19
- karrio/server/providers/extension/models/sendle.py +0 -22
- karrio/server/providers/extension/models/tge.py +0 -63
- karrio/server/providers/extension/models/tnt.py +0 -23
- karrio/server/providers/extension/models/ups.py +0 -23
- karrio/server/providers/extension/models/usps.py +0 -23
- karrio/server/providers/extension/models/usps_international.py +0 -23
- karrio/server/providers/extension/models/usps_wt.py +0 -24
- karrio/server/providers/extension/models/usps_wt_international.py +0 -24
- karrio/server/providers/extension/models/zoom2u.py +0 -23
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Generated migration to remove permission groups
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def remove_permission_groups(apps, schema_editor):
|
|
7
|
+
"""
|
|
8
|
+
Remove all permission groups that were created by setup_groups().
|
|
9
|
+
|
|
10
|
+
This migration removes the following groups:
|
|
11
|
+
- manage_apps
|
|
12
|
+
- manage_carriers (deprecated)
|
|
13
|
+
- read_carriers
|
|
14
|
+
- write_carriers
|
|
15
|
+
- manage_orders
|
|
16
|
+
- manage_team
|
|
17
|
+
- manage_org_owner
|
|
18
|
+
- manage_webhooks
|
|
19
|
+
- manage_data
|
|
20
|
+
- manage_shipments
|
|
21
|
+
- manage_system
|
|
22
|
+
"""
|
|
23
|
+
Group = apps.get_model("user", "Group")
|
|
24
|
+
ContextPermission = apps.get_model("iam", "ContextPermission")
|
|
25
|
+
|
|
26
|
+
# List of groups to remove
|
|
27
|
+
groups_to_remove = [
|
|
28
|
+
"manage_apps",
|
|
29
|
+
"manage_carriers",
|
|
30
|
+
"read_carriers",
|
|
31
|
+
"write_carriers",
|
|
32
|
+
"manage_orders",
|
|
33
|
+
"manage_team",
|
|
34
|
+
"manage_org_owner",
|
|
35
|
+
"manage_webhooks",
|
|
36
|
+
"manage_data",
|
|
37
|
+
"manage_shipments",
|
|
38
|
+
"manage_system",
|
|
39
|
+
"manage_pickups",
|
|
40
|
+
"manage_trackers",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# First, remove the groups from all ContextPermissions
|
|
44
|
+
for group_name in groups_to_remove:
|
|
45
|
+
group = Group.objects.filter(name=group_name).first()
|
|
46
|
+
if group:
|
|
47
|
+
# Remove this group from all context permissions
|
|
48
|
+
for ctx_perm in ContextPermission.objects.filter(groups=group):
|
|
49
|
+
ctx_perm.groups.remove(group)
|
|
50
|
+
|
|
51
|
+
# Then delete the groups themselves
|
|
52
|
+
Group.objects.filter(name__in=groups_to_remove).delete()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def reverse_migration(apps, schema_editor):
|
|
56
|
+
"""
|
|
57
|
+
Reverse migration - recreate groups (without permissions, which were set dynamically).
|
|
58
|
+
Note: This won't restore the full permission setup, only creates empty groups.
|
|
59
|
+
"""
|
|
60
|
+
Group = apps.get_model("user", "Group")
|
|
61
|
+
|
|
62
|
+
groups_to_create = [
|
|
63
|
+
"manage_apps",
|
|
64
|
+
"manage_carriers",
|
|
65
|
+
"read_carriers",
|
|
66
|
+
"write_carriers",
|
|
67
|
+
"manage_orders",
|
|
68
|
+
"manage_team",
|
|
69
|
+
"manage_org_owner",
|
|
70
|
+
"manage_webhooks",
|
|
71
|
+
"manage_data",
|
|
72
|
+
"manage_shipments",
|
|
73
|
+
"manage_system",
|
|
74
|
+
"manage_pickups",
|
|
75
|
+
"manage_trackers",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
for group_name in groups_to_create:
|
|
79
|
+
Group.objects.get_or_create(name=group_name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Migration(migrations.Migration):
|
|
83
|
+
|
|
84
|
+
dependencies = [
|
|
85
|
+
("iam", "0002_setup_carrier_permission_groups"),
|
|
86
|
+
("user", "0004_group"),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
operations = [
|
|
90
|
+
migrations.RunPython(remove_permission_groups, reverse_migration),
|
|
91
|
+
]
|
karrio/server/iam/permissions.py
CHANGED
|
@@ -1,134 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import karrio.server.user.models as users
|
|
9
|
-
import karrio.server.iam.serializers as serializers
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
User = get_user_model()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@utils.skip_on_loadata
|
|
16
|
-
@utils.async_wrapper
|
|
17
|
-
@utils.tenant_aware
|
|
18
|
-
def setup_groups(**_):
|
|
19
|
-
"""This function create all standard group permissions if they don't exsist."""
|
|
20
|
-
print("> setting up permissions")
|
|
21
|
-
|
|
22
|
-
# manage_apps
|
|
23
|
-
setup_group(
|
|
24
|
-
serializers.PermissionGroup.manage_apps.name,
|
|
25
|
-
permissions=Permission.objects.filter(content_type__app_label="apps"),
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
# manage_carriers
|
|
29
|
-
setup_group(
|
|
30
|
-
serializers.PermissionGroup.manage_carriers.name,
|
|
31
|
-
permissions=[
|
|
32
|
-
*Permission.objects.filter(content_type__app_label="providers"),
|
|
33
|
-
*Permission.objects.filter(
|
|
34
|
-
models.Q(content_type__app_label="orgs")
|
|
35
|
-
& models.Q(name__icontains="carrier")
|
|
36
|
-
),
|
|
37
|
-
],
|
|
38
|
-
override=True,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
# manage_orders
|
|
42
|
-
setup_group(
|
|
43
|
-
serializers.PermissionGroup.manage_orders.name,
|
|
44
|
-
permissions=Permission.objects.filter(content_type__app_label="orders"),
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# manage_team
|
|
48
|
-
setup_group(
|
|
49
|
-
serializers.PermissionGroup.manage_team.name,
|
|
50
|
-
permissions=(
|
|
51
|
-
Permission.objects.filter(
|
|
52
|
-
content_type__app_label="orgs", name__icontains="organization"
|
|
53
|
-
).exclude(name__icontains="owner")
|
|
54
|
-
),
|
|
55
|
-
override=True,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# manage_org_owner
|
|
59
|
-
setup_group(
|
|
60
|
-
serializers.PermissionGroup.manage_org_owner.name,
|
|
61
|
-
permissions=Permission.objects.filter(
|
|
62
|
-
content_type__model="OrganizationOwner".lower()
|
|
63
|
-
),
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# manage_webhooks
|
|
67
|
-
setup_group(
|
|
68
|
-
serializers.PermissionGroup.manage_webhooks.name,
|
|
69
|
-
permissions=Permission.objects.filter(content_type__model="Webhook".lower()),
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# manage_data
|
|
73
|
-
setup_group(
|
|
74
|
-
serializers.PermissionGroup.manage_data.name,
|
|
75
|
-
permissions=[
|
|
76
|
-
*Permission.objects.filter(
|
|
77
|
-
content_type__app_label__in=["data", "graph", "documents"]
|
|
78
|
-
),
|
|
79
|
-
*Permission.objects.filter(
|
|
80
|
-
content_type__app_label="audit", name__icontains="view"
|
|
81
|
-
),
|
|
82
|
-
*Permission.objects.filter(
|
|
83
|
-
content_type__app_label="rest_framework_tracking",
|
|
84
|
-
name__icontains="view",
|
|
85
|
-
),
|
|
86
|
-
],
|
|
87
|
-
override=True,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
# manage_shipments
|
|
91
|
-
setup_group(
|
|
92
|
-
serializers.PermissionGroup.manage_shipments.name,
|
|
93
|
-
permissions=[
|
|
94
|
-
*Permission.objects.filter(content_type__app_label="manager"),
|
|
95
|
-
*Permission.objects.filter(
|
|
96
|
-
models.Q(content_type__app_label="orgs")
|
|
97
|
-
& (
|
|
98
|
-
models.Q(name__icontains="address")
|
|
99
|
-
| models.Q(name__icontains="parcel")
|
|
100
|
-
| models.Q(name__icontains="commodity")
|
|
101
|
-
| models.Q(name__icontains="customs")
|
|
102
|
-
| models.Q(name__icontains="pickup")
|
|
103
|
-
| models.Q(name__icontains="tracker")
|
|
104
|
-
| models.Q(name__icontains="shipment")
|
|
105
|
-
)
|
|
106
|
-
),
|
|
107
|
-
],
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
# manage_system
|
|
111
|
-
setup_group(
|
|
112
|
-
serializers.PermissionGroup.manage_system.name,
|
|
113
|
-
permissions=Permission.objects.filter(
|
|
114
|
-
content_type__app_label__in=[
|
|
115
|
-
"admin",
|
|
116
|
-
"user",
|
|
117
|
-
"pricing",
|
|
118
|
-
"providers",
|
|
119
|
-
"audit",
|
|
120
|
-
"database",
|
|
121
|
-
"rest_framework_tracking",
|
|
122
|
-
]
|
|
123
|
-
),
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def setup_group(
|
|
128
|
-
name: str, permissions: typing.List[Permission], override: bool = False
|
|
129
|
-
):
|
|
130
|
-
group, created = users.Group.objects.get_or_create(name=name)
|
|
131
|
-
|
|
132
|
-
if created or override:
|
|
133
|
-
group.permissions.set(permissions)
|
|
134
|
-
group.save()
|
|
1
|
+
# This module previously contained permission group setup logic.
|
|
2
|
+
# The setup_groups() function has been removed to:
|
|
3
|
+
# 1. Fix Django warning about database access during app initialization
|
|
4
|
+
# 2. Prepare for a better RBAC implementation in the future
|
|
5
|
+
#
|
|
6
|
+
# The PermissionGroup enum and ROLES_GROUPS mapping are preserved in
|
|
7
|
+
# karrio.server.iam.serializers for organization role management.
|
karrio/server/iam/serializers.py
CHANGED
|
@@ -9,7 +9,9 @@ class PermissionGroup(lib.StrEnum):
|
|
|
9
9
|
manage_orders = "manage_orders"
|
|
10
10
|
manage_data = "manage_data"
|
|
11
11
|
manage_pickups = "manage_pickups"
|
|
12
|
-
manage_carriers = "manage_carriers"
|
|
12
|
+
manage_carriers = "manage_carriers" # Deprecated: use read_carriers + write_carriers
|
|
13
|
+
read_carriers = "read_carriers"
|
|
14
|
+
write_carriers = "write_carriers"
|
|
13
15
|
manage_trackers = "manage_trackers"
|
|
14
16
|
manage_webhooks = "manage_webhooks"
|
|
15
17
|
manage_shipments = "manage_shipments"
|
|
@@ -20,14 +22,26 @@ PERMISSION_GROUPS = [(p.name, p.name) for p in list(PermissionGroup)]
|
|
|
20
22
|
ROLES_GROUPS: typing.Dict[str, typing.List[str]] = {
|
|
21
23
|
"owner": [
|
|
22
24
|
PermissionGroup.manage_org_owner.value,
|
|
25
|
+
PermissionGroup.manage_team.value,
|
|
26
|
+
PermissionGroup.manage_apps.value,
|
|
27
|
+
PermissionGroup.read_carriers.value,
|
|
28
|
+
PermissionGroup.write_carriers.value,
|
|
29
|
+
PermissionGroup.manage_webhooks.value,
|
|
30
|
+
PermissionGroup.manage_data.value,
|
|
31
|
+
PermissionGroup.manage_orders.value,
|
|
32
|
+
PermissionGroup.manage_pickups.value,
|
|
33
|
+
PermissionGroup.manage_trackers.value,
|
|
34
|
+
PermissionGroup.manage_shipments.value,
|
|
23
35
|
],
|
|
24
36
|
"admin": [
|
|
25
37
|
PermissionGroup.manage_team.value,
|
|
26
38
|
PermissionGroup.manage_apps.value,
|
|
27
|
-
PermissionGroup.
|
|
39
|
+
PermissionGroup.read_carriers.value,
|
|
40
|
+
PermissionGroup.write_carriers.value,
|
|
28
41
|
],
|
|
29
42
|
"developer": [
|
|
30
43
|
PermissionGroup.manage_webhooks.value,
|
|
44
|
+
PermissionGroup.read_carriers.value,
|
|
31
45
|
],
|
|
32
46
|
"member": [
|
|
33
47
|
PermissionGroup.manage_data.value,
|
|
@@ -35,5 +49,6 @@ ROLES_GROUPS: typing.Dict[str, typing.List[str]] = {
|
|
|
35
49
|
PermissionGroup.manage_pickups.value,
|
|
36
50
|
PermissionGroup.manage_trackers.value,
|
|
37
51
|
PermissionGroup.manage_shipments.value,
|
|
52
|
+
PermissionGroup.read_carriers.value,
|
|
38
53
|
],
|
|
39
54
|
}
|
karrio/server/iam/signals.py
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from django.db.models import signals
|
|
3
2
|
|
|
3
|
+
from karrio.server.core.logging import logger
|
|
4
4
|
import karrio.server.core.utils as utils
|
|
5
5
|
import karrio.server.user.models as user
|
|
6
6
|
import karrio.server.iam.models as models
|
|
7
7
|
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
8
|
|
|
11
9
|
def register_all():
|
|
12
10
|
signals.post_delete.connect(context_object_deleted, sender=user.Token)
|
|
13
11
|
|
|
14
|
-
logger.info("karrio.iam
|
|
12
|
+
logger.info("Signal registration complete", module="karrio.iam")
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
@utils.disable_for_loaddata
|
karrio/server/providers/admin.py
CHANGED
|
@@ -354,7 +354,7 @@ class LabelTemplateAdmin(admin.ModelAdmin):
|
|
|
354
354
|
return False
|
|
355
355
|
|
|
356
356
|
|
|
357
|
-
@utils.skip_on_commands()
|
|
357
|
+
@utils.skip_on_commands(["loaddata", "migrate", "makemigrations", "shell"])
|
|
358
358
|
def register_carrier_admins():
|
|
359
359
|
for carrier_name, display_name in ref.REFERENCES["carriers"].items():
|
|
360
360
|
proxy = providers.create_carrier_proxy(carrier_name, display_name)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from django.core.management.base import BaseCommand
|
|
2
|
+
from karrio.server.providers.models import RateSheet
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Command(BaseCommand):
|
|
6
|
+
help = 'Migrate existing rate sheets from legacy format to optimized zone reuse structure'
|
|
7
|
+
|
|
8
|
+
def add_arguments(self, parser):
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
'--dry-run',
|
|
11
|
+
action='store_true',
|
|
12
|
+
help='Show what would be migrated without making changes',
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
'--force',
|
|
16
|
+
action='store_true',
|
|
17
|
+
help='Force migration even if rate sheet already has optimized structure',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
def handle(self, *args, **options):
|
|
21
|
+
dry_run = options['dry_run']
|
|
22
|
+
force = options['force']
|
|
23
|
+
|
|
24
|
+
rate_sheets = RateSheet.objects.all()
|
|
25
|
+
|
|
26
|
+
if not rate_sheets.exists():
|
|
27
|
+
self.stdout.write(self.style.WARNING('No rate sheets found.'))
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
migrated_count = 0
|
|
31
|
+
skipped_count = 0
|
|
32
|
+
error_count = 0
|
|
33
|
+
|
|
34
|
+
for rate_sheet in rate_sheets:
|
|
35
|
+
try:
|
|
36
|
+
# Check if already migrated
|
|
37
|
+
if not force and (rate_sheet.zones or rate_sheet.service_rates):
|
|
38
|
+
self.stdout.write(
|
|
39
|
+
self.style.WARNING(f'Skipping {rate_sheet.name} - already has optimized structure')
|
|
40
|
+
)
|
|
41
|
+
skipped_count += 1
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Check if has services with zones to migrate
|
|
45
|
+
has_zones = any(
|
|
46
|
+
service.zones for service in rate_sheet.services.all()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if not has_zones:
|
|
50
|
+
self.stdout.write(
|
|
51
|
+
self.style.WARNING(f'Skipping {rate_sheet.name} - no zones to migrate')
|
|
52
|
+
)
|
|
53
|
+
skipped_count += 1
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if dry_run:
|
|
57
|
+
self.stdout.write(
|
|
58
|
+
self.style.SUCCESS(f'Would migrate: {rate_sheet.name}')
|
|
59
|
+
)
|
|
60
|
+
migrated_count += 1
|
|
61
|
+
else:
|
|
62
|
+
# Perform migration
|
|
63
|
+
old_zones_count = sum(
|
|
64
|
+
len(service.zones or []) for service in rate_sheet.services.all()
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
rate_sheet.migrate_from_legacy_format()
|
|
68
|
+
|
|
69
|
+
new_zones_count = len(rate_sheet.zones or [])
|
|
70
|
+
new_rates_count = len(rate_sheet.service_rates or [])
|
|
71
|
+
|
|
72
|
+
self.stdout.write(
|
|
73
|
+
self.style.SUCCESS(
|
|
74
|
+
f'Migrated {rate_sheet.name}: '
|
|
75
|
+
f'{old_zones_count} duplicated zones → '
|
|
76
|
+
f'{new_zones_count} shared zones + {new_rates_count} rates'
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
migrated_count += 1
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
self.stdout.write(
|
|
83
|
+
self.style.ERROR(f'Error migrating {rate_sheet.name}: {str(e)}')
|
|
84
|
+
)
|
|
85
|
+
error_count += 1
|
|
86
|
+
|
|
87
|
+
# Summary
|
|
88
|
+
if dry_run:
|
|
89
|
+
self.stdout.write(
|
|
90
|
+
self.style.SUCCESS(
|
|
91
|
+
f'\nDry run complete: {migrated_count} rate sheets would be migrated, '
|
|
92
|
+
f'{skipped_count} skipped, {error_count} errors'
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
self.stdout.write(
|
|
97
|
+
self.style.SUCCESS(
|
|
98
|
+
f'\nMigration complete: {migrated_count} rate sheets migrated, '
|
|
99
|
+
f'{skipped_count} skipped, {error_count} errors'
|
|
100
|
+
)
|
|
101
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated migration to add unique identifiers to ServiceLevel zones
|
|
2
|
+
from django.db import migrations
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_zone_identifiers(apps, schema_editor):
|
|
7
|
+
"""Add unique IDs to existing zones in ServiceLevel objects"""
|
|
8
|
+
ServiceLevel = apps.get_model('providers', 'ServiceLevel')
|
|
9
|
+
|
|
10
|
+
for service in ServiceLevel.objects.all():
|
|
11
|
+
if service.zones:
|
|
12
|
+
updated = False
|
|
13
|
+
for i, zone in enumerate(service.zones):
|
|
14
|
+
if 'id' not in zone:
|
|
15
|
+
# Generate unique zone ID
|
|
16
|
+
zone['id'] = f"zone_{uuid.uuid4().hex[:8]}"
|
|
17
|
+
updated = True
|
|
18
|
+
|
|
19
|
+
if updated:
|
|
20
|
+
service.save(update_fields=['zones'])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def reverse_zone_identifiers(apps, schema_editor):
|
|
24
|
+
"""Remove zone IDs (for rollback)"""
|
|
25
|
+
ServiceLevel = apps.get_model('providers', 'ServiceLevel')
|
|
26
|
+
|
|
27
|
+
for service in ServiceLevel.objects.all():
|
|
28
|
+
if service.zones:
|
|
29
|
+
updated = False
|
|
30
|
+
for zone in service.zones:
|
|
31
|
+
if 'id' in zone:
|
|
32
|
+
del zone['id']
|
|
33
|
+
updated = True
|
|
34
|
+
|
|
35
|
+
if updated:
|
|
36
|
+
service.save(update_fields=['zones'])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Migration(migrations.Migration):
|
|
40
|
+
|
|
41
|
+
dependencies = [
|
|
42
|
+
('providers', '0081_remove_alliedexpresssettings_carrier_ptr_and_more'),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
operations = [
|
|
46
|
+
migrations.RunPython(
|
|
47
|
+
add_zone_identifiers,
|
|
48
|
+
reverse_zone_identifiers
|
|
49
|
+
),
|
|
50
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated migration to add optimized rate sheet structure
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import karrio.server.core.models as core
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
('providers', '0082_add_zone_identifiers'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='ratesheet',
|
|
15
|
+
name='zones',
|
|
16
|
+
field=models.JSONField(
|
|
17
|
+
blank=True,
|
|
18
|
+
null=True,
|
|
19
|
+
default=core.field_default([]),
|
|
20
|
+
help_text="Shared zone definitions: [{'id': 'zone_1', 'label': 'Zone 1', 'cities': [...], 'country_codes': [...]}]"
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
migrations.AddField(
|
|
24
|
+
model_name='ratesheet',
|
|
25
|
+
name='service_rates',
|
|
26
|
+
field=models.JSONField(
|
|
27
|
+
blank=True,
|
|
28
|
+
null=True,
|
|
29
|
+
default=core.field_default([]),
|
|
30
|
+
help_text="Service-zone rate mapping: [{'service_id': 'svc_1', 'zone_id': 'zone_1', 'rate': 10.50}]"
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Generated by Django 5.2.5 on 2025-08-19 19:09
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("providers", "0083_add_optimized_rate_sheet_structure"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name="servicelevel",
|
|
15
|
+
name="currency",
|
|
16
|
+
field=models.CharField(
|
|
17
|
+
blank=True,
|
|
18
|
+
choices=[
|
|
19
|
+
("EUR", "EUR"),
|
|
20
|
+
("AED", "AED"),
|
|
21
|
+
("USD", "USD"),
|
|
22
|
+
("XCD", "XCD"),
|
|
23
|
+
("AMD", "AMD"),
|
|
24
|
+
("ANG", "ANG"),
|
|
25
|
+
("AOA", "AOA"),
|
|
26
|
+
("ARS", "ARS"),
|
|
27
|
+
("AUD", "AUD"),
|
|
28
|
+
("AWG", "AWG"),
|
|
29
|
+
("AZN", "AZN"),
|
|
30
|
+
("BAM", "BAM"),
|
|
31
|
+
("BBD", "BBD"),
|
|
32
|
+
("BDT", "BDT"),
|
|
33
|
+
("XOF", "XOF"),
|
|
34
|
+
("BGN", "BGN"),
|
|
35
|
+
("BHD", "BHD"),
|
|
36
|
+
("BIF", "BIF"),
|
|
37
|
+
("BMD", "BMD"),
|
|
38
|
+
("BND", "BND"),
|
|
39
|
+
("BOB", "BOB"),
|
|
40
|
+
("BRL", "BRL"),
|
|
41
|
+
("BSD", "BSD"),
|
|
42
|
+
("BTN", "BTN"),
|
|
43
|
+
("BWP", "BWP"),
|
|
44
|
+
("BYN", "BYN"),
|
|
45
|
+
("BZD", "BZD"),
|
|
46
|
+
("CAD", "CAD"),
|
|
47
|
+
("CDF", "CDF"),
|
|
48
|
+
("XAF", "XAF"),
|
|
49
|
+
("CHF", "CHF"),
|
|
50
|
+
("NZD", "NZD"),
|
|
51
|
+
("CLP", "CLP"),
|
|
52
|
+
("CNY", "CNY"),
|
|
53
|
+
("COP", "COP"),
|
|
54
|
+
("CRC", "CRC"),
|
|
55
|
+
("CUC", "CUC"),
|
|
56
|
+
("CVE", "CVE"),
|
|
57
|
+
("CZK", "CZK"),
|
|
58
|
+
("DJF", "DJF"),
|
|
59
|
+
("DKK", "DKK"),
|
|
60
|
+
("DOP", "DOP"),
|
|
61
|
+
("DZD", "DZD"),
|
|
62
|
+
("EGP", "EGP"),
|
|
63
|
+
("ERN", "ERN"),
|
|
64
|
+
("ETB", "ETB"),
|
|
65
|
+
("FJD", "FJD"),
|
|
66
|
+
("GBP", "GBP"),
|
|
67
|
+
("GEL", "GEL"),
|
|
68
|
+
("GHS", "GHS"),
|
|
69
|
+
("GMD", "GMD"),
|
|
70
|
+
("GNF", "GNF"),
|
|
71
|
+
("GTQ", "GTQ"),
|
|
72
|
+
("GYD", "GYD"),
|
|
73
|
+
("HKD", "HKD"),
|
|
74
|
+
("HNL", "HNL"),
|
|
75
|
+
("HRK", "HRK"),
|
|
76
|
+
("HTG", "HTG"),
|
|
77
|
+
("HUF", "HUF"),
|
|
78
|
+
("IDR", "IDR"),
|
|
79
|
+
("ILS", "ILS"),
|
|
80
|
+
("INR", "INR"),
|
|
81
|
+
("IRR", "IRR"),
|
|
82
|
+
("ISK", "ISK"),
|
|
83
|
+
("JMD", "JMD"),
|
|
84
|
+
("JOD", "JOD"),
|
|
85
|
+
("JPY", "JPY"),
|
|
86
|
+
("KES", "KES"),
|
|
87
|
+
("KGS", "KGS"),
|
|
88
|
+
("KHR", "KHR"),
|
|
89
|
+
("KMF", "KMF"),
|
|
90
|
+
("KPW", "KPW"),
|
|
91
|
+
("KRW", "KRW"),
|
|
92
|
+
("KWD", "KWD"),
|
|
93
|
+
("KYD", "KYD"),
|
|
94
|
+
("KZT", "KZT"),
|
|
95
|
+
("LAK", "LAK"),
|
|
96
|
+
("LKR", "LKR"),
|
|
97
|
+
("LRD", "LRD"),
|
|
98
|
+
("LSL", "LSL"),
|
|
99
|
+
("LYD", "LYD"),
|
|
100
|
+
("MAD", "MAD"),
|
|
101
|
+
("MDL", "MDL"),
|
|
102
|
+
("MGA", "MGA"),
|
|
103
|
+
("MKD", "MKD"),
|
|
104
|
+
("MMK", "MMK"),
|
|
105
|
+
("MNT", "MNT"),
|
|
106
|
+
("MOP", "MOP"),
|
|
107
|
+
("MRO", "MRO"),
|
|
108
|
+
("MUR", "MUR"),
|
|
109
|
+
("MVR", "MVR"),
|
|
110
|
+
("MWK", "MWK"),
|
|
111
|
+
("MXN", "MXN"),
|
|
112
|
+
("MYR", "MYR"),
|
|
113
|
+
("MZN", "MZN"),
|
|
114
|
+
("NAD", "NAD"),
|
|
115
|
+
("XPF", "XPF"),
|
|
116
|
+
("NGN", "NGN"),
|
|
117
|
+
("NIO", "NIO"),
|
|
118
|
+
("NOK", "NOK"),
|
|
119
|
+
("NPR", "NPR"),
|
|
120
|
+
("OMR", "OMR"),
|
|
121
|
+
("PEN", "PEN"),
|
|
122
|
+
("PGK", "PGK"),
|
|
123
|
+
("PHP", "PHP"),
|
|
124
|
+
("PKR", "PKR"),
|
|
125
|
+
("PLN", "PLN"),
|
|
126
|
+
("PYG", "PYG"),
|
|
127
|
+
("QAR", "QAR"),
|
|
128
|
+
("RON", "RON"),
|
|
129
|
+
("RSD", "RSD"),
|
|
130
|
+
("RUB", "RUB"),
|
|
131
|
+
("RWF", "RWF"),
|
|
132
|
+
("SAR", "SAR"),
|
|
133
|
+
("SBD", "SBD"),
|
|
134
|
+
("SCR", "SCR"),
|
|
135
|
+
("SDG", "SDG"),
|
|
136
|
+
("SEK", "SEK"),
|
|
137
|
+
("SGD", "SGD"),
|
|
138
|
+
("SHP", "SHP"),
|
|
139
|
+
("SLL", "SLL"),
|
|
140
|
+
("SOS", "SOS"),
|
|
141
|
+
("SRD", "SRD"),
|
|
142
|
+
("SSP", "SSP"),
|
|
143
|
+
("STD", "STD"),
|
|
144
|
+
("SYP", "SYP"),
|
|
145
|
+
("SZL", "SZL"),
|
|
146
|
+
("THB", "THB"),
|
|
147
|
+
("TJS", "TJS"),
|
|
148
|
+
("TND", "TND"),
|
|
149
|
+
("TOP", "TOP"),
|
|
150
|
+
("TRY", "TRY"),
|
|
151
|
+
("TTD", "TTD"),
|
|
152
|
+
("TWD", "TWD"),
|
|
153
|
+
("TZS", "TZS"),
|
|
154
|
+
("UAH", "UAH"),
|
|
155
|
+
("UYU", "UYU"),
|
|
156
|
+
("UZS", "UZS"),
|
|
157
|
+
("VEF", "VEF"),
|
|
158
|
+
("VND", "VND"),
|
|
159
|
+
("VUV", "VUV"),
|
|
160
|
+
("WST", "WST"),
|
|
161
|
+
("YER", "YER"),
|
|
162
|
+
("ZAR", "ZAR"),
|
|
163
|
+
],
|
|
164
|
+
max_length=4,
|
|
165
|
+
null=True,
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
]
|