arthexis 0.1.23__py3-none-any.whl → 0.1.24__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.23
3
+ Version: 0.1.24
4
4
  Summary: Power & Energy Infrastructure
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -15,7 +15,7 @@ Requires-Dist: amqp==5.3.1
15
15
  Requires-Dist: annotated-types==0.7.0
16
16
  Requires-Dist: anyio==4.9.0
17
17
  Requires-Dist: asgiref==3.10.0
18
- Requires-Dist: atproto==0.0.62
18
+ Requires-Dist: atproto<0.1.0,>=0.0.63
19
19
  Requires-Dist: attrs==25.3.0
20
20
  Requires-Dist: autobahn==24.4.2
21
21
  Requires-Dist: Automat==25.4.16
@@ -70,8 +70,8 @@ Requires-Dist: psycopg-binary==3.2.12
70
70
  Requires-Dist: pyasn1==0.6.1
71
71
  Requires-Dist: pyasn1_modules==0.4.2
72
72
  Requires-Dist: pycparser==2.22
73
- Requires-Dist: pydantic==2.11.7
74
- Requires-Dist: pydantic_core==2.33.2
73
+ Requires-Dist: pydantic==2.12.3
74
+ Requires-Dist: pydantic_core==2.41.4
75
75
  Requires-Dist: pyOpenSSL==25.1.0
76
76
  Requires-Dist: pyperclip==1.11.0
77
77
  Requires-Dist: PySocks==1.7.1
@@ -106,7 +106,7 @@ Requires-Dist: vine==5.1.0
106
106
  Requires-Dist: wcwidth==0.2.14
107
107
  Requires-Dist: webencodings==0.5.1
108
108
  Requires-Dist: websocket-client==1.8.0
109
- Requires-Dist: websockets==13.1
109
+ Requires-Dist: websockets==15.0.1
110
110
  Requires-Dist: whitenoise==6.11.0
111
111
  Requires-Dist: plyer==2.1.0; sys_platform == "win32"
112
112
  Requires-Dist: wsproto==1.2.0
@@ -1,4 +1,4 @@
1
- arthexis-0.1.23.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
1
+ arthexis-0.1.24.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  config/__init__.py,sha256=AwpOX7il-DAOmkdJ5dVfVJ3CWWebn1lHyQNmkw1EkDw,103
3
3
  config/active_app.py,sha256=KJqYh-o91nPQjVXPEdbiJHzsI6cN9IZsBZ9O3iZ6Hyc,373
4
4
  config/asgi.py,sha256=Z2HjWrxOxVU9BXcqS7dMEfOGJC48H-WPwFwokRdermY,774
@@ -10,12 +10,12 @@ config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
10
10
  config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
11
11
  config/middleware.py,sha256=zF8Cma0n5G8NNdh2LVeNJi7Hgl1G4mF9msRE2eRi1RU,2328
12
12
  config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
13
- config/settings.py,sha256=b27y7mmQMJmcIFuceNxrX-9yO_R1GqbIf_wFdZNvTWo,20789
13
+ config/settings.py,sha256=tFf0C4PO1whB4a_U7KcVlYnp2_gNC16t-cce3nNoStc,20914
14
14
  config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
15
15
  config/urls.py,sha256=979WyL05v7nb-Lz_3Qf6Z2QzGtWpXADbIKZ28Zm12ts,5535
16
16
  config/wsgi.py,sha256=zU_mKlya6hejQ21PxKacTui3dUWd4ca_-YJNSYAoMX0,433
17
17
  core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- core/admin.py,sha256=DhDHDYQM2Za6FbgWoJs31wsVi6zUo0GS0CawOgONOPw,149468
18
+ core/admin.py,sha256=N_x_4t7lSBIg1f13lN5qW54l29giXcGAy3_GWRVn4C0,153652
19
19
  core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
20
20
  core/admindocs.py,sha256=ycD0bJ_VE6rTGf9ebXTiKdYkD8Y8hD2oQ4HxxoBURCM,6756
21
21
  core/apps.py,sha256=S6fySxtxUzfvz8FI9dii0KI4wSyLhh5API_oeERLIsc,14084
@@ -34,7 +34,7 @@ core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
34
34
  core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
35
35
  core/mailer.py,sha256=JpW0RnD9uZ4O-wvlqeW7CMw95IFeCSkdvbankJDwHq0,2886
36
36
  core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
37
- core/models.py,sha256=A2QOFW6mDp2aDDOkZh9GiAGxGfjzrWD4gdDEaM60fCU,159883
37
+ core/models.py,sha256=jwr_LL0CmtLLybiFjeRFMPY1aNRLaE_IOPvQuihwnJ0,171669
38
38
  core/notifications.py,sha256=jNLSuSCrhb8x5cDu_APeDlkrmbMejufk5eJOhssAC4I,3917
39
39
  core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
40
40
  core/reference_utils.py,sha256=tffCoyE1w4_SmYzXVWOsW8aR_ZVVTSPzrGhBq8K2xzA,3631
@@ -44,7 +44,7 @@ core/sigil_builder.py,sha256=nMuhYlw3j3LosrK85Q0pYsMcfGWCmrmdnv8UG7GTq_o,4856
44
44
  core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
45
45
  core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
46
46
  core/system.py,sha256=6ndxYDPswKkC3ySTwbgXzH0CdQYCZJytfA-99smyv_Q,42249
47
- core/tasks.py,sha256=d0MQP5fmn5pA2VCFGxDMEX0xppDIIh8IAzPfGdnk8J4,12340
47
+ core/tasks.py,sha256=ptO44VTBAoTwf7Y3pI6TnniIs4lTUgN4MKCgNAUjhm4,13135
48
48
  core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
49
49
  core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
50
50
  core/tests.py,sha256=PuxoarDS4reHNV4EDIyVRW7xIOFxZJYou1K_LI9ZNHY,105265
@@ -54,7 +54,7 @@ core/user_data.py,sha256=4pheHB5RqLJtmWMql30CLaCpuVqSyShXb7Sy-crRk_4,22400
54
54
  core/views.py,sha256=QIYcBDjzn3YQzP53ub99wVR79d8SCNXRfSX_ENW3snE,88310
55
55
  core/widgets.py,sha256=vlR9PlFfZGlkHm5X2cqNXuEBZSj8gmWaR6MO1mMy6kg,6904
56
56
  nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- nodes/admin.py,sha256=2lkeWBCgKuHQQs0olHeE10MvOqhdS8eO9M82EDWhmFc,73679
57
+ nodes/admin.py,sha256=oOPXFsqQXGUKpgH0B9SIdcPJziBqZRrgFEwo-yEH-O4,72895
58
58
  nodes/apps.py,sha256=oi_M2Ya8CAR8N_MoYU68u7_9u-9SlIMelzLOgYM9tDs,3059
59
59
  nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
60
60
  nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
@@ -65,23 +65,23 @@ nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
65
65
  nodes/rfid_sync.py,sha256=oeblawcp6xeLApdIuhsJS83OAk58Eu7pVVmgpAc0Nt8,6953
66
66
  nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
67
67
  nodes/tasks.py,sha256=7m9pKO-iI6JDdfPQ-GWRGown4mdyKrcroOnhbiWN7dY,5246
68
- nodes/tests.py,sha256=22fCi_o6lDf-hZmAgQuCNa8i2u4kjZUvN90unCbKi_E,188926
69
- nodes/urls.py,sha256=-o9_pLo6XHerKMQwL0TW80wm6wmtVZqyNWcUhpdq9vk,915
68
+ nodes/tests.py,sha256=IikaUCBOUo7r0VHJmqcnk-CsXbbBDT8687p0BmVVJOM,186523
69
+ nodes/urls.py,sha256=c1C-4rROmp51HbVf3KTERuFYvTRXwD5LlApoX4SIwBg,1135
70
70
  nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
71
- nodes/views.py,sha256=Z-bFzCLaP4w1l2NzqknmwsBMwuLSWRtzsbroNfA2vJU,39794
71
+ nodes/views.py,sha256=b7g2TD4pHO01SK6e7uDf78zKbnQVVw3v0N7S2VBvdKQ,57780
72
72
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- ocpp/admin.py,sha256=O5dqd_bwK8k612ltr4Tt9xR3rs3VxxJXCTxarx0mW0w,40970
73
+ ocpp/admin.py,sha256=tRB7F9a4nQxcM-xV1AAeGRlEKQxm8i235yIq57xDU90,54379
74
74
  ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
75
75
  ocpp/consumers.py,sha256=7PYlOkSlHSlIz2FUcBjui4uLFEIHOBWIHfnYpvITrMY,71719
76
76
  ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
77
77
  ocpp/evcs_discovery.py,sha256=OmrzgaOHwveDRJs8AIhrM3apX8_k2PPXh_oYaYpNW3c,3876
78
- ocpp/models.py,sha256=1m3kVeuyGoJXLeaR__t_DgcVhAK3MHbCdnv3hF0WZMw,37587
78
+ ocpp/models.py,sha256=VmkNPEACRks2kbyUa-2qt3xjrtBaipBonlgOk8xFTXg,38481
79
79
  ocpp/reference_utils.py,sha256=_UR82GfE93kv4766mHyVIfdhhyYvrT59660r3H6W55M,1072
80
80
  ocpp/routing.py,sha256=3kQya-MdJ00778xDmX0esQLBP05P200V45asg-CGNoo,438
81
81
  ocpp/simulator.py,sha256=vnyd59QffT79AaPhmfM_jipni_nqfG57X5tXyx1rBoc,28016
82
82
  ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
83
83
  ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
84
- ocpp/tasks.py,sha256=VSna6Mi1jp3zjYTo4y0c5N-n-4efIq5FRKu3z2y8Oio,8950
84
+ ocpp/tasks.py,sha256=Hv_YUzT0dIq8OZE0yHIKzViGU7fEPKknXcXuNkJqC-g,20287
85
85
  ocpp/test_export_import.py,sha256=ouQbTCp4mxfqoK6gondlu3PPcyrT9jSbWAX5gqqgaNk,4561
86
86
  ocpp/test_rfid.py,sha256=IhFSlvsI8A8D3S32sRE298nYfrmqxbv7GfVErtNU3DQ,39137
87
87
  ocpp/tests.py,sha256=0AsSR7UKZQUhWFPEwCE4FJHH1Ykl2UpooSGlBIPJOeU,207815
@@ -103,8 +103,8 @@ pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
103
103
  pages/tests.py,sha256=akLS7p62PeUCaSXTAIsjAfDyv4KWOMJ2MkAGjPuX7AE,154350
104
104
  pages/urls.py,sha256=Oe88tm67iVHRFcGJLSBidZ0rkRQPRZ_vRt6ahxNqPek,1499
105
105
  pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
106
- pages/views.py,sha256=48I-XlW5Dop5ZEZPpm8iZmkRlGM20XP9-wEC4HtC3pg,63633
107
- arthexis-0.1.23.dist-info/METADATA,sha256=riViVTMwAbUo-CDK3doWMDCV_wyznoApGZE_AZJxEhc,11886
108
- arthexis-0.1.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
109
- arthexis-0.1.23.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
110
- arthexis-0.1.23.dist-info/RECORD,,
106
+ pages/views.py,sha256=7-10W-GYDzlef98Warr2JIs3oQkTwKoOMo1aFGglj14,65214
107
+ arthexis-0.1.24.dist-info/METADATA,sha256=9xkmegLGMFbSSMo1b3Fk8lGrksWPvJN57AeSDRWJ7so,11895
108
+ arthexis-0.1.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
109
+ arthexis-0.1.24.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
110
+ arthexis-0.1.24.dist-info/RECORD,,
config/settings.py CHANGED
@@ -666,4 +666,8 @@ CELERY_BEAT_SCHEDULE = {
666
666
  "task": "ocpp.tasks.schedule_daily_charge_point_configuration_checks",
667
667
  "schedule": crontab(minute=0, hour=0),
668
668
  },
669
+ "ocpp_remote_sync": {
670
+ "task": "ocpp.tasks.sync_remote_chargers",
671
+ "schedule": crontab(minute="*"),
672
+ },
669
673
  }
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
@@ -3615,13 +3616,62 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3615
3616
  return JsonResponse(result, status=status)
3616
3617
 
3617
3618
 
3619
+ class ClientReportRecurrencyFilter(admin.SimpleListFilter):
3620
+ title = "Recurrency"
3621
+ parameter_name = "recurrency"
3622
+
3623
+ def lookups(self, request, model_admin):
3624
+ for value, label in ClientReportSchedule.PERIODICITY_CHOICES:
3625
+ yield (value, label)
3626
+
3627
+ def queryset(self, request, queryset):
3628
+ value = self.value()
3629
+ if not value:
3630
+ return queryset
3631
+ if value == ClientReportSchedule.PERIODICITY_NONE:
3632
+ return queryset.filter(
3633
+ Q(schedule__isnull=True) | Q(schedule__periodicity=value)
3634
+ )
3635
+ return queryset.filter(schedule__periodicity=value)
3636
+
3637
+
3618
3638
  @admin.register(ClientReport)
3619
3639
  class ClientReportAdmin(EntityModelAdmin):
3620
- list_display = ("created_on", "start_date", "end_date")
3640
+ list_display = (
3641
+ "created_on",
3642
+ "period_range",
3643
+ "owner",
3644
+ "recurrency_display",
3645
+ "total_kw_period_display",
3646
+ "download_link",
3647
+ )
3648
+ list_select_related = ("schedule", "owner")
3649
+ list_filter = ("owner", ClientReportRecurrencyFilter)
3621
3650
  readonly_fields = ("created_on", "data")
3622
3651
 
3623
3652
  change_list_template = "admin/core/clientreport/change_list.html"
3624
3653
 
3654
+ def period_range(self, obj):
3655
+ return str(obj)
3656
+
3657
+ period_range.short_description = "Period"
3658
+
3659
+ def recurrency_display(self, obj):
3660
+ return obj.periodicity_label
3661
+
3662
+ recurrency_display.short_description = "Recurrency"
3663
+
3664
+ def total_kw_period_display(self, obj):
3665
+ return f"{obj.total_kw_period:.2f}"
3666
+
3667
+ total_kw_period_display.short_description = "Total kW (period)"
3668
+
3669
+ def download_link(self, obj):
3670
+ url = reverse("admin:core_clientreport_download", args=[obj.pk])
3671
+ return format_html('<a href="{}">Download</a>', url)
3672
+
3673
+ download_link.short_description = "Download"
3674
+
3625
3675
  class ClientReportForm(forms.Form):
3626
3676
  PERIOD_CHOICES = [
3627
3677
  ("range", "Date range"),
@@ -3657,8 +3707,28 @@ class ClientReportAdmin(EntityModelAdmin):
3657
3707
  label="Month",
3658
3708
  required=False,
3659
3709
  widget=forms.DateInput(attrs={"type": "month"}),
3710
+ input_formats=["%Y-%m"],
3660
3711
  help_text="Generates the report for the calendar month that you select.",
3661
3712
  )
3713
+ language = forms.ChoiceField(
3714
+ label="Report language",
3715
+ choices=settings.LANGUAGES,
3716
+ help_text="Choose the language used for the generated report.",
3717
+ )
3718
+ title = forms.CharField(
3719
+ label="Report title",
3720
+ required=False,
3721
+ max_length=200,
3722
+ help_text="Optional heading that replaces the default report title.",
3723
+ )
3724
+ chargers = forms.ModelMultipleChoiceField(
3725
+ label="Charge points",
3726
+ queryset=Charger.objects.filter(connector_id__isnull=True)
3727
+ .order_by("display_name", "charger_id"),
3728
+ required=False,
3729
+ widget=forms.CheckboxSelectMultiple,
3730
+ help_text="Choose which charge points are included in the report.",
3731
+ )
3662
3732
  owner = forms.ModelChoiceField(
3663
3733
  queryset=get_user_model().objects.all(),
3664
3734
  required=False,
@@ -3691,6 +3761,13 @@ class ClientReportAdmin(EntityModelAdmin):
3691
3761
  and request.user.is_authenticated
3692
3762
  ):
3693
3763
  self.fields["owner"].initial = request.user.pk
3764
+ self.fields["chargers"].widget.attrs["class"] = "charger-options"
3765
+ language_initial = ClientReport.default_language()
3766
+ if request:
3767
+ language_initial = ClientReport.normalize_language(
3768
+ getattr(request, "LANGUAGE_CODE", language_initial)
3769
+ )
3770
+ self.fields["language"].initial = language_initial
3694
3771
 
3695
3772
  def clean(self):
3696
3773
  cleaned = super().clean()
@@ -3735,6 +3812,10 @@ class ClientReportAdmin(EntityModelAdmin):
3735
3812
  emails.append(candidate)
3736
3813
  return emails
3737
3814
 
3815
+ def clean_title(self):
3816
+ title = self.cleaned_data.get("title")
3817
+ return ClientReport.normalize_title(title)
3818
+
3738
3819
  def get_urls(self):
3739
3820
  urls = super().get_urls()
3740
3821
  custom = [
@@ -3743,6 +3824,11 @@ class ClientReportAdmin(EntityModelAdmin):
3743
3824
  self.admin_site.admin_view(self.generate_view),
3744
3825
  name="core_clientreport_generate",
3745
3826
  ),
3827
+ path(
3828
+ "generate/action/",
3829
+ self.admin_site.admin_view(self.generate_report),
3830
+ name="core_clientreport_generate_report",
3831
+ ),
3746
3832
  path(
3747
3833
  "download/<int:report_id>/",
3748
3834
  self.admin_site.admin_view(self.download_view),
@@ -3763,14 +3849,37 @@ class ClientReportAdmin(EntityModelAdmin):
3763
3849
  enable_emails = form.cleaned_data.get("enable_emails", False)
3764
3850
  disable_emails = not enable_emails
3765
3851
  recipients = form.cleaned_data.get("destinations") if enable_emails else []
3852
+ chargers = list(form.cleaned_data.get("chargers") or [])
3853
+ language = form.cleaned_data.get("language")
3854
+ title = form.cleaned_data.get("title")
3766
3855
  report = ClientReport.generate(
3767
3856
  form.cleaned_data["start"],
3768
3857
  form.cleaned_data["end"],
3769
3858
  owner=owner,
3770
3859
  recipients=recipients,
3771
3860
  disable_emails=disable_emails,
3861
+ chargers=chargers,
3862
+ language=language,
3863
+ title=title,
3772
3864
  )
3773
3865
  report.store_local_copy()
3866
+ if chargers:
3867
+ report.chargers.set(chargers)
3868
+ if enable_emails and recipients:
3869
+ delivered = report.send_delivery(
3870
+ to=recipients,
3871
+ cc=[],
3872
+ outbox=ClientReport.resolve_outbox_for_owner(owner),
3873
+ reply_to=ClientReport.resolve_reply_to_for_owner(owner),
3874
+ )
3875
+ if delivered:
3876
+ report.recipients = delivered
3877
+ report.save(update_fields=["recipients"])
3878
+ self.message_user(
3879
+ request,
3880
+ "Consumer report emailed to the selected recipients.",
3881
+ messages.SUCCESS,
3882
+ )
3774
3883
  recurrence = form.cleaned_data.get("recurrence")
3775
3884
  if recurrence and recurrence != ClientReportSchedule.PERIODICITY_NONE:
3776
3885
  schedule = ClientReportSchedule.objects.create(
@@ -3779,12 +3888,16 @@ class ClientReportAdmin(EntityModelAdmin):
3779
3888
  periodicity=recurrence,
3780
3889
  email_recipients=recipients,
3781
3890
  disable_emails=disable_emails,
3891
+ language=language,
3892
+ title=title,
3782
3893
  )
3894
+ if chargers:
3895
+ schedule.chargers.set(chargers)
3783
3896
  report.schedule = schedule
3784
3897
  report.save(update_fields=["schedule"])
3785
3898
  self.message_user(
3786
3899
  request,
3787
- "Client report schedule created; future reports will be generated automatically.",
3900
+ "Consumer report schedule created; future reports will be generated automatically.",
3788
3901
  messages.SUCCESS,
3789
3902
  )
3790
3903
  if disable_emails:
@@ -3812,45 +3925,44 @@ class ClientReportAdmin(EntityModelAdmin):
3812
3925
  "report": report,
3813
3926
  "schedule": schedule,
3814
3927
  "download_url": download_url,
3815
- "previous_reports": self._build_report_history(request),
3928
+ "opts": self.model._meta,
3816
3929
  }
3817
3930
  )
3818
3931
  return TemplateResponse(
3819
3932
  request, "admin/core/clientreport/generate.html", context
3820
3933
  )
3821
3934
 
3935
+ def get_changelist_actions(self, request):
3936
+ parent = getattr(super(), "get_changelist_actions", None)
3937
+ actions: list[str] = []
3938
+ if callable(parent):
3939
+ parent_actions = parent(request)
3940
+ if parent_actions:
3941
+ actions.extend(parent_actions)
3942
+ if "generate_report" not in actions:
3943
+ actions.append("generate_report")
3944
+ return actions
3945
+
3946
+ def generate_report(self, request):
3947
+ return HttpResponseRedirect(reverse("admin:core_clientreport_generate"))
3948
+
3949
+ generate_report.label = _("Generate report")
3950
+
3822
3951
  def download_view(self, request, report_id: int):
3823
3952
  report = get_object_or_404(ClientReport, pk=report_id)
3824
3953
  pdf_path = report.ensure_pdf()
3825
3954
  if not pdf_path.exists():
3826
3955
  raise Http404("Report file unavailable")
3827
- filename = f"consumer-report-{report.start_date}-{report.end_date}.pdf"
3956
+ end_date = report.end_date
3957
+ if hasattr(end_date, "isoformat"):
3958
+ end_date_str = end_date.isoformat()
3959
+ else: # pragma: no cover - fallback for unexpected values
3960
+ end_date_str = str(end_date)
3961
+ filename = f"consumer-report-{end_date_str}.pdf"
3828
3962
  response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
3829
3963
  response["Content-Disposition"] = f'attachment; filename="{filename}"'
3830
3964
  return response
3831
3965
 
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
3966
  @admin.register(PackageRelease)
3855
3967
  class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
3856
3968
  change_list_template = "admin/core/packagerelease/change_list.html"