arthexis 0.1.23__py3-none-any.whl → 0.1.25__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.23.dist-info → arthexis-0.1.25.dist-info}/METADATA +39 -18
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/RECORD +31 -30
- config/settings.py +7 -0
- config/urls.py +2 -0
- core/admin.py +140 -213
- core/backends.py +3 -1
- core/models.py +612 -207
- core/system.py +67 -2
- core/tasks.py +25 -0
- core/views.py +0 -3
- nodes/admin.py +465 -292
- nodes/models.py +299 -23
- nodes/tasks.py +13 -16
- nodes/tests.py +291 -130
- nodes/urls.py +11 -0
- nodes/utils.py +9 -2
- nodes/views.py +588 -20
- ocpp/admin.py +729 -175
- ocpp/consumers.py +98 -0
- ocpp/models.py +299 -0
- ocpp/network.py +398 -0
- ocpp/tasks.py +177 -1
- ocpp/tests.py +179 -0
- ocpp/views.py +2 -0
- pages/middleware.py +3 -2
- pages/tests.py +40 -0
- pages/utils.py +70 -0
- pages/views.py +64 -32
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/WHEEL +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -50,6 +50,7 @@ import uuid
|
|
|
50
50
|
import requests
|
|
51
51
|
import datetime
|
|
52
52
|
from django.db import IntegrityError, transaction
|
|
53
|
+
from django.db.models import Q
|
|
53
54
|
import calendar
|
|
54
55
|
import re
|
|
55
56
|
from django_object_actions import DjangoObjectActions
|
|
@@ -63,7 +64,7 @@ from reportlab.graphics.barcode import qr
|
|
|
63
64
|
from reportlab.graphics.shapes import Drawing
|
|
64
65
|
from reportlab.lib.styles import getSampleStyleSheet
|
|
65
66
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
|
66
|
-
from ocpp.models import Transaction
|
|
67
|
+
from ocpp.models import Charger, Transaction
|
|
67
68
|
from ocpp.rfid.utils import build_mode_toggle
|
|
68
69
|
from nodes.models import EmailOutbox
|
|
69
70
|
from .github_helper import GitHubRepositoryError, create_repository_for_package
|
|
@@ -2265,199 +2266,15 @@ class ProductAdminForm(forms.ModelForm):
|
|
|
2265
2266
|
widgets = {"odoo_product": OdooProductWidget}
|
|
2266
2267
|
|
|
2267
2268
|
|
|
2268
|
-
class ProductFetchWizardForm(forms.Form):
|
|
2269
|
-
name = forms.CharField(label="Name", required=False)
|
|
2270
|
-
default_code = forms.CharField(label="Internal reference", required=False)
|
|
2271
|
-
barcode = forms.CharField(label="Barcode", required=False)
|
|
2272
|
-
renewal_period = forms.IntegerField(
|
|
2273
|
-
label="Renewal period (days)", min_value=1, initial=30
|
|
2274
|
-
)
|
|
2275
|
-
|
|
2276
|
-
def __init__(self, *args, require_search_terms=True, **kwargs):
|
|
2277
|
-
self.require_search_terms = require_search_terms
|
|
2278
|
-
super().__init__(*args, **kwargs)
|
|
2279
|
-
|
|
2280
|
-
def clean(self):
|
|
2281
|
-
cleaned = super().clean()
|
|
2282
|
-
if self.require_search_terms:
|
|
2283
|
-
if not any(
|
|
2284
|
-
cleaned.get(field) for field in ("name", "default_code", "barcode")
|
|
2285
|
-
):
|
|
2286
|
-
raise forms.ValidationError(
|
|
2287
|
-
_("Enter at least one field to search for a product.")
|
|
2288
|
-
)
|
|
2289
|
-
return cleaned
|
|
2290
|
-
|
|
2291
|
-
def build_domain(self):
|
|
2292
|
-
domain = []
|
|
2293
|
-
if self.cleaned_data.get("name"):
|
|
2294
|
-
domain.append(("name", "ilike", self.cleaned_data["name"]))
|
|
2295
|
-
if self.cleaned_data.get("default_code"):
|
|
2296
|
-
domain.append(("default_code", "ilike", self.cleaned_data["default_code"]))
|
|
2297
|
-
if self.cleaned_data.get("barcode"):
|
|
2298
|
-
domain.append(("barcode", "ilike", self.cleaned_data["barcode"]))
|
|
2299
|
-
return domain
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
2269
|
@admin.register(Product)
|
|
2303
2270
|
class ProductAdmin(EntityModelAdmin):
|
|
2304
2271
|
form = ProductAdminForm
|
|
2305
|
-
actions = ["
|
|
2272
|
+
actions = ["register_from_odoo"]
|
|
2306
2273
|
change_list_template = "admin/core/product/change_list.html"
|
|
2307
2274
|
|
|
2308
2275
|
def _odoo_profile_admin(self):
|
|
2309
2276
|
return self.admin_site._registry.get(OdooProfile)
|
|
2310
2277
|
|
|
2311
|
-
def _search_odoo_products(self, profile, form):
|
|
2312
|
-
domain = form.build_domain()
|
|
2313
|
-
return profile.execute(
|
|
2314
|
-
"product.product",
|
|
2315
|
-
"search_read",
|
|
2316
|
-
[domain],
|
|
2317
|
-
fields=[
|
|
2318
|
-
"name",
|
|
2319
|
-
"default_code",
|
|
2320
|
-
"barcode",
|
|
2321
|
-
"description_sale",
|
|
2322
|
-
],
|
|
2323
|
-
limit=50,
|
|
2324
|
-
)
|
|
2325
|
-
|
|
2326
|
-
@admin.action(description="Fetch Odoo Product")
|
|
2327
|
-
def fetch_odoo_product(self, request, queryset):
|
|
2328
|
-
profile = getattr(request.user, "odoo_profile", None)
|
|
2329
|
-
has_credentials = bool(profile and profile.is_verified)
|
|
2330
|
-
profile_admin = self._odoo_profile_admin()
|
|
2331
|
-
profile_url = None
|
|
2332
|
-
if profile_admin is not None:
|
|
2333
|
-
profile_url = profile_admin.get_my_profile_url(request)
|
|
2334
|
-
|
|
2335
|
-
context = {
|
|
2336
|
-
"opts": self.model._meta,
|
|
2337
|
-
"queryset": queryset,
|
|
2338
|
-
"action": "fetch_odoo_product",
|
|
2339
|
-
"has_credentials": has_credentials,
|
|
2340
|
-
"profile_url": profile_url,
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
if not has_credentials:
|
|
2344
|
-
context["credential_error"] = _(
|
|
2345
|
-
"Configure your Odoo employee credentials before fetching products."
|
|
2346
|
-
)
|
|
2347
|
-
return TemplateResponse(
|
|
2348
|
-
request, "admin/core/product/fetch_odoo.html", context
|
|
2349
|
-
)
|
|
2350
|
-
|
|
2351
|
-
is_import = "import" in request.POST
|
|
2352
|
-
form_kwargs = {"require_search_terms": not is_import}
|
|
2353
|
-
if request.method == "POST":
|
|
2354
|
-
form = ProductFetchWizardForm(request.POST, **form_kwargs)
|
|
2355
|
-
else:
|
|
2356
|
-
form = ProductFetchWizardForm()
|
|
2357
|
-
|
|
2358
|
-
results = None
|
|
2359
|
-
selected_product_id = request.POST.get("product_id", "")
|
|
2360
|
-
|
|
2361
|
-
if request.method == "POST" and form.is_valid():
|
|
2362
|
-
try:
|
|
2363
|
-
results = self._search_odoo_products(profile, form)
|
|
2364
|
-
except Exception:
|
|
2365
|
-
logger.exception(
|
|
2366
|
-
"Failed to fetch Odoo products for user %s (profile_id=%s, host=%s, database=%s)",
|
|
2367
|
-
getattr(getattr(request, "user", None), "pk", None),
|
|
2368
|
-
getattr(profile, "pk", None),
|
|
2369
|
-
getattr(profile, "host", None),
|
|
2370
|
-
getattr(profile, "database", None),
|
|
2371
|
-
)
|
|
2372
|
-
form.add_error(None, _("Unable to fetch products from Odoo."))
|
|
2373
|
-
results = []
|
|
2374
|
-
else:
|
|
2375
|
-
if is_import:
|
|
2376
|
-
if not self.has_add_permission(request):
|
|
2377
|
-
form.add_error(
|
|
2378
|
-
None, _("You do not have permission to add products.")
|
|
2379
|
-
)
|
|
2380
|
-
else:
|
|
2381
|
-
product_id = request.POST.get("product_id")
|
|
2382
|
-
if not product_id:
|
|
2383
|
-
form.add_error(None, _("Select a product to import."))
|
|
2384
|
-
else:
|
|
2385
|
-
try:
|
|
2386
|
-
odoo_id = int(product_id)
|
|
2387
|
-
except (TypeError, ValueError):
|
|
2388
|
-
form.add_error(None, _("Invalid product selection."))
|
|
2389
|
-
else:
|
|
2390
|
-
match = next(
|
|
2391
|
-
(item for item in results if item.get("id") == odoo_id),
|
|
2392
|
-
None,
|
|
2393
|
-
)
|
|
2394
|
-
if not match:
|
|
2395
|
-
form.add_error(
|
|
2396
|
-
None,
|
|
2397
|
-
_(
|
|
2398
|
-
"The selected product was not found. Run the search again."
|
|
2399
|
-
),
|
|
2400
|
-
)
|
|
2401
|
-
else:
|
|
2402
|
-
existing = self.model.objects.filter(
|
|
2403
|
-
odoo_product__id=odoo_id
|
|
2404
|
-
).first()
|
|
2405
|
-
if existing:
|
|
2406
|
-
self.message_user(
|
|
2407
|
-
request,
|
|
2408
|
-
_(
|
|
2409
|
-
"Product %(name)s already imported; opening existing record."
|
|
2410
|
-
)
|
|
2411
|
-
% {"name": existing.name},
|
|
2412
|
-
level=messages.WARNING,
|
|
2413
|
-
)
|
|
2414
|
-
return HttpResponseRedirect(
|
|
2415
|
-
reverse(
|
|
2416
|
-
"admin:%s_%s_change"
|
|
2417
|
-
% (
|
|
2418
|
-
existing._meta.app_label,
|
|
2419
|
-
existing._meta.model_name,
|
|
2420
|
-
),
|
|
2421
|
-
args=[existing.pk],
|
|
2422
|
-
)
|
|
2423
|
-
)
|
|
2424
|
-
product = self.model.objects.create(
|
|
2425
|
-
name=match.get("name") or f"Odoo Product {odoo_id}",
|
|
2426
|
-
description=match.get("description_sale", "") or "",
|
|
2427
|
-
renewal_period=form.cleaned_data["renewal_period"],
|
|
2428
|
-
odoo_product={
|
|
2429
|
-
"id": odoo_id,
|
|
2430
|
-
"name": match.get("name", ""),
|
|
2431
|
-
},
|
|
2432
|
-
)
|
|
2433
|
-
self.log_addition(
|
|
2434
|
-
request, product, "Imported product from Odoo"
|
|
2435
|
-
)
|
|
2436
|
-
self.message_user(
|
|
2437
|
-
request,
|
|
2438
|
-
_("Imported %(name)s from Odoo.")
|
|
2439
|
-
% {"name": product.name},
|
|
2440
|
-
)
|
|
2441
|
-
return HttpResponseRedirect(
|
|
2442
|
-
reverse(
|
|
2443
|
-
"admin:%s_%s_change"
|
|
2444
|
-
% (
|
|
2445
|
-
product._meta.app_label,
|
|
2446
|
-
product._meta.model_name,
|
|
2447
|
-
),
|
|
2448
|
-
args=[product.pk],
|
|
2449
|
-
)
|
|
2450
|
-
)
|
|
2451
|
-
context.update(
|
|
2452
|
-
{
|
|
2453
|
-
"form": form,
|
|
2454
|
-
"results": results,
|
|
2455
|
-
"selected_product_id": selected_product_id,
|
|
2456
|
-
}
|
|
2457
|
-
)
|
|
2458
|
-
context["media"] = self.media + form.media
|
|
2459
|
-
return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
|
|
2460
|
-
|
|
2461
2278
|
def get_urls(self):
|
|
2462
2279
|
urls = super().get_urls()
|
|
2463
2280
|
custom = [
|
|
@@ -2506,7 +2323,6 @@ class ProductAdmin(EntityModelAdmin):
|
|
|
2506
2323
|
products = profile.execute(
|
|
2507
2324
|
"product.product",
|
|
2508
2325
|
"search_read",
|
|
2509
|
-
[[]],
|
|
2510
2326
|
fields=[
|
|
2511
2327
|
"name",
|
|
2512
2328
|
"description_sale",
|
|
@@ -3615,13 +3431,62 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3615
3431
|
return JsonResponse(result, status=status)
|
|
3616
3432
|
|
|
3617
3433
|
|
|
3434
|
+
class ClientReportRecurrencyFilter(admin.SimpleListFilter):
|
|
3435
|
+
title = "Recurrency"
|
|
3436
|
+
parameter_name = "recurrency"
|
|
3437
|
+
|
|
3438
|
+
def lookups(self, request, model_admin):
|
|
3439
|
+
for value, label in ClientReportSchedule.PERIODICITY_CHOICES:
|
|
3440
|
+
yield (value, label)
|
|
3441
|
+
|
|
3442
|
+
def queryset(self, request, queryset):
|
|
3443
|
+
value = self.value()
|
|
3444
|
+
if not value:
|
|
3445
|
+
return queryset
|
|
3446
|
+
if value == ClientReportSchedule.PERIODICITY_NONE:
|
|
3447
|
+
return queryset.filter(
|
|
3448
|
+
Q(schedule__isnull=True) | Q(schedule__periodicity=value)
|
|
3449
|
+
)
|
|
3450
|
+
return queryset.filter(schedule__periodicity=value)
|
|
3451
|
+
|
|
3452
|
+
|
|
3618
3453
|
@admin.register(ClientReport)
|
|
3619
3454
|
class ClientReportAdmin(EntityModelAdmin):
|
|
3620
|
-
list_display = (
|
|
3455
|
+
list_display = (
|
|
3456
|
+
"created_on",
|
|
3457
|
+
"period_range",
|
|
3458
|
+
"owner",
|
|
3459
|
+
"recurrency_display",
|
|
3460
|
+
"total_kw_period_display",
|
|
3461
|
+
"download_link",
|
|
3462
|
+
)
|
|
3463
|
+
list_select_related = ("schedule", "owner")
|
|
3464
|
+
list_filter = ("owner", ClientReportRecurrencyFilter)
|
|
3621
3465
|
readonly_fields = ("created_on", "data")
|
|
3622
3466
|
|
|
3623
3467
|
change_list_template = "admin/core/clientreport/change_list.html"
|
|
3624
3468
|
|
|
3469
|
+
def period_range(self, obj):
|
|
3470
|
+
return str(obj)
|
|
3471
|
+
|
|
3472
|
+
period_range.short_description = "Period"
|
|
3473
|
+
|
|
3474
|
+
def recurrency_display(self, obj):
|
|
3475
|
+
return obj.periodicity_label
|
|
3476
|
+
|
|
3477
|
+
recurrency_display.short_description = "Recurrency"
|
|
3478
|
+
|
|
3479
|
+
def total_kw_period_display(self, obj):
|
|
3480
|
+
return f"{obj.total_kw_period:.2f}"
|
|
3481
|
+
|
|
3482
|
+
total_kw_period_display.short_description = "Total kW (period)"
|
|
3483
|
+
|
|
3484
|
+
def download_link(self, obj):
|
|
3485
|
+
url = reverse("admin:core_clientreport_download", args=[obj.pk])
|
|
3486
|
+
return format_html('<a href="{}">Download</a>', url)
|
|
3487
|
+
|
|
3488
|
+
download_link.short_description = "Download"
|
|
3489
|
+
|
|
3625
3490
|
class ClientReportForm(forms.Form):
|
|
3626
3491
|
PERIOD_CHOICES = [
|
|
3627
3492
|
("range", "Date range"),
|
|
@@ -3657,8 +3522,28 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3657
3522
|
label="Month",
|
|
3658
3523
|
required=False,
|
|
3659
3524
|
widget=forms.DateInput(attrs={"type": "month"}),
|
|
3525
|
+
input_formats=["%Y-%m"],
|
|
3660
3526
|
help_text="Generates the report for the calendar month that you select.",
|
|
3661
3527
|
)
|
|
3528
|
+
language = forms.ChoiceField(
|
|
3529
|
+
label="Report language",
|
|
3530
|
+
choices=settings.LANGUAGES,
|
|
3531
|
+
help_text="Choose the language used for the generated report.",
|
|
3532
|
+
)
|
|
3533
|
+
title = forms.CharField(
|
|
3534
|
+
label="Report title",
|
|
3535
|
+
required=False,
|
|
3536
|
+
max_length=200,
|
|
3537
|
+
help_text="Optional heading that replaces the default report title.",
|
|
3538
|
+
)
|
|
3539
|
+
chargers = forms.ModelMultipleChoiceField(
|
|
3540
|
+
label="Charge points",
|
|
3541
|
+
queryset=Charger.objects.filter(connector_id__isnull=True)
|
|
3542
|
+
.order_by("display_name", "charger_id"),
|
|
3543
|
+
required=False,
|
|
3544
|
+
widget=forms.CheckboxSelectMultiple,
|
|
3545
|
+
help_text="Choose which charge points are included in the report.",
|
|
3546
|
+
)
|
|
3662
3547
|
owner = forms.ModelChoiceField(
|
|
3663
3548
|
queryset=get_user_model().objects.all(),
|
|
3664
3549
|
required=False,
|
|
@@ -3691,6 +3576,13 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3691
3576
|
and request.user.is_authenticated
|
|
3692
3577
|
):
|
|
3693
3578
|
self.fields["owner"].initial = request.user.pk
|
|
3579
|
+
self.fields["chargers"].widget.attrs["class"] = "charger-options"
|
|
3580
|
+
language_initial = ClientReport.default_language()
|
|
3581
|
+
if request:
|
|
3582
|
+
language_initial = ClientReport.normalize_language(
|
|
3583
|
+
getattr(request, "LANGUAGE_CODE", language_initial)
|
|
3584
|
+
)
|
|
3585
|
+
self.fields["language"].initial = language_initial
|
|
3694
3586
|
|
|
3695
3587
|
def clean(self):
|
|
3696
3588
|
cleaned = super().clean()
|
|
@@ -3735,6 +3627,10 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3735
3627
|
emails.append(candidate)
|
|
3736
3628
|
return emails
|
|
3737
3629
|
|
|
3630
|
+
def clean_title(self):
|
|
3631
|
+
title = self.cleaned_data.get("title")
|
|
3632
|
+
return ClientReport.normalize_title(title)
|
|
3633
|
+
|
|
3738
3634
|
def get_urls(self):
|
|
3739
3635
|
urls = super().get_urls()
|
|
3740
3636
|
custom = [
|
|
@@ -3743,6 +3639,11 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3743
3639
|
self.admin_site.admin_view(self.generate_view),
|
|
3744
3640
|
name="core_clientreport_generate",
|
|
3745
3641
|
),
|
|
3642
|
+
path(
|
|
3643
|
+
"generate/action/",
|
|
3644
|
+
self.admin_site.admin_view(self.generate_report),
|
|
3645
|
+
name="core_clientreport_generate_report",
|
|
3646
|
+
),
|
|
3746
3647
|
path(
|
|
3747
3648
|
"download/<int:report_id>/",
|
|
3748
3649
|
self.admin_site.admin_view(self.download_view),
|
|
@@ -3763,14 +3664,37 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3763
3664
|
enable_emails = form.cleaned_data.get("enable_emails", False)
|
|
3764
3665
|
disable_emails = not enable_emails
|
|
3765
3666
|
recipients = form.cleaned_data.get("destinations") if enable_emails else []
|
|
3667
|
+
chargers = list(form.cleaned_data.get("chargers") or [])
|
|
3668
|
+
language = form.cleaned_data.get("language")
|
|
3669
|
+
title = form.cleaned_data.get("title")
|
|
3766
3670
|
report = ClientReport.generate(
|
|
3767
3671
|
form.cleaned_data["start"],
|
|
3768
3672
|
form.cleaned_data["end"],
|
|
3769
3673
|
owner=owner,
|
|
3770
3674
|
recipients=recipients,
|
|
3771
3675
|
disable_emails=disable_emails,
|
|
3676
|
+
chargers=chargers,
|
|
3677
|
+
language=language,
|
|
3678
|
+
title=title,
|
|
3772
3679
|
)
|
|
3773
3680
|
report.store_local_copy()
|
|
3681
|
+
if chargers:
|
|
3682
|
+
report.chargers.set(chargers)
|
|
3683
|
+
if enable_emails and recipients:
|
|
3684
|
+
delivered = report.send_delivery(
|
|
3685
|
+
to=recipients,
|
|
3686
|
+
cc=[],
|
|
3687
|
+
outbox=ClientReport.resolve_outbox_for_owner(owner),
|
|
3688
|
+
reply_to=ClientReport.resolve_reply_to_for_owner(owner),
|
|
3689
|
+
)
|
|
3690
|
+
if delivered:
|
|
3691
|
+
report.recipients = delivered
|
|
3692
|
+
report.save(update_fields=["recipients"])
|
|
3693
|
+
self.message_user(
|
|
3694
|
+
request,
|
|
3695
|
+
"Consumer report emailed to the selected recipients.",
|
|
3696
|
+
messages.SUCCESS,
|
|
3697
|
+
)
|
|
3774
3698
|
recurrence = form.cleaned_data.get("recurrence")
|
|
3775
3699
|
if recurrence and recurrence != ClientReportSchedule.PERIODICITY_NONE:
|
|
3776
3700
|
schedule = ClientReportSchedule.objects.create(
|
|
@@ -3779,12 +3703,16 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3779
3703
|
periodicity=recurrence,
|
|
3780
3704
|
email_recipients=recipients,
|
|
3781
3705
|
disable_emails=disable_emails,
|
|
3706
|
+
language=language,
|
|
3707
|
+
title=title,
|
|
3782
3708
|
)
|
|
3709
|
+
if chargers:
|
|
3710
|
+
schedule.chargers.set(chargers)
|
|
3783
3711
|
report.schedule = schedule
|
|
3784
3712
|
report.save(update_fields=["schedule"])
|
|
3785
3713
|
self.message_user(
|
|
3786
3714
|
request,
|
|
3787
|
-
"
|
|
3715
|
+
"Consumer report schedule created; future reports will be generated automatically.",
|
|
3788
3716
|
messages.SUCCESS,
|
|
3789
3717
|
)
|
|
3790
3718
|
if disable_emails:
|
|
@@ -3812,45 +3740,44 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3812
3740
|
"report": report,
|
|
3813
3741
|
"schedule": schedule,
|
|
3814
3742
|
"download_url": download_url,
|
|
3815
|
-
"
|
|
3743
|
+
"opts": self.model._meta,
|
|
3816
3744
|
}
|
|
3817
3745
|
)
|
|
3818
3746
|
return TemplateResponse(
|
|
3819
3747
|
request, "admin/core/clientreport/generate.html", context
|
|
3820
3748
|
)
|
|
3821
3749
|
|
|
3750
|
+
def get_changelist_actions(self, request):
|
|
3751
|
+
parent = getattr(super(), "get_changelist_actions", None)
|
|
3752
|
+
actions: list[str] = []
|
|
3753
|
+
if callable(parent):
|
|
3754
|
+
parent_actions = parent(request)
|
|
3755
|
+
if parent_actions:
|
|
3756
|
+
actions.extend(parent_actions)
|
|
3757
|
+
if "generate_report" not in actions:
|
|
3758
|
+
actions.append("generate_report")
|
|
3759
|
+
return actions
|
|
3760
|
+
|
|
3761
|
+
def generate_report(self, request):
|
|
3762
|
+
return HttpResponseRedirect(reverse("admin:core_clientreport_generate"))
|
|
3763
|
+
|
|
3764
|
+
generate_report.label = _("Generate report")
|
|
3765
|
+
|
|
3822
3766
|
def download_view(self, request, report_id: int):
|
|
3823
3767
|
report = get_object_or_404(ClientReport, pk=report_id)
|
|
3824
3768
|
pdf_path = report.ensure_pdf()
|
|
3825
3769
|
if not pdf_path.exists():
|
|
3826
3770
|
raise Http404("Report file unavailable")
|
|
3827
|
-
|
|
3771
|
+
end_date = report.end_date
|
|
3772
|
+
if hasattr(end_date, "isoformat"):
|
|
3773
|
+
end_date_str = end_date.isoformat()
|
|
3774
|
+
else: # pragma: no cover - fallback for unexpected values
|
|
3775
|
+
end_date_str = str(end_date)
|
|
3776
|
+
filename = f"consumer-report-{end_date_str}.pdf"
|
|
3828
3777
|
response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
|
|
3829
3778
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
3830
3779
|
return response
|
|
3831
3780
|
|
|
3832
|
-
def _build_report_history(self, request):
|
|
3833
|
-
queryset = ClientReport.objects.order_by("-created_on")[:20]
|
|
3834
|
-
history = []
|
|
3835
|
-
for item in queryset:
|
|
3836
|
-
totals = item.rows_for_display.get("totals", {})
|
|
3837
|
-
history.append(
|
|
3838
|
-
{
|
|
3839
|
-
"instance": item,
|
|
3840
|
-
"download_url": reverse(
|
|
3841
|
-
"admin:core_clientreport_download", args=[item.pk]
|
|
3842
|
-
),
|
|
3843
|
-
"email_enabled": not item.disable_emails,
|
|
3844
|
-
"recipients": item.recipients or [],
|
|
3845
|
-
"totals": {
|
|
3846
|
-
"total_kw": totals.get("total_kw", 0.0),
|
|
3847
|
-
"total_kw_period": totals.get("total_kw_period", 0.0),
|
|
3848
|
-
},
|
|
3849
|
-
}
|
|
3850
|
-
)
|
|
3851
|
-
return history
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
3781
|
@admin.register(PackageRelease)
|
|
3855
3782
|
class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
3856
3783
|
change_list_template = "admin/core/packagerelease/change_list.html"
|
core/backends.py
CHANGED
|
@@ -217,7 +217,9 @@ class LocalhostAdminBackend(ModelBackend):
|
|
|
217
217
|
try:
|
|
218
218
|
ipaddress.ip_address(host)
|
|
219
219
|
except ValueError:
|
|
220
|
-
if
|
|
220
|
+
if host.lower() == "localhost":
|
|
221
|
+
host = "127.0.0.1"
|
|
222
|
+
elif not self._is_test_environment(request):
|
|
221
223
|
return None
|
|
222
224
|
forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
223
225
|
if forwarded:
|