arthexis 0.1.22__tar.gz → 0.1.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. {arthexis-0.1.22 → arthexis-0.1.24}/PKG-INFO +6 -5
  2. {arthexis-0.1.22 → arthexis-0.1.24}/README.md +1 -0
  3. {arthexis-0.1.22 → arthexis-0.1.24}/arthexis.egg-info/PKG-INFO +6 -5
  4. {arthexis-0.1.22 → arthexis-0.1.24}/arthexis.egg-info/SOURCES.txt +3 -0
  5. {arthexis-0.1.22 → arthexis-0.1.24}/arthexis.egg-info/requires.txt +4 -4
  6. {arthexis-0.1.22 → arthexis-0.1.24}/config/settings.py +4 -0
  7. {arthexis-0.1.22 → arthexis-0.1.24}/core/admin.py +200 -16
  8. {arthexis-0.1.22 → arthexis-0.1.24}/core/models.py +878 -118
  9. {arthexis-0.1.22 → arthexis-0.1.24}/core/release.py +0 -5
  10. {arthexis-0.1.22 → arthexis-0.1.24}/core/tasks.py +25 -0
  11. {arthexis-0.1.22 → arthexis-0.1.24}/core/tests.py +29 -1
  12. {arthexis-0.1.22 → arthexis-0.1.24}/core/user_data.py +42 -2
  13. {arthexis-0.1.22 → arthexis-0.1.24}/core/views.py +33 -26
  14. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/admin.py +153 -132
  15. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/models.py +9 -1
  16. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/tests.py +106 -81
  17. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/urls.py +6 -0
  18. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/views.py +620 -48
  19. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/admin.py +543 -166
  20. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/models.py +57 -2
  21. arthexis-0.1.24/ocpp/tasks.py +622 -0
  22. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/tests.py +123 -0
  23. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/views.py +19 -3
  24. {arthexis-0.1.22 → arthexis-0.1.24}/pages/tests.py +25 -6
  25. {arthexis-0.1.22 → arthexis-0.1.24}/pages/urls.py +5 -0
  26. {arthexis-0.1.22 → arthexis-0.1.24}/pages/views.py +117 -11
  27. arthexis-0.1.24/pyproject.toml +26 -0
  28. arthexis-0.1.24/tests/test_admin_client_report.py +221 -0
  29. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_client_report_form.py +20 -0
  30. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_client_report_generation.py +61 -12
  31. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_client_report_schedule.py +48 -6
  32. arthexis-0.1.24/tests/test_csrf_origin_subnet.py +138 -0
  33. arthexis-0.1.24/tests/test_env_refresh_dependency_conflict.py +82 -0
  34. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_fixture_presence.py +18 -6
  35. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_network_setup_interactive.py +3 -7
  36. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_power_admin_group.py +1 -1
  37. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_progress.py +45 -0
  38. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_seed_data.py +10 -5
  39. arthexis-0.1.24/tests/test_stop_script.py +44 -0
  40. arthexis-0.1.24/tests/test_transaction_meter_readings.py +113 -0
  41. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_uninstall_script.py +2 -2
  42. arthexis-0.1.22/ocpp/tasks.py +0 -287
  43. arthexis-0.1.22/pyproject.toml +0 -26
  44. arthexis-0.1.22/tests/test_admin_client_report.py +0 -133
  45. arthexis-0.1.22/tests/test_csrf_origin_subnet.py +0 -21
  46. {arthexis-0.1.22 → arthexis-0.1.24}/LICENSE +0 -0
  47. {arthexis-0.1.22 → arthexis-0.1.24}/arthexis.egg-info/dependency_links.txt +0 -0
  48. {arthexis-0.1.22 → arthexis-0.1.24}/arthexis.egg-info/top_level.txt +0 -0
  49. {arthexis-0.1.22 → arthexis-0.1.24}/config/__init__.py +0 -0
  50. {arthexis-0.1.22 → arthexis-0.1.24}/config/active_app.py +0 -0
  51. {arthexis-0.1.22 → arthexis-0.1.24}/config/asgi.py +0 -0
  52. {arthexis-0.1.22 → arthexis-0.1.24}/config/auth_app.py +0 -0
  53. {arthexis-0.1.22 → arthexis-0.1.24}/config/celery.py +0 -0
  54. {arthexis-0.1.22 → arthexis-0.1.24}/config/context_processors.py +0 -0
  55. {arthexis-0.1.22 → arthexis-0.1.24}/config/horologia_app.py +0 -0
  56. {arthexis-0.1.22 → arthexis-0.1.24}/config/loadenv.py +0 -0
  57. {arthexis-0.1.22 → arthexis-0.1.24}/config/logging.py +0 -0
  58. {arthexis-0.1.22 → arthexis-0.1.24}/config/middleware.py +0 -0
  59. {arthexis-0.1.22 → arthexis-0.1.24}/config/offline.py +0 -0
  60. {arthexis-0.1.22 → arthexis-0.1.24}/config/settings_helpers.py +0 -0
  61. {arthexis-0.1.22 → arthexis-0.1.24}/config/urls.py +0 -0
  62. {arthexis-0.1.22 → arthexis-0.1.24}/config/wsgi.py +0 -0
  63. {arthexis-0.1.22 → arthexis-0.1.24}/core/__init__.py +0 -0
  64. {arthexis-0.1.22 → arthexis-0.1.24}/core/admin_history.py +0 -0
  65. {arthexis-0.1.22 → arthexis-0.1.24}/core/admindocs.py +0 -0
  66. {arthexis-0.1.22 → arthexis-0.1.24}/core/apps.py +0 -0
  67. {arthexis-0.1.22 → arthexis-0.1.24}/core/auto_upgrade.py +0 -0
  68. {arthexis-0.1.22 → arthexis-0.1.24}/core/backends.py +0 -0
  69. {arthexis-0.1.22 → arthexis-0.1.24}/core/changelog.py +0 -0
  70. {arthexis-0.1.22 → arthexis-0.1.24}/core/entity.py +0 -0
  71. {arthexis-0.1.22 → arthexis-0.1.24}/core/environment.py +0 -0
  72. {arthexis-0.1.22 → arthexis-0.1.24}/core/fields.py +0 -0
  73. {arthexis-0.1.22 → arthexis-0.1.24}/core/form_fields.py +0 -0
  74. {arthexis-0.1.22 → arthexis-0.1.24}/core/github_helper.py +0 -0
  75. {arthexis-0.1.22 → arthexis-0.1.24}/core/github_issues.py +0 -0
  76. {arthexis-0.1.22 → arthexis-0.1.24}/core/github_repos.py +0 -0
  77. {arthexis-0.1.22 → arthexis-0.1.24}/core/lcd_screen.py +0 -0
  78. {arthexis-0.1.22 → arthexis-0.1.24}/core/liveupdate.py +0 -0
  79. {arthexis-0.1.22 → arthexis-0.1.24}/core/log_paths.py +0 -0
  80. {arthexis-0.1.22 → arthexis-0.1.24}/core/mailer.py +0 -0
  81. {arthexis-0.1.22 → arthexis-0.1.24}/core/middleware.py +0 -0
  82. {arthexis-0.1.22 → arthexis-0.1.24}/core/notifications.py +0 -0
  83. {arthexis-0.1.22 → arthexis-0.1.24}/core/public_wifi.py +0 -0
  84. {arthexis-0.1.22 → arthexis-0.1.24}/core/reference_utils.py +0 -0
  85. {arthexis-0.1.22 → arthexis-0.1.24}/core/rfid_import_export.py +0 -0
  86. {arthexis-0.1.22 → arthexis-0.1.24}/core/sigil_builder.py +0 -0
  87. {arthexis-0.1.22 → arthexis-0.1.24}/core/sigil_context.py +0 -0
  88. {arthexis-0.1.22 → arthexis-0.1.24}/core/sigil_resolver.py +0 -0
  89. {arthexis-0.1.22 → arthexis-0.1.24}/core/system.py +0 -0
  90. {arthexis-0.1.22 → arthexis-0.1.24}/core/temp_passwords.py +0 -0
  91. {arthexis-0.1.22 → arthexis-0.1.24}/core/test_system_info.py +0 -0
  92. {arthexis-0.1.22 → arthexis-0.1.24}/core/tests_liveupdate.py +0 -0
  93. {arthexis-0.1.22 → arthexis-0.1.24}/core/urls.py +0 -0
  94. {arthexis-0.1.22 → arthexis-0.1.24}/core/widgets.py +0 -0
  95. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/__init__.py +0 -0
  96. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/apps.py +0 -0
  97. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/backends.py +0 -0
  98. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/dns.py +0 -0
  99. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/feature_checks.py +0 -0
  100. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/lcd.py +0 -0
  101. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/reports.py +0 -0
  102. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/rfid_sync.py +0 -0
  103. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/signals.py +0 -0
  104. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/tasks.py +0 -0
  105. {arthexis-0.1.22 → arthexis-0.1.24}/nodes/utils.py +0 -0
  106. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/__init__.py +0 -0
  107. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/apps.py +0 -0
  108. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/consumers.py +0 -0
  109. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/evcs.py +0 -0
  110. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/evcs_discovery.py +0 -0
  111. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/reference_utils.py +0 -0
  112. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/routing.py +0 -0
  113. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/simulator.py +0 -0
  114. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/status_display.py +0 -0
  115. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/store.py +0 -0
  116. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/test_export_import.py +0 -0
  117. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/test_rfid.py +0 -0
  118. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/transactions_io.py +0 -0
  119. {arthexis-0.1.22 → arthexis-0.1.24}/ocpp/urls.py +0 -0
  120. {arthexis-0.1.22 → arthexis-0.1.24}/pages/__init__.py +0 -0
  121. {arthexis-0.1.22 → arthexis-0.1.24}/pages/admin.py +0 -0
  122. {arthexis-0.1.22 → arthexis-0.1.24}/pages/apps.py +0 -0
  123. {arthexis-0.1.22 → arthexis-0.1.24}/pages/checks.py +0 -0
  124. {arthexis-0.1.22 → arthexis-0.1.24}/pages/context_processors.py +0 -0
  125. {arthexis-0.1.22 → arthexis-0.1.24}/pages/defaults.py +0 -0
  126. {arthexis-0.1.22 → arthexis-0.1.24}/pages/forms.py +0 -0
  127. {arthexis-0.1.22 → arthexis-0.1.24}/pages/middleware.py +0 -0
  128. {arthexis-0.1.22 → arthexis-0.1.24}/pages/models.py +0 -0
  129. {arthexis-0.1.22 → arthexis-0.1.24}/pages/module_defaults.py +0 -0
  130. {arthexis-0.1.22 → arthexis-0.1.24}/pages/site_config.py +0 -0
  131. {arthexis-0.1.22 → arthexis-0.1.24}/pages/tasks.py +0 -0
  132. {arthexis-0.1.22 → arthexis-0.1.24}/pages/utils.py +0 -0
  133. {arthexis-0.1.22 → arthexis-0.1.24}/setup.cfg +0 -0
  134. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_acronym_capitalization.py +0 -0
  135. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_doc_commands.py +0 -0
  136. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_doc_model_groups.py +0 -0
  137. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_history.py +0 -0
  138. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_index_actions.py +0 -0
  139. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_model_graph.py +0 -0
  140. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_object_history.py +0 -0
  141. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_profile_link.py +0 -0
  142. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_admin_system_stop.py +0 -0
  143. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_allowed_hosts_hostname.py +0 -0
  144. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_api_login_required.py +0 -0
  145. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_auto_upgrade_scheduler.py +0 -0
  146. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_awg_admin.py +0 -0
  147. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_benchmark_command.py +0 -0
  148. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_build_pypi_command.py +0 -0
  149. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_celery_no_debug.py +0 -0
  150. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_changelog_builder.py +0 -0
  151. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_check_admin_command.py +0 -0
  152. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_check_migrations_script.py +0 -0
  153. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_check_pypi_command.py +0 -0
  154. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_clean_release_logs_command.py +0 -0
  155. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_csrf_failure.py +0 -0
  156. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_dist_cleanup.py +0 -0
  157. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_collector.py +0 -0
  158. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_inbox.py +0 -0
  159. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_inbox_admin.py +0 -0
  160. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_inbox_search_action.py +0 -0
  161. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_outbox_admin.py +0 -0
  162. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_profiles.py +0 -0
  163. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_email_transaction.py +0 -0
  164. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_env_refresh_clean.py +0 -0
  165. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_env_refresh_pip.py +0 -0
  166. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_env_refresh_unlink.py +0 -0
  167. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_experience_admin_group.py +0 -0
  168. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_footer_no_references.py +0 -0
  169. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_footer_presence.py +0 -0
  170. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_footer_render.py +0 -0
  171. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_git_checks.py +0 -0
  172. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_github_issue_reporting.py +0 -0
  173. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_install_script.py +0 -0
  174. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_invitation_login_view.py +0 -0
  175. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_language_switch.py +0 -0
  176. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_lcd_check_command.py +0 -0
  177. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_lcd_smbus2.py +0 -0
  178. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_localhost_admin_backend.py +0 -0
  179. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_log_paths.py +0 -0
  180. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_login_view_no_site.py +0 -0
  181. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_manage_debug_flag.py +0 -0
  182. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_manuals.py +0 -0
  183. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_message_command.py +0 -0
  184. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_migrations.py +0 -0
  185. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_model_verbose_name_capitalization.py +0 -0
  186. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_node_info_view.py +0 -0
  187. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_notifications_fallback.py +0 -0
  188. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_notify_command.py +0 -0
  189. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_ocpp_session_lock.py +0 -0
  190. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_odoo_product.py +0 -0
  191. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_odoo_profile.py +0 -0
  192. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_odoo_profile_admin.py +0 -0
  193. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_odoo_quote_report.py +0 -0
  194. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_offline.py +0 -0
  195. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_package_admin_next_release.py +0 -0
  196. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_profile_inline_deletion.py +0 -0
  197. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_pypi_check.py +0 -0
  198. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_pypi_token.py +0 -0
  199. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_readme_assets.py +0 -0
  200. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_readme_editor.py +0 -0
  201. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_readme_language.py +0 -0
  202. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_reference_qr_code.py +0 -0
  203. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_reference_transaction_uuid.py +0 -0
  204. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_register_site_apps_command.py +0 -0
  205. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_build.py +0 -0
  206. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_build_flow.py +0 -0
  207. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_checklist.py +0 -0
  208. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_logs.py +0 -0
  209. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_manager_admin.py +0 -0
  210. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_packages.py +0 -0
  211. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_progress_pre_release_integration.py +0 -0
  212. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_push.py +0 -0
  213. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_release_tasks.py +0 -0
  214. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_render_nginx_sites.py +0 -0
  215. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_request_invite.py +0 -0
  216. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_admin_print_labels.py +0 -0
  217. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_admin_reference_clear.py +0 -0
  218. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_admin_scan_csrf.py +0 -0
  219. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_always_on.py +0 -0
  220. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_backend.py +0 -0
  221. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_background_reader.py +0 -0
  222. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_client_report.py +0 -0
  223. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_rfid_watch_command.py +0 -0
  224. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_role_marker_filtering.py +0 -0
  225. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_send_invite_command.py +0 -0
  226. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_settings_helpers.py +0 -0
  227. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_settings_mcp_port.py +0 -0
  228. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_shell_scripts.py +0 -0
  229. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_show_leads_command.py +0 -0
  230. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_sigil_builder.py +0 -0
  231. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_sigil_resolution.py +0 -0
  232. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_sites_utils.py +0 -0
  233. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_social_profile.py +0 -0
  234. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_staff_login_net_message.py +0 -0
  235. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_staff_required_decorator.py +0 -0
  236. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_suite_gateway.py +0 -0
  237. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_switch_role_script.py +0 -0
  238. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_system_changelog_report.py +0 -0
  239. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_temp_passwords.py +0 -0
  240. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_totp_admin.py +0 -0
  241. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_totp_backend.py +0 -0
  242. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_update_fixtures_command.py +0 -0
  243. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_upgrade_report.py +0 -0
  244. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_urls_autodiscover.py +0 -0
  245. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_user_data_admin.py +0 -0
  246. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_version_endpoint.py +0 -0
  247. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_version_file.py +0 -0
  248. {arthexis-0.1.22 → arthexis-0.1.24}/tests/test_vscode_manage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.22
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
@@ -203,6 +203,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
203
203
  - `--constellation` – enables the multi-user orchestration stack.
204
204
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
205
205
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
206
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
206
207
 
207
208
  - **Windows:**
208
209
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -88,6 +88,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
88
88
  - `--constellation` – enables the multi-user orchestration stack.
89
89
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
90
90
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
91
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
91
92
 
92
93
  - **Windows:**
93
94
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.22
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
@@ -203,6 +203,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
203
203
  - `--constellation` – enables the multi-user orchestration stack.
204
204
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
205
205
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
206
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
206
207
 
207
208
  - **Windows:**
208
209
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -147,6 +147,7 @@ tests/test_email_outbox_admin.py
147
147
  tests/test_email_profiles.py
148
148
  tests/test_email_transaction.py
149
149
  tests/test_env_refresh_clean.py
150
+ tests/test_env_refresh_dependency_conflict.py
150
151
  tests/test_env_refresh_pip.py
151
152
  tests/test_env_refresh_unlink.py
152
153
  tests/test_experience_admin_group.py
@@ -223,12 +224,14 @@ tests/test_sites_utils.py
223
224
  tests/test_social_profile.py
224
225
  tests/test_staff_login_net_message.py
225
226
  tests/test_staff_required_decorator.py
227
+ tests/test_stop_script.py
226
228
  tests/test_suite_gateway.py
227
229
  tests/test_switch_role_script.py
228
230
  tests/test_system_changelog_report.py
229
231
  tests/test_temp_passwords.py
230
232
  tests/test_totp_admin.py
231
233
  tests/test_totp_backend.py
234
+ tests/test_transaction_meter_readings.py
232
235
  tests/test_uninstall_script.py
233
236
  tests/test_update_fixtures_command.py
234
237
  tests/test_upgrade_report.py
@@ -2,7 +2,7 @@ amqp==5.3.1
2
2
  annotated-types==0.7.0
3
3
  anyio==4.9.0
4
4
  asgiref==3.10.0
5
- atproto==0.0.62
5
+ atproto<0.1.0,>=0.0.63
6
6
  attrs==25.3.0
7
7
  autobahn==24.4.2
8
8
  Automat==25.4.16
@@ -55,8 +55,8 @@ psycopg-binary==3.2.12
55
55
  pyasn1==0.6.1
56
56
  pyasn1_modules==0.4.2
57
57
  pycparser==2.22
58
- pydantic==2.11.7
59
- pydantic_core==2.33.2
58
+ pydantic==2.12.3
59
+ pydantic_core==2.41.4
60
60
  pyOpenSSL==25.1.0
61
61
  pyperclip==1.11.0
62
62
  PySocks==1.7.1
@@ -91,7 +91,7 @@ vine==5.1.0
91
91
  wcwidth==0.2.14
92
92
  webencodings==0.5.1
93
93
  websocket-client==1.8.0
94
- websockets==13.1
94
+ websockets==15.0.1
95
95
  whitenoise==6.11.0
96
96
  wsproto==1.2.0
97
97
  zope.interface==8.0.1
@@ -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
  }
@@ -9,10 +9,13 @@ from django.urls import NoReverseMatch, path, reverse
9
9
  from urllib.parse import urlencode, urlparse
10
10
  from django.shortcuts import get_object_or_404, redirect, render
11
11
  from django.http import (
12
+ FileResponse,
13
+ Http404,
12
14
  HttpResponse,
13
15
  JsonResponse,
14
16
  HttpResponseBase,
15
17
  HttpResponseRedirect,
18
+ HttpResponseNotAllowed,
16
19
  )
17
20
  from django.template.response import TemplateResponse
18
21
  from django.conf import settings
@@ -47,6 +50,7 @@ import uuid
47
50
  import requests
48
51
  import datetime
49
52
  from django.db import IntegrityError, transaction
53
+ from django.db.models import Q
50
54
  import calendar
51
55
  import re
52
56
  from django_object_actions import DjangoObjectActions
@@ -60,7 +64,7 @@ from reportlab.graphics.barcode import qr
60
64
  from reportlab.graphics.shapes import Drawing
61
65
  from reportlab.lib.styles import getSampleStyleSheet
62
66
  from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
63
- from ocpp.models import Transaction
67
+ from ocpp.models import Charger, Transaction
64
68
  from ocpp.rfid.utils import build_mode_toggle
65
69
  from nodes.models import EmailOutbox
66
70
  from .github_helper import GitHubRepositoryError, create_repository_for_package
@@ -606,15 +610,18 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
606
610
  change_actions = ["create_repository_action", "prepare_next_release_action"]
607
611
 
608
612
  def _prepare(self, request, package):
613
+ if request.method not in {"POST", "GET"}:
614
+ return HttpResponseNotAllowed(["GET", "POST"])
609
615
  from pathlib import Path
610
616
  from packaging.version import Version
611
617
 
612
618
  ver_file = Path("VERSION")
613
- repo_version = (
614
- Version(ver_file.read_text().strip())
615
- if ver_file.exists()
616
- else Version("0.0.0")
617
- )
619
+ if ver_file.exists():
620
+ raw_version = ver_file.read_text().strip()
621
+ cleaned_version = raw_version.rstrip("+") or "0.0.0"
622
+ repo_version = Version(cleaned_version)
623
+ else:
624
+ repo_version = Version("0.0.0")
618
625
 
619
626
  pypi_latest = Version("0.0.0")
620
627
  try:
@@ -3609,13 +3616,62 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3609
3616
  return JsonResponse(result, status=status)
3610
3617
 
3611
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
+
3612
3638
  @admin.register(ClientReport)
3613
3639
  class ClientReportAdmin(EntityModelAdmin):
3614
- 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)
3615
3650
  readonly_fields = ("created_on", "data")
3616
3651
 
3617
3652
  change_list_template = "admin/core/clientreport/change_list.html"
3618
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
+
3619
3675
  class ClientReportForm(forms.Form):
3620
3676
  PERIOD_CHOICES = [
3621
3677
  ("range", "Date range"),
@@ -3651,8 +3707,28 @@ class ClientReportAdmin(EntityModelAdmin):
3651
3707
  label="Month",
3652
3708
  required=False,
3653
3709
  widget=forms.DateInput(attrs={"type": "month"}),
3710
+ input_formats=["%Y-%m"],
3654
3711
  help_text="Generates the report for the calendar month that you select.",
3655
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
+ )
3656
3732
  owner = forms.ModelChoiceField(
3657
3733
  queryset=get_user_model().objects.all(),
3658
3734
  required=False,
@@ -3670,10 +3746,10 @@ class ClientReportAdmin(EntityModelAdmin):
3670
3746
  initial=ClientReportSchedule.PERIODICITY_NONE,
3671
3747
  help_text="Defines how often the report should be generated automatically.",
3672
3748
  )
3673
- disable_emails = forms.BooleanField(
3674
- label="Disable email delivery",
3749
+ enable_emails = forms.BooleanField(
3750
+ label="Enable email delivery",
3675
3751
  required=False,
3676
- help_text="Generate files without sending emails.",
3752
+ help_text="Send the report via email to the recipients listed above.",
3677
3753
  )
3678
3754
 
3679
3755
  def __init__(self, *args, request=None, **kwargs):
@@ -3685,6 +3761,13 @@ class ClientReportAdmin(EntityModelAdmin):
3685
3761
  and request.user.is_authenticated
3686
3762
  ):
3687
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
3688
3771
 
3689
3772
  def clean(self):
3690
3773
  cleaned = super().clean()
@@ -3729,6 +3812,10 @@ class ClientReportAdmin(EntityModelAdmin):
3729
3812
  emails.append(candidate)
3730
3813
  return emails
3731
3814
 
3815
+ def clean_title(self):
3816
+ title = self.cleaned_data.get("title")
3817
+ return ClientReport.normalize_title(title)
3818
+
3732
3819
  def get_urls(self):
3733
3820
  urls = super().get_urls()
3734
3821
  custom = [
@@ -3737,6 +3824,16 @@ class ClientReportAdmin(EntityModelAdmin):
3737
3824
  self.admin_site.admin_view(self.generate_view),
3738
3825
  name="core_clientreport_generate",
3739
3826
  ),
3827
+ path(
3828
+ "generate/action/",
3829
+ self.admin_site.admin_view(self.generate_report),
3830
+ name="core_clientreport_generate_report",
3831
+ ),
3832
+ path(
3833
+ "download/<int:report_id>/",
3834
+ self.admin_site.admin_view(self.download_view),
3835
+ name="core_clientreport_download",
3836
+ ),
3740
3837
  ]
3741
3838
  return custom + urls
3742
3839
 
@@ -3744,40 +3841,127 @@ class ClientReportAdmin(EntityModelAdmin):
3744
3841
  form = self.ClientReportForm(request.POST or None, request=request)
3745
3842
  report = None
3746
3843
  schedule = None
3844
+ download_url = None
3747
3845
  if request.method == "POST" and form.is_valid():
3748
3846
  owner = form.cleaned_data.get("owner")
3749
3847
  if not owner and request.user.is_authenticated:
3750
3848
  owner = request.user
3849
+ enable_emails = form.cleaned_data.get("enable_emails", False)
3850
+ disable_emails = not enable_emails
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")
3751
3855
  report = ClientReport.generate(
3752
3856
  form.cleaned_data["start"],
3753
3857
  form.cleaned_data["end"],
3754
3858
  owner=owner,
3755
- recipients=form.cleaned_data.get("destinations"),
3756
- disable_emails=form.cleaned_data.get("disable_emails", False),
3859
+ recipients=recipients,
3860
+ disable_emails=disable_emails,
3861
+ chargers=chargers,
3862
+ language=language,
3863
+ title=title,
3757
3864
  )
3758
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
+ )
3759
3883
  recurrence = form.cleaned_data.get("recurrence")
3760
3884
  if recurrence and recurrence != ClientReportSchedule.PERIODICITY_NONE:
3761
3885
  schedule = ClientReportSchedule.objects.create(
3762
3886
  owner=owner,
3763
3887
  created_by=request.user if request.user.is_authenticated else None,
3764
3888
  periodicity=recurrence,
3765
- email_recipients=form.cleaned_data.get("destinations", []),
3766
- disable_emails=form.cleaned_data.get("disable_emails", False),
3889
+ email_recipients=recipients,
3890
+ disable_emails=disable_emails,
3891
+ language=language,
3892
+ title=title,
3767
3893
  )
3894
+ if chargers:
3895
+ schedule.chargers.set(chargers)
3768
3896
  report.schedule = schedule
3769
3897
  report.save(update_fields=["schedule"])
3770
3898
  self.message_user(
3771
3899
  request,
3772
- "Client report schedule created; future reports will be generated automatically.",
3900
+ "Consumer report schedule created; future reports will be generated automatically.",
3901
+ messages.SUCCESS,
3902
+ )
3903
+ if disable_emails:
3904
+ self.message_user(
3905
+ request,
3906
+ "Consumer report generated. The download will begin automatically.",
3773
3907
  messages.SUCCESS,
3774
3908
  )
3909
+ redirect_url = f"{reverse('admin:core_clientreport_generate')}?download={report.pk}"
3910
+ return HttpResponseRedirect(redirect_url)
3911
+ download_param = request.GET.get("download")
3912
+ if download_param:
3913
+ try:
3914
+ download_report = ClientReport.objects.get(pk=download_param)
3915
+ except ClientReport.DoesNotExist:
3916
+ pass
3917
+ else:
3918
+ download_url = reverse(
3919
+ "admin:core_clientreport_download", args=[download_report.pk]
3920
+ )
3775
3921
  context = self.admin_site.each_context(request)
3776
- context.update({"form": form, "report": report, "schedule": schedule})
3922
+ context.update(
3923
+ {
3924
+ "form": form,
3925
+ "report": report,
3926
+ "schedule": schedule,
3927
+ "download_url": download_url,
3928
+ "opts": self.model._meta,
3929
+ }
3930
+ )
3777
3931
  return TemplateResponse(
3778
3932
  request, "admin/core/clientreport/generate.html", context
3779
3933
  )
3780
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
+
3951
+ def download_view(self, request, report_id: int):
3952
+ report = get_object_or_404(ClientReport, pk=report_id)
3953
+ pdf_path = report.ensure_pdf()
3954
+ if not pdf_path.exists():
3955
+ raise Http404("Report file unavailable")
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"
3962
+ response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
3963
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
3964
+ return response
3781
3965
 
3782
3966
  @admin.register(PackageRelease)
3783
3967
  class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):