karrio-server-core 2025.5rc12__py3-none-any.whl → 2025.5rc14__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.
Potentially problematic release.
This version of karrio-server-core might be problematic. Click here for more details.
- 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/models/service.py +130 -0
- karrio/server/providers/models/sheet.py +227 -0
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2025.5rc14.dist-info}/METADATA +1 -1
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2025.5rc14.dist-info}/RECORD +9 -6
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2025.5rc14.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2025.5rc14.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
]
|
|
@@ -60,3 +60,133 @@ class ServiceLevel(core.OwnedEntity):
|
|
|
60
60
|
@property
|
|
61
61
|
def object_type(self):
|
|
62
62
|
return "service_level"
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def computed_zones(self):
|
|
66
|
+
"""
|
|
67
|
+
Computed property that returns zones in legacy format for backward compatibility.
|
|
68
|
+
If the service belongs to a rate sheet with optimized structure, reconstruct from there.
|
|
69
|
+
Otherwise, fall back to the service's own zones field.
|
|
70
|
+
"""
|
|
71
|
+
# Check if this service belongs to a rate sheet with optimized structure
|
|
72
|
+
rate_sheet = getattr(self, '_rate_sheet_cache', None)
|
|
73
|
+
if not rate_sheet:
|
|
74
|
+
# Try to find rate sheet this service belongs to
|
|
75
|
+
try:
|
|
76
|
+
rate_sheet = self.service_sheet.first()
|
|
77
|
+
self._rate_sheet_cache = rate_sheet
|
|
78
|
+
except:
|
|
79
|
+
rate_sheet = None
|
|
80
|
+
|
|
81
|
+
if rate_sheet and rate_sheet.zones and rate_sheet.service_rates:
|
|
82
|
+
# Use optimized structure
|
|
83
|
+
return rate_sheet.get_service_zones_legacy(self.id)
|
|
84
|
+
else:
|
|
85
|
+
# Fall back to legacy zones field
|
|
86
|
+
return self.zones or []
|
|
87
|
+
|
|
88
|
+
def update_zone_cell(self, zone_id: str, field: str, value):
|
|
89
|
+
"""Update a single field in a zone by ID or index with validation"""
|
|
90
|
+
# Define allowed fields with their validators
|
|
91
|
+
allowed_fields = {
|
|
92
|
+
'rate': float,
|
|
93
|
+
'min_weight': float,
|
|
94
|
+
'max_weight': float,
|
|
95
|
+
'transit_days': int,
|
|
96
|
+
'transit_time': float,
|
|
97
|
+
'label': str,
|
|
98
|
+
'radius': float,
|
|
99
|
+
'latitude': float,
|
|
100
|
+
'longitude': float,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if field not in allowed_fields:
|
|
104
|
+
raise ValueError(f"Field '{field}' is not allowed for zone updates")
|
|
105
|
+
|
|
106
|
+
# Validate and convert the value
|
|
107
|
+
try:
|
|
108
|
+
if value is not None and value != '':
|
|
109
|
+
value = allowed_fields[field](value)
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
raise ValueError(f"Invalid value '{value}' for field '{field}' (expected {allowed_fields[field].__name__})")
|
|
112
|
+
|
|
113
|
+
zones = self.zones or []
|
|
114
|
+
|
|
115
|
+
# First try to find by zone ID
|
|
116
|
+
for zone in zones:
|
|
117
|
+
if zone.get('id') == zone_id:
|
|
118
|
+
zone[field] = value
|
|
119
|
+
self.save(update_fields=['zones'])
|
|
120
|
+
return zone
|
|
121
|
+
|
|
122
|
+
# Fallback: try to find by index for zones without IDs
|
|
123
|
+
try:
|
|
124
|
+
zone_index = int(zone_id)
|
|
125
|
+
if 0 <= zone_index < len(zones):
|
|
126
|
+
zones[zone_index][field] = value
|
|
127
|
+
self.save(update_fields=['zones'])
|
|
128
|
+
return zones[zone_index]
|
|
129
|
+
except (ValueError, IndexError):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
raise ValueError(f"Zone {zone_id} not found")
|
|
133
|
+
|
|
134
|
+
def batch_update_cells(self, updates: list):
|
|
135
|
+
"""
|
|
136
|
+
Batch update multiple zone cells with validation
|
|
137
|
+
updates format: [{'zone_id': str, 'field': str, 'value': any}, ...]
|
|
138
|
+
"""
|
|
139
|
+
# Define allowed fields with their validators
|
|
140
|
+
allowed_fields = {
|
|
141
|
+
'rate': float,
|
|
142
|
+
'min_weight': float,
|
|
143
|
+
'max_weight': float,
|
|
144
|
+
'transit_days': int,
|
|
145
|
+
'transit_time': float,
|
|
146
|
+
'label': str,
|
|
147
|
+
'radius': float,
|
|
148
|
+
'latitude': float,
|
|
149
|
+
'longitude': float,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
zones = list(self.zones or [])
|
|
153
|
+
|
|
154
|
+
for update in updates:
|
|
155
|
+
zone_id = update.get('zone_id')
|
|
156
|
+
field = update.get('field')
|
|
157
|
+
value = update.get('value')
|
|
158
|
+
|
|
159
|
+
if field not in allowed_fields:
|
|
160
|
+
raise ValueError(f"Field '{field}' is not allowed for zone updates")
|
|
161
|
+
|
|
162
|
+
# Validate and convert the value
|
|
163
|
+
try:
|
|
164
|
+
if value is not None and value != '':
|
|
165
|
+
value = allowed_fields[field](value)
|
|
166
|
+
except (ValueError, TypeError):
|
|
167
|
+
raise ValueError(f"Invalid value '{value}' for field '{field}' (expected {allowed_fields[field].__name__})")
|
|
168
|
+
|
|
169
|
+
# Find zone by ID first, then by index
|
|
170
|
+
zone_found = False
|
|
171
|
+
for zone in zones:
|
|
172
|
+
if zone.get('id') == zone_id:
|
|
173
|
+
zone[field] = value
|
|
174
|
+
zone_found = True
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
# Fallback to index if zone_id is numeric and zone not found by ID
|
|
178
|
+
if not zone_found:
|
|
179
|
+
try:
|
|
180
|
+
zone_index = int(zone_id)
|
|
181
|
+
if 0 <= zone_index < len(zones):
|
|
182
|
+
zones[zone_index][field] = value
|
|
183
|
+
zone_found = True
|
|
184
|
+
except (ValueError, IndexError):
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
if not zone_found:
|
|
188
|
+
raise ValueError(f"Zone {zone_id} not found")
|
|
189
|
+
|
|
190
|
+
self.zones = zones
|
|
191
|
+
self.save(update_fields=['zones'])
|
|
192
|
+
return self.zones
|
|
@@ -29,6 +29,22 @@ class RateSheet(core.OwnedEntity):
|
|
|
29
29
|
services = models.ManyToManyField(
|
|
30
30
|
"ServiceLevel", blank=True, related_name="service_sheet"
|
|
31
31
|
)
|
|
32
|
+
|
|
33
|
+
# New optimized structure
|
|
34
|
+
zones = models.JSONField(
|
|
35
|
+
blank=True,
|
|
36
|
+
null=True,
|
|
37
|
+
default=core.field_default([]),
|
|
38
|
+
help_text="Shared zone definitions: [{'id': 'zone_1', 'label': 'Zone 1', 'cities': [...], 'country_codes': [...]}]"
|
|
39
|
+
)
|
|
40
|
+
service_rates = models.JSONField(
|
|
41
|
+
blank=True,
|
|
42
|
+
null=True,
|
|
43
|
+
default=core.field_default([]),
|
|
44
|
+
help_text="Service-zone rate mapping: [{'service_id': 'svc_1', 'zone_id': 'zone_1', 'rate': 10.50}]"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Keep old structure for backward compatibility during migration
|
|
32
48
|
metadata = models.JSONField(
|
|
33
49
|
blank=True,
|
|
34
50
|
null=True,
|
|
@@ -58,3 +74,214 @@ class RateSheet(core.OwnedEntity):
|
|
|
58
74
|
return providers.Carrier.objects.filter(
|
|
59
75
|
carrier_code=self.carrier_name, rate_sheet__id=self.id
|
|
60
76
|
)
|
|
77
|
+
|
|
78
|
+
def get_service_zones_legacy(self, service_id: str):
|
|
79
|
+
"""
|
|
80
|
+
Backward compatible method - returns zones in old format for SDK compatibility
|
|
81
|
+
Combines shared zones with service-specific rates
|
|
82
|
+
"""
|
|
83
|
+
zones = self.zones or []
|
|
84
|
+
service_rates = self.service_rates or []
|
|
85
|
+
|
|
86
|
+
# Get rates for this service
|
|
87
|
+
service_rate_map = {
|
|
88
|
+
sr['zone_id']: sr for sr in service_rates
|
|
89
|
+
if sr.get('service_id') == service_id
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Combine zone definitions with service rates
|
|
93
|
+
legacy_zones = []
|
|
94
|
+
for zone in zones:
|
|
95
|
+
zone_id = zone.get('id')
|
|
96
|
+
rate_data = service_rate_map.get(zone_id, {})
|
|
97
|
+
|
|
98
|
+
legacy_zone = {
|
|
99
|
+
**zone, # Zone definition (label, cities, country_codes, etc.)
|
|
100
|
+
'rate': rate_data.get('rate', 0),
|
|
101
|
+
'min_weight': rate_data.get('min_weight'),
|
|
102
|
+
'max_weight': rate_data.get('max_weight'),
|
|
103
|
+
'transit_days': rate_data.get('transit_days'),
|
|
104
|
+
'transit_time': rate_data.get('transit_time'),
|
|
105
|
+
}
|
|
106
|
+
legacy_zones.append(legacy_zone)
|
|
107
|
+
|
|
108
|
+
return legacy_zones
|
|
109
|
+
|
|
110
|
+
def update_service_zone_rate(self, service_id: str, zone_id: str, field: str, value):
|
|
111
|
+
"""
|
|
112
|
+
Update a rate field for a specific service-zone combination
|
|
113
|
+
"""
|
|
114
|
+
allowed_fields = {
|
|
115
|
+
'rate': float,
|
|
116
|
+
'min_weight': float,
|
|
117
|
+
'max_weight': float,
|
|
118
|
+
'transit_days': int,
|
|
119
|
+
'transit_time': float,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if field not in allowed_fields:
|
|
123
|
+
raise ValueError(f"Field '{field}' is not allowed for rate updates")
|
|
124
|
+
|
|
125
|
+
# Validate value
|
|
126
|
+
try:
|
|
127
|
+
if value is not None and value != '':
|
|
128
|
+
value = allowed_fields[field](value)
|
|
129
|
+
except (ValueError, TypeError):
|
|
130
|
+
raise ValueError(f"Invalid value '{value}' for field '{field}'")
|
|
131
|
+
|
|
132
|
+
service_rates = list(self.service_rates or [])
|
|
133
|
+
|
|
134
|
+
# Find existing rate record
|
|
135
|
+
for rate_record in service_rates:
|
|
136
|
+
if (rate_record.get('service_id') == service_id and
|
|
137
|
+
rate_record.get('zone_id') == zone_id):
|
|
138
|
+
rate_record[field] = value
|
|
139
|
+
break
|
|
140
|
+
else:
|
|
141
|
+
# Create new rate record
|
|
142
|
+
service_rates.append({
|
|
143
|
+
'service_id': service_id,
|
|
144
|
+
'zone_id': zone_id,
|
|
145
|
+
field: value
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
self.service_rates = service_rates
|
|
149
|
+
self.save(update_fields=['service_rates'])
|
|
150
|
+
|
|
151
|
+
def batch_update_service_rates(self, updates):
|
|
152
|
+
"""
|
|
153
|
+
Batch update service rates
|
|
154
|
+
updates format: [{'service_id': str, 'zone_id': str, 'field': str, 'value': any}]
|
|
155
|
+
"""
|
|
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
|
+
service_rates = list(self.service_rates or [])
|
|
165
|
+
service_rate_map = {}
|
|
166
|
+
|
|
167
|
+
# Create lookup map for existing rates
|
|
168
|
+
for i, rate in enumerate(service_rates):
|
|
169
|
+
key = f"{rate.get('service_id')}:{rate.get('zone_id')}"
|
|
170
|
+
service_rate_map[key] = i
|
|
171
|
+
|
|
172
|
+
for update in updates:
|
|
173
|
+
service_id = update.get('service_id')
|
|
174
|
+
zone_id = update.get('zone_id')
|
|
175
|
+
field = update.get('field')
|
|
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):
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
key = f"{service_id}:{zone_id}"
|
|
189
|
+
|
|
190
|
+
if key in service_rate_map:
|
|
191
|
+
# Update existing rate
|
|
192
|
+
service_rates[service_rate_map[key]][field] = value
|
|
193
|
+
else:
|
|
194
|
+
# Create new rate record
|
|
195
|
+
new_rate = {
|
|
196
|
+
'service_id': service_id,
|
|
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
|
+
|
|
203
|
+
self.service_rates = service_rates
|
|
204
|
+
self.save(update_fields=['service_rates'])
|
|
205
|
+
|
|
206
|
+
def add_zone(self, zone_data):
|
|
207
|
+
"""
|
|
208
|
+
Add a new shared zone definition
|
|
209
|
+
"""
|
|
210
|
+
zones = list(self.zones or [])
|
|
211
|
+
|
|
212
|
+
# Generate zone ID if not provided
|
|
213
|
+
if not zone_data.get('id'):
|
|
214
|
+
zone_data['id'] = f"zone_{len(zones) + 1}"
|
|
215
|
+
|
|
216
|
+
zones.append(zone_data)
|
|
217
|
+
self.zones = zones
|
|
218
|
+
self.save(update_fields=['zones'])
|
|
219
|
+
return zone_data['id']
|
|
220
|
+
|
|
221
|
+
def remove_zone(self, zone_id: str):
|
|
222
|
+
"""
|
|
223
|
+
Remove a zone and all its associated rates
|
|
224
|
+
"""
|
|
225
|
+
# Remove zone definition
|
|
226
|
+
zones = [z for z in (self.zones or []) if z.get('id') != zone_id]
|
|
227
|
+
self.zones = zones
|
|
228
|
+
|
|
229
|
+
# Remove all rates for this zone
|
|
230
|
+
service_rates = [sr for sr in (self.service_rates or []) if sr.get('zone_id') != zone_id]
|
|
231
|
+
self.service_rates = service_rates
|
|
232
|
+
|
|
233
|
+
self.save(update_fields=['zones', 'service_rates'])
|
|
234
|
+
|
|
235
|
+
def migrate_from_legacy_format(self):
|
|
236
|
+
"""
|
|
237
|
+
Migrate from old format where zones are stored per service to new shared format
|
|
238
|
+
"""
|
|
239
|
+
if self.zones or self.service_rates:
|
|
240
|
+
# Already in new format
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
all_zones = {}
|
|
244
|
+
service_rates = []
|
|
245
|
+
zone_counter = 1
|
|
246
|
+
|
|
247
|
+
# Extract unique zones across all services
|
|
248
|
+
for service in self.services.all():
|
|
249
|
+
service_zones = service.zones or []
|
|
250
|
+
|
|
251
|
+
for zone_index, zone_data in enumerate(service_zones):
|
|
252
|
+
# Create zone signature for deduplication
|
|
253
|
+
zone_signature = {
|
|
254
|
+
'label': zone_data.get('label', f'Zone {zone_index + 1}'),
|
|
255
|
+
'cities': sorted(zone_data.get('cities', [])),
|
|
256
|
+
'postal_codes': sorted(zone_data.get('postal_codes', [])),
|
|
257
|
+
'country_codes': sorted(zone_data.get('country_codes', [])),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Use signature as key for deduplication
|
|
261
|
+
sig_key = str(zone_signature)
|
|
262
|
+
|
|
263
|
+
if sig_key not in all_zones:
|
|
264
|
+
zone_id = f"zone_{zone_counter}"
|
|
265
|
+
all_zones[sig_key] = {
|
|
266
|
+
'id': zone_id,
|
|
267
|
+
**zone_signature
|
|
268
|
+
}
|
|
269
|
+
zone_counter += 1
|
|
270
|
+
|
|
271
|
+
zone_id = all_zones[sig_key]['id']
|
|
272
|
+
|
|
273
|
+
# Store service rate
|
|
274
|
+
service_rates.append({
|
|
275
|
+
'service_id': service.id,
|
|
276
|
+
'zone_id': zone_id,
|
|
277
|
+
'rate': zone_data.get('rate', 0),
|
|
278
|
+
'min_weight': zone_data.get('min_weight'),
|
|
279
|
+
'max_weight': zone_data.get('max_weight'),
|
|
280
|
+
'transit_days': zone_data.get('transit_days'),
|
|
281
|
+
'transit_time': zone_data.get('transit_time'),
|
|
282
|
+
})
|
|
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'])
|
|
@@ -106,6 +106,7 @@ karrio/server/providers/extension/models/usps_international.py,sha256=k3opE3W4sv
|
|
|
106
106
|
karrio/server/providers/extension/models/usps_wt.py,sha256=6RkR8mnFT9AWMEtW7l-ZB1BY9TkEYnLKtIiLyhwIqw0,731
|
|
107
107
|
karrio/server/providers/extension/models/usps_wt_international.py,sha256=5QJdeNeshl1KCbQ6ravoh3vrU94QEN89noYZEF9kAW0,825
|
|
108
108
|
karrio/server/providers/extension/models/zoom2u.py,sha256=3RvkZkAYvpXDW9fCmViOJB5NrSPo2_d_qpjcsWuCVtQ,585
|
|
109
|
+
karrio/server/providers/management/commands/migrate_rate_sheets.py,sha256=piRV1n0itHQR4HZiWXVSjJKzHZPnFGKlrCjWOcy723A,3814
|
|
109
110
|
karrio/server/providers/migrations/0001_initial.py,sha256=DkKU91a1tMqRisLoarYndyKX3PJplT4Bor6tT53yETw,6772
|
|
110
111
|
karrio/server/providers/migrations/0002_carrier_active.py,sha256=zW_6cFEKmsZIbiuLimcUlwzVvpckYroJmzkCwep-D2A,378
|
|
111
112
|
karrio/server/providers/migrations/0003_auto_20201230_0820.py,sha256=qkvoSgtkGxg0XX0Srd35hanzl5tI7bBo3znli_h002g,699
|
|
@@ -187,12 +188,14 @@ karrio/server/providers/migrations/0078_auto_20240813_1552.py,sha256=1DXKf6Ak_GP
|
|
|
187
188
|
karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py,sha256=wppkE5LZaPiedrLMAkUpVly5FlSg6zbLBzO7a_yyUuk,864
|
|
188
189
|
karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py,sha256=ETTlvcAP1kk9Sr4u6f2EaG0hXcIaYVZKz0c6k7f-Cg8,101848
|
|
189
190
|
karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py,sha256=Lb8w5LEnJgCd-a4suFAdZ3-gWHlIbLIJLahTGlUmi_U,9557
|
|
191
|
+
karrio/server/providers/migrations/0082_add_zone_identifiers.py,sha256=0gRIFemNGNRkvV_pt0u3e-wL2LI89AKwaFWoqOqjAsA,1509
|
|
192
|
+
karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py,sha256=K9jj1LGEs5WQvIUxWac1wbCI6YnrrzTIcD3PzjZZcHA,1057
|
|
190
193
|
karrio/server/providers/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
191
194
|
karrio/server/providers/models/__init__.py,sha256=7D-pgQDTDSfptHhWxfJlOzSQqvYB_71D22zmNqjuBqk,628
|
|
192
195
|
karrio/server/providers/models/carrier.py,sha256=E3zPcypTAQi1ninAQjKbelGAV8cZI8g_voRJ2KETrxQ,9436
|
|
193
196
|
karrio/server/providers/models/config.py,sha256=8zuDXFGXBrC4fj2dW1-NpLAtFYPunEJi8uK-PezD7eo,759
|
|
194
|
-
karrio/server/providers/models/service.py,sha256=
|
|
195
|
-
karrio/server/providers/models/sheet.py,sha256=
|
|
197
|
+
karrio/server/providers/models/service.py,sha256=XTkZKyzE2g0I7HxGluty6Nk4El7iSyj2Ze0YXnrF7WQ,7038
|
|
198
|
+
karrio/server/providers/models/sheet.py,sha256=Fuy8KIA719ZOeoqe-VfL6ZjLjhzdYS6Jp-hgE9680u4,9943
|
|
196
199
|
karrio/server/providers/models/template.py,sha256=rrQXfimqFEFkelKzjyVeAruiu3UmrhpOT4M7JrGOtI0,1109
|
|
197
200
|
karrio/server/providers/models/utils.py,sha256=fuvrpDz2KzcRC7w94zFQAaI4qtQPvs8f1r6jZ5xM-64,1703
|
|
198
201
|
karrio/server/providers/serializers/__init__.py,sha256=bc5x1z7wkHyrHA2jP1wZVQF5SoPG-ueRVfn1RndK8VM,140
|
|
@@ -235,7 +238,7 @@ karrio/server/user/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
235
238
|
karrio/server/user/templates/registration/login.html,sha256=3_tj-0rKfwkCk-fp_GT8xFQhLqjGcJs3uZzOAaI40Sw,3690
|
|
236
239
|
karrio/server/user/templates/registration/registration_confirm_email.html,sha256=zFDkNN_BHMQyrBv_mU8aoqXxYxG91TGuf6pKwRa5jxE,247
|
|
237
240
|
karrio/server/user/templates/registration/registration_confirm_email.txt,sha256=I_zN_pJTRigfyiYbyQK0wFfrI5Zq1JG8lf0TyLA9fN0,94
|
|
238
|
-
karrio_server_core-2025.
|
|
239
|
-
karrio_server_core-2025.
|
|
240
|
-
karrio_server_core-2025.
|
|
241
|
-
karrio_server_core-2025.
|
|
241
|
+
karrio_server_core-2025.5rc14.dist-info/METADATA,sha256=bShtD4cjTO3YbcAL-jd3VDLnqwbwHeaHloU9Aufiphc,797
|
|
242
|
+
karrio_server_core-2025.5rc14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
243
|
+
karrio_server_core-2025.5rc14.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
244
|
+
karrio_server_core-2025.5rc14.dist-info/RECORD,,
|
|
File without changes
|
{karrio_server_core-2025.5rc12.dist-info → karrio_server_core-2025.5rc14.dist-info}/top_level.txt
RENAMED
|
File without changes
|