karrio-server-pricing 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/pricing/admin.py +119 -23
- karrio/server/pricing/apps.py +5 -1
- karrio/server/pricing/migrations/0076_create_markup_and_fee_models.py +258 -0
- karrio/server/pricing/migrations/0077_migrate_surcharge_to_markup_data.py +70 -0
- karrio/server/pricing/migrations/0078_cleanup.py +109 -0
- karrio/server/pricing/migrations/0079_fee_snapshot_model.py +283 -0
- karrio/server/pricing/models.py +268 -87
- karrio/server/pricing/serializers.py +16 -11
- karrio/server/pricing/signals.py +136 -12
- karrio/server/pricing/tests.py +397 -9
- {karrio_server_pricing-2026.1.dist-info → karrio_server_pricing-2026.1.3.dist-info}/METADATA +1 -1
- {karrio_server_pricing-2026.1.dist-info → karrio_server_pricing-2026.1.3.dist-info}/RECORD +14 -10
- {karrio_server_pricing-2026.1.dist-info → karrio_server_pricing-2026.1.3.dist-info}/WHEEL +1 -1
- {karrio_server_pricing-2026.1.dist-info → karrio_server_pricing-2026.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration: Convert Fee model from FK-based to denormalized snapshot model.
|
|
3
|
+
|
|
4
|
+
Changes:
|
|
5
|
+
- Replace FK shipment → CharField shipment_id (snapshot)
|
|
6
|
+
- Replace FK markup → CharField markup_id (snapshot)
|
|
7
|
+
- Add account_id CharField (org context snapshot)
|
|
8
|
+
- Add test_mode BooleanField
|
|
9
|
+
- Rename markup_type → fee_type
|
|
10
|
+
- Rename markup_percentage → percentage
|
|
11
|
+
- Add composite indexes for time-series queries
|
|
12
|
+
- Data migration: populate account_id and test_mode from shipment data
|
|
13
|
+
|
|
14
|
+
Strategy for FK → CharField conversion:
|
|
15
|
+
1. Add temporary CharField fields (_shipment_id, _markup_id)
|
|
16
|
+
2. Data migration: copy FK values to temp fields
|
|
17
|
+
3. Remove FK fields (drops constraints automatically)
|
|
18
|
+
4. Rename temp fields to final names
|
|
19
|
+
This is fully portable across SQLite, PostgreSQL, and MySQL.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from django.db import migrations, models
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def copy_fk_values_to_temp_fields(apps, schema_editor):
|
|
26
|
+
"""Copy FK IDs to temporary CharField fields before dropping FKs."""
|
|
27
|
+
Fee = apps.get_model("pricing", "Fee")
|
|
28
|
+
batch_size = 500
|
|
29
|
+
update_batch = []
|
|
30
|
+
|
|
31
|
+
for fee in Fee.objects.all().iterator(chunk_size=batch_size):
|
|
32
|
+
fee._shipment_id = fee.shipment_id or ""
|
|
33
|
+
fee._markup_id = fee.markup_id
|
|
34
|
+
update_batch.append(fee)
|
|
35
|
+
|
|
36
|
+
if len(update_batch) >= batch_size:
|
|
37
|
+
Fee.objects.bulk_update(
|
|
38
|
+
update_batch, ["_shipment_id", "_markup_id"], batch_size=batch_size
|
|
39
|
+
)
|
|
40
|
+
update_batch = []
|
|
41
|
+
|
|
42
|
+
if update_batch:
|
|
43
|
+
Fee.objects.bulk_update(
|
|
44
|
+
update_batch, ["_shipment_id", "_markup_id"], batch_size=batch_size
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def populate_snapshot_fields(apps, schema_editor):
|
|
49
|
+
"""Populate account_id and test_mode from existing shipment data."""
|
|
50
|
+
Fee = apps.get_model("pricing", "Fee")
|
|
51
|
+
batch_size = 500
|
|
52
|
+
|
|
53
|
+
# Populate test_mode from shipment
|
|
54
|
+
try:
|
|
55
|
+
Shipment = apps.get_model("manager", "Shipment")
|
|
56
|
+
shipment_ids = list(
|
|
57
|
+
Fee.objects.values_list("shipment_id", flat=True).distinct()[:10000]
|
|
58
|
+
)
|
|
59
|
+
test_mode_map = dict(
|
|
60
|
+
Shipment.objects.filter(id__in=shipment_ids)
|
|
61
|
+
.values_list("id", "test_mode")
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
update_batch = []
|
|
65
|
+
for fee in Fee.objects.all().iterator(chunk_size=batch_size):
|
|
66
|
+
test_mode = test_mode_map.get(fee.shipment_id, False)
|
|
67
|
+
if test_mode:
|
|
68
|
+
fee.test_mode = test_mode
|
|
69
|
+
update_batch.append(fee)
|
|
70
|
+
if len(update_batch) >= batch_size:
|
|
71
|
+
Fee.objects.bulk_update(update_batch, ["test_mode"], batch_size=batch_size)
|
|
72
|
+
update_batch = []
|
|
73
|
+
if update_batch:
|
|
74
|
+
Fee.objects.bulk_update(update_batch, ["test_mode"], batch_size=batch_size)
|
|
75
|
+
except LookupError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# Populate account_id from shipment org links
|
|
79
|
+
try:
|
|
80
|
+
ShipmentLink = apps.get_model("orgs", "ShipmentLink")
|
|
81
|
+
shipment_ids = list(
|
|
82
|
+
Fee.objects.filter(account_id__isnull=True)
|
|
83
|
+
.values_list("shipment_id", flat=True)
|
|
84
|
+
.distinct()[:10000]
|
|
85
|
+
)
|
|
86
|
+
link_map = dict(
|
|
87
|
+
ShipmentLink.objects.filter(item_id__in=shipment_ids)
|
|
88
|
+
.values_list("item_id", "org_id")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
update_batch = []
|
|
92
|
+
for fee in Fee.objects.filter(account_id__isnull=True).iterator(chunk_size=batch_size):
|
|
93
|
+
org_id = link_map.get(fee.shipment_id)
|
|
94
|
+
if org_id:
|
|
95
|
+
fee.account_id = org_id
|
|
96
|
+
update_batch.append(fee)
|
|
97
|
+
if len(update_batch) >= batch_size:
|
|
98
|
+
Fee.objects.bulk_update(update_batch, ["account_id"], batch_size=batch_size)
|
|
99
|
+
update_batch = []
|
|
100
|
+
if update_batch:
|
|
101
|
+
Fee.objects.bulk_update(update_batch, ["account_id"], batch_size=batch_size)
|
|
102
|
+
except LookupError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Migration(migrations.Migration):
|
|
107
|
+
|
|
108
|
+
dependencies = [
|
|
109
|
+
("pricing", "0078_cleanup"),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
operations = [
|
|
113
|
+
# ─── Step 1: Remove old indexes ──────────────────────────────────
|
|
114
|
+
migrations.RemoveIndex(
|
|
115
|
+
model_name="fee",
|
|
116
|
+
name="fee_shipmen_cf6644_idx",
|
|
117
|
+
),
|
|
118
|
+
migrations.RemoveIndex(
|
|
119
|
+
model_name="fee",
|
|
120
|
+
name="fee_markup__6438ea_idx",
|
|
121
|
+
),
|
|
122
|
+
migrations.RemoveIndex(
|
|
123
|
+
model_name="fee",
|
|
124
|
+
name="fee_carrier_86bb46_idx",
|
|
125
|
+
),
|
|
126
|
+
migrations.RemoveIndex(
|
|
127
|
+
model_name="fee",
|
|
128
|
+
name="fee_created_050ccc_idx",
|
|
129
|
+
),
|
|
130
|
+
|
|
131
|
+
# ─── Step 2: Add temporary CharField fields to hold FK values ────
|
|
132
|
+
migrations.AddField(
|
|
133
|
+
model_name="fee",
|
|
134
|
+
name="_shipment_id",
|
|
135
|
+
field=models.CharField(
|
|
136
|
+
max_length=50,
|
|
137
|
+
default="",
|
|
138
|
+
),
|
|
139
|
+
preserve_default=False,
|
|
140
|
+
),
|
|
141
|
+
migrations.AddField(
|
|
142
|
+
model_name="fee",
|
|
143
|
+
name="_markup_id",
|
|
144
|
+
field=models.CharField(
|
|
145
|
+
max_length=50,
|
|
146
|
+
null=True,
|
|
147
|
+
blank=True,
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
|
|
151
|
+
# ─── Step 3: Copy FK values to temp fields ──────────────────────
|
|
152
|
+
migrations.RunPython(
|
|
153
|
+
copy_fk_values_to_temp_fields,
|
|
154
|
+
migrations.RunPython.noop,
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
# ─── Step 4: Remove FK fields (drops constraints automatically) ─
|
|
158
|
+
migrations.RemoveField(
|
|
159
|
+
model_name="fee",
|
|
160
|
+
name="shipment",
|
|
161
|
+
),
|
|
162
|
+
migrations.RemoveField(
|
|
163
|
+
model_name="fee",
|
|
164
|
+
name="markup",
|
|
165
|
+
),
|
|
166
|
+
|
|
167
|
+
# ─── Step 5: Rename temp fields to final names ──────────────────
|
|
168
|
+
migrations.RenameField(
|
|
169
|
+
model_name="fee",
|
|
170
|
+
old_name="_shipment_id",
|
|
171
|
+
new_name="shipment_id",
|
|
172
|
+
),
|
|
173
|
+
migrations.RenameField(
|
|
174
|
+
model_name="fee",
|
|
175
|
+
old_name="_markup_id",
|
|
176
|
+
new_name="markup_id",
|
|
177
|
+
),
|
|
178
|
+
|
|
179
|
+
# ─── Step 6: Update field attributes (add indexes, help_text) ───
|
|
180
|
+
migrations.AlterField(
|
|
181
|
+
model_name="fee",
|
|
182
|
+
name="shipment_id",
|
|
183
|
+
field=models.CharField(
|
|
184
|
+
max_length=50,
|
|
185
|
+
db_index=True,
|
|
186
|
+
help_text="The shipment this fee was applied to",
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
migrations.AlterField(
|
|
190
|
+
model_name="fee",
|
|
191
|
+
name="markup_id",
|
|
192
|
+
field=models.CharField(
|
|
193
|
+
max_length=50,
|
|
194
|
+
null=True,
|
|
195
|
+
blank=True,
|
|
196
|
+
db_index=True,
|
|
197
|
+
help_text="The markup ID that generated this fee",
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
|
|
201
|
+
# ─── Step 7: Rename existing fields ─────────────────────────────
|
|
202
|
+
migrations.RenameField(
|
|
203
|
+
model_name="fee",
|
|
204
|
+
old_name="markup_type",
|
|
205
|
+
new_name="fee_type",
|
|
206
|
+
),
|
|
207
|
+
migrations.RenameField(
|
|
208
|
+
model_name="fee",
|
|
209
|
+
old_name="markup_percentage",
|
|
210
|
+
new_name="percentage",
|
|
211
|
+
),
|
|
212
|
+
|
|
213
|
+
# ─── Step 8: Add new snapshot fields ─────────────────────────────
|
|
214
|
+
migrations.AddField(
|
|
215
|
+
model_name="fee",
|
|
216
|
+
name="account_id",
|
|
217
|
+
field=models.CharField(
|
|
218
|
+
max_length=50,
|
|
219
|
+
null=True,
|
|
220
|
+
blank=True,
|
|
221
|
+
db_index=True,
|
|
222
|
+
help_text="The organization/account this fee belongs to",
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
migrations.AddField(
|
|
226
|
+
model_name="fee",
|
|
227
|
+
name="test_mode",
|
|
228
|
+
field=models.BooleanField(default=False),
|
|
229
|
+
),
|
|
230
|
+
|
|
231
|
+
# ─── Step 9: Update connection_id to be indexed ─────────────────
|
|
232
|
+
migrations.AlterField(
|
|
233
|
+
model_name="fee",
|
|
234
|
+
name="connection_id",
|
|
235
|
+
field=models.CharField(
|
|
236
|
+
max_length=50,
|
|
237
|
+
db_index=True,
|
|
238
|
+
help_text="Connection ID used for this shipment",
|
|
239
|
+
),
|
|
240
|
+
),
|
|
241
|
+
|
|
242
|
+
# ─── Step 10: Data migration — populate snapshot fields ─────────
|
|
243
|
+
migrations.RunPython(
|
|
244
|
+
populate_snapshot_fields,
|
|
245
|
+
migrations.RunPython.noop,
|
|
246
|
+
),
|
|
247
|
+
|
|
248
|
+
# ─── Step 11: Add indexes ───────────────────────────────────────
|
|
249
|
+
# Single-field indexes for fields without db_index=True
|
|
250
|
+
# (shipment_id, markup_id, account_id, connection_id already
|
|
251
|
+
# get indexes via db_index=True on the field definition)
|
|
252
|
+
migrations.AddIndex(
|
|
253
|
+
model_name="fee",
|
|
254
|
+
index=models.Index(
|
|
255
|
+
fields=["carrier_code"], name="fee_carrier_86bb46_idx"
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
migrations.AddIndex(
|
|
259
|
+
model_name="fee",
|
|
260
|
+
index=models.Index(
|
|
261
|
+
fields=["created_at"], name="fee_created_050ccc_idx"
|
|
262
|
+
),
|
|
263
|
+
),
|
|
264
|
+
# Composite indexes for time-series queries
|
|
265
|
+
migrations.AddIndex(
|
|
266
|
+
model_name="fee",
|
|
267
|
+
index=models.Index(
|
|
268
|
+
fields=["account_id", "created_at"], name="fee_account_507a12_idx"
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
migrations.AddIndex(
|
|
272
|
+
model_name="fee",
|
|
273
|
+
index=models.Index(
|
|
274
|
+
fields=["connection_id", "created_at"], name="fee_connect_3aeeec_idx"
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
migrations.AddIndex(
|
|
278
|
+
model_name="fee",
|
|
279
|
+
index=models.Index(
|
|
280
|
+
fields=["markup_id", "created_at"], name="fee_markup__130c94_idx"
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
]
|