feat: sync latest ARR implementation
This commit is contained in:
@@ -7,8 +7,11 @@
|
||||
- Finance source: `finance.v_active_daily_facts`, selected by `DEPARTURE` from month start through the requested as-of date.
|
||||
- Booking source: `booking.v_current_room_items` and `booking.v_group_room_item_summary`, matched by normalized full Group Code only.
|
||||
- Dates/nights come from Finance. Booking data only supplies room type and booked quantity.
|
||||
- A Finance fact with no Group Code remains as its own output row and has blank `RES_COMMENT` and `Booking Room`; it is
|
||||
never merged with another no-code fact. A present Group Code keeps its normal stay-segment row and `RES_COMMENT`, but
|
||||
`Booking Room` is blank when no current accepted Booking room can be resolved.
|
||||
- `Total Booking Price` uses each Finance daily `TOTAL PRICE`; it is not recomputed from booking unit price and is not collapsed across all rows sharing a Group Code.
|
||||
- The program splits output by each Finance `(Group Code, ARRIVAL, DEPARTURE)` segment.
|
||||
- The program splits matched output by each Finance `(Group Code, ARRIVAL, DEPARTURE)` segment.
|
||||
- Report rows/versions are not persisted in PostgreSQL; a deterministic generation identity and source pins protect generation consistency.
|
||||
|
||||
Supported companies: `LianTai`, `QBD`, `DY-AI-Easy-KB`, `FengRun`, `HanaTour`.
|
||||
|
||||
@@ -10,7 +10,7 @@ from decimal import Decimal
|
||||
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
|
||||
PROCESSOR_VERSION = "1.0.0"
|
||||
PROCESSOR_VERSION = "1.2.0"
|
||||
RESULT_SCHEMA_VERSION = "1.0"
|
||||
|
||||
ENGLISH_MONTH_NAMES: Tuple[str, ...] = (
|
||||
@@ -271,7 +271,12 @@ def rule_set_sha256() -> str:
|
||||
"companies": [rule.__dict__ for rule in COMPANY_RULES],
|
||||
"period_basis": "departure",
|
||||
"group_code_normalization": "nfkc_trim_upper",
|
||||
"row_key": ["group_code", "arrival", "departure"],
|
||||
"row_key": {
|
||||
"with_group_code": ["group_code", "arrival", "departure"],
|
||||
"without_group_code": ["daily_record_id"],
|
||||
},
|
||||
"missing_group_code_booking_room": "blank",
|
||||
"unmatched_group_code_booking_room": "blank",
|
||||
"price_group_key": ["room_category_label", "static_total_price"],
|
||||
"filename_pattern": "<Company>-<EnglishFullMonth>-<YYYY>.xlsx",
|
||||
"sheet_pattern": "<Company> <DD-DD> <EnglishMon> <YYYY>",
|
||||
|
||||
@@ -193,6 +193,7 @@ def build_company_report(
|
||||
|
||||
errors: List[ReportProblem] = []
|
||||
eligible: List[Tuple[FinanceFact, str]] = []
|
||||
blank_booking_facts: List[FinanceFact] = []
|
||||
for fact in facts:
|
||||
generic_period = f"{year:04d}-{month:02d}"
|
||||
if not isinstance(fact.arrival, date) or not isinstance(fact.departure, date):
|
||||
@@ -237,6 +238,24 @@ def build_company_report(
|
||||
continue
|
||||
group_code = normalize_group_code(fact.res_comment)
|
||||
stored_group = normalize_group_code(fact.group_code_key)
|
||||
if not group_code and not stored_group:
|
||||
if (
|
||||
not str(fact.room_category_label or "").strip()
|
||||
or _decimal_or_none(fact.total_price) is None
|
||||
):
|
||||
errors.append(
|
||||
_problem(
|
||||
ErrorCode.TOTAL_PRICE_INVALID,
|
||||
"pricing",
|
||||
company,
|
||||
period_key,
|
||||
"the static price detail is missing or invalid",
|
||||
[fact],
|
||||
)
|
||||
)
|
||||
continue
|
||||
blank_booking_facts.append(fact)
|
||||
continue
|
||||
if not group_code or not stored_group:
|
||||
errors.append(
|
||||
_problem(
|
||||
@@ -244,7 +263,7 @@ def build_company_report(
|
||||
"group_lookup",
|
||||
company,
|
||||
period_key,
|
||||
"the source record has no usable Group Code",
|
||||
"the source Group Code fields are inconsistent",
|
||||
[fact],
|
||||
)
|
||||
)
|
||||
@@ -290,6 +309,24 @@ def build_company_report(
|
||||
warnings: List[ReportProblem] = []
|
||||
rows: List[ReportRow] = []
|
||||
used_booking_versions: Dict[str, int] = {}
|
||||
for fact in blank_booking_facts:
|
||||
total_booking_price, multi_price = _price_text([fact])
|
||||
rows.append(
|
||||
ReportRow(
|
||||
arrival=fact.arrival, # type: ignore[arg-type]
|
||||
departure=fact.departure, # type: ignore[arg-type]
|
||||
nights=fact.nights, # type: ignore[arg-type]
|
||||
block_code=_distinct_join([fact.block_code]),
|
||||
res_comment=unicodedata.normalize(
|
||||
"NFKC", str(fact.res_comment or "")
|
||||
).strip(),
|
||||
booking_room="",
|
||||
total_booking_price=total_booking_price,
|
||||
normalized_group_code="",
|
||||
record_ids=(fact.daily_record_id,),
|
||||
multi_price_review=multi_price,
|
||||
)
|
||||
)
|
||||
for (group_code, arrival, departure), segment_facts in sorted(
|
||||
grouped_facts.items(), key=lambda item: (item[0][2], item[0][1], item[0][0])
|
||||
):
|
||||
@@ -307,39 +344,6 @@ def build_company_report(
|
||||
)
|
||||
)
|
||||
continue
|
||||
parse_version_id = snapshot.group_parse_versions.get(group_code)
|
||||
if parse_version_id is None:
|
||||
errors.append(
|
||||
_problem(
|
||||
ErrorCode.BOOKING_PARSE_FAILED,
|
||||
"group_lookup",
|
||||
company,
|
||||
period_key,
|
||||
"no accepted current booking parse exists for the Group Code",
|
||||
segment_facts,
|
||||
)
|
||||
)
|
||||
continue
|
||||
room_items = items_by_segment.get((group_code, arrival, departure), [])
|
||||
valid_room_items = [
|
||||
item
|
||||
for item in room_items
|
||||
if item.quantity > 0
|
||||
and bool(item.room_type_raw.strip())
|
||||
and item.nights == (departure - arrival).days
|
||||
]
|
||||
if not valid_room_items:
|
||||
errors.append(
|
||||
_problem(
|
||||
ErrorCode.ROOM_ITEMS_MISSING,
|
||||
"group_lookup",
|
||||
company,
|
||||
period_key,
|
||||
"the Group Code has no room items for this output stay segment",
|
||||
segment_facts,
|
||||
)
|
||||
)
|
||||
continue
|
||||
try:
|
||||
total_booking_price, multi_price = _price_text(segment_facts)
|
||||
except ValueError:
|
||||
@@ -365,7 +369,18 @@ def build_company_report(
|
||||
segment_facts,
|
||||
)
|
||||
)
|
||||
used_booking_versions[group_code] = parse_version_id
|
||||
parse_version_id = snapshot.group_parse_versions.get(group_code)
|
||||
valid_room_items: List[BookingRoomItem] = []
|
||||
if parse_version_id is not None:
|
||||
used_booking_versions[group_code] = parse_version_id
|
||||
room_items = items_by_segment.get((group_code, arrival, departure), [])
|
||||
valid_room_items = [
|
||||
item
|
||||
for item in room_items
|
||||
if item.quantity > 0
|
||||
and bool(item.room_type_raw.strip())
|
||||
and item.nights == (departure - arrival).days
|
||||
]
|
||||
rows.append(
|
||||
ReportRow(
|
||||
arrival=arrival,
|
||||
@@ -383,9 +398,17 @@ def build_company_report(
|
||||
)
|
||||
)
|
||||
|
||||
duplicate_counts = Counter(row.normalized_group_code for row in rows)
|
||||
duplicate_counts = Counter(
|
||||
row.normalized_group_code for row in rows if row.normalized_group_code
|
||||
)
|
||||
rows = [
|
||||
replace(row, duplicate_group=duplicate_counts[row.normalized_group_code] > 1)
|
||||
replace(
|
||||
row,
|
||||
duplicate_group=bool(
|
||||
row.normalized_group_code
|
||||
and duplicate_counts[row.normalized_group_code] > 1
|
||||
),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
@@ -398,13 +421,20 @@ def build_company_report(
|
||||
period_rows = tuple(
|
||||
sorted(
|
||||
rows_by_period.get(period.key, []),
|
||||
key=lambda row: (row.departure, row.arrival, row.normalized_group_code),
|
||||
key=lambda row: (
|
||||
row.departure,
|
||||
row.arrival,
|
||||
row.normalized_group_code,
|
||||
row.record_ids,
|
||||
),
|
||||
)
|
||||
)
|
||||
populated_periods.append(replace(period, rows=period_rows if period.active else tuple()))
|
||||
|
||||
filename = f"{company}-{ENGLISH_MONTH_NAMES[month]}-{year}.xlsx"
|
||||
used_daily_ids = {fact.daily_version_id for fact, _ in eligible}
|
||||
used_daily_ids = {
|
||||
fact.daily_version_id for fact, _ in eligible
|
||||
} | {fact.daily_version_id for fact in blank_booking_facts}
|
||||
daily_versions = tuple(
|
||||
pin
|
||||
for pin in sorted(snapshot.daily_versions, key=lambda current: current.business_date)
|
||||
|
||||
Reference in New Issue
Block a user