191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from datetime import date, timedelta
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
from openpyxl import load_workbook
|
|
|
|
from monthly_reports.contracts import (
|
|
ChannelObservation,
|
|
DailyVersionPin,
|
|
MonthlyFact,
|
|
MonthlySnapshot,
|
|
STANDARD_SHEETS,
|
|
)
|
|
from monthly_reports.core import build_monthly_report
|
|
from monthly_reports.publishing import OpenpyxlWorkbookBuilder
|
|
|
|
|
|
def report_fixture():
|
|
source = MonthlyFact(
|
|
daily_record_id=1,
|
|
daily_version_id=100,
|
|
business_date=date(2026, 7, 8),
|
|
channel_key="QBD",
|
|
arrival=date(2026, 7, 8),
|
|
departure=date(2026, 7, 10),
|
|
nights=2,
|
|
adults=2,
|
|
children=0,
|
|
block_code="=SYN-FORMULA-LIKE",
|
|
no_of_rooms=1,
|
|
company_name="SYN-COMPANY",
|
|
confirmation_no="SYN-CONFIRMATION",
|
|
disp_room_no="SYN-ROOM",
|
|
effective_rate_amount=Decimal("450.50"),
|
|
full_name="SYN-GUEST",
|
|
res_comment="SYN-COMMENT",
|
|
trace_text="",
|
|
products="",
|
|
rate_code="SYN-RATE",
|
|
room_category_label="SYN-TYPE",
|
|
real_price=Decimal("450.50"),
|
|
total_price=Decimal("901.00"),
|
|
kb_amount=None,
|
|
)
|
|
return build_monthly_report(
|
|
2026,
|
|
7,
|
|
date(2026, 7, 8),
|
|
MonthlySnapshot(
|
|
(source,),
|
|
(DailyVersionPin(date(2026, 7, 8), 100),),
|
|
(ChannelObservation(date(2026, 7, 8), 100, "QBD", 1, 1),),
|
|
),
|
|
)
|
|
|
|
|
|
def large_decimal_report_fixture():
|
|
facts = []
|
|
daily_versions = []
|
|
channel_observations = []
|
|
record_id = 1000
|
|
daily_plan = (
|
|
(date(2026, 8, 1), 501, 28),
|
|
(date(2026, 8, 2), 502, 30),
|
|
(date(2026, 8, 3), 503, 30),
|
|
)
|
|
for business_date, daily_version_id, rows_per_sheet in daily_plan:
|
|
daily_versions.append(DailyVersionPin(business_date, daily_version_id))
|
|
for channel_index, channel_key in enumerate(STANDARD_SHEETS, start=1):
|
|
channel_observations.append(
|
|
ChannelObservation(
|
|
business_date,
|
|
daily_version_id,
|
|
channel_key,
|
|
channel_index,
|
|
rows_per_sheet,
|
|
)
|
|
)
|
|
for row_index in range(rows_per_sheet):
|
|
nights = 1 + (row_index % 3)
|
|
no_of_rooms = 1 + (row_index % 2)
|
|
real_price = Decimal("321.17") + Decimal(row_index % 19) / Decimal("100")
|
|
sequence = record_id
|
|
facts.append(
|
|
MonthlyFact(
|
|
daily_record_id=sequence,
|
|
daily_version_id=daily_version_id,
|
|
business_date=business_date,
|
|
channel_key=channel_key,
|
|
arrival=business_date,
|
|
departure=business_date + timedelta(days=nights),
|
|
nights=nights,
|
|
adults=2,
|
|
children=row_index % 2,
|
|
block_code=f"BLOCK-{sequence}",
|
|
no_of_rooms=no_of_rooms,
|
|
company_name=f"COMPANY-{channel_index}",
|
|
confirmation_no=f"CONF-{sequence}",
|
|
disp_room_no=f"ROOM-{sequence}",
|
|
effective_rate_amount=Decimal("247.35")
|
|
+ Decimal((row_index + channel_index) % 7) / Decimal("100"),
|
|
full_name=f"GUEST-{sequence}",
|
|
res_comment=f"COMMENT-{sequence}",
|
|
trace_text="TRACE",
|
|
products="ROOM",
|
|
rate_code="BAR",
|
|
room_category_label="STD",
|
|
real_price=real_price,
|
|
total_price=real_price * no_of_rooms * nights,
|
|
kb_amount=Decimal(no_of_rooms * 100)
|
|
if channel_key == "DY-AI-Easy-KB"
|
|
else None,
|
|
)
|
|
)
|
|
record_id += 1
|
|
return build_monthly_report(
|
|
2026,
|
|
8,
|
|
date(2026, 8, 3),
|
|
MonthlySnapshot(
|
|
tuple(facts),
|
|
tuple(daily_versions),
|
|
tuple(channel_observations),
|
|
),
|
|
)
|
|
|
|
|
|
class MonthlyReportsXlsxTests(unittest.TestCase):
|
|
def test_openpyxl_builder_reopens_checks_formulas_and_semantic_identity(self):
|
|
report = report_fixture()
|
|
with tempfile.TemporaryDirectory(prefix="monthly-report-xlsx-") as temp_dir:
|
|
built = OpenpyxlWorkbookBuilder().build(report, Path(temp_dir))
|
|
|
|
self.assertTrue(built.path.is_file())
|
|
self.assertEqual(built.path.stat().st_mode & 0o777, 0o600)
|
|
self.assertEqual(built.summary["sheet_names"][:5], [
|
|
"LIANTAI-GROUP", "LIANTAI-FIT", "QBD", "DY-AI-Easy-KB", "FENGRUN"
|
|
])
|
|
self.assertEqual(built.summary["row_counts"], [0, 0, 1, 0, 0])
|
|
self.assertEqual(built.summary["formula_count"], 1)
|
|
self.assertEqual(built.summary["preview_count"], 5)
|
|
self.assertEqual(len(built.summary["semantic_sha256"]), 64)
|
|
|
|
workbook = load_workbook(built.path, data_only=False, read_only=True)
|
|
try:
|
|
self.assertEqual(workbook["QBD"]["S2"].value, "=R2*C2*G2")
|
|
self.assertEqual(workbook["QBD"]["F2"].value, "'=SYN-FORMULA-LIKE")
|
|
formulas = [
|
|
cell.value
|
|
for sheet in workbook.worksheets
|
|
for row in sheet.iter_rows()
|
|
for cell in row
|
|
if cell.data_type == "f"
|
|
]
|
|
self.assertEqual(formulas, ["=R2*C2*G2"])
|
|
finally:
|
|
workbook.close()
|
|
|
|
def test_openpyxl_builder_validates_440_decimal_rows_across_multiple_sheets(self):
|
|
report = large_decimal_report_fixture()
|
|
self.assertEqual(report.row_count, 440)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="monthly-report-xlsx-large-") as temp_dir:
|
|
built = OpenpyxlWorkbookBuilder().build(report, Path(temp_dir))
|
|
|
|
self.assertEqual(built.summary["sheet_names"], list(STANDARD_SHEETS))
|
|
self.assertEqual(built.summary["row_counts"], [88, 88, 88, 88, 88])
|
|
self.assertEqual(built.summary["formula_count"], 440)
|
|
self.assertTrue(
|
|
OpenpyxlWorkbookBuilder._decimal_matches("321.1700005", "321.17")
|
|
)
|
|
self.assertFalse(
|
|
OpenpyxlWorkbookBuilder._decimal_matches("321.1700011", "321.17")
|
|
)
|
|
self.assertEqual(
|
|
OpenpyxlWorkbookBuilder._validate_workbook(
|
|
built.path,
|
|
report,
|
|
report.to_workbook_payload(),
|
|
),
|
|
440,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|