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.
Files changed (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,3 @@
1
+ from django.shortcuts import render
2
+
3
+ # Create your views here.
@@ -0,0 +1,75 @@
1
+ from django.conf import settings
2
+ from drf_spectacular.types import *
3
+ from drf_spectacular.utils import *
4
+ from drf_spectacular.extensions import OpenApiAuthenticationExtension
5
+
6
+
7
+ class JWTAuthentication(OpenApiAuthenticationExtension):
8
+ target_class = "karrio.server.core.authentication.JWTAuthentication"
9
+ name = "JWT"
10
+
11
+ def get_security_definition(self, auto_schema):
12
+ return {
13
+ "in": "header",
14
+ "type": "apiKey",
15
+ "scheme": "bearer",
16
+ "bearerFormat": "JWT",
17
+ "name": "Authorization",
18
+ "description": "Authorization: Bearer xxx.xxx.xxx",
19
+ }
20
+
21
+
22
+ class TokenAuthentication(OpenApiAuthenticationExtension):
23
+ target_class = "karrio.server.core.authentication.TokenAuthentication"
24
+ name = "Token"
25
+
26
+ def get_security_definition(self, auto_schema):
27
+ return {
28
+ "type": "apiKey",
29
+ "in": "header",
30
+ "name": "Authorization",
31
+ "description": "Authorization: Token key_xxxxxxxx",
32
+ }
33
+
34
+
35
+ class TokenBasicAuthentication(OpenApiAuthenticationExtension):
36
+ target_class = "karrio.server.core.authentication.TokenBasicAuthentication"
37
+ name = "TokenBasic"
38
+
39
+ def get_security_definition(self, auto_schema):
40
+ return {
41
+ "type": "http",
42
+ "scheme": "basic",
43
+ "name": "Authorization",
44
+ "description": "-u key_xxxxxxxx:",
45
+ }
46
+
47
+
48
+ class OAuth2Authentication(OpenApiAuthenticationExtension):
49
+ target_class = "karrio.server.core.authentication.OAuth2Authentication"
50
+ name = "OAuth2"
51
+
52
+ def get_security_definition(self, auto_schema):
53
+ return {
54
+ "type": "oauth2",
55
+ "in": "header",
56
+ "name": "Authorization",
57
+ "flows": {
58
+ "authorizationCode": {
59
+ "authorizationUrl": "/oauth/authorize/",
60
+ "tokenUrl": "/oauth/token/",
61
+ "scopes": settings.OAUTH2_PROVIDER["SCOPES"],
62
+ }
63
+ },
64
+ "description": "Authorization: Bearer xxxxxxxx",
65
+ }
66
+
67
+
68
+ def custom_postprocessing_hook(result, generator, request, public):
69
+ if "docs" in request.query_params:
70
+ for path in result["paths"].values():
71
+ for method in path.values():
72
+ if "x-operationId" in method:
73
+ method["operationId"] = method["x-operationId"]
74
+
75
+ return result
@@ -0,0 +1 @@
1
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
@@ -0,0 +1,364 @@
1
+ import functools
2
+ from django import forms
3
+ from django.db import models
4
+ from django.contrib import admin
5
+ from django.conf import settings
6
+ from django.contrib.auth import get_user_model
7
+ from django.utils.translation import gettext_lazy as _
8
+
9
+ import karrio.lib as lib
10
+ import karrio.references as ref
11
+ import karrio.server.core.utils as utils
12
+ import karrio.server.serializers as serializers
13
+ import karrio.server.core.dataunits as dataunits
14
+ import karrio.server.providers.models as providers
15
+
16
+ User = get_user_model()
17
+
18
+
19
+ def model_admin(ext: str, carrierProxy):
20
+ references = dataunits.contextual_reference(reduced=False)
21
+ class_name = carrierProxy.__name__
22
+ connection_fields = references["connection_fields"].get(ext) or {}
23
+ connection_configs = references["connection_configs"].get(ext) or {}
24
+ carrier_services = (references["services"].get(ext) or {}).keys()
25
+ carrier_options = (references["options"].get(ext) or {}).keys()
26
+
27
+ class _Form(forms.ModelForm):
28
+
29
+ for key, field in connection_fields.items():
30
+ if field["type"] == "boolean":
31
+ locals()[key] = forms.NullBooleanField(
32
+ required=field.get("required", False),
33
+ initial=None,
34
+ )
35
+
36
+ elif field["type"] == "integer":
37
+ locals()[key] = forms.IntegerField(
38
+ required=field.get("required", False),
39
+ )
40
+
41
+ elif field["type"] == "float":
42
+ locals()[key] = forms.FloatField(
43
+ required=field.get("required", False),
44
+ )
45
+
46
+ elif field["type"] == "string" and any(field.get("enum", [])):
47
+ locals()[key] = forms.ChoiceField(
48
+ choices=[(None, ""), *[(_, _) for _ in field.get("enum", [])]],
49
+ widget=forms.Select(attrs={"class": "vTextField"}),
50
+ required=field.get("required", False),
51
+ initial=field.get("default"),
52
+ )
53
+
54
+ elif field["type"] == "string":
55
+ locals()[key] = forms.CharField(
56
+ required=field.get("required", False),
57
+ )
58
+
59
+ else:
60
+ pass
61
+
62
+ for key, field in connection_configs.items():
63
+
64
+ if key == "shipping_services":
65
+ shipping_services = forms.MultipleChoiceField(
66
+ choices=[(_, _) for _ in carrier_services],
67
+ widget=forms.SelectMultiple(attrs={"class": "vTextField"}),
68
+ required=False,
69
+ initial=None,
70
+ )
71
+ continue
72
+
73
+ if key == "shipping_options":
74
+ shipping_options = forms.MultipleChoiceField(
75
+ choices=[(_, _) for _ in carrier_options],
76
+ widget=forms.SelectMultiple(attrs={"class": "vTextField"}),
77
+ required=False,
78
+ initial=None,
79
+ )
80
+ continue
81
+
82
+ if field["type"] == "boolean":
83
+ locals()[key] = forms.NullBooleanField(
84
+ required=False,
85
+ initial=None,
86
+ )
87
+
88
+ elif field["type"] == "integer":
89
+ locals()[key] = forms.IntegerField(
90
+ required=False,
91
+ )
92
+
93
+ elif field["type"] == "float":
94
+ locals()[key] = forms.FloatField(
95
+ required=False,
96
+ )
97
+
98
+ elif field["type"] == "string" and any(field.get("enum", [])):
99
+ locals()[key] = forms.ChoiceField(
100
+ choices=[(None, ""), *[(_, _) for _ in field.get("enum", [])]],
101
+ widget=forms.Select(attrs={"class": "vTextField"}),
102
+ required=field.get("required", False),
103
+ initial=field.get("default"),
104
+ )
105
+
106
+ elif field["type"] == "string":
107
+ locals()[key] = forms.CharField(
108
+ required=False,
109
+ )
110
+
111
+ else:
112
+ pass
113
+
114
+ class Meta:
115
+ model = carrierProxy
116
+ fields = "__all__"
117
+
118
+ def __init__(self, *args, instance: providers.Carrier = None, **kwargs):
119
+ if instance is not None:
120
+ kwargs.update({"instance": instance})
121
+ credentials = instance.credentials
122
+ config = providers.Carrier.resolve_config(
123
+ instance, is_system_config=True
124
+ )
125
+
126
+ for key in [
127
+ _ for _ in self.base_fields.keys() if _ in connection_fields.keys()
128
+ ]:
129
+ self.base_fields[key].initial = (
130
+ None if credentials is None else credentials.get(key)
131
+ )
132
+
133
+ for key in [
134
+ _ for _ in self.base_fields.keys() if _ in connection_configs.keys()
135
+ ]:
136
+ self.base_fields[key].initial = (
137
+ None if config is None else config.config.get(key)
138
+ )
139
+
140
+ super(_Form, self).__init__(*args, **kwargs)
141
+
142
+ def save(self, commit: bool = True):
143
+ config_data = lib.to_dict(
144
+ {key: self.cleaned_data.get(key) for key in connection_configs.keys()}
145
+ )
146
+ credentials_data = lib.to_dict(
147
+ {key: self.cleaned_data.get(key) for key in connection_fields.keys()}
148
+ )
149
+
150
+ for key in connection_fields.keys():
151
+ if key in self.cleaned_data:
152
+ self.cleaned_data.pop(key)
153
+
154
+ for key in connection_configs.keys():
155
+ if key in self.cleaned_data:
156
+ self.cleaned_data.pop(key)
157
+
158
+ carrier = super(_Form, self).save(commit)
159
+
160
+ if any(connection_fields.keys()) and (commit or carrier.pk is not None):
161
+ carrier.credentials = serializers.process_dictionaries_mutations(
162
+ ["credentials"], credentials_data, carrier
163
+ )
164
+ carrier.save()
165
+
166
+ if any(connection_configs.keys()) and (commit or carrier.pk is not None):
167
+ config = providers.Carrier.resolve_config(
168
+ carrier, is_system_config=True
169
+ )
170
+ created_by = getattr(config, "created_by", self.request.user)
171
+ config_value = lib.to_dict(
172
+ serializers.process_dictionaries_mutations(
173
+ ["config"], config_data, config
174
+ )
175
+ )
176
+
177
+ if config is None and len(config_value.keys()) == 0:
178
+ # Skip configuration persistence...
179
+ return carrier
180
+
181
+ # Save or update the carrier config...
182
+ lib.identity(
183
+ providers.CarrierConfig.objects.create(
184
+ created_by=created_by,
185
+ carrier=carrier,
186
+ config=config_value,
187
+ )
188
+ if config is None
189
+ else providers.CarrierConfig.objects.filter(carrier=carrier).update(
190
+ config=config_value
191
+ )
192
+ )
193
+
194
+ return carrier
195
+
196
+ _form: forms.ModelForm = type(f"{class_name}AdminForm", (_Form,), {})
197
+ _fields = _form.base_fields.keys()
198
+
199
+ class _Admin(admin.ModelAdmin):
200
+ form = _form
201
+ inlines = []
202
+ list_display = ("__str__", "test_mode", "active")
203
+ exclude = ["active_users"]
204
+ fieldsets = [
205
+ (
206
+ None,
207
+ {
208
+ "fields": [
209
+ _
210
+ for _ in _fields
211
+ if _
212
+ not in [
213
+ *connection_fields.keys(),
214
+ *connection_configs.keys(),
215
+ "carrier_code",
216
+ "credentials",
217
+ "active_users",
218
+ "is_system",
219
+ "rate_sheet",
220
+ "metadata",
221
+ ]
222
+ ],
223
+ },
224
+ ),
225
+ ]
226
+
227
+ if any(connection_fields.keys()):
228
+ fieldsets += [
229
+ ( # type: ignore
230
+ "Connection Fields",
231
+ {
232
+ "fields": [_ for _ in connection_fields.keys() if _ in _fields],
233
+ },
234
+ ),
235
+ ]
236
+
237
+ if any(connection_configs.keys()):
238
+ fieldsets += [
239
+ ( # type: ignore
240
+ "Connection Config",
241
+ {
242
+ "fields": [
243
+ _ for _ in connection_configs.keys() if _ in _fields
244
+ ],
245
+ },
246
+ ),
247
+ ]
248
+
249
+ formfield_overrides = {
250
+ models.CharField: {
251
+ "widget": forms.TextInput(
252
+ attrs={
253
+ "type": "text",
254
+ "readonly": "true",
255
+ "class": "vTextField",
256
+ "data - lpignore": "true",
257
+ "autocomplete": "keep-off",
258
+ "onfocus": "this.removeAttribute('readonly');",
259
+ }
260
+ )
261
+ },
262
+ }
263
+
264
+ if settings.MULTI_ORGANIZATIONS:
265
+
266
+ class ActiveOrgInline(admin.TabularInline):
267
+ model = carrierProxy.active_orgs.through
268
+ verbose_name = "Activated for organization"
269
+ extra = 0
270
+
271
+ def get_formset(self, request, obj, **kwargs):
272
+ from karrio.server.orgs.models import Organization
273
+
274
+ initial = []
275
+ orgs = Organization.objects.filter(
276
+ users__id=request.user.id
277
+ ).distinct()
278
+ self.max_num = orgs.count()
279
+
280
+ if obj is None and request.method == "GET":
281
+ self.extra = orgs.count()
282
+ initial += [{"organization": o.id} for o in orgs]
283
+
284
+ formset = super().get_formset(request, obj, **kwargs)
285
+ formset.__init__ = functools.partialmethod(
286
+ formset.__init__, initial=initial
287
+ )
288
+ organization_field = formset.form.base_fields["organization"]
289
+ organization_field.queryset = orgs
290
+ organization_field.widget.can_add_related = False
291
+ organization_field.widget.can_change_related = False
292
+
293
+ return formset
294
+
295
+ inlines += [ActiveOrgInline]
296
+
297
+ else:
298
+
299
+ class ActiveUserInline(admin.TabularInline):
300
+ model = carrierProxy.active_users.through
301
+ exta = 0
302
+ verbose_name = "Activated for user"
303
+
304
+ def get_formset(self, request, obj, **kwargs):
305
+ initial = []
306
+ users = User.objects.all()
307
+ self.max_num = users.count()
308
+
309
+ if obj is None and request.method == "GET":
310
+ self.extra = users.count()
311
+ initial += [{"user": o.id} for o in users]
312
+
313
+ formset = super().get_formset(request, obj, **kwargs)
314
+ formset.__init__ = functools.partialmethod(
315
+ formset.__init__, initial=initial
316
+ )
317
+ user_field = formset.form.base_fields["user"]
318
+ user_field.queryset = users
319
+ user_field.widget.can_add_related = False
320
+ user_field.widget.can_change_related = False
321
+
322
+ return formset
323
+
324
+ inlines += [ActiveUserInline]
325
+
326
+ def get_queryset(self, request):
327
+ query = super().get_queryset(request)
328
+ return query.filter(models.Q(is_system=True) | models.Q(created_by=None))
329
+
330
+ def get_form(self, request, *args, **kwargs):
331
+ form = super(_Admin, self).get_form(request, *args, **kwargs)
332
+ form.request = request
333
+
334
+ # Customize capabilities options specific to a carrier.
335
+ raw_capabilities = ref.get_carrier_capabilities(ext)
336
+ form.base_fields["capabilities"].choices = [
337
+ (c, c) for c in raw_capabilities
338
+ ]
339
+ form.base_fields["capabilities"].initial = raw_capabilities
340
+
341
+ return form
342
+
343
+ def save_model(self, request, obj, form, change):
344
+ obj.is_system = True
345
+ obj.carrier_code = ext
346
+ return super().save_model(request, obj, form, change)
347
+
348
+ return type(f"{class_name}Admin", (_Admin,), {})
349
+
350
+
351
+ @admin.register(providers.LabelTemplate)
352
+ class LabelTemplateAdmin(admin.ModelAdmin):
353
+ def has_module_permission(self, request):
354
+ return False
355
+
356
+
357
+ @utils.skip_on_commands(["loaddata", "migrate", "makemigrations", "shell"])
358
+ def register_carrier_admins():
359
+ for carrier_name, display_name in ref.REFERENCES["carriers"].items():
360
+ proxy = providers.create_carrier_proxy(carrier_name, display_name)
361
+ admin.site.register(proxy, model_admin(carrier_name, proxy))
362
+
363
+
364
+ register_carrier_admins()
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ProvidersConfig(AppConfig):
5
+ name = "karrio.server.providers"
6
+
7
+ def ready(self):
8
+ from karrio.server.providers import signals
9
+
10
+ signals.register_signals()
@@ -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
+ )