diff --git a/comptages/comptages.py b/comptages/comptages.py index 06fc2e47..cb8be7be 100644 --- a/comptages/comptages.py +++ b/comptages/comptages.py @@ -1,6 +1,6 @@ import os import pytz -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from functools import partial from qgis.PyQt.QtGui import QIcon @@ -29,6 +29,7 @@ from comptages.report.yearly_report_bike import YearlyReportBike from comptages.ics.ics_importer import IcsImporter from comptages.ui.resources import * +from comptages.ui.select_reports import SelectSectionsToReport from comptages.core import definitions @@ -515,34 +516,51 @@ def do_generate_report_action(self, count_id): return file_dialog = QFileDialog() - title = "Exporter un rapport" - path = self.settings.value("report_export_directory") - file_path = QFileDialog.getExistingDirectory(file_dialog, title, path) + mondays = list(report._mondays_of_count(count)) + sections_ids = ( + models.Section.objects.filter(lane__id_installation__count=count) + .distinct() + .values_list("id", flat=True) + ) + report_selection_dialog = SelectSectionsToReport( + sections_ids=list(sections_ids), mondays=mondays + ) - if not file_path: + if report_selection_dialog.exec_(): + selected_sections_dates: dict[ + str, list[date] + ] = report_selection_dialog.get_inputs() + title = "Exporter un rapport" + + path = self.settings.value("report_export_directory") + file_path = QFileDialog.getExistingDirectory(file_dialog, title, path) + + if not file_path: + QgsMessageLog.logMessage( + "{} - Generate report action ended: No file_path given".format( + datetime.now() + ), + "Comptages", + Qgis.Info, + ) + return QgsMessageLog.logMessage( - "{} - Generate report action ended: No file_path given".format( - datetime.now() - ), + f""" + {datetime.now()} - Generate report action can really begin now for count {count.id} with file_path: {file_path}. + Selected sections and dates: {selected_sections_dates} + """, "Comptages", Qgis.Info, ) - return - QgsMessageLog.logMessage( - "{} - Generate report action can really begin now for count {} with file_path: {}".format( - datetime.now(), count.id, file_path - ), - "Comptages", - Qgis.Info, - ) - self.tm.allTasksFinished.connect(partial(self.all_tasks_finished, "report")) - self.tm.addTask( - report_task.ReportTask( - file_path=file_path, - count=count, + self.tm.allTasksFinished.connect(partial(self.all_tasks_finished, "report")) + self.tm.addTask( + report_task.ReportTask( + file_path=file_path, + count=count, + selected_sections_dates=selected_sections_dates, + ) ) - ) def do_export_plan_action(self, count_id): count = models.Count.objects.get(id=count_id) diff --git a/comptages/core/report.py b/comptages/core/report.py index 24c8e6ae..08cb28ce 100644 --- a/comptages/core/report.py +++ b/comptages/core/report.py @@ -1,8 +1,10 @@ import os - -from datetime import timedelta, datetime +from datetime import date, timedelta, datetime +from typing import Generator, Optional from openpyxl import load_workbook, Workbook +from qgis.core import Qgis, QgsMessageLog + from comptages.datamodel import models from comptages.core import statistics @@ -13,10 +15,11 @@ def simple_print_callback(progress): def prepare_reports( file_path, - count=None, + count: Optional[models.Count] = None, year=None, template="default", section_id=None, + sections_days: Optional[dict[str, list[date]]] = None, callback_progress=simple_print_callback, ): current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -24,7 +27,10 @@ def prepare_reports( if template == "default": template_name = "template.xlsx" template_path = os.path.join(current_dir, os.pardir, "report", template_name) - _prepare_default_reports(file_path, count, template_path, callback_progress) + assert count + _prepare_default_reports( + file_path, count, template_path, callback_progress, sections_days + ) elif template == "yearly": template_name = "template_yearly.xlsx" template_path = os.path.join(current_dir, os.pardir, "report", template_name) @@ -38,17 +44,32 @@ def prepare_reports( def _prepare_default_reports( - file_path: str, count: models.Count, template_path: str, callback_progress + file_path: str, + count: models.Count, + template_path: str, + callback_progress, + sections_days: Optional[dict[str, list[date]]] = None, ): # We do by section and not by count because of special cases. - sections = models.Section.objects.filter( - lane__id_installation__count=count - ).distinct() + sections = models.Section.objects.filter(lane__id_installation__count=count) - mondays_qty = len(list(_mondays_of_count(count))) - mondays = _mondays_of_count(count) + # Filter out sections if the user narrowed down the section to include + # in report + if sections_days: + sections = sections.filter(id__in=list(sections_days.keys())) + + mondays = list(_mondays_of_count(count)) + mondays_qty = len(mondays) + + QgsMessageLog.logMessage( + f"Reporting on {sections.distinct().count()} sections", "Report", Qgis.Info + ) for section in sections: for i, monday in enumerate(mondays): + # Filter out date based on parameter + if sections_days and monday not in sections_days[section.id]: + continue + QgsMessageLog.logMessage("Adding to workbook", "Report", Qgis.Info) progress = int(100 / mondays_qty * (i - 1)) callback_progress(progress) @@ -89,7 +110,7 @@ def _prepare_yearly_report( workbook.save(filename=output) -def _mondays_of_count(count: models.Count): +def _mondays_of_count(count: models.Count) -> Generator[date, None, None]: """Generator that return the Mondays of the count""" start = count.start_process_date diff --git a/comptages/core/report_task.py b/comptages/core/report_task.py index 8d9bdd74..e48d0a0d 100644 --- a/comptages/core/report_task.py +++ b/comptages/core/report_task.py @@ -1,5 +1,6 @@ import os -from datetime import datetime +from datetime import date, datetime +from typing import Optional from qgis.core import QgsTask, Qgis, QgsMessageLog @@ -8,7 +9,13 @@ class ReportTask(QgsTask): def __init__( - self, file_path, count=None, year=None, template="default", section_id=None + self, + file_path, + count=None, + year=None, + template="default", + section_id=None, + selected_sections_dates: Optional[dict[str, list[date]]] = None, ): self.basename = os.path.basename(file_path) super().__init__("Génération du rapport: {}".format(self.basename)) @@ -18,6 +25,7 @@ def __init__( self.template = template self.year = year self.section_id = section_id + self.only_sections_ids = selected_sections_dates def run(self): try: @@ -27,6 +35,7 @@ def run(self): self.year, self.template, self.section_id, + self.only_sections_ids, callback_progress=self.setProgress, ) return True diff --git a/comptages/report/yearly_report_bike.py b/comptages/report/yearly_report_bike.py index 419b207b..f3604ab7 100644 --- a/comptages/report/yearly_report_bike.py +++ b/comptages/report/yearly_report_bike.py @@ -40,6 +40,7 @@ def values_by_direction(self): .annotate(total=Sum("times")) .values("weekday", "id_lane__direction", "total") ) + return result def values_by_day_and_hour(self): # Get all the count details for section and the year diff --git a/comptages/ui/select_reports.py b/comptages/ui/select_reports.py new file mode 100644 index 00000000..80628244 --- /dev/null +++ b/comptages/ui/select_reports.py @@ -0,0 +1,150 @@ +from datetime import date, datetime +from functools import partial +from qgis.PyQt.QtWidgets import ( + QCheckBox, + QDialog, + QDialogButtonBox, + QHLine, + QLabel, + QRadioButton, + QVBoxLayout, + QScrollArea, + QWidget, +) + + +class SelectSectionsToReport(QDialog): + fmt = "%d-%m-%Y" + + def __init__(self, *args, **kwargs): + sections_ids: list[str] = kwargs.pop("sections_ids") + mondays: list[datetime] = kwargs.pop("mondays") + mondays_as_datestr = [d.strftime(self.fmt) for d in mondays] + + super().__init__(*args, **kwargs) + + self.max_selected = len(sections_ids) * len(mondays) + self.setMinimumWidth(550) + self.setWindowTitle( + "Please select the sections and dates to include in the report..." + ) + + # Parent layout + self.layout = QVBoxLayout() + + # Basic controls + QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + # Create `all` and `none` selectors only if there are several sections + if len(sections_ids) > 1: + # Radio: all + self.all_selector = QRadioButton() + self.all_selector.setChecked(True) + self.all_selector.toggled.connect(partial(self.select_all_none, "all")) + self.all_selector_label = QLabel() + self.all_selector_label.setText("Select all") + self.all_selector_label.setBuddy(self.all_selector) + self.layout.addWidget(self.all_selector_label) + self.layout.addWidget(self.all_selector) + + # Radio: none + self.none_selector = QRadioButton() + self.none_selector.toggled.connect(partial(self.select_all_none, "none")) + self.none_selector_label = QLabel() + self.none_selector_label.setText("Unselect all") + self.none_selector_label.setBuddy(self.none_selector) + self.layout.addWidget(self.none_selector_label) + self.layout.addWidget(self.none_selector) + + # Checkboxes: containers + self.scrollarea = QScrollArea() + self.widget = QWidget() + self.vbox = QVBoxLayout() + + # Checkboxes: items + self.items_check_boxes = {} + for item in sections_ids: + item_checkbox = QCheckBox(f"section {item}") + item_checkbox.setChecked(True) + item_checkbox.clicked.connect(partial(self.update_children_of, item)) + label = QLabel() + label.setBuddy(item_checkbox) + + self.vbox.addWidget(label) + self.vbox.addWidget(item_checkbox) + self.items_check_boxes[item] = { + "checkbox": item_checkbox, + "subcheckboxes": {}, + } + + # Checkboxes: subitems + for subitem in mondays_as_datestr: + subitem_checkbox = QCheckBox(subitem) + subitem_checkbox.setStyleSheet("QCheckBox { padding-left: 15px; }") + subitem_checkbox.setChecked(True) + subitem_checkbox.clicked.connect(self.update_selected_count) + label = QLabel() + label.setBuddy(subitem_checkbox) + + self.vbox.addWidget(label) + self.vbox.addWidget(subitem_checkbox) + self.items_check_boxes[item]["subcheckboxes"][ + subitem + ] = subitem_checkbox + + self.vbox.addWidget(QHLine()) + + # Checkbox: containers: populate layout + self.widget.setLayout(self.vbox) + self.scrollarea.setWidget(self.widget) + self.layout.addWidget(self.scrollarea) + + # Selected + self.selected = QLabel() + selected_text = f"Selected: {self.count_selected()} out of {self.max_selected}" + self.selected.setText(selected_text) + self.layout.addWidget(self.selected) + + # Buttons + self.layout.addWidget(self.buttonBox) + self.setLayout(self.layout) + + def update_selected_count(self): + self.selected.setText( + f"Selected: {self.count_selected()} out of {self.max_selected}" + ) + + def count_selected(self) -> int: + count = 0 + for item in self.items_check_boxes.values(): + for subcheckbox in item["subcheckboxes"].values(): + if subcheckbox.isChecked(): + count += 1 + return count + + def update_children_of(self, item: str): + new_state = self.items_check_boxes[item]["checkbox"].isChecked() + for subcheckbox in self.items_check_boxes[item]["subcheckboxes"].values(): + subcheckbox.setChecked(new_state) + self.update_selected_count() + + def select_all_none(self, desired: str): + new_state = desired == "all" + for item in self.items_check_boxes.values(): + item["checkbox"].setChecked(new_state) + for subcheckbox in item["subcheckboxes"].values(): + subcheckbox.setChecked(new_state) + self.update_selected_count() + + def get_inputs(self) -> dict[str, list[date]]: + builder = {} + for section_id, item in self.items_check_boxes.items(): + builder[section_id] = [ + datetime.strptime(monday_datestr, self.fmt).date() + for monday_datestr, subcheckbox in item["subcheckboxes"].items() + if subcheckbox.isChecked() + ] + return builder