diff --git a/comptages/core/report.py b/comptages/core/report.py index e1b7940..b22c52e 100644 --- a/comptages/core/report.py +++ b/comptages/core/report.py @@ -1,11 +1,9 @@ import os from datetime import date, datetime, timedelta -from typing import Generator +from typing import Generator, Optional +from qgis.core import Qgis, QgsMessageLog -from datetime import timedelta, datetime -from typing import Optional from openpyxl import load_workbook, Workbook -from qgis.core import Qgis, QgsMessageLog from comptages.core import statistics from comptages.datamodel import models @@ -24,6 +22,7 @@ def prepare_reports( callback_progress=simple_print_callback, sections_days: Optional[dict[str, list[date]]] = None, ): + print(f"{datetime.now()}: _prepare_reports: begin, folder: {file_path}") current_dir = os.path.dirname(os.path.abspath(__file__)) if template == "default": @@ -48,6 +47,7 @@ def prepare_reports( ) elif template == "yearly_bike": pass + print(f"{datetime.now()}: _prepare_reports: ended, folder: {file_path}") def _prepare_default_reports( @@ -58,6 +58,7 @@ def _prepare_default_reports( sections_days: Optional[dict[str, list[date]]] = None, ): """Write default reports to disk (1 per section in count, per week)""" + print(f"{datetime.now()}: _prepare_default_reports: begin, count: {count}") # We do by section and not by count because of special cases. sections = models.Section.objects.filter( lane__id_installation__count=count @@ -95,6 +96,8 @@ def _prepare_default_reports( workbook.save(filename=output) + print(f"{datetime.now()}: _prepare_default_reports: ended, count: {count}") + def _prepare_yearly_report( file_path: str, @@ -105,6 +108,9 @@ def _prepare_yearly_report( sections_days: Optional[dict[str, list[date]]] = None, ): """Write default reports to disk (1 per section included in the count)""" + print( + f"{datetime.now()}: _prepare_yearly_report: begin, sections_ids: {sections_ids}" + ) # Get first count to be used as example count_qs = models.Count.objects.filter( id_installation__lane__id_section=sections_ids[0], @@ -137,6 +143,10 @@ def _prepare_yearly_report( output = os.path.join(file_path, f"{section.id}_{year}_r.xlsx") workbook.save(filename=output) + print( + f"{datetime.now()}: _prepare_yearly_report: ended, sections_ids: {sections_ids}" + ) + def _mondays_of_count(count: models.Count) -> Generator[date, None, None]: """Generator that return the Mondays of the count""" @@ -285,6 +295,7 @@ def _data_day(count: models.Count, section: models.Section, monday, workbook: Wo section, start=monday + timedelta(days=i), end=monday + timedelta(days=i + 1), + exclude_trash=True, ) for row in df.itertuples(): @@ -300,6 +311,7 @@ def _data_day(count: models.Count, section: models.Section, monday, workbook: Wo start=monday + timedelta(days=i), end=monday + timedelta(days=i + 1), direction=1, + exclude_trash=True, ) for row in df.itertuples(): @@ -315,6 +327,7 @@ def _data_day(count: models.Count, section: models.Section, monday, workbook: Wo start=monday + timedelta(days=i), end=monday + timedelta(days=i + 1), direction=1, + exclude_trash=True, ) ws.cell(row=row_offset, column=col_offset + i, value=light.get(True, 0)) ws.cell(row=row_offset + 1, column=col_offset + i, value=light.get(False, 0)) @@ -330,6 +343,7 @@ def _data_day(count: models.Count, section: models.Section, monday, workbook: Wo start=monday + timedelta(days=i), end=monday + timedelta(days=i + 1), direction=2, + exclude_trash=True, ) for row in df.itertuples(): @@ -345,6 +359,7 @@ def _data_day(count: models.Count, section: models.Section, monday, workbook: Wo start=monday + timedelta(days=i), end=monday + timedelta(days=i + 1), direction=2, + exclude_trash=True, ) ws.cell(row=row_offset, column=col_offset + i, value=light.get(True, 0)) ws.cell( @@ -360,8 +375,11 @@ def _data_day_yearly( # Total (section) row_offset = 69 col_offset = 2 - - df = statistics.get_time_data_yearly(year, section) + df = statistics.get_time_data_yearly( + year, + section, + exclude_trash=True, + ) if df is None: print( @@ -378,7 +396,10 @@ def _data_day_yearly( row_offset = 95 col_offset = 2 df = statistics.get_light_numbers_yearly( - section, start=datetime(year, 1, 1), end=datetime(year + 1, 1, 1) + section, + start=datetime(year, 1, 1), + end=datetime(year + 1, 1, 1), + exclude_trash=True, ) for i in range(7): @@ -396,10 +417,17 @@ def _data_day_yearly( # Direction 1 row_offset = 5 col_offset = 2 - - df = statistics.get_time_data_yearly(year, section, direction=1) + df = statistics.get_time_data_yearly( + year, + section, + direction=1, + exclude_trash=True, + ) if df is None: + print( + f"{datetime.now()}:_data_day_yearly - Pas de données pour cette section:{section}, cette direction:{direction} et cette année:{year} /!\\/!\\/!\\" + ) return for i in range(7): @@ -411,7 +439,11 @@ def _data_day_yearly( row_offset = 31 col_offset = 2 df = statistics.get_light_numbers_yearly( - section, start=datetime(year, 1, 1), end=datetime(year + 1, 1, 1), direction=1 + section, + start=datetime(year, 1, 1), + end=datetime(year + 1, 1, 1), + direction=1, + exclude_trash=True, ) for i in range(7): @@ -430,8 +462,12 @@ def _data_day_yearly( if len(section.lane_set.all()) == 2: row_offset = 37 col_offset = 2 - - df = statistics.get_time_data_yearly(year, section, direction=2) + df = statistics.get_time_data_yearly( + year, + section, + direction=2, + exclude_trash=True, + ) for i in range(7): day_df = df[df["date"] == i] @@ -446,6 +482,7 @@ def _data_day_yearly( start=datetime(year, 1, 1), end=datetime(year + 1, 1, 1), direction=2, + exclude_trash=True, ) for i in range(7): @@ -469,28 +506,42 @@ def _data_month_yearly( end = datetime(year + 1, 1, 1) # Section - df = statistics.get_month_data(section, start, end) - row_offset = 14 col_offset = 2 + df = statistics.get_month_data( + section, + start, + end, + exclude_trash=True, + ) for col in df.itertuples(): ws.cell(row=row_offset, column=col_offset + col.Index, value=col.tm) # Direction 1 - df = statistics.get_month_data(section, start, end, direction=1) - row_offset = 4 col_offset = 2 + df = statistics.get_month_data( + section, + start, + end, + direction=1, + exclude_trash=True, + ) for col in df.itertuples(): ws.cell(row=row_offset, column=col_offset + col.Index, value=col.tm) # Direction 2 - df = statistics.get_month_data(section, start, end, direction=2) - row_offset = 9 col_offset = 2 + df = statistics.get_month_data( + section, + start, + end, + direction=2, + exclude_trash=True, + ) for col in df.itertuples(): ws.cell(row=row_offset, column=col_offset + col.Index, value=col.tm) @@ -573,8 +624,8 @@ def _data_speed( end=monday + timedelta(days=7), speed_low=range_[0], speed_high=range_[1], + exclude_trash=True, ) - for row in res: ws.cell(row=row_offset + row[0], column=col_offset + i, value=row[1]) @@ -590,6 +641,7 @@ def _data_speed( start=monday, end=monday + timedelta(days=7), v=v, + exclude_trash=True, ) for row in df.itertuples(): ws.cell( @@ -599,9 +651,13 @@ def _data_speed( # Average speed direction 1 row_offset = 5 col_offset = 19 - df = statistics.get_average_speed_by_hour( - count, section, direction=1, start=monday, end=monday + timedelta(days=7) + count, + section, + direction=1, + start=monday, + end=monday + timedelta(days=7), + exclude_trash=True, ) for row in df.itertuples(): ws.cell(row=row_offset + row.Index, column=col_offset, value=row.speed) @@ -619,8 +675,8 @@ def _data_speed( end=monday + timedelta(days=7), speed_low=range_[0], speed_high=range_[1], + exclude_trash=True, ) - for row in res: ws.cell(row=row_offset + row[0], column=col_offset + i, value=row[1]) @@ -636,6 +692,7 @@ def _data_speed( start=monday, end=monday + timedelta(days=7), v=v, + exclude_trash=True, ) for row in df.itertuples(): ws.cell( @@ -647,9 +704,13 @@ def _data_speed( # Average speed direction 2 row_offset = 33 col_offset = 19 - df = statistics.get_average_speed_by_hour( - count, section, direction=2, start=monday, end=monday + timedelta(days=7) + count, + section, + direction=2, + start=monday, + end=monday + timedelta(days=7), + exclude_trash=True, ) for row in df.itertuples(): ws.cell(row=row_offset + row.Index, column=col_offset, value=row.speed) @@ -708,8 +769,8 @@ def _data_speed_yearly( end=end, speed_low=range_[0], speed_high=range_[1], + exclude_trash=True, ) - for row in res: ws.cell(row=row_offset + row[0], column=col_offset + i, value=row[1]) @@ -719,7 +780,13 @@ def _data_speed_yearly( col_offset = 16 for i, v in enumerate(characteristic_speeds): df = statistics.get_characteristic_speed_by_hour( - None, section, direction=1, start=start, end=end, v=v + None, + section, + direction=1, + start=start, + end=end, + v=v, + exclude_trash=True, ) for row in df.itertuples(): ws.cell( @@ -729,13 +796,13 @@ def _data_speed_yearly( # Average speed direction 1 row_offset = 5 col_offset = 19 - df = statistics.get_average_speed_by_hour( - count, + None, section, direction=1, start=start, end=end, + exclude_trash=True, ) for row in df.itertuples(): ws.cell(row=row_offset + row.Index, column=col_offset, value=row.speed) @@ -746,13 +813,14 @@ def _data_speed_yearly( col_offset = 2 for i, range_ in enumerate(speed_ranges): res = statistics.get_speed_data_by_hour( - count, + None, section, direction=2, start=start, end=end, speed_low=range_[0], speed_high=range_[1], + exclude_trash=True, ) for row in res: @@ -764,7 +832,13 @@ def _data_speed_yearly( col_offset = 16 for i, v in enumerate(characteristic_speeds): df = statistics.get_characteristic_speed_by_hour( - count, section, direction=2, start=start, end=end, v=v + None, + section, + direction=2, + start=start, + end=end, + v=v, + exclude_trash=True, ) for row in df.itertuples(): ws.cell( @@ -776,13 +850,13 @@ def _data_speed_yearly( # Average speed direction 2 row_offset = 33 col_offset = 19 - df = statistics.get_average_speed_by_hour( - count, + None, section, direction=2, start=start, end=end, + exclude_trash=True, ) for row in df.itertuples(): ws.cell(row=row_offset + row.Index, column=col_offset, value=row.speed) diff --git a/comptages/core/statistics.py b/comptages/core/statistics.py index 2c19377..a299c86 100644 --- a/comptages/core/statistics.py +++ b/comptages/core/statistics.py @@ -4,6 +4,8 @@ from datetime import timedelta, datetime from pytz import timezone +from pandas import DataFrame, cut +from pytz import timezone from django.db.models import F, CharField, Value, Q, Sum, QuerySet from django.db.models.functions import ExtractHour, Trunc, Concat @@ -57,16 +59,22 @@ def get_time_data( .annotate(thm=Sum("times")) .values("import_status", "date", "hour", "thm") ) + print(f"statistics.py : get_time_data - qs.query={str(qs.query)}") df = DataFrame.from_records(qs) if not df.empty: df["date"] = df["date"].dt.strftime("%a %d.%m.%Y") df["import_status"].replace({0: "Existant", 1: "Nouveau"}, inplace=True) + return df def get_time_data_yearly( - year, section: models.Section, lane=None, direction=None + year, + section: models.Section, + lane=None, + direction=None, + exclude_trash=False, ) -> DataFrame: """Vehicles by hour and day of the week""" start = datetime(year, 1, 1) @@ -82,6 +90,9 @@ def get_time_data_yearly( timestamp__lt=end, ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if lane is not None: qs = qs.filter(id_lane=lane) @@ -162,7 +173,7 @@ def get_day_data( .annotate(tj=Sum("times")) .values("date", "tj", "import_status") ) - print(f"statistics.py : get_day_data - qs.query=", str(qs.query)) + print("statistics.py : get_day_data - qs.query=", str(qs.query)) df = DataFrame.from_records(qs) mean = 0 @@ -290,6 +301,7 @@ def get_light_numbers( direction=None, start=None, end=None, + exclude_trash=False, ) -> dict: if not start: start = count.start_process_date @@ -305,6 +317,9 @@ def get_light_numbers( timestamp__lt=end, ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if lane is not None: qs = qs.filter(id_lane=lane) @@ -325,7 +340,12 @@ def get_light_numbers( def get_light_numbers_yearly( - section: models.Section, lane=None, direction=None, start=None, end=None + section: models.Section, + lane=None, + direction=None, + start=None, + end=None, + exclude_trash=False, ) -> DataFrame: qs = models.CountDetail.objects.filter( id_lane__id_section=section, @@ -334,6 +354,9 @@ def get_light_numbers_yearly( timestamp__lt=end, ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if lane is not None: qs = qs.filter(id_lane=lane) @@ -359,6 +382,7 @@ def get_speed_data_by_hour( end=None, speed_low=0, speed_high=15, + exclude_trash=False, ) -> "ValuesQuerySet[models.CountDetail, Any]": if not start: start = count.start_process_date @@ -374,6 +398,9 @@ def get_speed_data_by_hour( timestamp__lt=end, ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if count is not None: qs = qs.filter(id_count=count) @@ -403,6 +430,7 @@ def get_characteristic_speed_by_hour( start=None, end=None, v=0.15, + exclude_trash=False, ) -> DataFrame: if not start: start = count.start_process_date @@ -417,6 +445,9 @@ def get_characteristic_speed_by_hour( timestamp__lt=end, ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if count is not None: qs = qs.filter(id_count=count) @@ -450,6 +481,7 @@ def get_average_speed_by_hour( start=None, end=None, v=0.15, + exclude_trash=False, ) -> DataFrame: if not start: start = count.start_process_date @@ -464,6 +496,9 @@ def get_average_speed_by_hour( timestamp__lt=end, ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if count is not None: qs = qs.filter(id_count=count) @@ -541,7 +576,13 @@ def get_special_periods(first_day, last_day) -> QuerySet[models.SpecialPeriod]: return qs -def get_month_data(section: models.Section, start, end, direction=None) -> DataFrame: +def get_month_data( + section: models.Section, + start, + end, + direction=None, + exclude_trash=False, +) -> DataFrame: qs = models.CountDetail.objects.filter( id_lane__id_section=section, timestamp__gte=start, timestamp__lt=end ) @@ -554,6 +595,9 @@ def get_month_data(section: models.Section, start, end, direction=None) -> DataF .values("month", "tm", "import_status") ) + if exclude_trash: + qs = qs.exclude(id_category__trash=True) + if direction is not None: qs = qs.filter(id_lane__direction=direction) @@ -584,6 +628,7 @@ def get_valid_days(year: int, section: models.Section) -> int: .order_by("date") .values("date", "hour", "tj") ) + print(f"statistics.py : get_valid_days - iterator.query={str(iterator.query)}") def count_valid_blocks(acc: dict, item: dict) -> dict[str, int]: date = item["date"] diff --git a/comptages/report/yearly_report_bike.py b/comptages/report/yearly_report_bike.py index 9eea11b..3bb11b1 100644 --- a/comptages/report/yearly_report_bike.py +++ b/comptages/report/yearly_report_bike.py @@ -5,16 +5,16 @@ from decimal import Decimal from qgis.core import Qgis, QgsMessageLog -from django.db.models import Sum, F, Avg # , Count, ExpressionWrapper +from openpyxl import load_workbook +from django.db.models import Sum, F, Avg from django.db.models.functions import ( ExtractHour, ExtractIsoWeekDay, ExtractMonth, TruncDate, ) -from openpyxl import load_workbook -from comptages.core import definitions, utils # , statistics +from comptages.core import definitions, utils from comptages.datamodel.models import ( CountDetail, Section, @@ -33,7 +33,6 @@ def __init__(self, path_to_output_dir, year, section_id, classtxt): self.year = year self.section_id = section_id self.classtxt = classtxt - # Assuming seasons to run from 21 to 20 -> month20 = (date - timedelta(days=20)).month self.seasons = { "printemps": [3, 4, 5], "été": [6, 7, 8], @@ -127,6 +126,9 @@ def total_runs_by_hour_and_direction( ) .values("runs", "hour", "direction", "section") ) + print( + f"yearly_report_bike.py : total_runs_by_hour_and_direction - results.query:{str(results.query)}" + ) def partition(acc: dict, val: dict) -> dict: hour = val["hour"] @@ -159,6 +161,9 @@ def total_runs_by_hour_one_direction(self, direction: int) -> dict[int, Any]: .annotate(day=ExtractIsoWeekDay("timestamp")) .order_by("day") ) + print( + f"yearly_report_bike.py : total_runs_by_hour_one_direction - results.query:{str(results.query)}" + ) def reducer(acc: dict, val: dict) -> dict: day = val["day"] @@ -218,6 +223,9 @@ def tjms_by_weekday_and_month( .annotate(month=ExtractMonth("timestamp")) .values("week_day", "month", "daily_runs") ) + print( + f"yearly_report_bike.py : tjms_by_weekday_and_month - results.query:{str(results.query)}" + ) # FIXME # Aggregation via `values()` into `annotate()` all the way to the end result would be more performant. @@ -346,6 +354,7 @@ def tjms_total_runs_by_day_of_week(self) -> dict[str, Any]: print( f"yearly_report_bike.py : tjms_total_runs_by_day_of_week - results.query:{str(results.query)}" ) + # FIXME # Aggregation via `values()` into `annotate()` all the way to the end result would be more performant. builder = {} @@ -364,6 +373,7 @@ def tjms_total_runs_by_day_of_week(self) -> dict[str, Any]: builder[item["week_day"]]["runs"] / builder[item["week_day"]]["days"] ) + return builder def total_runs_by_class(self) -> dict[str, Any]: @@ -380,6 +390,9 @@ def total_runs_by_class(self) -> dict[str, Any]: .annotate(runs=Sum("times"), code=F("id_category__code")) .values("day", "runs", "code") ) + print( + f"yearly_report_bike.py : total_runs_by_class - results.query:{str(results.query)}" + ) def reducer(acc: dict, i: dict): code = i["code"] @@ -407,6 +420,9 @@ def tjms_by_direction_bike( ) assert qs.exists() results = qs.aggregate(res=Sum("times"))["res"] + print( + f"yearly_report_bike.py : tjms_by_direction_bike - results.query:{str(results.query)}" + ) # TODO: avoid the division? return results / 365 @@ -417,6 +433,8 @@ def total(self, categories=[1]) -> float: import_status=definitions.IMPORT_STATUS_DEFINITIVE, ) results = qs.aggregate(res=Sum("times"))["res"] + print(f"yearly_report_bike.py : total - results.query:{str(results.query)}") + return results def max_day(self, categories=[1]) -> tuple[str, Any]: @@ -431,6 +449,7 @@ def max_day(self, categories=[1]) -> tuple[str, Any]: .annotate(total=Sum("times")) .order_by("-total") ) + print(f"yearly_report_bike.py : max_day - qs.query:{str(qs.query)}") return qs[0]["total"], qs[0]["date"] @@ -446,6 +465,7 @@ def max_month(self, categories=[1]) -> tuple[str, Any]: .annotate(total=Sum("times")) .order_by("-total") ) + print(f"yearly_report_bike.py : max_month - qs.query:{str(qs.query)}") return qs[0]["total"], qs[0]["month"] @@ -461,6 +481,7 @@ def min_month(self, categories=[1]) -> tuple[str, Any]: .annotate(total=Sum("times")) .order_by("total") ) + print(f"yearly_report_bike.py : min_month - qs.query:{str(qs.query)}") return qs[0]["total"], qs[0]["month"] @@ -477,12 +498,19 @@ def count_details_by_various_criteria( .exclude(id_category__name__in=categories_name_to_exclude) .values_list("id_category", flat=True) ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - categories_ids.query:{str(categories_ids.query)}" + ) + # Base QuerySet base_qs = CountDetail.objects.filter( id_count=count.id, id_category__in=categories_ids, timestamp__year=self.year, ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - base_qs.query:{str(base_qs.query)}" + ) # Specialized QuerySets total_runs_in_year = ( @@ -490,15 +518,19 @@ def count_details_by_various_criteria( .values("category_name") .annotate(value=Sum("times")) ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - total_runs_in_year.query:{str(total_runs_in_year.query)}" + ) busy_date = ( - base_qs.annotate( - date=TruncDate("timestamp"), category_name=F("id_category__name") - ) - .values("date", "category_name") + base_qs.annotate(date=TruncDate("timestamp")) + .values("date") .annotate(value=Sum("times")) .order_by("-value") ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - busy_date.query:{str(busy_date.query)}" + ) busiest_date = busy_date.first() least_busy_date = busy_date.last() @@ -513,6 +545,9 @@ def count_details_by_various_criteria( .values("date", "category_name") .annotate(value=Sum("times")) ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - busiest_date_row.query:{str(busiest_date_row.query)}" + ) least_busy_date_row = ( base_qs.annotate( @@ -522,6 +557,9 @@ def count_details_by_various_criteria( .values("date", "category_name") .annotate(value=Sum("times")) ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - least_busy_date_row.query:{str(least_busy_date_row.query)}" + ) busy_month = ( base_qs.annotate(month=ExtractMonth("timestamp")) @@ -529,6 +567,9 @@ def count_details_by_various_criteria( .annotate(value=Sum("times")) .order_by("-value") ) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - busy_month.query:{str(busy_month.query)}" + ) busiest_month = busy_month.first() least_busy_month = busy_month.last() @@ -563,8 +604,12 @@ def count_details_by_various_criteria( .annotate(value=Sum("times")) .order_by("-value") ) + total_runs_busiest_hour_weekday = busiest_hour.exclude(week_day__gt=5) total_runs_busiest_hour_weekend = busiest_hour.exclude(week_day__lt=6) + print( + f"yearly_report_bike.py : count_details_by_various_criteria - busiest_weekend_hour.query:{str(total_runs_busiest_hour_weekend.query)}" + ) busiest_weekday = total_runs_busiest_hour_weekday.first() busiest_weekend = total_runs_busiest_hour_weekend.first() @@ -592,6 +637,7 @@ def count_details_by_various_criteria( def count_details_by_season(self, count_id) -> dict[int, Any]: """Break down count details by season x section x class""" + # Assuming seasons to run from 21 to 20 -> month20 = (date - timedelta(days=20)).month # Preparing to filter out categories that don't reference the class picked out by `class_name` class_name = self.classtxt # Excluding irrelevant @@ -616,6 +662,9 @@ def count_details_by_season(self, count_id) -> dict[int, Any]: .annotate(value=Sum("times")) .values("date", "category_name", "value") ) + print( + f"yearly_report_bike.py : count_details_by_season - count_details.query:{str(count_details.query)}" + ) # Preparing to collect data def reducer(acc: dict, detail) -> dict: @@ -703,9 +752,15 @@ def get_category_data_by_dow( .values("week_day", "value") .values_list("week_day", "value") ) + print( + "yearly_report_bike.py : get_category_data_by_dow - qs.query=", + str(qs.query), + ) + return qs def run(self): + print(f"{datetime.now()}: YRB_run - begin... ({self.path_to_output_dir})") current_dir = path.dirname(path.abspath(__file__)) template = path.join(current_dir, "template_yearly_bike.xlsx") workbook = load_workbook(filename=template) @@ -731,7 +786,7 @@ def render_section_dist(value: Union[str, Decimal, None]) -> str: ws[ "B3" ] = f""" - Poste de comptage : {section.id} + Poste de comptage : {section.id} Axe : {section.owner}:{section.road}{section.way} PR {section.start_pr} + {section_start_dist} m à PR {section.end_pr} + {section_end_dist} m """ @@ -1000,5 +1055,6 @@ def render_section_dist(value: Union[str, Decimal, None]) -> str: output = path.join( self.path_to_output_dir, "{}_{}_r.xlsx".format(self.section_id, self.year) ) + workbook.save(filename=output) print(f"{datetime.now()}: YRB_run - end: Saved report to {output}")