arthexis 0.1.26__py3-none-any.whl → 0.1.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/METADATA +16 -11
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/RECORD +39 -38
- config/settings.py +7 -1
- config/settings_helpers.py +176 -1
- config/urls.py +18 -2
- core/admin.py +265 -23
- core/apps.py +6 -2
- core/celery_utils.py +73 -0
- core/models.py +307 -63
- core/system.py +17 -2
- core/tasks.py +304 -129
- core/test_system_info.py +43 -5
- core/tests.py +202 -2
- core/user_data.py +52 -19
- core/views.py +70 -3
- nodes/admin.py +348 -3
- nodes/apps.py +1 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +146 -18
- nodes/tasks.py +1 -1
- nodes/tests.py +181 -48
- nodes/views.py +148 -3
- ocpp/admin.py +1001 -10
- ocpp/consumers.py +572 -7
- ocpp/models.py +499 -33
- ocpp/store.py +406 -40
- ocpp/tasks.py +109 -145
- ocpp/test_rfid.py +73 -2
- ocpp/tests.py +982 -90
- ocpp/urls.py +5 -0
- ocpp/views.py +172 -70
- pages/context_processors.py +2 -0
- pages/models.py +9 -0
- pages/tests.py +166 -18
- pages/urls.py +1 -0
- pages/views.py +66 -3
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -43,6 +43,7 @@ from django.utils import timezone, translation
|
|
|
43
43
|
from django.utils.formats import date_format
|
|
44
44
|
from django.utils.dateparse import parse_datetime
|
|
45
45
|
from django.utils.html import format_html
|
|
46
|
+
from django.utils.text import slugify
|
|
46
47
|
from django.utils.translation import gettext_lazy as _, ngettext
|
|
47
48
|
from django.forms.models import BaseInlineFormSet
|
|
48
49
|
import json
|
|
@@ -813,6 +814,7 @@ class SecurityGroupAdmin(DjangoGroupAdmin):
|
|
|
813
814
|
form = SecurityGroupAdminForm
|
|
814
815
|
fieldsets = ((None, {"fields": ("name", "parent", "users", "permissions")}),)
|
|
815
816
|
filter_horizontal = ("permissions",)
|
|
817
|
+
search_fields = ("name", "parent__name")
|
|
816
818
|
|
|
817
819
|
|
|
818
820
|
class InviteLeadAdmin(EntityModelAdmin):
|
|
@@ -881,6 +883,82 @@ class EnergyAccountRFIDInline(admin.TabularInline):
|
|
|
881
883
|
verbose_name_plural = "RFIDs"
|
|
882
884
|
|
|
883
885
|
|
|
886
|
+
class UserChangeRFIDForm(forms.ModelForm):
|
|
887
|
+
"""Admin change form exposing login RFID assignment."""
|
|
888
|
+
|
|
889
|
+
login_rfid = forms.ModelChoiceField(
|
|
890
|
+
label=_("Login RFID"),
|
|
891
|
+
queryset=RFID.objects.none(),
|
|
892
|
+
required=False,
|
|
893
|
+
help_text=_("Assign an RFID card to this user for RFID logins."),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
class Meta:
|
|
897
|
+
model = User
|
|
898
|
+
fields = "__all__"
|
|
899
|
+
|
|
900
|
+
def __init__(self, *args, **kwargs):
|
|
901
|
+
super().__init__(*args, **kwargs)
|
|
902
|
+
user = self.instance
|
|
903
|
+
field = self.fields["login_rfid"]
|
|
904
|
+
account = getattr(user, "energy_account", None)
|
|
905
|
+
if account is not None:
|
|
906
|
+
queryset = RFID.objects.filter(
|
|
907
|
+
Q(energy_accounts__isnull=True) | Q(energy_accounts=account)
|
|
908
|
+
)
|
|
909
|
+
current = account.rfids.order_by("label_id").first()
|
|
910
|
+
if current:
|
|
911
|
+
field.initial = current.pk
|
|
912
|
+
else:
|
|
913
|
+
queryset = RFID.objects.filter(energy_accounts__isnull=True)
|
|
914
|
+
field.queryset = queryset.order_by("label_id")
|
|
915
|
+
field.empty_label = _("Keep current assignment")
|
|
916
|
+
|
|
917
|
+
def _ensure_energy_account(self, user):
|
|
918
|
+
account = getattr(user, "energy_account", None)
|
|
919
|
+
if account is not None:
|
|
920
|
+
if account.user_id != user.pk:
|
|
921
|
+
account.user = user
|
|
922
|
+
account.save(update_fields=["user"])
|
|
923
|
+
return account
|
|
924
|
+
account = EnergyAccount.objects.filter(user=user).first()
|
|
925
|
+
if account is not None:
|
|
926
|
+
if account.user_id != user.pk:
|
|
927
|
+
account.user = user
|
|
928
|
+
account.save(update_fields=["user"])
|
|
929
|
+
return account
|
|
930
|
+
base_slug = slugify(
|
|
931
|
+
user.username
|
|
932
|
+
or user.get_full_name()
|
|
933
|
+
or user.email
|
|
934
|
+
or (str(user.pk) if user.pk is not None else "")
|
|
935
|
+
)
|
|
936
|
+
if not base_slug:
|
|
937
|
+
base_slug = f"user-{uuid.uuid4().hex[:8]}"
|
|
938
|
+
base_name = base_slug.upper()
|
|
939
|
+
candidate = base_name
|
|
940
|
+
suffix = 1
|
|
941
|
+
while EnergyAccount.objects.filter(name=candidate).exists():
|
|
942
|
+
suffix += 1
|
|
943
|
+
candidate = f"{base_name}-{suffix}"
|
|
944
|
+
return EnergyAccount.objects.create(user=user, name=candidate)
|
|
945
|
+
|
|
946
|
+
def save(self, commit=True):
|
|
947
|
+
user = super().save(commit)
|
|
948
|
+
rfid = self.cleaned_data.get("login_rfid")
|
|
949
|
+
if not rfid:
|
|
950
|
+
return user
|
|
951
|
+
account = self._ensure_energy_account(user)
|
|
952
|
+
if account.pk is None:
|
|
953
|
+
account.save()
|
|
954
|
+
other_accounts = list(rfid.energy_accounts.exclude(pk=account.pk))
|
|
955
|
+
if other_accounts:
|
|
956
|
+
rfid.energy_accounts.remove(*other_accounts)
|
|
957
|
+
if not account.rfids.filter(pk=rfid.pk).exists():
|
|
958
|
+
account.rfids.add(rfid)
|
|
959
|
+
return user
|
|
960
|
+
|
|
961
|
+
|
|
884
962
|
def _raw_instance_value(instance, field_name):
|
|
885
963
|
"""Return the stored value for ``field_name`` without resolving sigils."""
|
|
886
964
|
|
|
@@ -978,6 +1056,15 @@ class OpenPayProfileAdminForm(forms.ModelForm):
|
|
|
978
1056
|
required=False,
|
|
979
1057
|
help_text="Leave blank to keep the current secret.",
|
|
980
1058
|
)
|
|
1059
|
+
paypal_client_secret = forms.CharField(
|
|
1060
|
+
widget=forms.PasswordInput(render_value=True),
|
|
1061
|
+
required=False,
|
|
1062
|
+
help_text="Leave blank to keep the current secret.",
|
|
1063
|
+
)
|
|
1064
|
+
paypal_webhook_id = forms.CharField(
|
|
1065
|
+
required=False,
|
|
1066
|
+
help_text="Leave blank to keep the current webhook identifier.",
|
|
1067
|
+
)
|
|
981
1068
|
|
|
982
1069
|
class Meta:
|
|
983
1070
|
model = OpenPayProfile
|
|
@@ -985,13 +1072,43 @@ class OpenPayProfileAdminForm(forms.ModelForm):
|
|
|
985
1072
|
|
|
986
1073
|
def __init__(self, *args, **kwargs):
|
|
987
1074
|
super().__init__(*args, **kwargs)
|
|
1075
|
+
openpay_help = _(
|
|
1076
|
+
"Provide merchant ID, public and private keys, and webhook secret from OpenPay."
|
|
1077
|
+
)
|
|
1078
|
+
self.fields["merchant_id"].help_text = openpay_help
|
|
1079
|
+
self.fields["public_key"].help_text = _(
|
|
1080
|
+
"OpenPay public key used for browser integrations."
|
|
1081
|
+
)
|
|
1082
|
+
self.fields["private_key"].help_text = _(
|
|
1083
|
+
"OpenPay private key used for server-side requests. Leave blank to keep the current key."
|
|
1084
|
+
)
|
|
1085
|
+
self.fields["webhook_secret"].help_text = _(
|
|
1086
|
+
"Secret used to sign OpenPay webhooks. Leave blank to keep the current secret."
|
|
1087
|
+
)
|
|
1088
|
+
self.fields["is_production"].help_text = _(
|
|
1089
|
+
"Enable to send requests to OpenPay's live environment."
|
|
1090
|
+
)
|
|
1091
|
+
self.fields["default_processor"].help_text = _(
|
|
1092
|
+
"Select which configured processor to try first when charging."
|
|
1093
|
+
)
|
|
1094
|
+
self.fields["paypal_client_id"].help_text = _(
|
|
1095
|
+
"PayPal REST client ID for your application."
|
|
1096
|
+
)
|
|
1097
|
+
self.fields["paypal_client_secret"].help_text = _(
|
|
1098
|
+
"PayPal REST client secret. Leave blank to keep the current secret."
|
|
1099
|
+
)
|
|
1100
|
+
self.fields["paypal_webhook_id"].help_text = _(
|
|
1101
|
+
"PayPal webhook ID used to validate notifications. Leave blank to keep the current webhook identifier."
|
|
1102
|
+
)
|
|
1103
|
+
self.fields["paypal_is_production"].help_text = _(
|
|
1104
|
+
"Enable to send requests to PayPal's live environment."
|
|
1105
|
+
)
|
|
1106
|
+
|
|
988
1107
|
if self.instance.pk:
|
|
989
|
-
for field in ("private_key", "webhook_secret"):
|
|
1108
|
+
for field in ("private_key", "webhook_secret", "paypal_client_secret", "paypal_webhook_id"):
|
|
990
1109
|
if field in self.fields:
|
|
991
1110
|
self.fields[field].initial = ""
|
|
992
1111
|
self.initial[field] = ""
|
|
993
|
-
else:
|
|
994
|
-
self.fields["private_key"].required = True
|
|
995
1112
|
|
|
996
1113
|
def clean_private_key(self):
|
|
997
1114
|
key = self.cleaned_data.get("private_key")
|
|
@@ -1005,11 +1122,31 @@ class OpenPayProfileAdminForm(forms.ModelForm):
|
|
|
1005
1122
|
return keep_existing("webhook_secret")
|
|
1006
1123
|
return secret
|
|
1007
1124
|
|
|
1125
|
+
def clean_paypal_client_secret(self):
|
|
1126
|
+
secret = self.cleaned_data.get("paypal_client_secret")
|
|
1127
|
+
if not secret and self.instance.pk:
|
|
1128
|
+
return keep_existing("paypal_client_secret")
|
|
1129
|
+
return secret
|
|
1130
|
+
|
|
1131
|
+
def clean_paypal_webhook_id(self):
|
|
1132
|
+
identifier = self.cleaned_data.get("paypal_webhook_id")
|
|
1133
|
+
if identifier == "" and self.instance.pk:
|
|
1134
|
+
return keep_existing("paypal_webhook_id")
|
|
1135
|
+
return identifier
|
|
1136
|
+
|
|
1008
1137
|
def _post_clean(self):
|
|
1009
1138
|
super()._post_clean()
|
|
1010
1139
|
_restore_sigil_values(
|
|
1011
1140
|
self,
|
|
1012
|
-
[
|
|
1141
|
+
[
|
|
1142
|
+
"merchant_id",
|
|
1143
|
+
"private_key",
|
|
1144
|
+
"public_key",
|
|
1145
|
+
"webhook_secret",
|
|
1146
|
+
"paypal_client_id",
|
|
1147
|
+
"paypal_client_secret",
|
|
1148
|
+
"paypal_webhook_id",
|
|
1149
|
+
],
|
|
1013
1150
|
)
|
|
1014
1151
|
|
|
1015
1152
|
|
|
@@ -1232,7 +1369,7 @@ class OdooProfileInlineForm(ProfileFormMixin, OdooProfileAdminForm):
|
|
|
1232
1369
|
]
|
|
1233
1370
|
if provided and missing:
|
|
1234
1371
|
raise forms.ValidationError(
|
|
1235
|
-
"Provide host, database, username, and password to create
|
|
1372
|
+
"Provide host, database, username, and password to create a CRM employee.",
|
|
1236
1373
|
)
|
|
1237
1374
|
|
|
1238
1375
|
return cleaned
|
|
@@ -1249,19 +1386,51 @@ class OpenPayProfileInlineForm(ProfileFormMixin, OpenPayProfileAdminForm):
|
|
|
1249
1386
|
if cleaned.get("DELETE") or self.errors:
|
|
1250
1387
|
return cleaned
|
|
1251
1388
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1389
|
+
def _has_value(name: str) -> bool:
|
|
1390
|
+
value = cleaned.get(name)
|
|
1391
|
+
if isinstance(value, KeepExistingValue):
|
|
1392
|
+
return bool(getattr(self.instance, name))
|
|
1393
|
+
return not self._is_empty_value(value)
|
|
1394
|
+
|
|
1395
|
+
openpay_fields = ("merchant_id", "private_key", "public_key")
|
|
1396
|
+
openpay_provided = [name for name in openpay_fields if _has_value(name)]
|
|
1397
|
+
openpay_missing = [name for name in openpay_fields if not _has_value(name)]
|
|
1398
|
+
if openpay_provided and openpay_missing:
|
|
1260
1399
|
raise forms.ValidationError(
|
|
1261
1400
|
_(
|
|
1262
1401
|
"Provide merchant ID, private key, and public key to configure OpenPay."
|
|
1263
1402
|
)
|
|
1264
1403
|
)
|
|
1404
|
+
|
|
1405
|
+
paypal_fields = ("paypal_client_id", "paypal_client_secret")
|
|
1406
|
+
paypal_provided = [name for name in paypal_fields if _has_value(name)]
|
|
1407
|
+
paypal_missing = [name for name in paypal_fields if not _has_value(name)]
|
|
1408
|
+
if paypal_provided and paypal_missing:
|
|
1409
|
+
raise forms.ValidationError(
|
|
1410
|
+
_("Provide PayPal client ID and client secret to configure PayPal.")
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
has_openpay = len(openpay_provided) == len(openpay_fields)
|
|
1414
|
+
has_paypal = len(paypal_provided) == len(paypal_fields)
|
|
1415
|
+
|
|
1416
|
+
if not has_openpay and not has_paypal:
|
|
1417
|
+
raise forms.ValidationError(
|
|
1418
|
+
_("Provide OpenPay or PayPal credentials to configure a payment processor.")
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
default_processor = cleaned.get("default_processor") or OpenPayProfile.PROCESSOR_OPENPAY
|
|
1422
|
+
if default_processor == OpenPayProfile.PROCESSOR_OPENPAY and not has_openpay:
|
|
1423
|
+
raise forms.ValidationError(
|
|
1424
|
+
_(
|
|
1425
|
+
"OpenPay must be fully configured or select PayPal as the default processor."
|
|
1426
|
+
)
|
|
1427
|
+
)
|
|
1428
|
+
if default_processor == OpenPayProfile.PROCESSOR_PAYPAL and not has_paypal:
|
|
1429
|
+
raise forms.ValidationError(
|
|
1430
|
+
_(
|
|
1431
|
+
"PayPal must be fully configured or select OpenPay as the default processor."
|
|
1432
|
+
)
|
|
1433
|
+
)
|
|
1265
1434
|
return cleaned
|
|
1266
1435
|
|
|
1267
1436
|
|
|
@@ -1360,6 +1529,7 @@ PROFILE_INLINE_CONFIG = {
|
|
|
1360
1529
|
None,
|
|
1361
1530
|
{
|
|
1362
1531
|
"fields": (
|
|
1532
|
+
"crm",
|
|
1363
1533
|
"host",
|
|
1364
1534
|
"database",
|
|
1365
1535
|
"username",
|
|
@@ -1368,7 +1538,7 @@ PROFILE_INLINE_CONFIG = {
|
|
|
1368
1538
|
},
|
|
1369
1539
|
),
|
|
1370
1540
|
(
|
|
1371
|
-
"
|
|
1541
|
+
"CRM Employee",
|
|
1372
1542
|
{
|
|
1373
1543
|
"fields": ("verified_on", "odoo_uid", "name", "email"),
|
|
1374
1544
|
},
|
|
@@ -1556,12 +1726,25 @@ class UserPhoneNumberInline(admin.TabularInline):
|
|
|
1556
1726
|
|
|
1557
1727
|
|
|
1558
1728
|
class UserAdmin(UserDatumAdminMixin, DjangoUserAdmin):
|
|
1729
|
+
form = UserChangeRFIDForm
|
|
1559
1730
|
fieldsets = _append_operate_as(DjangoUserAdmin.fieldsets)
|
|
1560
1731
|
add_fieldsets = _append_operate_as(DjangoUserAdmin.add_fieldsets)
|
|
1561
1732
|
inlines = USER_PROFILE_INLINES + [UserPhoneNumberInline]
|
|
1562
1733
|
change_form_template = "admin/user_profile_change_form.html"
|
|
1563
1734
|
_skip_entity_user_datum = True
|
|
1564
1735
|
|
|
1736
|
+
def get_fieldsets(self, request, obj=None):
|
|
1737
|
+
fieldsets = list(super().get_fieldsets(request, obj))
|
|
1738
|
+
if obj is not None and fieldsets:
|
|
1739
|
+
name, options = fieldsets[0]
|
|
1740
|
+
fields = list(options.get("fields", ()))
|
|
1741
|
+
if "login_rfid" not in fields:
|
|
1742
|
+
fields.append("login_rfid")
|
|
1743
|
+
options = options.copy()
|
|
1744
|
+
options["fields"] = tuple(fields)
|
|
1745
|
+
fieldsets[0] = (name, options)
|
|
1746
|
+
return fieldsets
|
|
1747
|
+
|
|
1565
1748
|
def _get_operate_as_profile_template(self):
|
|
1566
1749
|
opts = self.model._meta
|
|
1567
1750
|
try:
|
|
@@ -1769,17 +1952,18 @@ class SocialProfileAdmin(
|
|
|
1769
1952
|
class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
1770
1953
|
change_form_template = "django_object_actions/change_form.html"
|
|
1771
1954
|
form = OdooProfileAdminForm
|
|
1772
|
-
list_display = ("owner", "host", "database", "verified_on")
|
|
1955
|
+
list_display = ("owner", "host", "database", "credentials_ok", "verified_on")
|
|
1956
|
+
list_filter = ("crm",)
|
|
1773
1957
|
readonly_fields = ("verified_on", "odoo_uid", "name", "email")
|
|
1774
1958
|
actions = ["verify_credentials"]
|
|
1775
1959
|
change_actions = ["verify_credentials_action", "my_profile_action"]
|
|
1776
1960
|
changelist_actions = ["my_profile", "generate_quote_report"]
|
|
1777
1961
|
fieldsets = (
|
|
1778
1962
|
("Owner", {"fields": ("user", "group")}),
|
|
1779
|
-
("Configuration", {"fields": ("host", "database")}),
|
|
1963
|
+
("Configuration", {"fields": ("crm", "host", "database")}),
|
|
1780
1964
|
("Credentials", {"fields": ("username", "password")}),
|
|
1781
1965
|
(
|
|
1782
|
-
"
|
|
1966
|
+
"CRM Employee",
|
|
1783
1967
|
{"fields": ("verified_on", "odoo_uid", "name", "email")},
|
|
1784
1968
|
),
|
|
1785
1969
|
)
|
|
@@ -1789,6 +1973,10 @@ class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdm
|
|
|
1789
1973
|
|
|
1790
1974
|
owner.short_description = "Owner"
|
|
1791
1975
|
|
|
1976
|
+
@admin.display(description=_("Credentials OK"), boolean=True)
|
|
1977
|
+
def credentials_ok(self, obj):
|
|
1978
|
+
return bool(obj.password) and obj.is_verified
|
|
1979
|
+
|
|
1792
1980
|
def _verify_credentials(self, request, profile):
|
|
1793
1981
|
try:
|
|
1794
1982
|
profile.verify()
|
|
@@ -1820,15 +2008,47 @@ class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdm
|
|
|
1820
2008
|
class OpenPayProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
1821
2009
|
change_form_template = "django_object_actions/change_form.html"
|
|
1822
2010
|
form = OpenPayProfileAdminForm
|
|
1823
|
-
list_display = ("owner", "
|
|
2011
|
+
list_display = ("owner", "default_processor_display", "environment", "verified_on")
|
|
1824
2012
|
readonly_fields = ("verified_on", "verification_reference")
|
|
1825
2013
|
actions = ["verify_credentials"]
|
|
1826
2014
|
change_actions = ["verify_credentials_action", "my_profile_action"]
|
|
1827
2015
|
changelist_actions = ["my_profile"]
|
|
1828
2016
|
fieldsets = (
|
|
1829
2017
|
(_("Owner"), {"fields": ("user", "group")}),
|
|
1830
|
-
(
|
|
1831
|
-
|
|
2018
|
+
(
|
|
2019
|
+
_("Default Processor"),
|
|
2020
|
+
{
|
|
2021
|
+
"fields": ("default_processor",),
|
|
2022
|
+
"description": _(
|
|
2023
|
+
"Choose which configured processor to contact first when processing payments."
|
|
2024
|
+
),
|
|
2025
|
+
},
|
|
2026
|
+
),
|
|
2027
|
+
(
|
|
2028
|
+
_("OpenPay"),
|
|
2029
|
+
{
|
|
2030
|
+
"fields": (
|
|
2031
|
+
"merchant_id",
|
|
2032
|
+
"public_key",
|
|
2033
|
+
"private_key",
|
|
2034
|
+
"webhook_secret",
|
|
2035
|
+
"is_production",
|
|
2036
|
+
),
|
|
2037
|
+
"description": _("Configure OpenPay merchant access."),
|
|
2038
|
+
},
|
|
2039
|
+
),
|
|
2040
|
+
(
|
|
2041
|
+
_("PayPal"),
|
|
2042
|
+
{
|
|
2043
|
+
"fields": (
|
|
2044
|
+
"paypal_client_id",
|
|
2045
|
+
"paypal_client_secret",
|
|
2046
|
+
"paypal_webhook_id",
|
|
2047
|
+
"paypal_is_production",
|
|
2048
|
+
),
|
|
2049
|
+
"description": _("Configure PayPal REST API access."),
|
|
2050
|
+
},
|
|
2051
|
+
),
|
|
1832
2052
|
(
|
|
1833
2053
|
_("Verification"),
|
|
1834
2054
|
{"fields": ("verified_on", "verification_reference")},
|
|
@@ -1839,12 +2059,23 @@ class OpenPayProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModel
|
|
|
1839
2059
|
def owner(self, obj):
|
|
1840
2060
|
return obj.owner_display()
|
|
1841
2061
|
|
|
2062
|
+
@admin.display(description=_("Default Processor"))
|
|
2063
|
+
def default_processor_display(self, obj):
|
|
2064
|
+
return obj.get_default_processor_display()
|
|
2065
|
+
|
|
1842
2066
|
@admin.display(description=_("Environment"))
|
|
1843
2067
|
def environment(self, obj):
|
|
1844
|
-
|
|
2068
|
+
if obj.default_processor == obj.PROCESSOR_PAYPAL:
|
|
2069
|
+
return _("PayPal Production") if obj.paypal_is_production else _("PayPal Sandbox")
|
|
2070
|
+
return _("OpenPay Production") if obj.is_production else _("OpenPay Sandbox")
|
|
1845
2071
|
|
|
1846
2072
|
def _verify_credentials(self, request, profile):
|
|
1847
|
-
owner =
|
|
2073
|
+
owner = (
|
|
2074
|
+
profile.owner_display()
|
|
2075
|
+
or profile.merchant_id
|
|
2076
|
+
or profile.paypal_client_id
|
|
2077
|
+
or _("Payment Processor")
|
|
2078
|
+
)
|
|
1848
2079
|
try:
|
|
1849
2080
|
profile.verify()
|
|
1850
2081
|
except ValidationError as exc:
|
|
@@ -2315,7 +2546,7 @@ class ProductAdmin(EntityModelAdmin):
|
|
|
2315
2546
|
profile = getattr(request.user, "odoo_profile", None)
|
|
2316
2547
|
if not profile or not profile.is_verified:
|
|
2317
2548
|
context["credential_error"] = _(
|
|
2318
|
-
"Configure your
|
|
2549
|
+
"Configure your CRM employee credentials before registering products."
|
|
2319
2550
|
)
|
|
2320
2551
|
return context, None
|
|
2321
2552
|
|
|
@@ -2523,6 +2754,12 @@ class RFIDResource(resources.ModelResource):
|
|
|
2523
2754
|
account_column = account_column_for_field(account_field)
|
|
2524
2755
|
self.fields["energy_accounts"].column_name = account_column
|
|
2525
2756
|
|
|
2757
|
+
def get_queryset(self):
|
|
2758
|
+
manager = getattr(self._meta.model, "all_objects", None)
|
|
2759
|
+
if manager is not None:
|
|
2760
|
+
return manager.all()
|
|
2761
|
+
return super().get_queryset()
|
|
2762
|
+
|
|
2526
2763
|
def dehydrate_energy_accounts(self, obj):
|
|
2527
2764
|
return serialize_accounts(obj, self.account_field)
|
|
2528
2765
|
|
|
@@ -2536,6 +2773,11 @@ class RFIDResource(resources.ModelResource):
|
|
|
2536
2773
|
else:
|
|
2537
2774
|
instance.energy_accounts.clear()
|
|
2538
2775
|
|
|
2776
|
+
def before_save_instance(self, instance, row, **kwargs):
|
|
2777
|
+
if getattr(instance, "is_deleted", False):
|
|
2778
|
+
instance.is_deleted = False
|
|
2779
|
+
super().before_save_instance(instance, row, **kwargs)
|
|
2780
|
+
|
|
2539
2781
|
class Meta:
|
|
2540
2782
|
model = RFID
|
|
2541
2783
|
fields = (
|
core/apps.py
CHANGED
|
@@ -38,6 +38,7 @@ class CoreConfig(AppConfig):
|
|
|
38
38
|
patch_admin_sigil_builder_view,
|
|
39
39
|
generate_model_sigils,
|
|
40
40
|
)
|
|
41
|
+
from .celery_utils import normalize_periodic_task_name
|
|
41
42
|
from .admin_history import patch_admin_history
|
|
42
43
|
|
|
43
44
|
from django_otp.plugins.otp_totp.models import TOTPDevice as OTP_TOTPDevice
|
|
@@ -222,8 +223,11 @@ class CoreConfig(AppConfig):
|
|
|
222
223
|
schedule, _ = IntervalSchedule.objects.get_or_create(
|
|
223
224
|
every=1, period=IntervalSchedule.HOURS
|
|
224
225
|
)
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
task_name = normalize_periodic_task_name(
|
|
227
|
+
PeriodicTask.objects, "poll_email_collectors"
|
|
228
|
+
)
|
|
229
|
+
PeriodicTask.objects.update_or_create(
|
|
230
|
+
name=task_name,
|
|
227
231
|
defaults={
|
|
228
232
|
"interval": schedule,
|
|
229
233
|
"task": "core.tasks.poll_email_collectors",
|
core/celery_utils.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Utilities for working with Celery periodic task names."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Set
|
|
7
|
+
|
|
8
|
+
from django.db import transaction
|
|
9
|
+
from django.db.utils import IntegrityError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def slugify_task_name(name: str) -> str:
|
|
13
|
+
"""Return a slugified task name using dashes.
|
|
14
|
+
|
|
15
|
+
Celery stores periodic task names in the database and historically these
|
|
16
|
+
values included underscores or dotted module paths. The scheduler UI reads
|
|
17
|
+
these values directly, so we collapse consecutive underscores or dots into a
|
|
18
|
+
single dash to keep them human readable while remaining unique.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
slug = re.sub(r"[._]+", "-", name)
|
|
22
|
+
# Collapse any accidental duplicate separators that may result from the
|
|
23
|
+
# replacement so ``foo__bar`` and ``foo..bar`` both become ``foo-bar``.
|
|
24
|
+
slug = re.sub(r"-{2,}", "-", slug)
|
|
25
|
+
return slug
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def periodic_task_name_variants(name: str) -> Set[str]:
|
|
29
|
+
"""Return legacy and slugified variants for a periodic task name."""
|
|
30
|
+
|
|
31
|
+
slug = slugify_task_name(name)
|
|
32
|
+
if slug == name:
|
|
33
|
+
return {name}
|
|
34
|
+
return {name, slug}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def normalize_periodic_task_name(manager, name: str) -> str:
|
|
38
|
+
"""Ensure the stored periodic task name matches the slugified form.
|
|
39
|
+
|
|
40
|
+
The helper renames any rows that still use the legacy value so that follow-up
|
|
41
|
+
``update_or_create`` calls keep working without leaving duplicate tasks in
|
|
42
|
+
the scheduler. When a conflicting slug already exists we keep the slugged
|
|
43
|
+
version and remove the legacy entry.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
slug = slugify_task_name(name)
|
|
47
|
+
if slug == name:
|
|
48
|
+
return slug
|
|
49
|
+
|
|
50
|
+
for task in manager.filter(name=name):
|
|
51
|
+
conflict = manager.filter(name=slug).exclude(pk=task.pk).first()
|
|
52
|
+
if conflict:
|
|
53
|
+
# Preserve foreign key references when possible before removing the
|
|
54
|
+
# legacy row.
|
|
55
|
+
related_attr = getattr(task, "client_report_schedule", None)
|
|
56
|
+
if related_attr and getattr(conflict, "client_report_schedule", None) is None:
|
|
57
|
+
related_attr.periodic_task = conflict
|
|
58
|
+
related_attr.save(update_fields=["periodic_task"])
|
|
59
|
+
task.delete()
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
task.name = slug
|
|
63
|
+
try:
|
|
64
|
+
with transaction.atomic():
|
|
65
|
+
task.save(update_fields=["name"])
|
|
66
|
+
except IntegrityError:
|
|
67
|
+
# Another process may have created the slug in between the select and
|
|
68
|
+
# the update. Fall back to deleting the legacy row to avoid duplicate
|
|
69
|
+
# scheduler entries.
|
|
70
|
+
task.refresh_from_db()
|
|
71
|
+
if task.name != slug:
|
|
72
|
+
task.delete()
|
|
73
|
+
return slug
|