arthexis 0.1.24__tar.gz → 0.1.25__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 (247) hide show
  1. {arthexis-0.1.24 → arthexis-0.1.25}/PKG-INFO +35 -14
  2. {arthexis-0.1.24 → arthexis-0.1.25}/README.md +33 -13
  3. {arthexis-0.1.24 → arthexis-0.1.25}/arthexis.egg-info/PKG-INFO +35 -14
  4. {arthexis-0.1.24 → arthexis-0.1.25}/arthexis.egg-info/SOURCES.txt +1 -0
  5. {arthexis-0.1.24 → arthexis-0.1.25}/arthexis.egg-info/requires.txt +1 -0
  6. {arthexis-0.1.24 → arthexis-0.1.25}/config/settings.py +6 -3
  7. {arthexis-0.1.24 → arthexis-0.1.25}/config/urls.py +2 -0
  8. {arthexis-0.1.24 → arthexis-0.1.25}/core/admin.py +1 -186
  9. {arthexis-0.1.24 → arthexis-0.1.25}/core/backends.py +3 -1
  10. {arthexis-0.1.24 → arthexis-0.1.25}/core/models.py +74 -8
  11. {arthexis-0.1.24 → arthexis-0.1.25}/core/system.py +67 -2
  12. {arthexis-0.1.24 → arthexis-0.1.25}/core/views.py +0 -3
  13. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/admin.py +444 -251
  14. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/models.py +299 -23
  15. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/tasks.py +13 -16
  16. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/tests.py +211 -1
  17. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/urls.py +5 -0
  18. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/utils.py +9 -2
  19. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/views.py +128 -80
  20. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/admin.py +190 -2
  21. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/consumers.py +98 -0
  22. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/models.py +271 -0
  23. arthexis-0.1.25/ocpp/network.py +398 -0
  24. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/tasks.py +108 -267
  25. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/tests.py +179 -0
  26. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/views.py +2 -0
  27. {arthexis-0.1.24 → arthexis-0.1.25}/pages/middleware.py +3 -2
  28. {arthexis-0.1.24 → arthexis-0.1.25}/pages/tests.py +40 -0
  29. arthexis-0.1.25/pages/utils.py +93 -0
  30. {arthexis-0.1.24 → arthexis-0.1.25}/pages/views.py +4 -2
  31. {arthexis-0.1.24 → arthexis-0.1.25}/pyproject.toml +2 -2
  32. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_localhost_admin_backend.py +14 -0
  33. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_network_setup_interactive.py +17 -0
  34. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_odoo_product.py +1 -113
  35. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_odoo_profile.py +4 -4
  36. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_seed_data.py +2 -0
  37. arthexis-0.1.25/tests/test_system_changelog_report.py +114 -0
  38. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_user_data_admin.py +18 -0
  39. arthexis-0.1.24/pages/utils.py +0 -23
  40. arthexis-0.1.24/tests/test_system_changelog_report.py +0 -45
  41. {arthexis-0.1.24 → arthexis-0.1.25}/LICENSE +0 -0
  42. {arthexis-0.1.24 → arthexis-0.1.25}/arthexis.egg-info/dependency_links.txt +0 -0
  43. {arthexis-0.1.24 → arthexis-0.1.25}/arthexis.egg-info/top_level.txt +0 -0
  44. {arthexis-0.1.24 → arthexis-0.1.25}/config/__init__.py +0 -0
  45. {arthexis-0.1.24 → arthexis-0.1.25}/config/active_app.py +0 -0
  46. {arthexis-0.1.24 → arthexis-0.1.25}/config/asgi.py +0 -0
  47. {arthexis-0.1.24 → arthexis-0.1.25}/config/auth_app.py +0 -0
  48. {arthexis-0.1.24 → arthexis-0.1.25}/config/celery.py +0 -0
  49. {arthexis-0.1.24 → arthexis-0.1.25}/config/context_processors.py +0 -0
  50. {arthexis-0.1.24 → arthexis-0.1.25}/config/horologia_app.py +0 -0
  51. {arthexis-0.1.24 → arthexis-0.1.25}/config/loadenv.py +0 -0
  52. {arthexis-0.1.24 → arthexis-0.1.25}/config/logging.py +0 -0
  53. {arthexis-0.1.24 → arthexis-0.1.25}/config/middleware.py +0 -0
  54. {arthexis-0.1.24 → arthexis-0.1.25}/config/offline.py +0 -0
  55. {arthexis-0.1.24 → arthexis-0.1.25}/config/settings_helpers.py +0 -0
  56. {arthexis-0.1.24 → arthexis-0.1.25}/config/wsgi.py +0 -0
  57. {arthexis-0.1.24 → arthexis-0.1.25}/core/__init__.py +0 -0
  58. {arthexis-0.1.24 → arthexis-0.1.25}/core/admin_history.py +0 -0
  59. {arthexis-0.1.24 → arthexis-0.1.25}/core/admindocs.py +0 -0
  60. {arthexis-0.1.24 → arthexis-0.1.25}/core/apps.py +0 -0
  61. {arthexis-0.1.24 → arthexis-0.1.25}/core/auto_upgrade.py +0 -0
  62. {arthexis-0.1.24 → arthexis-0.1.25}/core/changelog.py +0 -0
  63. {arthexis-0.1.24 → arthexis-0.1.25}/core/entity.py +0 -0
  64. {arthexis-0.1.24 → arthexis-0.1.25}/core/environment.py +0 -0
  65. {arthexis-0.1.24 → arthexis-0.1.25}/core/fields.py +0 -0
  66. {arthexis-0.1.24 → arthexis-0.1.25}/core/form_fields.py +0 -0
  67. {arthexis-0.1.24 → arthexis-0.1.25}/core/github_helper.py +0 -0
  68. {arthexis-0.1.24 → arthexis-0.1.25}/core/github_issues.py +0 -0
  69. {arthexis-0.1.24 → arthexis-0.1.25}/core/github_repos.py +0 -0
  70. {arthexis-0.1.24 → arthexis-0.1.25}/core/lcd_screen.py +0 -0
  71. {arthexis-0.1.24 → arthexis-0.1.25}/core/liveupdate.py +0 -0
  72. {arthexis-0.1.24 → arthexis-0.1.25}/core/log_paths.py +0 -0
  73. {arthexis-0.1.24 → arthexis-0.1.25}/core/mailer.py +0 -0
  74. {arthexis-0.1.24 → arthexis-0.1.25}/core/middleware.py +0 -0
  75. {arthexis-0.1.24 → arthexis-0.1.25}/core/notifications.py +0 -0
  76. {arthexis-0.1.24 → arthexis-0.1.25}/core/public_wifi.py +0 -0
  77. {arthexis-0.1.24 → arthexis-0.1.25}/core/reference_utils.py +0 -0
  78. {arthexis-0.1.24 → arthexis-0.1.25}/core/release.py +0 -0
  79. {arthexis-0.1.24 → arthexis-0.1.25}/core/rfid_import_export.py +0 -0
  80. {arthexis-0.1.24 → arthexis-0.1.25}/core/sigil_builder.py +0 -0
  81. {arthexis-0.1.24 → arthexis-0.1.25}/core/sigil_context.py +0 -0
  82. {arthexis-0.1.24 → arthexis-0.1.25}/core/sigil_resolver.py +0 -0
  83. {arthexis-0.1.24 → arthexis-0.1.25}/core/tasks.py +0 -0
  84. {arthexis-0.1.24 → arthexis-0.1.25}/core/temp_passwords.py +0 -0
  85. {arthexis-0.1.24 → arthexis-0.1.25}/core/test_system_info.py +0 -0
  86. {arthexis-0.1.24 → arthexis-0.1.25}/core/tests.py +0 -0
  87. {arthexis-0.1.24 → arthexis-0.1.25}/core/tests_liveupdate.py +0 -0
  88. {arthexis-0.1.24 → arthexis-0.1.25}/core/urls.py +0 -0
  89. {arthexis-0.1.24 → arthexis-0.1.25}/core/user_data.py +0 -0
  90. {arthexis-0.1.24 → arthexis-0.1.25}/core/widgets.py +0 -0
  91. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/__init__.py +0 -0
  92. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/apps.py +0 -0
  93. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/backends.py +0 -0
  94. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/dns.py +0 -0
  95. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/feature_checks.py +0 -0
  96. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/lcd.py +0 -0
  97. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/reports.py +0 -0
  98. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/rfid_sync.py +0 -0
  99. {arthexis-0.1.24 → arthexis-0.1.25}/nodes/signals.py +0 -0
  100. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/__init__.py +0 -0
  101. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/apps.py +0 -0
  102. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/evcs.py +0 -0
  103. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/evcs_discovery.py +0 -0
  104. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/reference_utils.py +0 -0
  105. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/routing.py +0 -0
  106. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/simulator.py +0 -0
  107. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/status_display.py +0 -0
  108. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/store.py +0 -0
  109. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/test_export_import.py +0 -0
  110. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/test_rfid.py +0 -0
  111. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/transactions_io.py +0 -0
  112. {arthexis-0.1.24 → arthexis-0.1.25}/ocpp/urls.py +0 -0
  113. {arthexis-0.1.24 → arthexis-0.1.25}/pages/__init__.py +0 -0
  114. {arthexis-0.1.24 → arthexis-0.1.25}/pages/admin.py +0 -0
  115. {arthexis-0.1.24 → arthexis-0.1.25}/pages/apps.py +0 -0
  116. {arthexis-0.1.24 → arthexis-0.1.25}/pages/checks.py +0 -0
  117. {arthexis-0.1.24 → arthexis-0.1.25}/pages/context_processors.py +0 -0
  118. {arthexis-0.1.24 → arthexis-0.1.25}/pages/defaults.py +0 -0
  119. {arthexis-0.1.24 → arthexis-0.1.25}/pages/forms.py +0 -0
  120. {arthexis-0.1.24 → arthexis-0.1.25}/pages/models.py +0 -0
  121. {arthexis-0.1.24 → arthexis-0.1.25}/pages/module_defaults.py +0 -0
  122. {arthexis-0.1.24 → arthexis-0.1.25}/pages/site_config.py +0 -0
  123. {arthexis-0.1.24 → arthexis-0.1.25}/pages/tasks.py +0 -0
  124. {arthexis-0.1.24 → arthexis-0.1.25}/pages/urls.py +0 -0
  125. {arthexis-0.1.24 → arthexis-0.1.25}/setup.cfg +0 -0
  126. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_acronym_capitalization.py +0 -0
  127. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_client_report.py +0 -0
  128. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_doc_commands.py +0 -0
  129. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_doc_model_groups.py +0 -0
  130. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_history.py +0 -0
  131. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_index_actions.py +0 -0
  132. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_model_graph.py +0 -0
  133. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_object_history.py +0 -0
  134. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_profile_link.py +0 -0
  135. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_admin_system_stop.py +0 -0
  136. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_allowed_hosts_hostname.py +0 -0
  137. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_api_login_required.py +0 -0
  138. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_auto_upgrade_scheduler.py +0 -0
  139. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_awg_admin.py +0 -0
  140. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_benchmark_command.py +0 -0
  141. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_build_pypi_command.py +0 -0
  142. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_celery_no_debug.py +0 -0
  143. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_changelog_builder.py +0 -0
  144. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_check_admin_command.py +0 -0
  145. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_check_migrations_script.py +0 -0
  146. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_check_pypi_command.py +0 -0
  147. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_clean_release_logs_command.py +0 -0
  148. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_client_report_form.py +0 -0
  149. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_client_report_generation.py +0 -0
  150. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_client_report_schedule.py +0 -0
  151. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_csrf_failure.py +0 -0
  152. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_csrf_origin_subnet.py +0 -0
  153. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_dist_cleanup.py +0 -0
  154. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_collector.py +0 -0
  155. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_inbox.py +0 -0
  156. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_inbox_admin.py +0 -0
  157. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_inbox_search_action.py +0 -0
  158. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_outbox_admin.py +0 -0
  159. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_profiles.py +0 -0
  160. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_email_transaction.py +0 -0
  161. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_env_refresh_clean.py +0 -0
  162. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_env_refresh_dependency_conflict.py +0 -0
  163. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_env_refresh_pip.py +0 -0
  164. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_env_refresh_unlink.py +0 -0
  165. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_experience_admin_group.py +0 -0
  166. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_fixture_presence.py +0 -0
  167. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_footer_no_references.py +0 -0
  168. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_footer_presence.py +0 -0
  169. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_footer_render.py +0 -0
  170. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_git_checks.py +0 -0
  171. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_github_issue_reporting.py +0 -0
  172. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_install_script.py +0 -0
  173. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_invitation_login_view.py +0 -0
  174. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_language_switch.py +0 -0
  175. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_lcd_check_command.py +0 -0
  176. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_lcd_smbus2.py +0 -0
  177. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_log_paths.py +0 -0
  178. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_login_view_no_site.py +0 -0
  179. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_manage_debug_flag.py +0 -0
  180. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_manuals.py +0 -0
  181. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_message_command.py +0 -0
  182. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_migrations.py +0 -0
  183. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_model_verbose_name_capitalization.py +0 -0
  184. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_node_info_view.py +0 -0
  185. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_notifications_fallback.py +0 -0
  186. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_notify_command.py +0 -0
  187. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_ocpp_session_lock.py +0 -0
  188. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_odoo_profile_admin.py +0 -0
  189. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_odoo_quote_report.py +0 -0
  190. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_offline.py +0 -0
  191. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_package_admin_next_release.py +0 -0
  192. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_power_admin_group.py +0 -0
  193. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_profile_inline_deletion.py +0 -0
  194. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_pypi_check.py +0 -0
  195. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_pypi_token.py +0 -0
  196. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_readme_assets.py +0 -0
  197. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_readme_editor.py +0 -0
  198. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_readme_language.py +0 -0
  199. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_reference_qr_code.py +0 -0
  200. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_reference_transaction_uuid.py +0 -0
  201. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_register_site_apps_command.py +0 -0
  202. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_build.py +0 -0
  203. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_build_flow.py +0 -0
  204. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_checklist.py +0 -0
  205. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_logs.py +0 -0
  206. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_manager_admin.py +0 -0
  207. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_packages.py +0 -0
  208. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_progress.py +0 -0
  209. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_progress_pre_release_integration.py +0 -0
  210. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_push.py +0 -0
  211. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_release_tasks.py +0 -0
  212. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_render_nginx_sites.py +0 -0
  213. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_request_invite.py +0 -0
  214. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_admin_print_labels.py +0 -0
  215. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_admin_reference_clear.py +0 -0
  216. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_admin_scan_csrf.py +0 -0
  217. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_always_on.py +0 -0
  218. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_backend.py +0 -0
  219. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_background_reader.py +0 -0
  220. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_client_report.py +0 -0
  221. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_rfid_watch_command.py +0 -0
  222. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_role_marker_filtering.py +0 -0
  223. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_send_invite_command.py +0 -0
  224. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_settings_helpers.py +0 -0
  225. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_settings_mcp_port.py +0 -0
  226. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_shell_scripts.py +0 -0
  227. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_show_leads_command.py +0 -0
  228. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_sigil_builder.py +0 -0
  229. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_sigil_resolution.py +0 -0
  230. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_sites_utils.py +0 -0
  231. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_social_profile.py +0 -0
  232. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_staff_login_net_message.py +0 -0
  233. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_staff_required_decorator.py +0 -0
  234. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_stop_script.py +0 -0
  235. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_suite_gateway.py +0 -0
  236. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_switch_role_script.py +0 -0
  237. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_temp_passwords.py +0 -0
  238. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_totp_admin.py +0 -0
  239. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_totp_backend.py +0 -0
  240. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_transaction_meter_readings.py +0 -0
  241. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_uninstall_script.py +0 -0
  242. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_update_fixtures_command.py +0 -0
  243. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_upgrade_report.py +0 -0
  244. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_urls_autodiscover.py +0 -0
  245. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_version_endpoint.py +0 -0
  246. {arthexis-0.1.24 → arthexis-0.1.25}/tests/test_version_file.py +0 -0
  247. {arthexis-0.1.24 → arthexis-0.1.25}/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.24
3
+ Version: 0.1.25
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
@@ -48,6 +48,7 @@ Requires-Dist: django-timezone-field==7.1
48
48
  Requires-Dist: dnspython==2.7.0
49
49
  Requires-Dist: docutils==0.22.2
50
50
  Requires-Dist: gpiozero==2.0.1; sys_platform == "linux"
51
+ Requires-Dist: graphene-django==3.2.2
51
52
  Requires-Dist: graphviz==0.21
52
53
  Requires-Dist: h11==0.16.0
53
54
  Requires-Dist: httpcore==1.0.9
@@ -124,19 +125,39 @@ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Nar
124
125
 
125
126
  ## Current Features
126
127
 
127
- - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system, handling:
128
- - Lifecycle & sessions
129
- - `BootNotification`
130
- - `Heartbeat`
131
- - `StatusNotification`
132
- - `StartTransaction`
133
- - `StopTransaction`
134
- - Access & metering
135
- - `Authorize`
136
- - `MeterValues`
137
- - Maintenance & firmware
138
- - `DiagnosticsStatusNotification`
139
- - `FirmwareStatusNotification`
128
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system. Supported actions are summarized below.
129
+
130
+ **Charge point → CSMS**
131
+
132
+ | Action | What we do |
133
+ | --- | --- |
134
+ | `Authorize` | Validate RFID or token authorization requests before a session starts. |
135
+ | `BootNotification` | Register the charge point and update identity, firmware, and status details. |
136
+ | `DataTransfer` | Accept vendor-specific payloads and record the results. |
137
+ | `DiagnosticsStatusNotification` | Track the progress of diagnostic uploads kicked off from the back office. |
138
+ | `FirmwareStatusNotification` | Track firmware update lifecycle events from charge points. |
139
+ | `Heartbeat` | Keep the websocket session alive and update last-seen timestamps. |
140
+ | `MeterValues` | Persist periodic energy and power readings while a transaction is active. |
141
+ | `StartTransaction` | Create charging sessions with initial meter values and identification data. |
142
+ | `StatusNotification` | Reflect connector availability and fault states in real time. |
143
+ | `StopTransaction` | Close charging sessions, capturing closing meter values and stop reasons. |
144
+
145
+ **CSMS → Charge point**
146
+
147
+ | Action | What we do |
148
+ | --- | --- |
149
+ | `ChangeAvailability` | Switch connectors or the whole station between operative and inoperative states. |
150
+ | `DataTransfer` | Send vendor-specific commands and log the charge point response. |
151
+ | `GetConfiguration` | Poll the device for the current values of tracked configuration keys. |
152
+ | `RemoteStartTransaction` | Initiate a charging session remotely for an identified customer or token. |
153
+ | `RemoteStopTransaction` | Terminate active charging sessions from the control center. |
154
+ | `ReserveNow` | Reserve connectors for upcoming sessions with automatic connector selection and confirmation tracking. |
155
+ | `Reset` | Request a soft or hard reboot to recover from faults. |
156
+ | `TriggerMessage` | Ask the device to send an immediate update (for example status or diagnostics). |
157
+
158
+ **OCPP 1.6 roadmap.** The following catalogue actions are in our backlog: `CancelReservation`, `ChangeConfiguration`, `ClearCache`, `ClearChargingProfile`, `GetCompositeSchedule`, `GetDiagnostics`, `GetLocalListVersion`, `SendLocalList`, `SetChargingProfile`, `UnlockConnector`, `UpdateFirmware`.
159
+
160
+ - Charge point reservations with automated connector assignment, energy account and RFID linkage, and EVCS confirmation tracking.
140
161
  - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/), syncing:
141
162
  - Employee credentials via `res.users`
142
163
  - Product catalog lookups via `product.product`
@@ -9,19 +9,39 @@ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Nar
9
9
 
10
10
  ## Current Features
11
11
 
12
- - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system, handling:
13
- - Lifecycle & sessions
14
- - `BootNotification`
15
- - `Heartbeat`
16
- - `StatusNotification`
17
- - `StartTransaction`
18
- - `StopTransaction`
19
- - Access & metering
20
- - `Authorize`
21
- - `MeterValues`
22
- - Maintenance & firmware
23
- - `DiagnosticsStatusNotification`
24
- - `FirmwareStatusNotification`
12
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system. Supported actions are summarized below.
13
+
14
+ **Charge point → CSMS**
15
+
16
+ | Action | What we do |
17
+ | --- | --- |
18
+ | `Authorize` | Validate RFID or token authorization requests before a session starts. |
19
+ | `BootNotification` | Register the charge point and update identity, firmware, and status details. |
20
+ | `DataTransfer` | Accept vendor-specific payloads and record the results. |
21
+ | `DiagnosticsStatusNotification` | Track the progress of diagnostic uploads kicked off from the back office. |
22
+ | `FirmwareStatusNotification` | Track firmware update lifecycle events from charge points. |
23
+ | `Heartbeat` | Keep the websocket session alive and update last-seen timestamps. |
24
+ | `MeterValues` | Persist periodic energy and power readings while a transaction is active. |
25
+ | `StartTransaction` | Create charging sessions with initial meter values and identification data. |
26
+ | `StatusNotification` | Reflect connector availability and fault states in real time. |
27
+ | `StopTransaction` | Close charging sessions, capturing closing meter values and stop reasons. |
28
+
29
+ **CSMS → Charge point**
30
+
31
+ | Action | What we do |
32
+ | --- | --- |
33
+ | `ChangeAvailability` | Switch connectors or the whole station between operative and inoperative states. |
34
+ | `DataTransfer` | Send vendor-specific commands and log the charge point response. |
35
+ | `GetConfiguration` | Poll the device for the current values of tracked configuration keys. |
36
+ | `RemoteStartTransaction` | Initiate a charging session remotely for an identified customer or token. |
37
+ | `RemoteStopTransaction` | Terminate active charging sessions from the control center. |
38
+ | `ReserveNow` | Reserve connectors for upcoming sessions with automatic connector selection and confirmation tracking. |
39
+ | `Reset` | Request a soft or hard reboot to recover from faults. |
40
+ | `TriggerMessage` | Ask the device to send an immediate update (for example status or diagnostics). |
41
+
42
+ **OCPP 1.6 roadmap.** The following catalogue actions are in our backlog: `CancelReservation`, `ChangeConfiguration`, `ClearCache`, `ClearChargingProfile`, `GetCompositeSchedule`, `GetDiagnostics`, `GetLocalListVersion`, `SendLocalList`, `SetChargingProfile`, `UnlockConnector`, `UpdateFirmware`.
43
+
44
+ - Charge point reservations with automated connector assignment, energy account and RFID linkage, and EVCS confirmation tracking.
25
45
  - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/), syncing:
26
46
  - Employee credentials via `res.users`
27
47
  - Product catalog lookups via `product.product`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.24
3
+ Version: 0.1.25
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
@@ -48,6 +48,7 @@ Requires-Dist: django-timezone-field==7.1
48
48
  Requires-Dist: dnspython==2.7.0
49
49
  Requires-Dist: docutils==0.22.2
50
50
  Requires-Dist: gpiozero==2.0.1; sys_platform == "linux"
51
+ Requires-Dist: graphene-django==3.2.2
51
52
  Requires-Dist: graphviz==0.21
52
53
  Requires-Dist: h11==0.16.0
53
54
  Requires-Dist: httpcore==1.0.9
@@ -124,19 +125,39 @@ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Nar
124
125
 
125
126
  ## Current Features
126
127
 
127
- - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system, handling:
128
- - Lifecycle & sessions
129
- - `BootNotification`
130
- - `Heartbeat`
131
- - `StatusNotification`
132
- - `StartTransaction`
133
- - `StopTransaction`
134
- - Access & metering
135
- - `Authorize`
136
- - `MeterValues`
137
- - Maintenance & firmware
138
- - `DiagnosticsStatusNotification`
139
- - `FirmwareStatusNotification`
128
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system. Supported actions are summarized below.
129
+
130
+ **Charge point → CSMS**
131
+
132
+ | Action | What we do |
133
+ | --- | --- |
134
+ | `Authorize` | Validate RFID or token authorization requests before a session starts. |
135
+ | `BootNotification` | Register the charge point and update identity, firmware, and status details. |
136
+ | `DataTransfer` | Accept vendor-specific payloads and record the results. |
137
+ | `DiagnosticsStatusNotification` | Track the progress of diagnostic uploads kicked off from the back office. |
138
+ | `FirmwareStatusNotification` | Track firmware update lifecycle events from charge points. |
139
+ | `Heartbeat` | Keep the websocket session alive and update last-seen timestamps. |
140
+ | `MeterValues` | Persist periodic energy and power readings while a transaction is active. |
141
+ | `StartTransaction` | Create charging sessions with initial meter values and identification data. |
142
+ | `StatusNotification` | Reflect connector availability and fault states in real time. |
143
+ | `StopTransaction` | Close charging sessions, capturing closing meter values and stop reasons. |
144
+
145
+ **CSMS → Charge point**
146
+
147
+ | Action | What we do |
148
+ | --- | --- |
149
+ | `ChangeAvailability` | Switch connectors or the whole station between operative and inoperative states. |
150
+ | `DataTransfer` | Send vendor-specific commands and log the charge point response. |
151
+ | `GetConfiguration` | Poll the device for the current values of tracked configuration keys. |
152
+ | `RemoteStartTransaction` | Initiate a charging session remotely for an identified customer or token. |
153
+ | `RemoteStopTransaction` | Terminate active charging sessions from the control center. |
154
+ | `ReserveNow` | Reserve connectors for upcoming sessions with automatic connector selection and confirmation tracking. |
155
+ | `Reset` | Request a soft or hard reboot to recover from faults. |
156
+ | `TriggerMessage` | Ask the device to send an immediate update (for example status or diagnostics). |
157
+
158
+ **OCPP 1.6 roadmap.** The following catalogue actions are in our backlog: `CancelReservation`, `ChangeConfiguration`, `ClearCache`, `ClearChargingProfile`, `GetCompositeSchedule`, `GetDiagnostics`, `GetLocalListVersion`, `SendLocalList`, `SetChargingProfile`, `UnlockConnector`, `UpdateFirmware`.
159
+
160
+ - Charge point reservations with automated connector assignment, energy account and RFID linkage, and EVCS confirmation tracking.
140
161
  - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/), syncing:
141
162
  - Employee credentials via `res.users`
142
163
  - Product catalog lookups via `product.product`
@@ -83,6 +83,7 @@ ocpp/consumers.py
83
83
  ocpp/evcs.py
84
84
  ocpp/evcs_discovery.py
85
85
  ocpp/models.py
86
+ ocpp/network.py
86
87
  ocpp/reference_utils.py
87
88
  ocpp/routing.py
88
89
  ocpp/simulator.py
@@ -34,6 +34,7 @@ django-otp==1.5.4
34
34
  django-timezone-field==7.1
35
35
  dnspython==2.7.0
36
36
  docutils==0.22.2
37
+ graphene-django==3.2.2
37
38
  graphviz==0.21
38
39
  h11==0.16.0
39
40
  httpcore==1.0.9
@@ -18,6 +18,8 @@ import ipaddress
18
18
  import socket
19
19
  from core.log_paths import select_log_dir
20
20
  from django.utils.translation import gettext_lazy as _
21
+ from datetime import timedelta
22
+
21
23
  from celery.schedules import crontab
22
24
  from django.http import request as http_request
23
25
  from django.http.request import split_domain_port
@@ -333,6 +335,7 @@ CsrfViewMiddleware._check_referer = _check_referer_with_forwarded
333
335
  # Application definition
334
336
 
335
337
  LOCAL_APPS = [
338
+ "api",
336
339
  "nodes",
337
340
  "core",
338
341
  "ocpp",
@@ -666,8 +669,8 @@ CELERY_BEAT_SCHEDULE = {
666
669
  "task": "ocpp.tasks.schedule_daily_charge_point_configuration_checks",
667
670
  "schedule": crontab(minute=0, hour=0),
668
671
  },
669
- "ocpp_remote_sync": {
670
- "task": "ocpp.tasks.sync_remote_chargers",
671
- "schedule": crontab(minute="*"),
672
+ "ocpp_forwarding_push": {
673
+ "task": "ocpp.tasks.push_forwarded_charge_points",
674
+ "schedule": timedelta(seconds=5),
672
675
  },
673
676
  }
@@ -20,6 +20,7 @@ from django.views.decorators.csrf import csrf_exempt
20
20
  from django.views.generic import RedirectView
21
21
  from django.views.i18n import set_language
22
22
  from django.utils.translation import gettext_lazy as _
23
+ from api.views import EnergyGraphQLView
23
24
  from core import views as core_views
24
25
  from core.admindocs import (
25
26
  CommandsView,
@@ -119,6 +120,7 @@ urlpatterns = [
119
120
  name="admin-model-graph",
120
121
  ),
121
122
  path("version/", core_views.version_info, name="version-info"),
123
+ path("graphql/", EnergyGraphQLView.as_view(), name="graphql"),
122
124
  path(
123
125
  "admin/core/releases/<int:pk>/<str:action>/",
124
126
  core_views.release_progress,
@@ -2266,199 +2266,15 @@ class ProductAdminForm(forms.ModelForm):
2266
2266
  widgets = {"odoo_product": OdooProductWidget}
2267
2267
 
2268
2268
 
2269
- class ProductFetchWizardForm(forms.Form):
2270
- name = forms.CharField(label="Name", required=False)
2271
- default_code = forms.CharField(label="Internal reference", required=False)
2272
- barcode = forms.CharField(label="Barcode", required=False)
2273
- renewal_period = forms.IntegerField(
2274
- label="Renewal period (days)", min_value=1, initial=30
2275
- )
2276
-
2277
- def __init__(self, *args, require_search_terms=True, **kwargs):
2278
- self.require_search_terms = require_search_terms
2279
- super().__init__(*args, **kwargs)
2280
-
2281
- def clean(self):
2282
- cleaned = super().clean()
2283
- if self.require_search_terms:
2284
- if not any(
2285
- cleaned.get(field) for field in ("name", "default_code", "barcode")
2286
- ):
2287
- raise forms.ValidationError(
2288
- _("Enter at least one field to search for a product.")
2289
- )
2290
- return cleaned
2291
-
2292
- def build_domain(self):
2293
- domain = []
2294
- if self.cleaned_data.get("name"):
2295
- domain.append(("name", "ilike", self.cleaned_data["name"]))
2296
- if self.cleaned_data.get("default_code"):
2297
- domain.append(("default_code", "ilike", self.cleaned_data["default_code"]))
2298
- if self.cleaned_data.get("barcode"):
2299
- domain.append(("barcode", "ilike", self.cleaned_data["barcode"]))
2300
- return domain
2301
-
2302
-
2303
2269
  @admin.register(Product)
2304
2270
  class ProductAdmin(EntityModelAdmin):
2305
2271
  form = ProductAdminForm
2306
- actions = ["fetch_odoo_product", "register_from_odoo"]
2272
+ actions = ["register_from_odoo"]
2307
2273
  change_list_template = "admin/core/product/change_list.html"
2308
2274
 
2309
2275
  def _odoo_profile_admin(self):
2310
2276
  return self.admin_site._registry.get(OdooProfile)
2311
2277
 
2312
- def _search_odoo_products(self, profile, form):
2313
- domain = form.build_domain()
2314
- return profile.execute(
2315
- "product.product",
2316
- "search_read",
2317
- [domain],
2318
- fields=[
2319
- "name",
2320
- "default_code",
2321
- "barcode",
2322
- "description_sale",
2323
- ],
2324
- limit=50,
2325
- )
2326
-
2327
- @admin.action(description="Fetch Odoo Product")
2328
- def fetch_odoo_product(self, request, queryset):
2329
- profile = getattr(request.user, "odoo_profile", None)
2330
- has_credentials = bool(profile and profile.is_verified)
2331
- profile_admin = self._odoo_profile_admin()
2332
- profile_url = None
2333
- if profile_admin is not None:
2334
- profile_url = profile_admin.get_my_profile_url(request)
2335
-
2336
- context = {
2337
- "opts": self.model._meta,
2338
- "queryset": queryset,
2339
- "action": "fetch_odoo_product",
2340
- "has_credentials": has_credentials,
2341
- "profile_url": profile_url,
2342
- }
2343
-
2344
- if not has_credentials:
2345
- context["credential_error"] = _(
2346
- "Configure your Odoo employee credentials before fetching products."
2347
- )
2348
- return TemplateResponse(
2349
- request, "admin/core/product/fetch_odoo.html", context
2350
- )
2351
-
2352
- is_import = "import" in request.POST
2353
- form_kwargs = {"require_search_terms": not is_import}
2354
- if request.method == "POST":
2355
- form = ProductFetchWizardForm(request.POST, **form_kwargs)
2356
- else:
2357
- form = ProductFetchWizardForm()
2358
-
2359
- results = None
2360
- selected_product_id = request.POST.get("product_id", "")
2361
-
2362
- if request.method == "POST" and form.is_valid():
2363
- try:
2364
- results = self._search_odoo_products(profile, form)
2365
- except Exception:
2366
- logger.exception(
2367
- "Failed to fetch Odoo products for user %s (profile_id=%s, host=%s, database=%s)",
2368
- getattr(getattr(request, "user", None), "pk", None),
2369
- getattr(profile, "pk", None),
2370
- getattr(profile, "host", None),
2371
- getattr(profile, "database", None),
2372
- )
2373
- form.add_error(None, _("Unable to fetch products from Odoo."))
2374
- results = []
2375
- else:
2376
- if is_import:
2377
- if not self.has_add_permission(request):
2378
- form.add_error(
2379
- None, _("You do not have permission to add products.")
2380
- )
2381
- else:
2382
- product_id = request.POST.get("product_id")
2383
- if not product_id:
2384
- form.add_error(None, _("Select a product to import."))
2385
- else:
2386
- try:
2387
- odoo_id = int(product_id)
2388
- except (TypeError, ValueError):
2389
- form.add_error(None, _("Invalid product selection."))
2390
- else:
2391
- match = next(
2392
- (item for item in results if item.get("id") == odoo_id),
2393
- None,
2394
- )
2395
- if not match:
2396
- form.add_error(
2397
- None,
2398
- _(
2399
- "The selected product was not found. Run the search again."
2400
- ),
2401
- )
2402
- else:
2403
- existing = self.model.objects.filter(
2404
- odoo_product__id=odoo_id
2405
- ).first()
2406
- if existing:
2407
- self.message_user(
2408
- request,
2409
- _(
2410
- "Product %(name)s already imported; opening existing record."
2411
- )
2412
- % {"name": existing.name},
2413
- level=messages.WARNING,
2414
- )
2415
- return HttpResponseRedirect(
2416
- reverse(
2417
- "admin:%s_%s_change"
2418
- % (
2419
- existing._meta.app_label,
2420
- existing._meta.model_name,
2421
- ),
2422
- args=[existing.pk],
2423
- )
2424
- )
2425
- product = self.model.objects.create(
2426
- name=match.get("name") or f"Odoo Product {odoo_id}",
2427
- description=match.get("description_sale", "") or "",
2428
- renewal_period=form.cleaned_data["renewal_period"],
2429
- odoo_product={
2430
- "id": odoo_id,
2431
- "name": match.get("name", ""),
2432
- },
2433
- )
2434
- self.log_addition(
2435
- request, product, "Imported product from Odoo"
2436
- )
2437
- self.message_user(
2438
- request,
2439
- _("Imported %(name)s from Odoo.")
2440
- % {"name": product.name},
2441
- )
2442
- return HttpResponseRedirect(
2443
- reverse(
2444
- "admin:%s_%s_change"
2445
- % (
2446
- product._meta.app_label,
2447
- product._meta.model_name,
2448
- ),
2449
- args=[product.pk],
2450
- )
2451
- )
2452
- context.update(
2453
- {
2454
- "form": form,
2455
- "results": results,
2456
- "selected_product_id": selected_product_id,
2457
- }
2458
- )
2459
- context["media"] = self.media + form.media
2460
- return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
2461
-
2462
2278
  def get_urls(self):
2463
2279
  urls = super().get_urls()
2464
2280
  custom = [
@@ -2507,7 +2323,6 @@ class ProductAdmin(EntityModelAdmin):
2507
2323
  products = profile.execute(
2508
2324
  "product.product",
2509
2325
  "search_read",
2510
- [[]],
2511
2326
  fields=[
2512
2327
  "name",
2513
2328
  "description_sale",
@@ -217,7 +217,9 @@ class LocalhostAdminBackend(ModelBackend):
217
217
  try:
218
218
  ipaddress.ip_address(host)
219
219
  except ValueError:
220
- if not self._is_test_environment(request):
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:
@@ -634,11 +634,21 @@ class OdooProfile(Profile):
634
634
  """Return the display label for this profile."""
635
635
 
636
636
  username = self._resolved_field_value("username")
637
+ database = self._resolved_field_value("database")
638
+ if username and database:
639
+ return f"{username}@{database}"
637
640
  if username:
638
641
  return username
639
- database = self._resolved_field_value("database")
640
642
  return database or ""
641
643
 
644
+ def _profile_name(self) -> str:
645
+ """Return the stored name for this profile without database suffix."""
646
+
647
+ username = self._resolved_field_value("username")
648
+ if username:
649
+ return username
650
+ return self._resolved_field_value("database")
651
+
642
652
  def save(self, *args, **kwargs):
643
653
  if self.pk:
644
654
  old = type(self).all_objects.get(pk=self.pk)
@@ -649,7 +659,7 @@ class OdooProfile(Profile):
649
659
  or old.host != self.host
650
660
  ):
651
661
  self._clear_verification()
652
- computed_name = self._display_identifier()
662
+ computed_name = self._profile_name()
653
663
  update_fields = kwargs.get("update_fields")
654
664
  update_fields_set = set(update_fields) if update_fields is not None else None
655
665
  if computed_name != self.name:
@@ -684,6 +694,7 @@ class OdooProfile(Profile):
684
694
  self.odoo_uid = uid
685
695
  self.email = info.get("email", "")
686
696
  self.verified_on = timezone.now()
697
+ self.name = self._profile_name()
687
698
  self.save(update_fields=["odoo_uid", "name", "email", "verified_on"])
688
699
  return True
689
700
 
@@ -3541,6 +3552,31 @@ class ClientReport(Entity):
3541
3552
 
3542
3553
  return start_value, end_value
3543
3554
 
3555
+ @staticmethod
3556
+ def _format_session_datetime(value):
3557
+ if not value:
3558
+ return None
3559
+ localized = timezone.localtime(value)
3560
+ date_part = formats.date_format(
3561
+ localized, format="MONTH_DAY_FORMAT", use_l10n=True
3562
+ )
3563
+ time_part = formats.time_format(
3564
+ localized, format="TIME_FORMAT", use_l10n=True
3565
+ )
3566
+ return gettext("%(date)s, %(time)s") % {
3567
+ "date": date_part,
3568
+ "time": time_part,
3569
+ }
3570
+
3571
+ @staticmethod
3572
+ def _calculate_duration_minutes(start, end):
3573
+ if not start or not end:
3574
+ return None
3575
+ total_seconds = (end - start).total_seconds()
3576
+ if total_seconds < 0:
3577
+ return None
3578
+ return int(round(total_seconds / 60.0))
3579
+
3544
3580
  @staticmethod
3545
3581
  def _normalize_dataset_for_display(dataset: dict[str, Any]):
3546
3582
  schema = dataset.get("schema")
@@ -3576,6 +3612,15 @@ class ClientReport(Entity):
3576
3612
  "session_kwh": row.get("session_kwh"),
3577
3613
  "start": start_dt,
3578
3614
  "end": end_dt,
3615
+ "start_display": ClientReport._format_session_datetime(
3616
+ start_dt
3617
+ ),
3618
+ "end_display": ClientReport._format_session_datetime(
3619
+ end_dt
3620
+ ),
3621
+ "duration_minutes": ClientReport._calculate_duration_minutes(
3622
+ start_dt, end_dt
3623
+ ),
3579
3624
  }
3580
3625
  )
3581
3626
 
@@ -3624,6 +3669,7 @@ class ClientReport(Entity):
3624
3669
  start_dt = timezone.make_aware(start_dt, timezone.utc)
3625
3670
  item["start"] = start_dt
3626
3671
  else:
3672
+ start_dt = None
3627
3673
  item["start"] = None
3628
3674
 
3629
3675
  if end_val:
@@ -3632,8 +3678,15 @@ class ClientReport(Entity):
3632
3678
  end_dt = timezone.make_aware(end_dt, timezone.utc)
3633
3679
  item["end"] = end_dt
3634
3680
  else:
3681
+ end_dt = None
3635
3682
  item["end"] = None
3636
3683
 
3684
+ item["start_display"] = ClientReport._format_session_datetime(start_dt)
3685
+ item["end_display"] = ClientReport._format_session_datetime(end_dt)
3686
+ item["duration_minutes"] = ClientReport._calculate_duration_minutes(
3687
+ start_dt, end_dt
3688
+ )
3689
+
3637
3690
  parsed.append(item)
3638
3691
 
3639
3692
  return {"schema": schema, "rows": parsed}
@@ -3749,7 +3802,8 @@ class ClientReport(Entity):
3749
3802
  total_kw_period_label = gettext("Total kW (period)")
3750
3803
  connector_label = gettext("Connector")
3751
3804
  account_label = gettext("Account")
3752
- session_kwh_label = gettext("Session kWh")
3805
+ session_kwh_label = gettext("Session kW")
3806
+ time_label = gettext("Time")
3753
3807
  no_sessions_period = gettext(
3754
3808
  "No charging sessions recorded for the selected period."
3755
3809
  )
@@ -3765,16 +3819,18 @@ class ClientReport(Entity):
3765
3819
  def format_datetime(value):
3766
3820
  if not value:
3767
3821
  return "—"
3768
- localized = timezone.localtime(value)
3769
- return formats.date_format(
3770
- localized, format="DATETIME_FORMAT", use_l10n=True
3771
- )
3822
+ return ClientReport._format_session_datetime(value) or "—"
3772
3823
 
3773
3824
  def format_decimal(value):
3774
3825
  if value is None:
3775
3826
  return "—"
3776
3827
  return formats.number_format(value, decimal_pos=2, use_l10n=True)
3777
3828
 
3829
+ def format_duration(value):
3830
+ if value is None:
3831
+ return "—"
3832
+ return formats.number_format(value, decimal_pos=0, use_l10n=True)
3833
+
3778
3834
  if schema == "evcs-session/v1":
3779
3835
  evcs_entries = dataset.get("evcs", [])
3780
3836
  if not evcs_entries:
@@ -3810,6 +3866,7 @@ class ClientReport(Entity):
3810
3866
  session_kwh_label,
3811
3867
  gettext("Session start"),
3812
3868
  gettext("Session end"),
3869
+ time_label,
3813
3870
  connector_label,
3814
3871
  gettext("RFID label"),
3815
3872
  account_label,
@@ -3819,11 +3876,13 @@ class ClientReport(Entity):
3819
3876
  for row in transactions:
3820
3877
  start_dt = row.get("start")
3821
3878
  end_dt = row.get("end")
3879
+ duration_value = row.get("duration_minutes")
3822
3880
  table_data.append(
3823
3881
  [
3824
3882
  format_decimal(row.get("session_kwh")),
3825
3883
  format_datetime(start_dt),
3826
3884
  format_datetime(end_dt),
3885
+ format_duration(duration_value),
3827
3886
  row.get("connector")
3828
3887
  if row.get("connector") is not None
3829
3888
  else "—",
@@ -3832,7 +3891,14 @@ class ClientReport(Entity):
3832
3891
  ]
3833
3892
  )
3834
3893
 
3835
- table = Table(table_data, repeatRows=1)
3894
+ column_count = len(table_data[0])
3895
+ col_width = document.width / column_count if column_count else None
3896
+ table = Table(
3897
+ table_data,
3898
+ repeatRows=1,
3899
+ colWidths=[col_width] * column_count if col_width else None,
3900
+ hAlign="LEFT",
3901
+ )
3836
3902
  table.setStyle(
3837
3903
  TableStyle(
3838
3904
  [