karrio-server-core 2025.5__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/conf.py +54 -0
- karrio/server/core/__init__.py +3 -0
- karrio/server/core/admin.py +1 -0
- karrio/server/core/apps.py +10 -0
- karrio/server/core/authentication.py +347 -0
- karrio/server/core/config.py +31 -0
- karrio/server/core/context_processors.py +12 -0
- karrio/server/core/datatypes.py +394 -0
- karrio/server/core/dataunits.py +187 -0
- karrio/server/core/exceptions.py +404 -0
- karrio/server/core/fields.py +12 -0
- karrio/server/core/filters.py +837 -0
- karrio/server/core/gateway.py +1011 -0
- karrio/server/core/logging.py +403 -0
- karrio/server/core/management/commands/cli.py +19 -0
- karrio/server/core/management/commands/create_oauth_client.py +41 -0
- karrio/server/core/management/commands/runserver.py +5 -0
- karrio/server/core/middleware.py +197 -0
- karrio/server/core/migrations/0001_initial.py +28 -0
- karrio/server/core/migrations/0002_apilogindex.py +69 -0
- karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
- karrio/server/core/migrations/0004_metafield.py +74 -0
- karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
- karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
- karrio/server/core/migrations/__init__.py +0 -0
- karrio/server/core/models/__init__.py +48 -0
- karrio/server/core/models/base.py +103 -0
- karrio/server/core/models/entity.py +24 -0
- karrio/server/core/models/metafield.py +144 -0
- karrio/server/core/models/third_party.py +21 -0
- karrio/server/core/oauth_validators.py +170 -0
- karrio/server/core/permissions.py +36 -0
- karrio/server/core/renderers.py +11 -0
- karrio/server/core/router.py +3 -0
- karrio/server/core/serializers.py +1971 -0
- karrio/server/core/signals.py +55 -0
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests.py +99 -0
- karrio/server/core/tests_resource_token.py +411 -0
- karrio/server/core/urls.py +12 -0
- karrio/server/core/utils.py +1025 -0
- karrio/server/core/validators.py +264 -0
- karrio/server/core/views/__init__.py +2 -0
- karrio/server/core/views/api.py +133 -0
- karrio/server/core/views/metadata.py +44 -0
- karrio/server/core/views/oauth.py +75 -0
- karrio/server/core/views/references.py +82 -0
- karrio/server/core/views/schema.py +310 -0
- karrio/server/filters/__init__.py +2 -0
- karrio/server/filters/abstract.py +26 -0
- karrio/server/iam/__init__.py +0 -0
- karrio/server/iam/admin.py +3 -0
- karrio/server/iam/apps.py +21 -0
- karrio/server/iam/migrations/0001_initial.py +33 -0
- karrio/server/iam/migrations/__init__.py +0 -0
- karrio/server/iam/models.py +48 -0
- karrio/server/iam/permissions.py +155 -0
- karrio/server/iam/serializers.py +54 -0
- karrio/server/iam/signals.py +18 -0
- karrio/server/iam/tests.py +3 -0
- karrio/server/iam/views.py +3 -0
- karrio/server/openapi.py +75 -0
- karrio/server/providers/__init__.py +1 -0
- karrio/server/providers/admin.py +364 -0
- karrio/server/providers/apps.py +10 -0
- karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
- karrio/server/providers/migrations/0001_initial.py +140 -0
- karrio/server/providers/migrations/0002_carrier_active.py +18 -0
- karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
- karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
- karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
- karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
- karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
- karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
- karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
- karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
- karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
- karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
- karrio/server/providers/migrations/0013_tntsettings.py +30 -0
- karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
- karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
- karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
- karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
- karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
- karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
- karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
- karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
- karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
- karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
- karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
- karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
- karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
- karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
- karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
- karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
- karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
- karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
- karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
- karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
- karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
- karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
- karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
- karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
- karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
- karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
- karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
- karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
- karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
- karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
- karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
- karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
- karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
- karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
- karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
- karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
- karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
- karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
- karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
- karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
- karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
- karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
- karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
- karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
- karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
- karrio/server/providers/migrations/0059_ratesheet.py +81 -0
- karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
- karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
- karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
- karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
- karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
- karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
- karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
- karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
- karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
- karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
- karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
- karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
- karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
- karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
- karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
- karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
- karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
- karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
- karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
- karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
- karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
- karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -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/__init__.py +0 -0
- karrio/server/providers/models/__init__.py +16 -0
- karrio/server/providers/models/carrier.py +387 -0
- karrio/server/providers/models/config.py +30 -0
- karrio/server/providers/models/service.py +192 -0
- karrio/server/providers/models/sheet.py +287 -0
- karrio/server/providers/models/template.py +39 -0
- karrio/server/providers/models/utils.py +58 -0
- karrio/server/providers/router.py +3 -0
- karrio/server/providers/serializers/__init__.py +3 -0
- karrio/server/providers/serializers/base.py +538 -0
- karrio/server/providers/signals.py +25 -0
- 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/urls.py +11 -0
- karrio/server/providers/views/__init__.py +0 -0
- karrio/server/providers/views/carriers.py +267 -0
- karrio/server/providers/views/connections.py +496 -0
- karrio/server/samples.py +352 -0
- karrio/server/serializers/__init__.py +2 -0
- karrio/server/serializers/abstract.py +602 -0
- karrio/server/tracing/__init__.py +0 -0
- karrio/server/tracing/admin.py +63 -0
- karrio/server/tracing/apps.py +8 -0
- karrio/server/tracing/migrations/0001_initial.py +41 -0
- karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
- karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
- karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
- karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
- karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
- karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
- karrio/server/tracing/migrations/__init__.py +0 -0
- karrio/server/tracing/models.py +82 -0
- karrio/server/tracing/tests.py +3 -0
- karrio/server/tracing/utils.py +109 -0
- karrio/server/user/__init__.py +0 -0
- karrio/server/user/admin.py +96 -0
- karrio/server/user/apps.py +7 -0
- karrio/server/user/forms.py +35 -0
- karrio/server/user/migrations/0001_initial.py +41 -0
- karrio/server/user/migrations/0002_token.py +29 -0
- karrio/server/user/migrations/0003_token_test_mode.py +20 -0
- karrio/server/user/migrations/0004_group.py +26 -0
- karrio/server/user/migrations/0005_token_label.py +21 -0
- karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
- karrio/server/user/migrations/0007_user_metadata.py +25 -0
- karrio/server/user/migrations/__init__.py +0 -0
- karrio/server/user/models.py +218 -0
- karrio/server/user/serializers.py +47 -0
- karrio/server/user/templates/registration/login.html +108 -0
- karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
- karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
- karrio/server/user/tests.py +3 -0
- karrio/server/user/urls.py +10 -0
- karrio/server/user/utils.py +60 -0
- karrio/server/user/views.py +9 -0
- karrio_server_core-2025.5.dist-info/METADATA +32 -0
- karrio_server_core-2025.5.dist-info/RECORD +213 -0
- karrio_server_core-2025.5.dist-info/WHEEL +5 -0
- karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Generated by Django migration to rename customer_number to billing_number
|
|
2
|
+
# for DHL Parcel DE connections. This keeps existing configuration values
|
|
3
|
+
# when the field was renamed in the settings schema.
|
|
4
|
+
|
|
5
|
+
from django.db import migrations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def rename_customer_number_to_billing_number(apps, schema_editor):
|
|
9
|
+
"""
|
|
10
|
+
Find all dhl_parcel_de carrier connections that have customer_number
|
|
11
|
+
and rename it to billing_number to preserve the configuration value.
|
|
12
|
+
"""
|
|
13
|
+
Carrier = apps.get_model("providers", "Carrier")
|
|
14
|
+
db_alias = schema_editor.connection.alias
|
|
15
|
+
|
|
16
|
+
# Find all dhl_parcel_de carriers
|
|
17
|
+
dhl_parcel_de_carriers = Carrier.objects.using(db_alias).filter(
|
|
18
|
+
carrier_code="dhl_parcel_de"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
for carrier in dhl_parcel_de_carriers:
|
|
22
|
+
credentials = carrier.credentials or {}
|
|
23
|
+
|
|
24
|
+
# Check if customer_number exists and billing_number doesn't
|
|
25
|
+
if credentials.get("customer_number") and not credentials.get("billing_number"):
|
|
26
|
+
# Copy customer_number value to billing_number
|
|
27
|
+
credentials["billing_number"] = credentials["customer_number"]
|
|
28
|
+
# Remove old customer_number field
|
|
29
|
+
del credentials["customer_number"]
|
|
30
|
+
|
|
31
|
+
carrier.credentials = credentials
|
|
32
|
+
carrier.save(using=db_alias, update_fields=["credentials"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rename_billing_number_to_customer_number(apps, schema_editor):
|
|
36
|
+
"""
|
|
37
|
+
Reverse migration: rename billing_number back to customer_number.
|
|
38
|
+
"""
|
|
39
|
+
Carrier = apps.get_model("providers", "Carrier")
|
|
40
|
+
db_alias = schema_editor.connection.alias
|
|
41
|
+
|
|
42
|
+
dhl_parcel_de_carriers = Carrier.objects.using(db_alias).filter(
|
|
43
|
+
carrier_code="dhl_parcel_de"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
for carrier in dhl_parcel_de_carriers:
|
|
47
|
+
credentials = carrier.credentials or {}
|
|
48
|
+
|
|
49
|
+
# Check if billing_number exists and customer_number doesn't
|
|
50
|
+
if credentials.get("billing_number") and not credentials.get("customer_number"):
|
|
51
|
+
# Copy billing_number value back to customer_number
|
|
52
|
+
credentials["customer_number"] = credentials["billing_number"]
|
|
53
|
+
# Remove the billing_number field
|
|
54
|
+
del credentials["billing_number"]
|
|
55
|
+
|
|
56
|
+
carrier.credentials = credentials
|
|
57
|
+
carrier.save(using=db_alias, update_fields=["credentials"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Migration(migrations.Migration):
|
|
61
|
+
|
|
62
|
+
dependencies = [
|
|
63
|
+
("providers", "0085_populate_dhl_parcel_de_oauth_credentials"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
operations = [
|
|
67
|
+
migrations.RunPython(
|
|
68
|
+
rename_customer_number_to_billing_number,
|
|
69
|
+
rename_billing_number_to_customer_number,
|
|
70
|
+
),
|
|
71
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
from karrio.server.providers.models.sheet import RateSheet
|
|
3
|
+
from karrio.server.providers.models.config import CarrierConfig
|
|
4
|
+
from karrio.server.providers.models.service import ServiceLevel
|
|
5
|
+
from karrio.server.providers.models.template import LabelTemplate
|
|
6
|
+
from karrio.server.providers.models.utils import has_rate_sheet
|
|
7
|
+
from karrio.server.core.models.base import register_model
|
|
8
|
+
from karrio.server.providers.models.carrier import (
|
|
9
|
+
Carrier,
|
|
10
|
+
COUNTRIES,
|
|
11
|
+
CURRENCIES,
|
|
12
|
+
WEIGHT_UNITS,
|
|
13
|
+
DIMENSION_UNITS,
|
|
14
|
+
CAPABILITIES_CHOICES,
|
|
15
|
+
create_carrier_proxy,
|
|
16
|
+
)
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
import functools
|
|
3
|
+
import django.conf as conf
|
|
4
|
+
import django.forms as forms
|
|
5
|
+
import django.db.models as models
|
|
6
|
+
from django.db.models import Q, OuterRef, Subquery, Case, When, IntegerField
|
|
7
|
+
|
|
8
|
+
import karrio.lib as lib
|
|
9
|
+
import karrio.sdk as karrio
|
|
10
|
+
import karrio.core.units as units
|
|
11
|
+
import django.core.cache as caching
|
|
12
|
+
import karrio.api.gateway as gateway
|
|
13
|
+
import karrio.server.core.models as core
|
|
14
|
+
import karrio.server.core.fields as fields
|
|
15
|
+
import karrio.server.core.datatypes as datatypes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
COUNTRIES = [(c.name, c.name) for c in units.Country]
|
|
19
|
+
CURRENCIES = [(c.name, c.name) for c in units.Currency]
|
|
20
|
+
WEIGHT_UNITS = [(c.name, c.name) for c in units.WeightUnit]
|
|
21
|
+
DIMENSION_UNITS = [(c.name, c.name) for c in units.DimensionUnit]
|
|
22
|
+
CAPABILITIES_CHOICES = [(c, c) for c in units.CarrierCapabilities.get_capabilities()]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CarrierQuerySet(models.QuerySet):
|
|
26
|
+
def resolve_config_for(self, context):
|
|
27
|
+
from karrio.server.providers.models.config import CarrierConfig
|
|
28
|
+
|
|
29
|
+
if context is None:
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
user = getattr(context, "user", None)
|
|
33
|
+
org = getattr(context, "org", None)
|
|
34
|
+
|
|
35
|
+
if isinstance(context, dict):
|
|
36
|
+
user = user or context.get("user")
|
|
37
|
+
org = org or context.get("org")
|
|
38
|
+
|
|
39
|
+
# 1. Define what "My Config" looks like (User or Org specific)
|
|
40
|
+
my_config_filter = Q()
|
|
41
|
+
if org:
|
|
42
|
+
my_config_filter = Q(org=org)
|
|
43
|
+
elif user and getattr(user, "is_authenticated", False):
|
|
44
|
+
my_config_filter = Q(created_by=user)
|
|
45
|
+
|
|
46
|
+
# 2. Define what "System Default" looks like
|
|
47
|
+
system_default_filter = Q(created_by__is_staff=True)
|
|
48
|
+
if hasattr(CarrierConfig, "org"):
|
|
49
|
+
system_default_filter &= Q(org__isnull=True)
|
|
50
|
+
|
|
51
|
+
# 3. Build the Subquery - only use priority if we have a user/org filter
|
|
52
|
+
config_filter = (
|
|
53
|
+
my_config_filter | system_default_filter
|
|
54
|
+
if my_config_filter
|
|
55
|
+
else system_default_filter
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
config_query = CarrierConfig.objects.filter(carrier=OuterRef("pk")).filter(
|
|
59
|
+
config_filter
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if my_config_filter:
|
|
63
|
+
# Prioritize user/org config over system default
|
|
64
|
+
config_query = config_query.annotate(
|
|
65
|
+
priority=Case(
|
|
66
|
+
When(my_config_filter, then=0),
|
|
67
|
+
default=1,
|
|
68
|
+
output_field=IntegerField(),
|
|
69
|
+
)
|
|
70
|
+
).order_by("priority")
|
|
71
|
+
|
|
72
|
+
# 4. Annotate the queryset
|
|
73
|
+
return self.annotate(
|
|
74
|
+
_computed_config=Subquery(config_query.values("config")[:1])
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Manager(models.Manager):
|
|
79
|
+
def get_queryset(self):
|
|
80
|
+
return (
|
|
81
|
+
CarrierQuerySet(self.model, using=self._db)
|
|
82
|
+
.select_related(
|
|
83
|
+
"created_by",
|
|
84
|
+
"rate_sheet",
|
|
85
|
+
*(("link",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
|
|
86
|
+
)
|
|
87
|
+
.prefetch_related(
|
|
88
|
+
"active_users",
|
|
89
|
+
*(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def resolve_config_for(self, context):
|
|
94
|
+
return self.get_queryset().resolve_config_for(context)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CarrierManager(Manager):
|
|
98
|
+
def get_queryset(self):
|
|
99
|
+
return super().get_queryset().filter(is_system=False)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SystemCarrierManager(models.Manager):
|
|
103
|
+
def get_queryset(self):
|
|
104
|
+
return (
|
|
105
|
+
CarrierQuerySet(self.model, using=self._db)
|
|
106
|
+
.prefetch_related(
|
|
107
|
+
*(("active_orgs",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
|
|
108
|
+
)
|
|
109
|
+
.select_related(
|
|
110
|
+
"created_by",
|
|
111
|
+
*(("link",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
|
|
112
|
+
)
|
|
113
|
+
.filter(is_system=True)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def resolve_config_for(self, context):
|
|
117
|
+
return self.get_queryset().resolve_config_for(context)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@core.register_model
|
|
121
|
+
class Carrier(core.OwnedEntity):
|
|
122
|
+
class Meta:
|
|
123
|
+
ordering = ["test_mode", "-created_at"]
|
|
124
|
+
|
|
125
|
+
CONTEXT_RELATIONS = ["rate_sheet"]
|
|
126
|
+
|
|
127
|
+
objects = Manager()
|
|
128
|
+
user_carriers = CarrierManager()
|
|
129
|
+
system_carriers = SystemCarrierManager()
|
|
130
|
+
|
|
131
|
+
id = models.CharField(
|
|
132
|
+
max_length=50,
|
|
133
|
+
primary_key=True,
|
|
134
|
+
default=functools.partial(core.uuid, prefix="car_"),
|
|
135
|
+
editable=False,
|
|
136
|
+
)
|
|
137
|
+
carrier_code = models.CharField(
|
|
138
|
+
max_length=100,
|
|
139
|
+
db_index=True,
|
|
140
|
+
default="generic",
|
|
141
|
+
help_text="eg. dhl_express, fedex, ups, usps, ...",
|
|
142
|
+
)
|
|
143
|
+
carrier_id = models.CharField(
|
|
144
|
+
max_length=150,
|
|
145
|
+
db_index=True,
|
|
146
|
+
help_text="eg. canadapost, dhl_express, fedex, purolator_courrier, ups...",
|
|
147
|
+
)
|
|
148
|
+
credentials = models.JSONField(
|
|
149
|
+
default=core.field_default({}),
|
|
150
|
+
help_text="Carrier connection credentials",
|
|
151
|
+
)
|
|
152
|
+
capabilities = fields.MultiChoiceField(
|
|
153
|
+
choices=datatypes.CAPABILITIES_CHOICES,
|
|
154
|
+
default=core.field_default([]),
|
|
155
|
+
help_text="Select the capabilities of the carrier that you want to enable",
|
|
156
|
+
)
|
|
157
|
+
metadata = models.JSONField(
|
|
158
|
+
blank=True,
|
|
159
|
+
null=True,
|
|
160
|
+
default=core.field_default({}),
|
|
161
|
+
help_text="User defined metadata",
|
|
162
|
+
)
|
|
163
|
+
active = models.BooleanField(
|
|
164
|
+
default=True,
|
|
165
|
+
db_index=True,
|
|
166
|
+
help_text="Disable/Hide carrier from clients",
|
|
167
|
+
)
|
|
168
|
+
is_system = models.BooleanField(
|
|
169
|
+
default=False,
|
|
170
|
+
db_index=True,
|
|
171
|
+
help_text="Determine that the carrier connection is available system wide.",
|
|
172
|
+
)
|
|
173
|
+
test_mode = models.BooleanField(
|
|
174
|
+
default=True,
|
|
175
|
+
db_column="test_mode",
|
|
176
|
+
help_text="Toggle carrier connection mode",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
created_by = models.ForeignKey(
|
|
180
|
+
conf.settings.AUTH_USER_MODEL,
|
|
181
|
+
blank=True,
|
|
182
|
+
null=True,
|
|
183
|
+
on_delete=models.CASCADE,
|
|
184
|
+
editable=False,
|
|
185
|
+
)
|
|
186
|
+
active_users = models.ManyToManyField(
|
|
187
|
+
conf.settings.AUTH_USER_MODEL,
|
|
188
|
+
blank=True,
|
|
189
|
+
related_name="connection_users",
|
|
190
|
+
)
|
|
191
|
+
rate_sheet = models.ForeignKey(
|
|
192
|
+
"RateSheet",
|
|
193
|
+
null=True,
|
|
194
|
+
blank=True,
|
|
195
|
+
on_delete=models.SET_NULL,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def resolve_context_data(cls, queryset, context):
|
|
200
|
+
"""Apply context-aware carrier config resolution."""
|
|
201
|
+
return queryset.resolve_config_for(context)
|
|
202
|
+
|
|
203
|
+
def __str__(self):
|
|
204
|
+
return self.carrier_id
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def object_type(self):
|
|
208
|
+
return "carrier-connection"
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def ext(self) -> str:
|
|
212
|
+
return (
|
|
213
|
+
"generic"
|
|
214
|
+
if "custom_carrier_name" in self.credentials
|
|
215
|
+
else lib.failsafe(lambda: self.carrier_code) or "generic"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def carrier_name(self):
|
|
220
|
+
return (
|
|
221
|
+
"generic"
|
|
222
|
+
if "custom_carrier_name" in self.credentials
|
|
223
|
+
else lib.failsafe(lambda: self.carrier_code) or "generic"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def display_name(self):
|
|
228
|
+
import karrio.references as references
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
self.credentials.get("display_name")
|
|
232
|
+
or references.REFERENCES["carriers"].get(self.ext)
|
|
233
|
+
or "generic"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def carrier_config(self):
|
|
238
|
+
return self.__class__.resolve_config(self)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def config(self) -> dict:
|
|
242
|
+
if hasattr(self, "_computed_config"):
|
|
243
|
+
annotated_config = self._computed_config
|
|
244
|
+
if annotated_config is not None:
|
|
245
|
+
return annotated_config
|
|
246
|
+
# If the annotation didn't resolve (eg. context missing), fall back to DB lookup.
|
|
247
|
+
resolved_config = getattr(self.carrier_config, "config", None)
|
|
248
|
+
if resolved_config is not None:
|
|
249
|
+
return resolved_config
|
|
250
|
+
# Return empty dict if no config is found - do NOT fallback to credentials
|
|
251
|
+
return {}
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def services(self) -> typing.Optional[typing.List[dict]]:
|
|
255
|
+
|
|
256
|
+
if self.rate_sheet is None:
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
return self.rate_sheet.services.all()
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def data(self) -> datatypes.CarrierSettings:
|
|
263
|
+
_computed_data: typing.Dict = dict(
|
|
264
|
+
id=self.id,
|
|
265
|
+
config=self.config,
|
|
266
|
+
test_mode=self.test_mode,
|
|
267
|
+
metadata=self.metadata,
|
|
268
|
+
carrier_id=self.carrier_id,
|
|
269
|
+
carrier_name=self.ext,
|
|
270
|
+
display_name=self.display_name,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if any(self.services or []):
|
|
274
|
+
_computed_data.update(
|
|
275
|
+
services=[forms.model_to_dict(s) for s in self.services]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# override the config with the system config
|
|
279
|
+
if self.is_system and self.carrier_config is None:
|
|
280
|
+
_config = self.__class__.resolve_config(self, is_system_config=True)
|
|
281
|
+
_computed_data.update(config=getattr(_config, "config", None))
|
|
282
|
+
|
|
283
|
+
return datatypes.CarrierSettings.create(
|
|
284
|
+
{
|
|
285
|
+
**self.credentials,
|
|
286
|
+
**_computed_data,
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def gateway(self) -> gateway.Gateway:
|
|
292
|
+
import karrio.server.core.middleware as middleware
|
|
293
|
+
import karrio.server.core.config as system_config
|
|
294
|
+
|
|
295
|
+
_context = middleware.SessionContext.get_current_request()
|
|
296
|
+
_tracer = getattr(_context, "tracer", lib.Tracer())
|
|
297
|
+
_cache = lib.Cache(caching.cache)
|
|
298
|
+
_config = lib.SystemConfig(system_config.config)
|
|
299
|
+
|
|
300
|
+
return karrio.gateway[self.ext].create(
|
|
301
|
+
self.data.to_dict(),
|
|
302
|
+
_tracer,
|
|
303
|
+
_cache,
|
|
304
|
+
_config,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def resolve_config(
|
|
309
|
+
carrier, is_user_config: bool = False, is_system_config: bool = False
|
|
310
|
+
):
|
|
311
|
+
import karrio.server.serializers as serializers
|
|
312
|
+
import karrio.server.core.middleware as middleware
|
|
313
|
+
from django.contrib.auth.models import AnonymousUser
|
|
314
|
+
from karrio.server.providers.models.config import CarrierConfig
|
|
315
|
+
|
|
316
|
+
if carrier.id is None:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
_ctx = serializers.get_object_context(carrier)
|
|
320
|
+
ctx = lib.identity(
|
|
321
|
+
_ctx
|
|
322
|
+
if (_ctx.user or _ctx.org)
|
|
323
|
+
else lib.failsafe(lambda: middleware.SessionContext.get_current_request())
|
|
324
|
+
)
|
|
325
|
+
has_ctx_user = lib.identity(
|
|
326
|
+
ctx and ((ctx.user and not isinstance(ctx.user, AnonymousUser)) or ctx.org)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
queryset = lib.identity(
|
|
330
|
+
CarrierConfig.objects.filter(carrier=carrier)
|
|
331
|
+
if carrier.is_system
|
|
332
|
+
else CarrierConfig.access_by(ctx).filter(carrier=carrier)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if carrier.is_system:
|
|
336
|
+
_config = queryset.filter(
|
|
337
|
+
created_by__is_staff=True,
|
|
338
|
+
**({"org": None} if hasattr(carrier, "org") else {}),
|
|
339
|
+
).first()
|
|
340
|
+
|
|
341
|
+
if has_ctx_user:
|
|
342
|
+
return queryset.filter(
|
|
343
|
+
**(
|
|
344
|
+
{"org": (None if is_system_config else ctx.org)}
|
|
345
|
+
if hasattr(carrier, "org")
|
|
346
|
+
else {"created_by": ctx.user}
|
|
347
|
+
)
|
|
348
|
+
).first() or (None if is_user_config else _config)
|
|
349
|
+
|
|
350
|
+
return _config
|
|
351
|
+
|
|
352
|
+
return queryset.first()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def create_carrier_proxy(carrier_name: str, display_name):
|
|
356
|
+
class _Manager(Manager):
|
|
357
|
+
def get_queryset(self):
|
|
358
|
+
return super().get_queryset().filter(carrier_code=carrier_name)
|
|
359
|
+
|
|
360
|
+
class _CarrierManager(CarrierManager):
|
|
361
|
+
def get_queryset(self):
|
|
362
|
+
return super().get_queryset().filter(carrier_code=carrier_name)
|
|
363
|
+
|
|
364
|
+
class _SystemCarrierManager(SystemCarrierManager):
|
|
365
|
+
def get_queryset(self):
|
|
366
|
+
return super().get_queryset().filter(carrier_code=carrier_name)
|
|
367
|
+
|
|
368
|
+
return type(
|
|
369
|
+
f"{carrier_name}Connection",
|
|
370
|
+
(Carrier,),
|
|
371
|
+
{
|
|
372
|
+
"Meta": type(
|
|
373
|
+
"Meta",
|
|
374
|
+
(),
|
|
375
|
+
{
|
|
376
|
+
"proxy": True,
|
|
377
|
+
"__module__": __name__,
|
|
378
|
+
"verbose_name": f"{display_name} Connection",
|
|
379
|
+
"verbose_name_plural": f"{display_name} Connections",
|
|
380
|
+
},
|
|
381
|
+
),
|
|
382
|
+
"__module__": __name__,
|
|
383
|
+
"objects": _Manager(),
|
|
384
|
+
"user_carriers": _CarrierManager(),
|
|
385
|
+
"system_carriers": _SystemCarrierManager(),
|
|
386
|
+
},
|
|
387
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import django.db.models as models
|
|
3
|
+
import karrio.server.core.models as core
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@core.register_model
|
|
7
|
+
class CarrierConfig(core.OwnedEntity):
|
|
8
|
+
class Meta:
|
|
9
|
+
db_table = "carrier-config"
|
|
10
|
+
verbose_name = "Carrier Config"
|
|
11
|
+
verbose_name_plural = "Carrier Configs"
|
|
12
|
+
ordering = ["-created_at"]
|
|
13
|
+
|
|
14
|
+
id = models.CharField(
|
|
15
|
+
max_length=50,
|
|
16
|
+
editable=False,
|
|
17
|
+
primary_key=True,
|
|
18
|
+
default=functools.partial(core.uuid, prefix="cfg_"),
|
|
19
|
+
)
|
|
20
|
+
carrier = models.ForeignKey(
|
|
21
|
+
"Carrier",
|
|
22
|
+
null=False,
|
|
23
|
+
related_name="configs",
|
|
24
|
+
on_delete=models.CASCADE,
|
|
25
|
+
)
|
|
26
|
+
config = models.JSONField(
|
|
27
|
+
null=False,
|
|
28
|
+
blank=False,
|
|
29
|
+
default=core.field_default({}),
|
|
30
|
+
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import django.db.models as models
|
|
3
|
+
import django.core.validators as validators
|
|
4
|
+
|
|
5
|
+
import karrio.server.core.models as core
|
|
6
|
+
import karrio.server.core.datatypes as datatypes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@core.register_model
|
|
10
|
+
class ServiceLevel(core.OwnedEntity):
|
|
11
|
+
class Meta:
|
|
12
|
+
db_table = "service-level"
|
|
13
|
+
verbose_name = "Service Level"
|
|
14
|
+
verbose_name_plural = "Service Levels"
|
|
15
|
+
ordering = ["-created_at"]
|
|
16
|
+
|
|
17
|
+
id = models.CharField(
|
|
18
|
+
max_length=50,
|
|
19
|
+
primary_key=True,
|
|
20
|
+
default=functools.partial(core.uuid, prefix="svc_"),
|
|
21
|
+
editable=False,
|
|
22
|
+
)
|
|
23
|
+
service_name = models.CharField(max_length=50)
|
|
24
|
+
service_code = models.CharField(
|
|
25
|
+
max_length=50, validators=[validators.RegexValidator(r"^[a-z0-9_]+$")]
|
|
26
|
+
)
|
|
27
|
+
carrier_service_code = models.CharField(max_length=50, null=True, blank=True)
|
|
28
|
+
description = models.CharField(max_length=250, null=True, blank=True)
|
|
29
|
+
active = models.BooleanField(null=True, default=True)
|
|
30
|
+
|
|
31
|
+
currency = models.CharField(
|
|
32
|
+
max_length=4, choices=datatypes.CURRENCIES, null=True, blank=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
transit_days = models.IntegerField(blank=True, null=True)
|
|
36
|
+
transit_time = models.FloatField(blank=True, null=True)
|
|
37
|
+
|
|
38
|
+
max_width = models.FloatField(blank=True, null=True)
|
|
39
|
+
max_height = models.FloatField(blank=True, null=True)
|
|
40
|
+
max_length = models.FloatField(blank=True, null=True)
|
|
41
|
+
dimension_unit = models.CharField(
|
|
42
|
+
max_length=2, choices=datatypes.DIMENSION_UNITS, null=True, blank=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
min_weight = models.FloatField(blank=True, null=True)
|
|
46
|
+
max_weight = models.FloatField(blank=True, null=True)
|
|
47
|
+
weight_unit = models.CharField(
|
|
48
|
+
max_length=2, choices=datatypes.WEIGHT_UNITS, null=True, blank=True
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
domicile = models.BooleanField(null=True)
|
|
52
|
+
international = models.BooleanField(null=True)
|
|
53
|
+
|
|
54
|
+
zones = models.JSONField(blank=True, null=True, default=core.field_default([]))
|
|
55
|
+
metadata = models.JSONField(blank=True, null=True, default=core.field_default({}))
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return f"{self.id} | {self.service_name}"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def object_type(self):
|
|
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
|