Initial commit (clean, ignores in place)

This commit is contained in:
2025-08-12 01:13:41 +01:00
commit c74790b014
26 changed files with 2331 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
"""Write mileage itineraries to Excel workbooks.
This module uses :mod:`openpyxl` to construct a workbook with one sheet
per month. Each row corresponds to a single hop between recognised
sites. Columns follow the specification used by the EveryHR system:
* ``Date`` calendar date in ISO format (YYYY-MM-DD).
* ``Purpose`` free text summarising the journey, e.g. ``"Travel from
Home to Lingwood Primary Academy 13.2mi"``.
* ``Miles`` numeric value rounded to one decimal place.
* ``Vehicle`` the vehicle descriptor configured for the user.
* ``Job Role`` the job role of the user.
* ``From`` friendly label of the origin site.
* ``To`` friendly label of the destination site.
* ``Notes`` blank for manual additions.
Rows are grouped by month (YYYY-MM). Each sheet is named after the
month and contains a header row followed by one row per hop in
chronological order.
"""
from __future__ import annotations
import os
from collections import defaultdict
from datetime import date
from typing import Dict, Iterable, List, Tuple
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
from ..logic.detect_itinerary import Hop, SiteConfig
def build_monthly_rows(hops: Iterable[Hop], site_config: SiteConfig, distance_resolver) -> Dict[str, List[Tuple[str, str, float, str, str, str, str, str]]]:
"""Prepare rows grouped by month for Excel output.
Parameters
----------
hops : iterable of :class:`Hop`
The hops produced by itinerary detection.
site_config : :class:`SiteConfig`
Used to look up friendly labels for canonical site names.
distance_resolver : object
An object with a ``resolve(origin_name, dest_name, origin_coords, dest_coords)``
method that returns a distance in miles. See
:class:`~mileage_logger.distance.resolve.DistanceResolver`.
Returns
-------
dict mapping str -> list of tuples
Keys are month strings in the form ``YYYY-MM``. Values are
lists of tuples containing the data for each row: (date_str,
purpose, miles, vehicle, job_role, from_label, to_label, notes).
"""
rows_by_month: Dict[str, List[Tuple[str, str, float, str, str, str, str, str]]] = defaultdict(list)
for hop in hops:
month_key = hop.date.strftime("%Y-%m")
origin_site = site_config.by_canonical.get(hop.origin)
dest_site = site_config.by_canonical.get(hop.destination)
if origin_site is None or dest_site is None:
continue
# Resolve distance
dist = distance_resolver.resolve(
hop.origin,
hop.destination,
(origin_site.lat, origin_site.lon),
(dest_site.lat, dest_site.lon),
)
# Build purpose string
purpose = f"Travel from {origin_site.label} to {dest_site.label} {dist:.1f}mi"
rows_by_month[month_key].append(
(
hop.date.isoformat(),
purpose,
dist,
distance_resolver.vehicle_label if hasattr(distance_resolver, "vehicle_label") else "SH11 DRV (Own 1.6CC Diesel Car/Van)",
distance_resolver.job_role if hasattr(distance_resolver, "job_role") else "ICT Technician",
origin_site.label,
dest_site.label,
"",
)
)
return rows_by_month
def write_monthly_workbook(rows_by_month: Dict[str, List[Tuple[str, str, float, str, str, str, str, str]]], output_path: str) -> None:
"""Write the grouped rows into an Excel workbook.
Parameters
----------
rows_by_month : dict
Mapping from month strings to lists of row tuples as returned
by :func:`build_monthly_rows`.
output_path : str
Path of the Excel workbook to write. Any existing file will be
overwritten.
"""
wb = Workbook()
# Remove the default sheet created by openpyxl
default_sheet = wb.active
wb.remove(default_sheet)
for month, rows in sorted(rows_by_month.items()):
ws = wb.create_sheet(title=month)
# Write header
header = ["Date", "Purpose", "Miles", "Vehicle", "Job Role", "From", "To", "Notes"]
ws.append(header)
for row in rows:
ws.append(list(row))
# Autosize columns (approximate)
for col_idx in range(1, len(header) + 1):
column_letter = get_column_letter(col_idx)
max_length = max(
len(str(ws.cell(row=r + 1, column=col_idx).value)) for r in range(len(rows) + 1)
)
# Add a little extra padding
ws.column_dimensions[column_letter].width = max_length + 2
# Ensure directory exists
os.makedirs(os.path.dirname(output_path), exist_ok=True)
wb.save(output_path)