arthexis 0.1.9__py3-none-any.whl → 0.1.10__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.9.dist-info → arthexis-0.1.10.dist-info}/METADATA +63 -20
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/RECORD +39 -36
- config/settings.py +221 -23
- config/urls.py +6 -0
- core/admin.py +401 -35
- core/apps.py +3 -0
- core/auto_upgrade.py +57 -0
- core/backends.py +77 -3
- core/fields.py +93 -0
- core/models.py +212 -7
- core/reference_utils.py +97 -0
- core/sigil_builder.py +16 -3
- core/system.py +157 -143
- core/tasks.py +151 -8
- core/test_system_info.py +37 -1
- core/tests.py +288 -12
- core/user_data.py +103 -8
- core/views.py +257 -15
- nodes/admin.py +12 -4
- nodes/backends.py +109 -17
- nodes/models.py +205 -2
- nodes/tests.py +370 -1
- nodes/views.py +140 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +252 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +49 -7
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/tests.py +384 -8
- ocpp/views.py +101 -76
- pages/context_processors.py +20 -0
- pages/forms.py +131 -0
- pages/tests.py +434 -13
- pages/urls.py +1 -0
- pages/views.py +334 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from django import forms
|
|
2
2
|
from django.contrib import admin
|
|
3
3
|
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
|
4
|
-
from django.urls import path, reverse
|
|
4
|
+
from django.urls import NoReverseMatch, path, reverse
|
|
5
|
+
from urllib.parse import urlencode
|
|
5
6
|
from django.shortcuts import redirect, render
|
|
6
7
|
from django.http import JsonResponse, HttpResponseBase, HttpResponseRedirect
|
|
7
8
|
from django.template.response import TemplateResponse
|
|
@@ -61,7 +62,15 @@ from .models import (
|
|
|
61
62
|
Todo,
|
|
62
63
|
hash_key,
|
|
63
64
|
)
|
|
64
|
-
from .user_data import
|
|
65
|
+
from .user_data import (
|
|
66
|
+
EntityModelAdmin,
|
|
67
|
+
UserDatumAdminMixin,
|
|
68
|
+
delete_user_fixture,
|
|
69
|
+
dump_user_fixture,
|
|
70
|
+
_fixture_path,
|
|
71
|
+
_resolve_fixture_user,
|
|
72
|
+
_user_allows_user_data,
|
|
73
|
+
)
|
|
65
74
|
from .widgets import OdooProductWidget
|
|
66
75
|
from .mcp import process as mcp_process
|
|
67
76
|
|
|
@@ -161,11 +170,97 @@ class SaveBeforeChangeAction(DjangoObjectActions):
|
|
|
161
170
|
return super().response_change(request, obj)
|
|
162
171
|
|
|
163
172
|
|
|
173
|
+
class ProfileAdminMixin:
|
|
174
|
+
"""Reusable actions for profile-bound admin classes."""
|
|
175
|
+
|
|
176
|
+
def _resolve_my_profile_target(self, request):
|
|
177
|
+
opts = self.model._meta
|
|
178
|
+
changelist_url = reverse(
|
|
179
|
+
f"admin:{opts.app_label}_{opts.model_name}_changelist"
|
|
180
|
+
)
|
|
181
|
+
user = getattr(request, "user", None)
|
|
182
|
+
if not getattr(user, "is_authenticated", False):
|
|
183
|
+
return (
|
|
184
|
+
changelist_url,
|
|
185
|
+
_("You must be logged in to manage your profile."),
|
|
186
|
+
messages.ERROR,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
profile = self.model._default_manager.filter(user=user).first()
|
|
190
|
+
if profile is not None:
|
|
191
|
+
permission_check = getattr(self, "has_view_or_change_permission", None)
|
|
192
|
+
has_permission = (
|
|
193
|
+
permission_check(request, obj=profile)
|
|
194
|
+
if callable(permission_check)
|
|
195
|
+
else self.has_change_permission(request, obj=profile)
|
|
196
|
+
)
|
|
197
|
+
if has_permission:
|
|
198
|
+
change_url = reverse(
|
|
199
|
+
f"admin:{opts.app_label}_{opts.model_name}_change",
|
|
200
|
+
args=[profile.pk],
|
|
201
|
+
)
|
|
202
|
+
return change_url, None, None
|
|
203
|
+
return (
|
|
204
|
+
changelist_url,
|
|
205
|
+
_("You do not have permission to view this profile."),
|
|
206
|
+
messages.ERROR,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if self.has_add_permission(request):
|
|
210
|
+
add_url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add")
|
|
211
|
+
params = {}
|
|
212
|
+
user_id = getattr(user, "pk", None)
|
|
213
|
+
if user_id:
|
|
214
|
+
params["user"] = user_id
|
|
215
|
+
if params:
|
|
216
|
+
add_url = f"{add_url}?{urlencode(params)}"
|
|
217
|
+
return add_url, None, None
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
changelist_url,
|
|
221
|
+
_("You do not have permission to create this profile."),
|
|
222
|
+
messages.ERROR,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def get_my_profile_url(self, request):
|
|
226
|
+
url, _message, _level = self._resolve_my_profile_target(request)
|
|
227
|
+
return url
|
|
228
|
+
|
|
229
|
+
def _redirect_to_my_profile(self, request):
|
|
230
|
+
target_url, message, level = self._resolve_my_profile_target(request)
|
|
231
|
+
if message:
|
|
232
|
+
self.message_user(request, message, level=level)
|
|
233
|
+
return HttpResponseRedirect(target_url)
|
|
234
|
+
|
|
235
|
+
@admin.action(description=_("Active Profile"))
|
|
236
|
+
def my_profile(self, request, queryset=None):
|
|
237
|
+
return self._redirect_to_my_profile(request)
|
|
238
|
+
|
|
239
|
+
def my_profile_action(self, request, obj=None):
|
|
240
|
+
return self._redirect_to_my_profile(request)
|
|
241
|
+
|
|
242
|
+
my_profile_action.label = _("Active Profile")
|
|
243
|
+
my_profile_action.short_description = _("Active Profile")
|
|
244
|
+
|
|
245
|
+
def get_actions(self, request):
|
|
246
|
+
actions = super().get_actions(request)
|
|
247
|
+
if "my_profile" not in actions:
|
|
248
|
+
action = getattr(self, "my_profile", None)
|
|
249
|
+
if action is not None:
|
|
250
|
+
actions["my_profile"] = (
|
|
251
|
+
action,
|
|
252
|
+
"my_profile",
|
|
253
|
+
getattr(action, "short_description", _("Active Profile")),
|
|
254
|
+
)
|
|
255
|
+
return actions
|
|
256
|
+
|
|
257
|
+
|
|
164
258
|
@admin.register(ExperienceReference)
|
|
165
259
|
class ReferenceAdmin(EntityModelAdmin):
|
|
166
260
|
list_display = (
|
|
167
261
|
"alt_text",
|
|
168
262
|
"content_type",
|
|
263
|
+
"header",
|
|
169
264
|
"footer",
|
|
170
265
|
"visibility",
|
|
171
266
|
"author",
|
|
@@ -182,6 +277,7 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
182
277
|
"features",
|
|
183
278
|
"sites",
|
|
184
279
|
"include_in_footer",
|
|
280
|
+
"show_in_header",
|
|
185
281
|
"footer_visibility",
|
|
186
282
|
"transaction_uuid",
|
|
187
283
|
"author",
|
|
@@ -200,6 +296,10 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
200
296
|
def footer(self, obj):
|
|
201
297
|
return obj.include_in_footer
|
|
202
298
|
|
|
299
|
+
@admin.display(description="Header", boolean=True, ordering="show_in_header")
|
|
300
|
+
def header(self, obj):
|
|
301
|
+
return obj.show_in_header
|
|
302
|
+
|
|
203
303
|
@admin.display(description="Visibility", ordering="footer_visibility")
|
|
204
304
|
def visibility(self, obj):
|
|
205
305
|
return obj.get_footer_visibility_display()
|
|
@@ -260,11 +360,12 @@ class ReleaseManagerAdminForm(forms.ModelForm):
|
|
|
260
360
|
|
|
261
361
|
|
|
262
362
|
@admin.register(ReleaseManager)
|
|
263
|
-
class ReleaseManagerAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
363
|
+
class ReleaseManagerAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
264
364
|
form = ReleaseManagerAdminForm
|
|
265
365
|
list_display = ("owner", "pypi_username", "pypi_url")
|
|
266
366
|
actions = ["test_credentials"]
|
|
267
|
-
change_actions = ["test_credentials_action"]
|
|
367
|
+
change_actions = ["test_credentials_action", "my_profile_action"]
|
|
368
|
+
changelist_actions = ["my_profile"]
|
|
268
369
|
fieldsets = (
|
|
269
370
|
("Owner", {"fields": ("user", "group")}),
|
|
270
371
|
(
|
|
@@ -325,9 +426,6 @@ class ReleaseManagerAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
325
426
|
request, f"{manager} credentials check failed: {exc}", messages.ERROR
|
|
326
427
|
)
|
|
327
428
|
|
|
328
|
-
def get_model_perms(self, request):
|
|
329
|
-
return {}
|
|
330
|
-
|
|
331
429
|
|
|
332
430
|
@admin.register(Package)
|
|
333
431
|
class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
@@ -338,7 +436,6 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
338
436
|
"release_manager",
|
|
339
437
|
"is_active",
|
|
340
438
|
)
|
|
341
|
-
actions = ["prepare_next_release"]
|
|
342
439
|
change_actions = ["prepare_next_release_action"]
|
|
343
440
|
|
|
344
441
|
def _prepare(self, request, package):
|
|
@@ -395,13 +492,6 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
395
492
|
return redirect("admin:core_package_changelist")
|
|
396
493
|
return self._prepare(request, package)
|
|
397
494
|
|
|
398
|
-
@admin.action(description="Prepare next Release")
|
|
399
|
-
def prepare_next_release(self, request, queryset):
|
|
400
|
-
if queryset.count() != 1:
|
|
401
|
-
self.message_user(request, "Select exactly one package", messages.ERROR)
|
|
402
|
-
return
|
|
403
|
-
return self._prepare(request, queryset.first())
|
|
404
|
-
|
|
405
495
|
def prepare_next_release_action(self, request, obj):
|
|
406
496
|
return self._prepare(request, obj)
|
|
407
497
|
|
|
@@ -617,6 +707,17 @@ class EmailInboxAdminForm(forms.ModelForm):
|
|
|
617
707
|
class ProfileInlineFormSet(BaseInlineFormSet):
|
|
618
708
|
"""Hide deletion controls and allow implicit removal when empty."""
|
|
619
709
|
|
|
710
|
+
@classmethod
|
|
711
|
+
def get_default_prefix(cls):
|
|
712
|
+
prefix = super().get_default_prefix()
|
|
713
|
+
if prefix:
|
|
714
|
+
return prefix
|
|
715
|
+
model_name = cls.model._meta.model_name
|
|
716
|
+
remote_field = getattr(cls.fk, "remote_field", None)
|
|
717
|
+
if remote_field is not None and getattr(remote_field, "one_to_one", False):
|
|
718
|
+
return model_name
|
|
719
|
+
return f"{model_name}_set"
|
|
720
|
+
|
|
620
721
|
def add_fields(self, form, index):
|
|
621
722
|
super().add_fields(form, index)
|
|
622
723
|
if "DELETE" in form.fields:
|
|
@@ -974,21 +1075,61 @@ class UserPhoneNumberInline(admin.TabularInline):
|
|
|
974
1075
|
fields = ("number", "priority")
|
|
975
1076
|
|
|
976
1077
|
|
|
977
|
-
class UserAdmin(DjangoUserAdmin):
|
|
1078
|
+
class UserAdmin(UserDatumAdminMixin, DjangoUserAdmin):
|
|
978
1079
|
fieldsets = _append_operate_as(DjangoUserAdmin.fieldsets)
|
|
979
1080
|
add_fieldsets = _append_operate_as(DjangoUserAdmin.add_fieldsets)
|
|
980
1081
|
inlines = USER_PROFILE_INLINES + [UserPhoneNumberInline]
|
|
981
1082
|
change_form_template = "admin/user_profile_change_form.html"
|
|
1083
|
+
_skip_entity_user_datum = True
|
|
1084
|
+
|
|
1085
|
+
def _get_operate_as_profile_template(self):
|
|
1086
|
+
opts = self.model._meta
|
|
1087
|
+
try:
|
|
1088
|
+
return reverse(
|
|
1089
|
+
f"{self.admin_site.name}:{opts.app_label}_{opts.model_name}_change",
|
|
1090
|
+
args=["__ID__"],
|
|
1091
|
+
)
|
|
1092
|
+
except NoReverseMatch:
|
|
1093
|
+
user_opts = User._meta
|
|
1094
|
+
try:
|
|
1095
|
+
return reverse(
|
|
1096
|
+
f"{self.admin_site.name}:{user_opts.app_label}_{user_opts.model_name}_change",
|
|
1097
|
+
args=["__ID__"],
|
|
1098
|
+
)
|
|
1099
|
+
except NoReverseMatch:
|
|
1100
|
+
return None
|
|
982
1101
|
|
|
983
1102
|
def render_change_form(
|
|
984
1103
|
self, request, context, add=False, change=False, form_url="", obj=None
|
|
985
1104
|
):
|
|
986
|
-
|
|
1105
|
+
response = super().render_change_form(
|
|
987
1106
|
request, context, add=add, change=change, form_url=form_url, obj=obj
|
|
988
1107
|
)
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1108
|
+
if isinstance(response, dict):
|
|
1109
|
+
context_data = response
|
|
1110
|
+
else:
|
|
1111
|
+
context_data = getattr(response, "context_data", None)
|
|
1112
|
+
if context_data is not None:
|
|
1113
|
+
context_data["show_user_datum"] = False
|
|
1114
|
+
context_data["show_seed_datum"] = False
|
|
1115
|
+
context_data["show_save_as_copy"] = False
|
|
1116
|
+
operate_as_user = None
|
|
1117
|
+
operate_as_template = self._get_operate_as_profile_template()
|
|
1118
|
+
operate_as_url = None
|
|
1119
|
+
if obj and getattr(obj, "operate_as_id", None):
|
|
1120
|
+
try:
|
|
1121
|
+
operate_as_user = obj.operate_as
|
|
1122
|
+
except User.DoesNotExist:
|
|
1123
|
+
operate_as_user = None
|
|
1124
|
+
if operate_as_user and operate_as_template:
|
|
1125
|
+
operate_as_url = operate_as_template.replace(
|
|
1126
|
+
"__ID__", str(operate_as_user.pk)
|
|
1127
|
+
)
|
|
1128
|
+
if context_data is not None:
|
|
1129
|
+
context_data["operate_as_user"] = operate_as_user
|
|
1130
|
+
context_data["operate_as_profile_url_template"] = operate_as_template
|
|
1131
|
+
context_data["operate_as_profile_url"] = operate_as_url
|
|
1132
|
+
return response
|
|
992
1133
|
|
|
993
1134
|
def get_inline_instances(self, request, obj=None):
|
|
994
1135
|
inline_instances = super().get_inline_instances(request, obj)
|
|
@@ -1035,6 +1176,25 @@ class UserAdmin(DjangoUserAdmin):
|
|
|
1035
1176
|
should_store = bool(inline_form.cleaned_data.get("user_datum"))
|
|
1036
1177
|
self._update_profile_fixture(instance, owner_user, store=should_store)
|
|
1037
1178
|
|
|
1179
|
+
def save_model(self, request, obj, form, change):
|
|
1180
|
+
super().save_model(request, obj, form, change)
|
|
1181
|
+
if not getattr(obj, "pk", None):
|
|
1182
|
+
return
|
|
1183
|
+
target_user = _resolve_fixture_user(obj, obj)
|
|
1184
|
+
allow_user_data = _user_allows_user_data(target_user)
|
|
1185
|
+
if request.POST.get("_user_datum") == "on":
|
|
1186
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
1187
|
+
obj.is_user_data = False
|
|
1188
|
+
delete_user_fixture(obj, target_user)
|
|
1189
|
+
self.message_user(
|
|
1190
|
+
request,
|
|
1191
|
+
_("User data for user accounts is managed through the profile sections."),
|
|
1192
|
+
)
|
|
1193
|
+
elif obj.is_user_data:
|
|
1194
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
1195
|
+
obj.is_user_data = False
|
|
1196
|
+
delete_user_fixture(obj, target_user)
|
|
1197
|
+
|
|
1038
1198
|
|
|
1039
1199
|
class EmailCollectorInline(admin.TabularInline):
|
|
1040
1200
|
model = EmailCollector
|
|
@@ -1047,16 +1207,20 @@ class EmailCollectorAdmin(EntityModelAdmin):
|
|
|
1047
1207
|
|
|
1048
1208
|
|
|
1049
1209
|
@admin.register(OdooProfile)
|
|
1050
|
-
class OdooProfileAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
1210
|
+
class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
1051
1211
|
change_form_template = "django_object_actions/change_form.html"
|
|
1052
1212
|
form = OdooProfileAdminForm
|
|
1053
1213
|
list_display = ("owner", "host", "database", "verified_on")
|
|
1054
1214
|
readonly_fields = ("verified_on", "odoo_uid", "name", "email")
|
|
1055
1215
|
actions = ["verify_credentials"]
|
|
1056
|
-
change_actions = ["verify_credentials_action"]
|
|
1216
|
+
change_actions = ["verify_credentials_action", "my_profile_action"]
|
|
1217
|
+
changelist_actions = ["my_profile"]
|
|
1057
1218
|
fieldsets = (
|
|
1058
1219
|
("Owner", {"fields": ("user", "group")}),
|
|
1059
|
-
(
|
|
1220
|
+
(
|
|
1221
|
+
"Configuration",
|
|
1222
|
+
{"fields": ("host", "database", "username", "password")},
|
|
1223
|
+
),
|
|
1060
1224
|
(
|
|
1061
1225
|
"Odoo Employee",
|
|
1062
1226
|
{"fields": ("verified_on", "odoo_uid", "name", "email")},
|
|
@@ -1088,9 +1252,6 @@ class OdooProfileAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1088
1252
|
verify_credentials_action.label = "Test credentials"
|
|
1089
1253
|
verify_credentials_action.short_description = "Test credentials"
|
|
1090
1254
|
|
|
1091
|
-
def get_model_perms(self, request):
|
|
1092
|
-
return {}
|
|
1093
|
-
|
|
1094
1255
|
|
|
1095
1256
|
class EmailSearchForm(forms.Form):
|
|
1096
1257
|
subject = forms.CharField(
|
|
@@ -1107,11 +1268,12 @@ class EmailSearchForm(forms.Form):
|
|
|
1107
1268
|
)
|
|
1108
1269
|
|
|
1109
1270
|
|
|
1110
|
-
class EmailInboxAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
1271
|
+
class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
1111
1272
|
form = EmailInboxAdminForm
|
|
1112
1273
|
list_display = ("owner_label", "username", "host", "protocol")
|
|
1113
1274
|
actions = ["test_connection", "search_inbox", "test_collectors"]
|
|
1114
|
-
change_actions = ["test_collectors_action"]
|
|
1275
|
+
change_actions = ["test_collectors_action", "my_profile_action"]
|
|
1276
|
+
changelist_actions = ["my_profile"]
|
|
1115
1277
|
change_form_template = "admin/core/emailinbox/change_form.html"
|
|
1116
1278
|
inlines = [EmailCollectorInline]
|
|
1117
1279
|
|
|
@@ -1244,12 +1406,16 @@ class EmailInboxAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1244
1406
|
|
|
1245
1407
|
|
|
1246
1408
|
@admin.register(AssistantProfile)
|
|
1247
|
-
class AssistantProfileAdmin(
|
|
1409
|
+
class AssistantProfileAdmin(
|
|
1410
|
+
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
1411
|
+
):
|
|
1248
1412
|
list_display = ("owner", "created_at", "last_used_at", "is_active")
|
|
1249
1413
|
readonly_fields = ("user_key_hash", "created_at", "last_used_at")
|
|
1250
1414
|
|
|
1251
1415
|
change_form_template = "admin/workgroupassistantprofile_change_form.html"
|
|
1252
1416
|
change_list_template = "admin/assistantprofile_change_list.html"
|
|
1417
|
+
change_actions = ["my_profile_action"]
|
|
1418
|
+
changelist_actions = ["my_profile"]
|
|
1253
1419
|
fieldsets = (
|
|
1254
1420
|
("Owner", {"fields": ("user", "group")}),
|
|
1255
1421
|
(
|
|
@@ -1406,9 +1572,6 @@ class AssistantProfileAdmin(EntityModelAdmin):
|
|
|
1406
1572
|
self.message_user(request, msg, level=level)
|
|
1407
1573
|
return self._redirect_to_changelist()
|
|
1408
1574
|
|
|
1409
|
-
def get_model_perms(self, request):
|
|
1410
|
-
return {}
|
|
1411
|
-
|
|
1412
1575
|
|
|
1413
1576
|
class EnergyCreditInline(admin.TabularInline):
|
|
1414
1577
|
model = EnergyCredit
|
|
@@ -1614,9 +1777,192 @@ class ProductAdminForm(forms.ModelForm):
|
|
|
1614
1777
|
widgets = {"odoo_product": OdooProductWidget}
|
|
1615
1778
|
|
|
1616
1779
|
|
|
1780
|
+
class ProductFetchWizardForm(forms.Form):
|
|
1781
|
+
name = forms.CharField(label="Name", required=False)
|
|
1782
|
+
default_code = forms.CharField(label="Internal reference", required=False)
|
|
1783
|
+
barcode = forms.CharField(label="Barcode", required=False)
|
|
1784
|
+
renewal_period = forms.IntegerField(
|
|
1785
|
+
label="Renewal period (days)", min_value=1, initial=30
|
|
1786
|
+
)
|
|
1787
|
+
|
|
1788
|
+
def __init__(self, *args, require_search_terms=True, **kwargs):
|
|
1789
|
+
self.require_search_terms = require_search_terms
|
|
1790
|
+
super().__init__(*args, **kwargs)
|
|
1791
|
+
|
|
1792
|
+
def clean(self):
|
|
1793
|
+
cleaned = super().clean()
|
|
1794
|
+
if self.require_search_terms:
|
|
1795
|
+
if not any(
|
|
1796
|
+
cleaned.get(field) for field in ("name", "default_code", "barcode")
|
|
1797
|
+
):
|
|
1798
|
+
raise forms.ValidationError(
|
|
1799
|
+
_("Enter at least one field to search for a product.")
|
|
1800
|
+
)
|
|
1801
|
+
return cleaned
|
|
1802
|
+
|
|
1803
|
+
def build_domain(self):
|
|
1804
|
+
domain = []
|
|
1805
|
+
if self.cleaned_data.get("name"):
|
|
1806
|
+
domain.append(("name", "ilike", self.cleaned_data["name"]))
|
|
1807
|
+
if self.cleaned_data.get("default_code"):
|
|
1808
|
+
domain.append(("default_code", "ilike", self.cleaned_data["default_code"]))
|
|
1809
|
+
if self.cleaned_data.get("barcode"):
|
|
1810
|
+
domain.append(("barcode", "ilike", self.cleaned_data["barcode"]))
|
|
1811
|
+
return domain
|
|
1812
|
+
|
|
1813
|
+
|
|
1617
1814
|
@admin.register(Product)
|
|
1618
1815
|
class ProductAdmin(EntityModelAdmin):
|
|
1619
1816
|
form = ProductAdminForm
|
|
1817
|
+
actions = ["fetch_odoo_product"]
|
|
1818
|
+
|
|
1819
|
+
def _odoo_profile_admin(self):
|
|
1820
|
+
return self.admin_site._registry.get(OdooProfile)
|
|
1821
|
+
|
|
1822
|
+
def _search_odoo_products(self, profile, form):
|
|
1823
|
+
domain = form.build_domain()
|
|
1824
|
+
return profile.execute(
|
|
1825
|
+
"product.product",
|
|
1826
|
+
"search_read",
|
|
1827
|
+
domain,
|
|
1828
|
+
{
|
|
1829
|
+
"fields": [
|
|
1830
|
+
"name",
|
|
1831
|
+
"default_code",
|
|
1832
|
+
"barcode",
|
|
1833
|
+
"description_sale",
|
|
1834
|
+
],
|
|
1835
|
+
"limit": 50,
|
|
1836
|
+
},
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
@admin.action(description="Fetch Odoo Product")
|
|
1840
|
+
def fetch_odoo_product(self, request, queryset):
|
|
1841
|
+
profile = getattr(request.user, "odoo_profile", None)
|
|
1842
|
+
has_credentials = bool(profile and profile.is_verified)
|
|
1843
|
+
profile_admin = self._odoo_profile_admin()
|
|
1844
|
+
profile_url = None
|
|
1845
|
+
if profile_admin is not None:
|
|
1846
|
+
profile_url = profile_admin.get_my_profile_url(request)
|
|
1847
|
+
|
|
1848
|
+
context = {
|
|
1849
|
+
"opts": self.model._meta,
|
|
1850
|
+
"queryset": queryset,
|
|
1851
|
+
"action": "fetch_odoo_product",
|
|
1852
|
+
"has_credentials": has_credentials,
|
|
1853
|
+
"profile_url": profile_url,
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if not has_credentials:
|
|
1857
|
+
context["credential_error"] = _(
|
|
1858
|
+
"Configure your Odoo employee credentials before fetching products."
|
|
1859
|
+
)
|
|
1860
|
+
return TemplateResponse(
|
|
1861
|
+
request, "admin/core/product/fetch_odoo.html", context
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
is_import = "import" in request.POST
|
|
1865
|
+
form_kwargs = {"require_search_terms": not is_import}
|
|
1866
|
+
if request.method == "POST":
|
|
1867
|
+
form = ProductFetchWizardForm(request.POST, **form_kwargs)
|
|
1868
|
+
else:
|
|
1869
|
+
form = ProductFetchWizardForm()
|
|
1870
|
+
|
|
1871
|
+
results = None
|
|
1872
|
+
selected_product_id = request.POST.get("product_id", "")
|
|
1873
|
+
|
|
1874
|
+
if request.method == "POST" and form.is_valid():
|
|
1875
|
+
try:
|
|
1876
|
+
results = self._search_odoo_products(profile, form)
|
|
1877
|
+
except Exception:
|
|
1878
|
+
form.add_error(None, _("Unable to fetch products from Odoo."))
|
|
1879
|
+
results = []
|
|
1880
|
+
else:
|
|
1881
|
+
if is_import:
|
|
1882
|
+
if not self.has_add_permission(request):
|
|
1883
|
+
form.add_error(
|
|
1884
|
+
None, _("You do not have permission to add products.")
|
|
1885
|
+
)
|
|
1886
|
+
else:
|
|
1887
|
+
product_id = request.POST.get("product_id")
|
|
1888
|
+
if not product_id:
|
|
1889
|
+
form.add_error(None, _("Select a product to import."))
|
|
1890
|
+
else:
|
|
1891
|
+
try:
|
|
1892
|
+
odoo_id = int(product_id)
|
|
1893
|
+
except (TypeError, ValueError):
|
|
1894
|
+
form.add_error(None, _("Invalid product selection."))
|
|
1895
|
+
else:
|
|
1896
|
+
match = next(
|
|
1897
|
+
(item for item in results if item.get("id") == odoo_id),
|
|
1898
|
+
None,
|
|
1899
|
+
)
|
|
1900
|
+
if not match:
|
|
1901
|
+
form.add_error(
|
|
1902
|
+
None,
|
|
1903
|
+
_(
|
|
1904
|
+
"The selected product was not found. Run the search again."
|
|
1905
|
+
),
|
|
1906
|
+
)
|
|
1907
|
+
else:
|
|
1908
|
+
existing = self.model.objects.filter(
|
|
1909
|
+
odoo_product__id=odoo_id
|
|
1910
|
+
).first()
|
|
1911
|
+
if existing:
|
|
1912
|
+
self.message_user(
|
|
1913
|
+
request,
|
|
1914
|
+
_(
|
|
1915
|
+
"Product %(name)s already imported; opening existing record."
|
|
1916
|
+
)
|
|
1917
|
+
% {"name": existing.name},
|
|
1918
|
+
level=messages.WARNING,
|
|
1919
|
+
)
|
|
1920
|
+
return HttpResponseRedirect(
|
|
1921
|
+
reverse(
|
|
1922
|
+
"admin:%s_%s_change"
|
|
1923
|
+
% (
|
|
1924
|
+
existing._meta.app_label,
|
|
1925
|
+
existing._meta.model_name,
|
|
1926
|
+
),
|
|
1927
|
+
args=[existing.pk],
|
|
1928
|
+
)
|
|
1929
|
+
)
|
|
1930
|
+
product = self.model.objects.create(
|
|
1931
|
+
name=match.get("name") or f"Odoo Product {odoo_id}",
|
|
1932
|
+
description=match.get("description_sale", "") or "",
|
|
1933
|
+
renewal_period=form.cleaned_data["renewal_period"],
|
|
1934
|
+
odoo_product={
|
|
1935
|
+
"id": odoo_id,
|
|
1936
|
+
"name": match.get("name", ""),
|
|
1937
|
+
},
|
|
1938
|
+
)
|
|
1939
|
+
self.log_addition(
|
|
1940
|
+
request, product, "Imported product from Odoo"
|
|
1941
|
+
)
|
|
1942
|
+
self.message_user(
|
|
1943
|
+
request,
|
|
1944
|
+
_("Imported %(name)s from Odoo.")
|
|
1945
|
+
% {"name": product.name},
|
|
1946
|
+
)
|
|
1947
|
+
return HttpResponseRedirect(
|
|
1948
|
+
reverse(
|
|
1949
|
+
"admin:%s_%s_change"
|
|
1950
|
+
% (
|
|
1951
|
+
product._meta.app_label,
|
|
1952
|
+
product._meta.model_name,
|
|
1953
|
+
),
|
|
1954
|
+
args=[product.pk],
|
|
1955
|
+
)
|
|
1956
|
+
)
|
|
1957
|
+
context.update(
|
|
1958
|
+
{
|
|
1959
|
+
"form": form,
|
|
1960
|
+
"results": results,
|
|
1961
|
+
"selected_product_id": selected_product_id,
|
|
1962
|
+
}
|
|
1963
|
+
)
|
|
1964
|
+
context["media"] = self.media + form.media
|
|
1965
|
+
return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
|
|
1620
1966
|
|
|
1621
1967
|
|
|
1622
1968
|
class RFIDResource(resources.ModelResource):
|
|
@@ -1756,30 +2102,39 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
1756
2102
|
]
|
|
1757
2103
|
RECURRENCE_CHOICES = ClientReportSchedule.PERIODICITY_CHOICES
|
|
1758
2104
|
period = forms.ChoiceField(
|
|
1759
|
-
choices=PERIOD_CHOICES,
|
|
2105
|
+
choices=PERIOD_CHOICES,
|
|
2106
|
+
widget=forms.RadioSelect,
|
|
2107
|
+
initial="range",
|
|
2108
|
+
help_text="Choose how the reporting window will be calculated.",
|
|
1760
2109
|
)
|
|
1761
2110
|
start = forms.DateField(
|
|
1762
2111
|
label="Start date",
|
|
1763
2112
|
required=False,
|
|
1764
2113
|
widget=forms.DateInput(attrs={"type": "date"}),
|
|
2114
|
+
help_text="First day included when using a custom date range.",
|
|
1765
2115
|
)
|
|
1766
2116
|
end = forms.DateField(
|
|
1767
2117
|
label="End date",
|
|
1768
2118
|
required=False,
|
|
1769
2119
|
widget=forms.DateInput(attrs={"type": "date"}),
|
|
2120
|
+
help_text="Last day included when using a custom date range.",
|
|
1770
2121
|
)
|
|
1771
2122
|
week = forms.CharField(
|
|
1772
2123
|
label="Week",
|
|
1773
2124
|
required=False,
|
|
1774
2125
|
widget=forms.TextInput(attrs={"type": "week"}),
|
|
2126
|
+
help_text="Generates the report for the ISO week that you select.",
|
|
1775
2127
|
)
|
|
1776
2128
|
month = forms.DateField(
|
|
1777
2129
|
label="Month",
|
|
1778
2130
|
required=False,
|
|
1779
2131
|
widget=forms.DateInput(attrs={"type": "month"}),
|
|
2132
|
+
help_text="Generates the report for the calendar month that you select.",
|
|
1780
2133
|
)
|
|
1781
2134
|
owner = forms.ModelChoiceField(
|
|
1782
|
-
queryset=get_user_model().objects.all(),
|
|
2135
|
+
queryset=get_user_model().objects.all(),
|
|
2136
|
+
required=False,
|
|
2137
|
+
help_text="Sets who owns the report schedule and is listed as the requestor.",
|
|
1783
2138
|
)
|
|
1784
2139
|
destinations = forms.CharField(
|
|
1785
2140
|
label="Email destinations",
|
|
@@ -1791,6 +2146,7 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
1791
2146
|
label="Recurrency",
|
|
1792
2147
|
choices=RECURRENCE_CHOICES,
|
|
1793
2148
|
initial=ClientReportSchedule.PERIODICITY_NONE,
|
|
2149
|
+
help_text="Defines how often the report should be generated automatically.",
|
|
1794
2150
|
)
|
|
1795
2151
|
disable_emails = forms.BooleanField(
|
|
1796
2152
|
label="Disable email delivery",
|
|
@@ -1915,7 +2271,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1915
2271
|
list_display_links = ("version",)
|
|
1916
2272
|
actions = ["publish_release", "validate_releases"]
|
|
1917
2273
|
change_actions = ["publish_release_action"]
|
|
1918
|
-
changelist_actions = ["refresh_from_pypi"]
|
|
2274
|
+
changelist_actions = ["refresh_from_pypi", "prepare_next_release"]
|
|
1919
2275
|
readonly_fields = ("pypi_url", "is_current", "revision")
|
|
1920
2276
|
fields = (
|
|
1921
2277
|
"package",
|
|
@@ -1977,6 +2333,16 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1977
2333
|
refresh_from_pypi.label = "Refresh from PyPI"
|
|
1978
2334
|
refresh_from_pypi.short_description = "Refresh from PyPI"
|
|
1979
2335
|
|
|
2336
|
+
def prepare_next_release(self, request, queryset):
|
|
2337
|
+
package = Package.objects.filter(is_active=True).first()
|
|
2338
|
+
if not package:
|
|
2339
|
+
self.message_user(request, "No active package", messages.ERROR)
|
|
2340
|
+
return redirect("admin:core_packagerelease_changelist")
|
|
2341
|
+
return PackageAdmin._prepare(self, request, package)
|
|
2342
|
+
|
|
2343
|
+
prepare_next_release.label = "Prepare next Release"
|
|
2344
|
+
prepare_next_release.short_description = "Prepare next release"
|
|
2345
|
+
|
|
1980
2346
|
def _publish_release(self, request, release):
|
|
1981
2347
|
try:
|
|
1982
2348
|
release.full_clean()
|
core/apps.py
CHANGED
|
@@ -105,6 +105,7 @@ class CoreConfig(AppConfig):
|
|
|
105
105
|
lock = Path(settings.BASE_DIR) / "locks" / "celery.lck"
|
|
106
106
|
|
|
107
107
|
if lock.exists():
|
|
108
|
+
from .auto_upgrade import ensure_auto_upgrade_periodic_task
|
|
108
109
|
|
|
109
110
|
def ensure_email_collector_task(**kwargs):
|
|
110
111
|
try: # pragma: no cover - optional dependency
|
|
@@ -131,6 +132,8 @@ class CoreConfig(AppConfig):
|
|
|
131
132
|
pass
|
|
132
133
|
|
|
133
134
|
post_migrate.connect(ensure_email_collector_task, sender=self)
|
|
135
|
+
post_migrate.connect(ensure_auto_upgrade_periodic_task, sender=self)
|
|
136
|
+
ensure_auto_upgrade_periodic_task()
|
|
134
137
|
|
|
135
138
|
from django.db.backends.signals import connection_created
|
|
136
139
|
|