karrio-server-manager 2026.1__py3-none-any.whl → 2026.1.3__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/manager/migrations/0070_add_meta_and_product_fields.py +98 -0
- karrio/server/manager/migrations/0071_product_proxy.py +25 -0
- karrio/server/manager/migrations/0072_populate_json_fields.py +267 -0
- karrio/server/manager/migrations/0073_make_shipment_fk_nullable.py +36 -0
- karrio/server/manager/migrations/0074_clean_model_refactoring.py +207 -0
- karrio/server/manager/migrations/0075_populate_template_meta.py +69 -0
- karrio/server/manager/migrations/0076_remove_customs_model.py +66 -0
- karrio/server/manager/migrations/0077_add_carrier_snapshot_fields.py +83 -0
- karrio/server/manager/migrations/0078_populate_carrier_snapshots.py +112 -0
- karrio/server/manager/migrations/0079_remove_carrier_fk_fields.py +56 -0
- karrio/server/manager/migrations/0080_add_carrier_json_indexes.py +137 -0
- karrio/server/manager/migrations/0081_cleanup.py +62 -0
- karrio/server/manager/migrations/0082_shipment_fees.py +26 -0
- karrio/server/manager/models.py +421 -321
- karrio/server/manager/serializers/__init__.py +5 -4
- karrio/server/manager/serializers/address.py +8 -2
- karrio/server/manager/serializers/commodity.py +11 -4
- karrio/server/manager/serializers/document.py +29 -15
- karrio/server/manager/serializers/manifest.py +6 -3
- karrio/server/manager/serializers/parcel.py +5 -2
- karrio/server/manager/serializers/pickup.py +194 -67
- karrio/server/manager/serializers/shipment.py +232 -152
- karrio/server/manager/serializers/tracking.py +53 -12
- karrio/server/manager/tests/__init__.py +0 -1
- karrio/server/manager/tests/test_addresses.py +53 -0
- karrio/server/manager/tests/test_parcels.py +50 -0
- karrio/server/manager/tests/test_pickups.py +286 -50
- karrio/server/manager/tests/test_products.py +597 -0
- karrio/server/manager/tests/test_shipments.py +237 -92
- karrio/server/manager/tests/test_trackers.py +65 -1
- karrio/server/manager/views/__init__.py +1 -1
- karrio/server/manager/views/addresses.py +38 -2
- karrio/server/manager/views/documents.py +1 -1
- karrio/server/manager/views/parcels.py +25 -2
- karrio/server/manager/views/products.py +239 -0
- karrio/server/manager/views/trackers.py +69 -1
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/METADATA +1 -1
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/RECORD +40 -28
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/WHEEL +1 -1
- karrio/server/manager/serializers/customs.py +0 -84
- karrio/server/manager/tests/test_custom_infos.py +0 -101
- karrio/server/manager/views/customs.py +0 -159
- {karrio_server_manager-2026.1.dist-info → karrio_server_manager-2026.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Data migration: Transfer labels from Template model to meta.label
|
|
2
|
+
# This ensures backward compatibility by preserving existing template labels
|
|
3
|
+
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def transfer_template_labels(apps, schema_editor):
|
|
8
|
+
"""
|
|
9
|
+
Transfer label and is_default from Template model to Address/Parcel meta field.
|
|
10
|
+
|
|
11
|
+
The Template model (in graph module) stores:
|
|
12
|
+
- label: The template name
|
|
13
|
+
- is_default: Whether it's the default template
|
|
14
|
+
- address: OneToOne FK to Address
|
|
15
|
+
- parcel: OneToOne FK to Parcel
|
|
16
|
+
|
|
17
|
+
We copy these to the meta.label and meta.is_default fields on the related models.
|
|
18
|
+
Note: Customs templates are not migrated as the Customs model is being removed.
|
|
19
|
+
"""
|
|
20
|
+
# Get models - Template is in graph module
|
|
21
|
+
Template = apps.get_model("graph", "Template")
|
|
22
|
+
Address = apps.get_model("manager", "Address")
|
|
23
|
+
Parcel = apps.get_model("manager", "Parcel")
|
|
24
|
+
|
|
25
|
+
# Process all templates
|
|
26
|
+
for template in Template.objects.all():
|
|
27
|
+
# Transfer to Address
|
|
28
|
+
if template.address_id:
|
|
29
|
+
try:
|
|
30
|
+
address = Address.objects.get(pk=template.address_id)
|
|
31
|
+
meta = address.meta or {}
|
|
32
|
+
meta["label"] = template.label
|
|
33
|
+
meta["is_default"] = template.is_default
|
|
34
|
+
address.meta = meta
|
|
35
|
+
address.save(update_fields=["meta"])
|
|
36
|
+
except Address.DoesNotExist:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# Transfer to Parcel
|
|
40
|
+
if template.parcel_id:
|
|
41
|
+
try:
|
|
42
|
+
parcel = Parcel.objects.get(pk=template.parcel_id)
|
|
43
|
+
meta = parcel.meta or {}
|
|
44
|
+
meta["label"] = template.label
|
|
45
|
+
meta["is_default"] = template.is_default
|
|
46
|
+
parcel.meta = meta
|
|
47
|
+
parcel.save(update_fields=["meta"])
|
|
48
|
+
except Parcel.DoesNotExist:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reverse_noop(apps, schema_editor):
|
|
53
|
+
"""Reverse migration is a no-op - we don't want to remove labels."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Migration(migrations.Migration):
|
|
58
|
+
|
|
59
|
+
dependencies = [
|
|
60
|
+
("manager", "0074_clean_model_refactoring"),
|
|
61
|
+
("graph", "0002_auto_20210512_1353"), # Ensure Template model exists
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
operations = [
|
|
65
|
+
migrations.RunPython(
|
|
66
|
+
transfer_template_labels,
|
|
67
|
+
reverse_noop,
|
|
68
|
+
),
|
|
69
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def cleanup_orgs_customs_links(apps, schema_editor):
|
|
5
|
+
"""
|
|
6
|
+
Clean up any orgs link tables that reference Customs before deleting the model.
|
|
7
|
+
This handles the case where the database was created in insiders mode but migrations
|
|
8
|
+
are running in OSS mode.
|
|
9
|
+
|
|
10
|
+
In insiders mode, orgs.0024_remove_organization_customs runs first (via run_before)
|
|
11
|
+
and handles this cleanup properly. This is a fallback for OSS mode with insiders DB.
|
|
12
|
+
"""
|
|
13
|
+
# Try to get orgs link models and clean them up using Django ORM
|
|
14
|
+
try:
|
|
15
|
+
CustomsLink = apps.get_model("orgs", "CustomsLink")
|
|
16
|
+
CustomsLink.objects.all().delete()
|
|
17
|
+
except LookupError:
|
|
18
|
+
# Model not registered - check if table exists using Django introspection
|
|
19
|
+
connection = schema_editor.connection
|
|
20
|
+
table_names = connection.introspection.table_names()
|
|
21
|
+
if "orgs_customslink" in table_names:
|
|
22
|
+
# Table exists but model isn't registered - use Django's cursor
|
|
23
|
+
with connection.cursor() as cursor:
|
|
24
|
+
cursor.execute("DELETE FROM orgs_customslink")
|
|
25
|
+
|
|
26
|
+
# Also try to clear any M2M relationship
|
|
27
|
+
try:
|
|
28
|
+
Organization = apps.get_model("orgs", "Organization")
|
|
29
|
+
for org in Organization.objects.all():
|
|
30
|
+
if hasattr(org, "customs"):
|
|
31
|
+
org.customs.clear()
|
|
32
|
+
except LookupError:
|
|
33
|
+
pass # Model doesn't exist in OSS mode
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def noop(apps, schema_editor):
|
|
37
|
+
"""Reverse migration is a no-op since we can't restore deleted links."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Migration(migrations.Migration):
|
|
42
|
+
|
|
43
|
+
dependencies = [
|
|
44
|
+
("manager", "0075_populate_template_meta"),
|
|
45
|
+
("graph", "0003_remove_template_customs"), # Remove template.customs FK first
|
|
46
|
+
# Note: orgs.0024_remove_organization_customs (insiders) uses run_before to ensure proper ordering
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
operations = [
|
|
50
|
+
# Clean up any orgs link tables first (handles insiders DB in OSS mode)
|
|
51
|
+
migrations.RunPython(cleanup_orgs_customs_links, noop),
|
|
52
|
+
# Remove M2M relationship first (customs_commodities junction table)
|
|
53
|
+
migrations.RemoveField(
|
|
54
|
+
model_name="customs",
|
|
55
|
+
name="commodities",
|
|
56
|
+
),
|
|
57
|
+
# Remove FK to Address
|
|
58
|
+
migrations.RemoveField(
|
|
59
|
+
model_name="customs",
|
|
60
|
+
name="duty_billing_address",
|
|
61
|
+
),
|
|
62
|
+
# Then delete the model (drops the customs table)
|
|
63
|
+
migrations.DeleteModel(
|
|
64
|
+
name="Customs",
|
|
65
|
+
),
|
|
66
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Add carrier JSONField to models before data migration
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import karrio.server.core.utils as utils
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
"""
|
|
10
|
+
Step 1: Add carrier JSONField to Pickup, Tracking, DocumentUploadRecord, Manifest, Shipment.
|
|
11
|
+
Also adds carrier_ids JSONField to Shipment.
|
|
12
|
+
|
|
13
|
+
This migration adds the new carrier snapshot fields but does NOT remove the old FK fields yet.
|
|
14
|
+
The old FK fields are needed by the data migration (0078) to populate the carrier snapshots.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
("manager", "0076_remove_customs_model"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
operations = [
|
|
22
|
+
# Add carrier field to Pickup
|
|
23
|
+
migrations.AddField(
|
|
24
|
+
model_name="pickup",
|
|
25
|
+
name="carrier",
|
|
26
|
+
field=models.JSONField(
|
|
27
|
+
blank=True,
|
|
28
|
+
null=True,
|
|
29
|
+
help_text="Carrier snapshot at time of pickup creation",
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
# Add carrier field to Tracking
|
|
33
|
+
migrations.AddField(
|
|
34
|
+
model_name="tracking",
|
|
35
|
+
name="carrier",
|
|
36
|
+
field=models.JSONField(
|
|
37
|
+
blank=True,
|
|
38
|
+
null=True,
|
|
39
|
+
help_text="Carrier snapshot at time of tracker creation",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
# Add carrier field to DocumentUploadRecord
|
|
43
|
+
migrations.AddField(
|
|
44
|
+
model_name="documentuploadrecord",
|
|
45
|
+
name="carrier",
|
|
46
|
+
field=models.JSONField(
|
|
47
|
+
blank=True,
|
|
48
|
+
null=True,
|
|
49
|
+
help_text="Carrier snapshot at time of document upload",
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
# Add carrier field to Manifest
|
|
53
|
+
migrations.AddField(
|
|
54
|
+
model_name="manifest",
|
|
55
|
+
name="carrier",
|
|
56
|
+
field=models.JSONField(
|
|
57
|
+
blank=True,
|
|
58
|
+
null=True,
|
|
59
|
+
help_text="Carrier snapshot at time of manifest creation",
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
# Add carrier field to Shipment (consistent with other models)
|
|
63
|
+
migrations.AddField(
|
|
64
|
+
model_name="shipment",
|
|
65
|
+
name="carrier",
|
|
66
|
+
field=models.JSONField(
|
|
67
|
+
blank=True,
|
|
68
|
+
null=True,
|
|
69
|
+
help_text="Carrier snapshot at time of label purchase",
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
# Add carrier_ids field to Shipment
|
|
73
|
+
migrations.AddField(
|
|
74
|
+
model_name="shipment",
|
|
75
|
+
name="carrier_ids",
|
|
76
|
+
field=models.JSONField(
|
|
77
|
+
blank=True,
|
|
78
|
+
null=True,
|
|
79
|
+
default=functools.partial(utils.identity, value=[]),
|
|
80
|
+
help_text="List of carrier IDs to filter rate requests",
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Data migration: Populate carrier JSONField from FK relationships
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_carrier_name(carrier):
|
|
7
|
+
"""Get carrier name from carrier code."""
|
|
8
|
+
from karrio.core.utils import DP
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
return DP.to_dict(carrier.data).get("carrier_name", carrier.carrier_code)
|
|
12
|
+
except Exception:
|
|
13
|
+
return carrier.carrier_code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_carrier_snapshot(carrier):
|
|
17
|
+
"""Create carrier snapshot dict from Carrier model instance."""
|
|
18
|
+
if carrier is None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"connection_id": carrier.id,
|
|
23
|
+
"connection_type": "account", # All existing carriers are account connections
|
|
24
|
+
"carrier_code": carrier.carrier_code,
|
|
25
|
+
"carrier_id": carrier.carrier_id,
|
|
26
|
+
"carrier_name": get_carrier_name(carrier),
|
|
27
|
+
"test_mode": carrier.test_mode,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def populate_carrier_snapshots(apps, schema_editor):
|
|
32
|
+
"""
|
|
33
|
+
Populate carrier JSONField from FK relationships.
|
|
34
|
+
|
|
35
|
+
This copies carrier information from:
|
|
36
|
+
- Pickup.pickup_carrier -> Pickup.carrier
|
|
37
|
+
- Tracking.tracking_carrier -> Tracking.carrier
|
|
38
|
+
- DocumentUploadRecord.upload_carrier -> DocumentUploadRecord.carrier
|
|
39
|
+
- Manifest.manifest_carrier -> Manifest.carrier
|
|
40
|
+
- Shipment.selected_rate_carrier -> Shipment.carrier (dedicated field, consistent with other models)
|
|
41
|
+
"""
|
|
42
|
+
Pickup = apps.get_model("manager", "Pickup")
|
|
43
|
+
Tracking = apps.get_model("manager", "Tracking")
|
|
44
|
+
DocumentUploadRecord = apps.get_model("manager", "DocumentUploadRecord")
|
|
45
|
+
Manifest = apps.get_model("manager", "Manifest")
|
|
46
|
+
Shipment = apps.get_model("manager", "Shipment")
|
|
47
|
+
|
|
48
|
+
# Populate Pickup.carrier from pickup_carrier FK
|
|
49
|
+
for pickup in Pickup.objects.select_related("pickup_carrier").all():
|
|
50
|
+
if pickup.pickup_carrier and not pickup.carrier:
|
|
51
|
+
pickup.carrier = create_carrier_snapshot(pickup.pickup_carrier)
|
|
52
|
+
pickup.save(update_fields=["carrier"])
|
|
53
|
+
|
|
54
|
+
# Populate Tracking.carrier from tracking_carrier FK
|
|
55
|
+
for tracking in Tracking.objects.select_related("tracking_carrier").all():
|
|
56
|
+
if tracking.tracking_carrier and not tracking.carrier:
|
|
57
|
+
tracking.carrier = create_carrier_snapshot(tracking.tracking_carrier)
|
|
58
|
+
tracking.save(update_fields=["carrier"])
|
|
59
|
+
|
|
60
|
+
# Populate DocumentUploadRecord.carrier from upload_carrier FK
|
|
61
|
+
for record in DocumentUploadRecord.objects.select_related("upload_carrier").all():
|
|
62
|
+
if record.upload_carrier and not record.carrier:
|
|
63
|
+
record.carrier = create_carrier_snapshot(record.upload_carrier)
|
|
64
|
+
record.save(update_fields=["carrier"])
|
|
65
|
+
|
|
66
|
+
# Populate Manifest.carrier from manifest_carrier FK
|
|
67
|
+
for manifest in Manifest.objects.select_related("manifest_carrier").all():
|
|
68
|
+
if manifest.manifest_carrier and not manifest.carrier:
|
|
69
|
+
manifest.carrier = create_carrier_snapshot(manifest.manifest_carrier)
|
|
70
|
+
manifest.save(update_fields=["carrier"])
|
|
71
|
+
|
|
72
|
+
# Populate Shipment.carrier from selected_rate_carrier FK (consistent with other models)
|
|
73
|
+
for shipment in Shipment.objects.select_related("selected_rate_carrier").all():
|
|
74
|
+
if shipment.selected_rate_carrier and not shipment.carrier:
|
|
75
|
+
shipment.carrier = create_carrier_snapshot(shipment.selected_rate_carrier)
|
|
76
|
+
shipment.save(update_fields=["carrier"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def reverse_migration(apps, schema_editor):
|
|
80
|
+
"""
|
|
81
|
+
Reverse migration: Clear carrier JSONField (data preserved in FKs).
|
|
82
|
+
|
|
83
|
+
Note: The FK data is still preserved, so this is safe to reverse.
|
|
84
|
+
"""
|
|
85
|
+
Pickup = apps.get_model("manager", "Pickup")
|
|
86
|
+
Tracking = apps.get_model("manager", "Tracking")
|
|
87
|
+
DocumentUploadRecord = apps.get_model("manager", "DocumentUploadRecord")
|
|
88
|
+
Manifest = apps.get_model("manager", "Manifest")
|
|
89
|
+
Shipment = apps.get_model("manager", "Shipment")
|
|
90
|
+
|
|
91
|
+
Pickup.objects.update(carrier=None)
|
|
92
|
+
Tracking.objects.update(carrier=None)
|
|
93
|
+
DocumentUploadRecord.objects.update(carrier=None)
|
|
94
|
+
Manifest.objects.update(carrier=None)
|
|
95
|
+
Shipment.objects.update(carrier=None)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Migration(migrations.Migration):
|
|
99
|
+
"""
|
|
100
|
+
Step 2: Data migration - Populate carrier snapshots from FK relationships.
|
|
101
|
+
|
|
102
|
+
This migration copies carrier information from FK fields to the new JSONField.
|
|
103
|
+
The FK fields are still preserved at this point.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
dependencies = [
|
|
107
|
+
("manager", "0077_add_carrier_snapshot_fields"),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
operations = [
|
|
111
|
+
migrations.RunPython(populate_carrier_snapshots, reverse_migration),
|
|
112
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Cleanup migration: Remove carrier FK/M2M fields after data migration
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
"""
|
|
8
|
+
Step 3: Remove carrier FK/M2M fields after data has been migrated to JSONField.
|
|
9
|
+
|
|
10
|
+
This migration removes:
|
|
11
|
+
- Pickup.pickup_carrier (FK)
|
|
12
|
+
- Tracking.tracking_carrier (FK)
|
|
13
|
+
- DocumentUploadRecord.upload_carrier (FK)
|
|
14
|
+
- Manifest.manifest_carrier (FK)
|
|
15
|
+
- Shipment.selected_rate_carrier (FK)
|
|
16
|
+
- Shipment.carriers (M2M)
|
|
17
|
+
|
|
18
|
+
All carrier data is now stored in JSONField carrier snapshots.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
("manager", "0078_populate_carrier_snapshots"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
operations = [
|
|
26
|
+
# Remove Pickup.pickup_carrier FK
|
|
27
|
+
migrations.RemoveField(
|
|
28
|
+
model_name="pickup",
|
|
29
|
+
name="pickup_carrier",
|
|
30
|
+
),
|
|
31
|
+
# Remove Tracking.tracking_carrier FK
|
|
32
|
+
migrations.RemoveField(
|
|
33
|
+
model_name="tracking",
|
|
34
|
+
name="tracking_carrier",
|
|
35
|
+
),
|
|
36
|
+
# Remove DocumentUploadRecord.upload_carrier FK
|
|
37
|
+
migrations.RemoveField(
|
|
38
|
+
model_name="documentuploadrecord",
|
|
39
|
+
name="upload_carrier",
|
|
40
|
+
),
|
|
41
|
+
# Remove Manifest.manifest_carrier FK
|
|
42
|
+
migrations.RemoveField(
|
|
43
|
+
model_name="manifest",
|
|
44
|
+
name="manifest_carrier",
|
|
45
|
+
),
|
|
46
|
+
# Remove Shipment.selected_rate_carrier FK
|
|
47
|
+
migrations.RemoveField(
|
|
48
|
+
model_name="shipment",
|
|
49
|
+
name="selected_rate_carrier",
|
|
50
|
+
),
|
|
51
|
+
# Remove Shipment.carriers M2M
|
|
52
|
+
migrations.RemoveField(
|
|
53
|
+
model_name="shipment",
|
|
54
|
+
name="carriers",
|
|
55
|
+
),
|
|
56
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Add indexes on JSONFields for query optimization
|
|
2
|
+
|
|
3
|
+
import django.db.models as models
|
|
4
|
+
import django.db.models.fields.json as json_fields
|
|
5
|
+
from django.db import migrations, connection
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_gin_indexes(apps, schema_editor):
|
|
9
|
+
"""Create GIN indexes for PostgreSQL only."""
|
|
10
|
+
if "postgresql" not in connection.vendor:
|
|
11
|
+
return # Skip for non-PostgreSQL databases
|
|
12
|
+
|
|
13
|
+
with connection.cursor() as cursor:
|
|
14
|
+
# Shipment GIN indexes
|
|
15
|
+
cursor.execute("""
|
|
16
|
+
CREATE INDEX IF NOT EXISTS "shipment_meta_gin_idx"
|
|
17
|
+
ON "shipments" USING gin ("meta" jsonb_path_ops)
|
|
18
|
+
WHERE "meta" IS NOT NULL;
|
|
19
|
+
""")
|
|
20
|
+
cursor.execute("""
|
|
21
|
+
CREATE INDEX IF NOT EXISTS "shipment_options_gin_idx"
|
|
22
|
+
ON "shipments" USING gin ("options" jsonb_path_ops)
|
|
23
|
+
WHERE "options" IS NOT NULL;
|
|
24
|
+
""")
|
|
25
|
+
cursor.execute("""
|
|
26
|
+
CREATE INDEX IF NOT EXISTS "shipment_metadata_gin_idx"
|
|
27
|
+
ON "shipments" USING gin ("metadata" jsonb_path_ops)
|
|
28
|
+
WHERE "metadata" IS NOT NULL;
|
|
29
|
+
""")
|
|
30
|
+
cursor.execute("""
|
|
31
|
+
CREATE INDEX IF NOT EXISTS "shipment_recipient_gin_idx"
|
|
32
|
+
ON "shipments" USING gin ("recipient" jsonb_path_ops)
|
|
33
|
+
WHERE "recipient" IS NOT NULL;
|
|
34
|
+
""")
|
|
35
|
+
|
|
36
|
+
# Tracking GIN indexes
|
|
37
|
+
cursor.execute("""
|
|
38
|
+
CREATE INDEX IF NOT EXISTS "tracking_meta_gin_idx"
|
|
39
|
+
ON "tracking-status" USING gin ("meta" jsonb_path_ops)
|
|
40
|
+
WHERE "meta" IS NOT NULL;
|
|
41
|
+
""")
|
|
42
|
+
|
|
43
|
+
# Pickup GIN index
|
|
44
|
+
cursor.execute("""
|
|
45
|
+
CREATE INDEX IF NOT EXISTS "pickup_address_gin_idx"
|
|
46
|
+
ON "pickups" USING gin ("address" jsonb_path_ops)
|
|
47
|
+
WHERE "address" IS NOT NULL;
|
|
48
|
+
""")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def drop_gin_indexes(apps, schema_editor):
|
|
52
|
+
"""Drop GIN indexes (reverse migration)."""
|
|
53
|
+
if "postgresql" not in connection.vendor:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
with connection.cursor() as cursor:
|
|
57
|
+
cursor.execute('DROP INDEX IF EXISTS "shipment_meta_gin_idx";')
|
|
58
|
+
cursor.execute('DROP INDEX IF EXISTS "shipment_options_gin_idx";')
|
|
59
|
+
cursor.execute('DROP INDEX IF EXISTS "shipment_metadata_gin_idx";')
|
|
60
|
+
cursor.execute('DROP INDEX IF EXISTS "shipment_recipient_gin_idx";')
|
|
61
|
+
cursor.execute('DROP INDEX IF EXISTS "tracking_meta_gin_idx";')
|
|
62
|
+
cursor.execute('DROP INDEX IF EXISTS "pickup_address_gin_idx";')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Migration(migrations.Migration):
|
|
66
|
+
"""
|
|
67
|
+
Step 4: Add indexes on JSONFields for performance optimization.
|
|
68
|
+
|
|
69
|
+
Index Types:
|
|
70
|
+
1. KeyTextTransform indexes - For exact match queries on specific JSON keys
|
|
71
|
+
- carrier.carrier_code, carrier.connection_id
|
|
72
|
+
2. GIN indexes (PostgreSQL only) - For has_key, contains, and varied lookups
|
|
73
|
+
- meta, options, metadata, recipient fields
|
|
74
|
+
|
|
75
|
+
Notes:
|
|
76
|
+
- PostgreSQL: Full support for both index types
|
|
77
|
+
- SQLite: KeyTextTransform indexes created but not used; GIN indexes skipped
|
|
78
|
+
- Conditional indexes avoid indexing NULL values to save space
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
dependencies = [
|
|
82
|
+
("manager", "0079_remove_carrier_fk_fields"),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
operations = [
|
|
86
|
+
# ─────────────────────────────────────────────────────────────────
|
|
87
|
+
# CARRIER SNAPSHOT INDEXES (KeyTextTransform - exact match queries)
|
|
88
|
+
# ─────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
# Shipment.carrier.carrier_code index
|
|
91
|
+
# Used by: ShipmentFilters.carrier_filter, ManifestSerializer shipment filter
|
|
92
|
+
migrations.AddIndex(
|
|
93
|
+
model_name="shipment",
|
|
94
|
+
index=models.Index(
|
|
95
|
+
json_fields.KeyTextTransform("carrier_code", "carrier"),
|
|
96
|
+
condition=models.Q(carrier__isnull=False),
|
|
97
|
+
name="shipment_carrier_code_idx",
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
# Tracking.carrier.carrier_code index
|
|
101
|
+
# Used by: TrackerFilters.carrier_filter, tracker views
|
|
102
|
+
migrations.AddIndex(
|
|
103
|
+
model_name="tracking",
|
|
104
|
+
index=models.Index(
|
|
105
|
+
json_fields.KeyTextTransform("carrier_code", "carrier"),
|
|
106
|
+
condition=models.Q(carrier__isnull=False),
|
|
107
|
+
name="tracking_carrier_code_idx",
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
# Tracking.carrier.connection_id index
|
|
111
|
+
# Used by: CarrierConnection serializer to find related trackers
|
|
112
|
+
migrations.AddIndex(
|
|
113
|
+
model_name="tracking",
|
|
114
|
+
index=models.Index(
|
|
115
|
+
json_fields.KeyTextTransform("connection_id", "carrier"),
|
|
116
|
+
condition=models.Q(carrier__isnull=False),
|
|
117
|
+
name="tracking_connection_id_idx",
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
# Manifest.carrier.carrier_code index
|
|
121
|
+
# Used by: ManifestFilters.carrier_filter
|
|
122
|
+
migrations.AddIndex(
|
|
123
|
+
model_name="manifest",
|
|
124
|
+
index=models.Index(
|
|
125
|
+
json_fields.KeyTextTransform("carrier_code", "carrier"),
|
|
126
|
+
condition=models.Q(carrier__isnull=False),
|
|
127
|
+
name="manifest_carrier_code_idx",
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
|
|
131
|
+
# ─────────────────────────────────────────────────────────────────
|
|
132
|
+
# GIN INDEXES (PostgreSQL only - for has_key, contains, text search)
|
|
133
|
+
# ─────────────────────────────────────────────────────────────────
|
|
134
|
+
# These indexes support: __has_key, __contains, __icontains on JSON keys
|
|
135
|
+
# RunPython is used to conditionally create indexes based on DB vendor
|
|
136
|
+
migrations.RunPython(create_gin_indexes, drop_gin_indexes),
|
|
137
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Generated by Django 5.2.10 on 2026-01-22 14:29
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import karrio.server.core.utils
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
("manager", "0080_add_carrier_json_indexes"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AlterField(
|
|
16
|
+
model_name="shipment",
|
|
17
|
+
name="billing_address",
|
|
18
|
+
field=models.JSONField(
|
|
19
|
+
blank=True, help_text="Billing address (embedded JSON)", null=True
|
|
20
|
+
),
|
|
21
|
+
),
|
|
22
|
+
migrations.AlterField(
|
|
23
|
+
model_name="shipment",
|
|
24
|
+
name="customs",
|
|
25
|
+
field=models.JSONField(
|
|
26
|
+
blank=True, help_text="Customs information (embedded JSON)", null=True
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
migrations.AlterField(
|
|
30
|
+
model_name="shipment",
|
|
31
|
+
name="parcels",
|
|
32
|
+
field=models.JSONField(
|
|
33
|
+
blank=True,
|
|
34
|
+
default=functools.partial(
|
|
35
|
+
karrio.server.core.utils.identity, *(), **{"value": []}
|
|
36
|
+
),
|
|
37
|
+
help_text="Parcels array with nested items (embedded JSON)",
|
|
38
|
+
null=True,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
migrations.AlterField(
|
|
42
|
+
model_name="shipment",
|
|
43
|
+
name="recipient",
|
|
44
|
+
field=models.JSONField(
|
|
45
|
+
blank=True, help_text="Recipient address (embedded JSON)", null=True
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
migrations.AlterField(
|
|
49
|
+
model_name="shipment",
|
|
50
|
+
name="return_address",
|
|
51
|
+
field=models.JSONField(
|
|
52
|
+
blank=True, help_text="Return address (embedded JSON)", null=True
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
migrations.AlterField(
|
|
56
|
+
model_name="shipment",
|
|
57
|
+
name="shipper",
|
|
58
|
+
field=models.JSONField(
|
|
59
|
+
blank=True, help_text="Shipper address (embedded JSON)", null=True
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by Django 5.2.10 on 2026-01-29
|
|
2
|
+
# Adds applied_fees field to Shipment for tracking COGS accounting data
|
|
3
|
+
|
|
4
|
+
import functools
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
import karrio.server.core.utils as utils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
("manager", "0081_cleanup"),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AddField(
|
|
17
|
+
model_name="shipment",
|
|
18
|
+
name="applied_fees",
|
|
19
|
+
field=models.JSONField(
|
|
20
|
+
blank=True,
|
|
21
|
+
null=True,
|
|
22
|
+
default=functools.partial(utils.identity, value=[]),
|
|
23
|
+
help_text="Applied fees for accounting: addons + surcharge COGS values",
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
]
|