fix: tolerate spreadsheet decimal roundoff
This commit is contained in:
@@ -90,6 +90,7 @@ class OpenpyxlWorkbookBuilder:
|
||||
|
||||
_INTEGER_HEADERS = frozenset({"NIGHTS", "ADULTS", "CHILDREN", "NO_OF_ROOMS"})
|
||||
_DECIMAL_HEADERS = frozenset({"RATE_AMOUNT", "REAL PRICE", "TOTAL PRICE", KB_HEADER})
|
||||
_DECIMAL_TOLERANCE = Decimal("0.000001")
|
||||
|
||||
def __init__(self, *_legacy_args: Any, **_legacy_options: Any) -> None:
|
||||
# The ignored arguments keep older local wrappers import-compatible while
|
||||
@@ -130,6 +131,17 @@ class OpenpyxlWorkbookBuilder:
|
||||
return float(Decimal(str(value)))
|
||||
return cls._safe_text(value)
|
||||
|
||||
@classmethod
|
||||
def _decimal_matches(cls, actual: Any, expected: Any) -> bool:
|
||||
try:
|
||||
actual_decimal = Decimal(str(actual))
|
||||
expected_decimal = Decimal(str(expected))
|
||||
except (InvalidOperation, TypeError, ValueError):
|
||||
return False
|
||||
if not actual_decimal.is_finite() or not expected_decimal.is_finite():
|
||||
return False
|
||||
return abs(actual_decimal - expected_decimal) <= cls._DECIMAL_TOLERANCE
|
||||
|
||||
@staticmethod
|
||||
def _style_sheet(worksheet: Any, headers: list[str]) -> None:
|
||||
worksheet.freeze_panes = "A2"
|
||||
@@ -185,14 +197,20 @@ class OpenpyxlWorkbookBuilder:
|
||||
for channel, expected_row_count in zip(report.channels, expected_rows):
|
||||
worksheet = workbook[channel.worksheet]
|
||||
headers = list(channel.headers)
|
||||
if worksheet.max_row != expected_row_count + 1 or worksheet.max_column != len(headers):
|
||||
raise ValueError("worksheet dimensions do not match")
|
||||
if [worksheet.cell(1, index).value for index in range(1, len(headers) + 1)] != headers:
|
||||
row_iterator = worksheet.iter_rows(values_only=False)
|
||||
header_cells = next(row_iterator, None)
|
||||
if (
|
||||
header_cells is None
|
||||
or len(header_cells) != len(headers)
|
||||
or [cell.value for cell in header_cells] != headers
|
||||
):
|
||||
raise ValueError("worksheet headers do not match")
|
||||
for row_index, expected_row in enumerate(channel.rows, start=2):
|
||||
cells = next(row_iterator, None)
|
||||
if cells is None or len(cells) != len(headers):
|
||||
raise ValueError("worksheet dimensions do not match")
|
||||
payload_row = expected_row.to_payload(channel.worksheet == "DY-AI-Easy-KB")
|
||||
for column, header in enumerate(headers, start=1):
|
||||
cell = worksheet.cell(row_index, column)
|
||||
for header, cell in zip(headers, cells):
|
||||
if header == "TOTAL PRICE":
|
||||
expected_formula = f"=R{row_index}*C{row_index}*G{row_index}"
|
||||
if cell.value != expected_formula or cell.data_type != "f":
|
||||
@@ -206,11 +224,13 @@ class OpenpyxlWorkbookBuilder:
|
||||
elif header in cls._INTEGER_HEADERS:
|
||||
matches = actual == int(expected)
|
||||
elif header in cls._DECIMAL_HEADERS:
|
||||
matches = Decimal(str(actual)) == Decimal(str(expected))
|
||||
matches = cls._decimal_matches(actual, expected)
|
||||
else:
|
||||
matches = ("" if actual is None else actual) == cls._safe_text(expected)
|
||||
if not matches:
|
||||
raise ValueError("worksheet values do not match")
|
||||
if next(row_iterator, None) is not None:
|
||||
raise ValueError("worksheet dimensions do not match")
|
||||
if formula_count != report.row_count:
|
||||
raise ValueError("formula count does not match")
|
||||
if cls._semantic_sha256(payload) != cls._semantic_sha256(report.to_workbook_payload()):
|
||||
|
||||
Reference in New Issue
Block a user