import os import tempfile import unittest from openpyxl import load_workbook from mileage_logger.ingest.semantic_reader import load_place_visits from mileage_logger.logic.detect_itinerary import SiteConfig, detect_itinerary from mileage_logger.distance.resolve import DistanceResolver from mileage_logger.export.excel_writer import build_monthly_rows, write_monthly_workbook FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures", "semantic") CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yml") ROUTES_PATH = os.path.join(os.path.dirname(__file__), "data", "routes_golden.csv") class TestExcelWriter(unittest.TestCase): @classmethod def setUpClass(cls): cls.site_config = SiteConfig.from_yaml(CONFIG_PATH) def _build_workbook(self, fixture_file: str) -> str: visits = load_place_visits(os.path.join(FIXTURES_DIR, fixture_file)) hops = detect_itinerary(visits, self.site_config) resolver = DistanceResolver(route_csv_path=ROUTES_PATH) rows_by_month = build_monthly_rows(hops, self.site_config, resolver) # Write to a temporary file fd, path = tempfile.mkstemp(suffix=".xlsx") os.close(fd) write_monthly_workbook(rows_by_month, path) return path def test_excel_layout_simple(self): # Build workbook for the simple fixture path = self._build_workbook("2025-08-08.one_day_simple.json") try: wb = load_workbook(path) # Determine expected month from the first hop date visits = load_place_visits(os.path.join(FIXTURES_DIR, "2025-08-08.one_day_simple.json")) hops = detect_itinerary(visits, self.site_config) if not hops: self.fail("No hops detected for simple fixture") expected_month = hops[0].date.strftime("%Y-%m") self.assertIn(expected_month, wb.sheetnames) ws = wb[expected_month] rows = list(ws.iter_rows(values_only=True)) # number of hops + header self.assertEqual(len(rows), len(hops) + 1) header = rows[0] expected_header = ("Date", "Purpose", "Miles", "Vehicle", "Job Role", "From", "To", "Notes") self.assertEqual(header, expected_header) # Validate each hop row resolver = DistanceResolver(route_csv_path=ROUTES_PATH) for i, hop in enumerate(hops, start=1): row = rows[i] origin_site = self.site_config.by_canonical[hop.origin] dest_site = self.site_config.by_canonical[hop.destination] dist = resolver.resolve(hop.origin, hop.destination, (origin_site.lat, origin_site.lon), (dest_site.lat, dest_site.lon)) self.assertEqual(row[0], hop.date.isoformat()) expected_purpose = f"Travel from {origin_site.label} to {dest_site.label} {dist:.1f}mi" self.assertEqual(row[1], expected_purpose) self.assertAlmostEqual(float(row[2]), dist, places=1) self.assertEqual(row[3], resolver.vehicle_label) self.assertEqual(row[4], resolver.job_role) self.assertEqual(row[5], origin_site.label) self.assertEqual(row[6], dest_site.label) # Notes may be returned as None when reading from Excel self.assertIn(row[7] or "", ["", None]) finally: os.unlink(path) def test_cross_midnight_sheet_rows(self): path = self._build_workbook("cross_midnight.json") try: wb = load_workbook(path) # Should still be month 2025-08 self.assertIn("2025-08", wb.sheetnames) ws = wb["2025-08"] rows = list(ws.iter_rows(values_only=True)) # two hops -> 3 rows including header self.assertEqual(len(rows), 3) # Dates should span two days dates = [r[0] for r in rows[1:]] self.assertEqual(dates, ["2025-08-08", "2025-08-09"]) finally: os.unlink(path) if __name__ == '__main__': unittest.main()