Files
HM_project_Viewer_Board/scripts/promote_wehago_recheck_projection.py
T

902 lines
32 KiB
Python

from __future__ import annotations
import sqlite3
import sys
import re
from datetime import datetime
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from runtime_config import DB_PATH
from wehago_compare import (
QUERY_PROJECTION_VERSION,
_account_base_names_compatible,
_contained_core_desc_match,
_erp_section_identity,
_is_obvious_recheck_group,
_recheck_group_rank_key,
_section_vat_exception_capacity,
_short_core_desc_fuzzy_match,
_same_or_similar_desc,
_wehago_section_identity,
clean,
)
TARGET_START_YEAR = 2025
TARGET_END_YEAR = 2025
GROUP_COLUMNS = (
"start_year",
"end_year",
"status_key",
"signature",
"group_index",
"fiscal_year",
"ledger_date",
"proof_date",
"voucher_no",
"draft_no",
"ledger_row_count",
"voucher_row_count",
"ledger_debit",
"ledger_credit",
"voucher_debit",
"voucher_credit",
"ledger_accounts",
"voucher_accounts",
"ledger_vendors",
"voucher_vendors",
"review_reason",
"search_text",
)
ROW_COLUMNS = (
"start_year",
"end_year",
"status_key",
"signature",
"group_index",
"row_index",
"fiscal_year",
"status_label",
"ledger_date",
"proof_date",
"voucher_no",
"draft_no",
"ledger_account_name",
"voucher_account_name",
"ledger_vendor",
"voucher_vendor",
"ledger_debit",
"ledger_credit",
"voucher_debit",
"voucher_credit",
"ledger_desc",
"voucher_desc",
"review_reason",
"matched_case",
"ledger_row_key",
"voucher_row_key",
"match_identity_key",
)
def _dict(row: sqlite3.Row) -> dict[str, Any]:
return {key: row[key] for key in row.keys()}
def _amount_key(value: Any) -> str:
try:
amount = float(str(value or "0").replace(",", ""))
except Exception:
amount = 0.0
if abs(amount - round(amount)) < 0.0001:
return str(int(round(amount)))
return f"{amount:.2f}".rstrip("0").rstrip(".")
def _parse_amount(value: Any) -> float:
try:
return float(str(value or "0").replace(",", ""))
except Exception:
return 0.0
def _has_ledger_value(row: dict[str, Any]) -> bool:
return bool(clean(row.get("ledger_account_name"))) and (
abs(_parse_amount(row.get("ledger_debit"))) > 0.0001
or abs(_parse_amount(row.get("ledger_credit"))) > 0.0001
)
def _has_voucher_value(row: dict[str, Any]) -> bool:
return bool(clean(row.get("voucher_account_name"))) and (
abs(_parse_amount(row.get("voucher_debit"))) > 0.0001
or abs(_parse_amount(row.get("voucher_credit"))) > 0.0001
)
def _same_side_amount_match(ledger_row: dict[str, Any], voucher_row: dict[str, Any]) -> bool:
return (
abs(_parse_amount(ledger_row.get("ledger_debit")) - _parse_amount(voucher_row.get("voucher_debit"))) < 0.5
and abs(_parse_amount(ledger_row.get("ledger_credit")) - _parse_amount(voucher_row.get("voucher_credit"))) < 0.5
and (
abs(_parse_amount(ledger_row.get("ledger_debit"))) > 0.0001
or abs(_parse_amount(ledger_row.get("ledger_credit"))) > 0.0001
)
)
def _desc_core_match(ledger_row: dict[str, Any], voucher_row: dict[str, Any]) -> bool:
probe = {
"ledger_desc": ledger_row.get("ledger_desc"),
"voucher_desc": voucher_row.get("voucher_desc"),
}
matched = (
_same_or_similar_desc(probe)
or _contained_core_desc_match(ledger_row.get("ledger_desc"), voucher_row.get("voucher_desc"))
or _short_core_desc_fuzzy_match(ledger_row.get("ledger_desc"), voucher_row.get("voucher_desc"))
)
if matched:
return True
stop_words = {
"관련",
"전표",
"처리",
"정산",
"금액",
"비용",
"지급",
"입금",
"출금",
"매입",
"매출",
"급여",
"제경비",
}
def tokens(value: Any) -> set[str]:
found: set[str] = set()
for token in re.split(r"[^0-9A-Za-z가-힣]+", clean(value)):
token = token.strip()
if len(token) < 2 or token in stop_words or re.fullmatch(r"\d+월?", token):
continue
found.add(token)
return found
shared = tokens(ledger_row.get("ledger_desc")) & tokens(voucher_row.get("voucher_desc"))
return len(shared) >= 2 or any(len(token) >= 3 for token in shared)
def _merge_internal_recheck_pairs(group: dict[str, Any]) -> dict[str, Any]:
rows = [dict(row) for row in group.get("rows") or []]
ledger_only = [row for row in rows if _has_ledger_value(row) and not _has_voucher_value(row)]
voucher_only = [row for row in rows if _has_voucher_value(row) and not _has_ledger_value(row)]
if not ledger_only or not voucher_only:
return group
used_ledger: set[int] = set()
used_voucher: set[int] = set()
merged_rows: list[dict[str, Any]] = []
candidates: list[tuple[float, int, int]] = []
for ledger_index, ledger_row in enumerate(ledger_only):
for voucher_index, voucher_row in enumerate(voucher_only):
if not _same_side_amount_match(ledger_row, voucher_row):
continue
if not _account_base_names_compatible(ledger_row.get("ledger_account_name"), voucher_row.get("voucher_account_name")):
continue
if not _desc_core_match(ledger_row, voucher_row):
continue
amount = max(abs(_parse_amount(ledger_row.get("ledger_debit"))), abs(_parse_amount(ledger_row.get("ledger_credit"))))
candidates.append((amount, ledger_index, voucher_index))
for _amount, ledger_index, voucher_index in sorted(candidates, reverse=True):
if ledger_index in used_ledger or voucher_index in used_voucher:
continue
ledger_row = ledger_only[ledger_index]
voucher_row = voucher_only[voucher_index]
merged = dict(ledger_row)
for field in (
"proof_date",
"draft_no",
"voucher_account_name",
"voucher_vendor",
"voucher_debit",
"voucher_credit",
"voucher_desc",
"voucher_row_key",
"match_identity_key",
):
merged[field] = voucher_row.get(field, "")
merged["review_reason"] = "RECHECK_INTERNAL_CORE_MATCH"
merged_rows.append(merged)
used_ledger.add(ledger_index)
used_voucher.add(voucher_index)
if not merged_rows:
return group
remaining_rows: list[dict[str, Any]] = []
ledger_ids = {id(row): index for index, row in enumerate(ledger_only)}
voucher_ids = {id(row): index for index, row in enumerate(voucher_only)}
for row in rows:
if _has_ledger_value(row) and not _has_voucher_value(row):
index = ledger_ids.get(id(row))
if index is not None and index in used_ledger:
continue
if _has_voucher_value(row) and not _has_ledger_value(row):
index = voucher_ids.get(id(row))
if index is not None and index in used_voucher:
continue
remaining_rows.append(row)
return {**group, "rows": merged_rows + remaining_rows}
def _group_manual_review_key(group: dict[str, Any]) -> str:
summary = group.get("summary") or {}
return "|".join(
[
"manual-recheck",
clean(summary.get("fiscal_year")),
clean(summary.get("ledger_date")),
clean(summary.get("voucher_no")),
clean(summary.get("draft_no")),
"",
"",
_amount_key(summary.get("ledger_debit")),
_amount_key(summary.get("ledger_credit")),
_amount_key(summary.get("voucher_debit")),
_amount_key(summary.get("voucher_credit")),
]
)
def _row_change_key(row: dict[str, Any], change_type: str) -> str:
explicit_key = clean(row.get("review_key")) or clean(row.get("match_identity_key"))
if explicit_key:
return f"{change_type}:{explicit_key}"
return "|".join(
[
change_type,
clean(row.get("fiscal_year")),
clean(row.get("ledger_date")),
clean(row.get("voucher_no")),
clean(row.get("draft_no")),
clean(row.get("ledger_account_name")),
clean(row.get("voucher_account_name")),
_amount_key(row.get("ledger_debit")),
_amount_key(row.get("ledger_credit")),
_amount_key(row.get("voucher_debit")),
_amount_key(row.get("voucher_credit")),
clean(row.get("ledger_desc")),
clean(row.get("voucher_desc")),
]
)
def _ensure_change_table(conn: sqlite3.Connection) -> None:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS wehago_recheck_row_changes (
change_key TEXT PRIMARY KEY,
change_type TEXT NOT NULL DEFAULT 'match',
fiscal_year INTEGER,
voucher_no TEXT NOT NULL DEFAULT '',
draft_no TEXT NOT NULL DEFAULT '',
ledger_date TEXT NOT NULL DEFAULT '',
proof_date TEXT NOT NULL DEFAULT '',
ledger_account_name TEXT NOT NULL DEFAULT '',
voucher_account_name TEXT NOT NULL DEFAULT '',
ledger_debit REAL NOT NULL DEFAULT 0,
ledger_credit REAL NOT NULL DEFAULT 0,
voucher_debit REAL NOT NULL DEFAULT 0,
voucher_credit REAL NOT NULL DEFAULT 0,
ledger_desc TEXT NOT NULL DEFAULT '',
voucher_desc TEXT NOT NULL DEFAULT '',
changed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
def _load_manual_change_keys(conn: sqlite3.Connection) -> tuple[set[str], set[str], set[str]]:
_ensure_change_table(conn)
review_keys = {
str(row[0])
for row in conn.execute(
"""
SELECT review_key
FROM wehago_recheck_reviews
WHERE fiscal_year BETWEEN ? AND ?
""",
(TARGET_START_YEAR, TARGET_END_YEAR),
).fetchall()
}
match_change_keys = {
str(row[0])
for row in conn.execute(
"""
SELECT change_key
FROM wehago_recheck_row_changes
WHERE fiscal_year BETWEEN ? AND ?
AND change_type = 'match'
""",
(TARGET_START_YEAR, TARGET_END_YEAR),
).fetchall()
}
split_change_keys = {
str(row[0])
for row in conn.execute(
"""
SELECT change_key
FROM wehago_recheck_row_changes
WHERE fiscal_year BETWEEN ? AND ?
AND change_type = 'split'
""",
(TARGET_START_YEAR, TARGET_END_YEAR),
).fetchall()
}
return review_keys, match_change_keys, split_change_keys
def _snapshot_signature(conn: sqlite3.Connection) -> str:
row = conn.execute(
"""
SELECT snapshot_signature
FROM wehago_snapshot_status
WHERE fiscal_year = ?
AND state = 'ready'
LIMIT 1
""",
(TARGET_START_YEAR,),
).fetchone()
if row is not None and str(row["snapshot_signature"] or ""):
signature = str(row["snapshot_signature"])
exists = conn.execute(
"""
SELECT 1
FROM wehago_compare_export_row_cache
WHERE fiscal_year BETWEEN ? AND ?
AND snapshot_signature = ?
LIMIT 1
""",
(TARGET_START_YEAR, TARGET_END_YEAR, signature),
).fetchone()
if exists:
return signature
row = conn.execute(
"""
SELECT snapshot_signature, COUNT(*) AS row_count, MAX(rowid) AS max_rowid
FROM wehago_compare_export_row_cache
WHERE fiscal_year BETWEEN ? AND ?
GROUP BY snapshot_signature
ORDER BY
CASE WHEN snapshot_signature LIKE 'voucher-summary-v7|recheck-v20%' THEN 0 ELSE 1 END ASC,
row_count DESC,
max_rowid DESC
LIMIT 1
""",
(TARGET_START_YEAR, TARGET_END_YEAR),
).fetchone()
if row is None or not str(row["snapshot_signature"] or ""):
raise RuntimeError("No snapshot/export signature was found.")
return str(row["snapshot_signature"])
def _latest_query_projection_signature(conn: sqlite3.Connection) -> str | None:
row = conn.execute(
"""
SELECT signature, COUNT(DISTINCT status_key) AS status_count, MAX(updated_at) AS updated_at
FROM wehago_compare_query_groups
WHERE start_year = ?
AND end_year = ?
AND signature LIKE ?
AND signature NOT LIKE '%|snapshot-recheck-promote|%'
GROUP BY signature
HAVING status_count >= 5
ORDER BY updated_at DESC
LIMIT 1
""",
(TARGET_START_YEAR, TARGET_END_YEAR, f"{QUERY_PROJECTION_VERSION}|%"),
).fetchone()
return str(row["signature"] or "") if row is not None else None
def _load_groups_from_query_projection(conn: sqlite3.Connection, signature: str) -> dict[str, list[dict[str, Any]]]:
group_rows = conn.execute(
"""
SELECT *
FROM wehago_compare_query_groups
WHERE start_year = ?
AND end_year = ?
AND signature = ?
ORDER BY status_key, group_index
""",
(TARGET_START_YEAR, TARGET_END_YEAR, signature),
).fetchall()
detail_rows = conn.execute(
"""
SELECT *
FROM wehago_compare_query_rows
WHERE start_year = ?
AND end_year = ?
AND signature = ?
ORDER BY status_key, group_index, row_index
""",
(TARGET_START_YEAR, TARGET_END_YEAR, signature),
).fetchall()
rows_by_key: dict[tuple[str, int], list[dict[str, Any]]] = {}
for row in detail_rows:
payload = _dict(row)
rows_by_key.setdefault((str(payload["status_key"]), int(payload["group_index"])), []).append(payload)
groups: dict[str, list[dict[str, Any]]] = {}
for row in group_rows:
summary = _dict(row)
status_key = str(summary["status_key"])
group_index = int(summary["group_index"])
groups.setdefault(status_key, []).append(
{
"summary": summary,
"rows": rows_by_key.get((status_key, group_index), []),
"source_group_index": group_index,
}
)
return groups
def _source_signature(conn: sqlite3.Connection) -> str:
row = conn.execute(
"""
SELECT signature, COUNT(DISTINCT status_key) AS status_count, COUNT(*) AS group_count
FROM wehago_compare_query_groups
WHERE fiscal_year BETWEEN ? AND ?
AND signature NOT LIKE ?
GROUP BY signature
HAVING status_count >= 5
ORDER BY group_count DESC
LIMIT 1
""",
(TARGET_START_YEAR, TARGET_END_YEAR, f"{QUERY_PROJECTION_VERSION}|%"),
).fetchone()
if row is None:
raise RuntimeError("No source query projection with voucher status groups was found.")
return str(row["signature"])
def _matched_source_signature(conn: sqlite3.Connection) -> str:
row = conn.execute(
"""
SELECT signature, COUNT(*) AS group_count
FROM wehago_compare_query_groups
WHERE fiscal_year BETWEEN ? AND ?
AND status_key = 'voucher_matched'
AND signature NOT LIKE ?
GROUP BY signature
ORDER BY group_count DESC
LIMIT 1
""",
(TARGET_START_YEAR, TARGET_END_YEAR, f"{QUERY_PROJECTION_VERSION}|%"),
).fetchone()
if row is None:
raise RuntimeError("No matched source query projection was found.")
return str(row["signature"])
def _load_groups(
conn: sqlite3.Connection,
signature: str,
statuses: tuple[str, ...] | None = None,
) -> dict[str, list[dict[str, Any]]]:
status_filter = ""
params: list[Any] = [signature, TARGET_START_YEAR, TARGET_END_YEAR]
if statuses:
status_filter = f" AND status_key IN ({', '.join('?' for _ in statuses)})"
params.extend(statuses)
groups: dict[str, list[dict[str, Any]]] = {}
group_rows = conn.execute(
f"""
SELECT *
FROM wehago_compare_query_groups
WHERE signature = ?
AND fiscal_year BETWEEN ? AND ?
{status_filter}
ORDER BY status_key, group_index
""",
params,
).fetchall()
detail_rows = conn.execute(
f"""
SELECT *
FROM wehago_compare_query_rows
WHERE signature = ?
AND fiscal_year BETWEEN ? AND ?
{status_filter}
ORDER BY status_key, group_index, row_index
""",
params,
).fetchall()
rows_by_key: dict[tuple[str, int], list[dict[str, Any]]] = {}
for row in detail_rows:
item = _dict(row)
rows_by_key.setdefault((str(item["status_key"]), int(item["group_index"])), []).append(item)
for row in group_rows:
summary = _dict(row)
status_key = str(summary["status_key"])
group_index = int(summary["group_index"])
groups.setdefault(status_key, []).append(
{
"summary": summary,
"rows": rows_by_key.get((status_key, group_index), []),
"source_group_index": group_index,
}
)
return groups
def _load_groups_from_export_cache(conn: sqlite3.Connection, snapshot_signature: str) -> dict[str, list[dict[str, Any]]]:
export_rows = conn.execute(
"""
SELECT *
FROM wehago_compare_export_row_cache
WHERE fiscal_year BETWEEN ? AND ?
AND snapshot_signature = ?
ORDER BY status_key, group_sort, row_sort
""",
(TARGET_START_YEAR, TARGET_END_YEAR, snapshot_signature),
).fetchall()
grouped_rows: dict[tuple[str, int], list[dict[str, Any]]] = {}
for row in export_rows:
item = _dict(row)
grouped_rows.setdefault((str(item["status_key"]), int(item["group_sort"])), []).append(item)
groups: dict[str, list[dict[str, Any]]] = {}
for (status_key, group_index), rows in grouped_rows.items():
first = rows[0]
detail_rows: list[dict[str, Any]] = []
ledger_accounts: list[str] = []
voucher_accounts: list[str] = []
ledger_vendors: list[str] = []
voucher_vendors: list[str] = []
def append_unique(target: list[str], value: Any) -> None:
text_value = clean(value)
if text_value and text_value not in target:
target.append(text_value)
ledger_row_count = 0
voucher_row_count = 0
for row_index, row in enumerate(rows):
detail = {
"fiscal_year": int(row.get("fiscal_year") or 0),
"status_label": "Matched" if status_key in {"voucher_matched", "erp_voucher_matched"} else "Recheck" if status_key == "voucher_recheck" else "Unmatched",
"ledger_date": clean(row.get("ledger_date")),
"proof_date": "",
"voucher_no": clean(row.get("voucher_no")),
"draft_no": clean(row.get("draft_no")),
"ledger_account_name": clean(row.get("ledger_account_name")),
"voucher_account_name": clean(row.get("voucher_account_name")),
"ledger_vendor": clean(row.get("ledger_vendor")),
"voucher_vendor": clean(row.get("voucher_vendor")),
"ledger_debit": float(row.get("ledger_debit") or 0),
"ledger_credit": float(row.get("ledger_credit") or 0),
"voucher_debit": float(row.get("voucher_debit") or 0),
"voucher_credit": float(row.get("voucher_credit") or 0),
"ledger_desc": clean(row.get("ledger_desc")),
"voucher_desc": clean(row.get("voucher_desc")),
"review_reason": "SNAPSHOT_EXPORT_CACHE",
"matched_case": "",
"ledger_row_key": "",
"voucher_row_key": "",
"match_identity_key": "",
"row_index": row_index,
}
if detail["ledger_account_name"] or detail["ledger_desc"]:
ledger_row_count += 1
if detail["voucher_account_name"] or detail["voucher_desc"]:
voucher_row_count += 1
append_unique(ledger_accounts, detail["ledger_account_name"])
append_unique(voucher_accounts, detail["voucher_account_name"])
append_unique(ledger_vendors, detail["ledger_vendor"])
append_unique(voucher_vendors, detail["voucher_vendor"])
detail_rows.append(detail)
summary = {
"fiscal_year": int(first.get("fiscal_year") or 0),
"status_label": detail_rows[0]["status_label"] if detail_rows else "",
"ledger_date": clean(first.get("ledger_date")),
"proof_date": "",
"voucher_no": clean(first.get("group_voucher_no")) or clean(first.get("voucher_no")),
"draft_no": clean(first.get("group_draft_no")) or clean(first.get("draft_no")),
"ledger_row_count": ledger_row_count,
"voucher_row_count": voucher_row_count,
"ledger_debit": float(first.get("group_ledger_debit") or 0),
"ledger_credit": float(first.get("group_ledger_credit") or 0),
"voucher_debit": float(first.get("group_voucher_debit") or 0),
"voucher_credit": float(first.get("group_voucher_credit") or 0),
"ledger_accounts": clean(first.get("group_ledger_accounts")) or ", ".join(ledger_accounts),
"voucher_accounts": clean(first.get("group_voucher_accounts")) or ", ".join(voucher_accounts),
"ledger_vendors": clean(first.get("group_ledger_vendors")) or ", ".join(ledger_vendors),
"voucher_vendors": clean(first.get("group_voucher_vendors")) or ", ".join(voucher_vendors),
"review_reason": "SNAPSHOT_EXPORT_CACHE",
"search_text": "",
}
summary["search_text"] = _search_text(summary, detail_rows)
groups.setdefault(status_key, []).append(
{
"summary": summary,
"rows": detail_rows,
"source_group_index": group_index,
}
)
return groups
def _seed_used_identities(groups: dict[str, list[dict[str, Any]]]) -> tuple[dict[str, int], dict[str, int]]:
used_wehago: dict[str, int] = {}
used_erp: dict[str, int] = {}
for group in groups.get("voucher_matched", []):
identity = _wehago_section_identity(group)
if identity:
used_wehago[identity] = used_wehago.get(identity, 0) + 1
for group in groups.get("erp_voucher_matched", []):
identity = _erp_section_identity(group)
if identity:
used_erp[identity] = used_erp.get(identity, 0) + 1
return used_wehago, used_erp
def _group_has_manual_match(
group: dict[str, Any],
review_keys: set[str],
match_change_keys: set[str],
) -> bool:
if _group_manual_review_key(group) in review_keys:
return True
for row in group.get("rows") or []:
if _row_change_key(row, "match") in match_change_keys:
return True
return False
def _select_promotions(
groups: dict[str, list[dict[str, Any]]],
review_keys: set[str],
match_change_keys: set[str],
) -> set[int]:
used_wehago, _used_erp = _seed_used_identities(groups)
candidates: list[dict[str, Any]] = []
for group in groups.get("voucher_recheck", []):
manual_match = _group_has_manual_match(group, review_keys, match_change_keys)
if not manual_match and not _is_obvious_recheck_group(group):
continue
wehago_identity = _wehago_section_identity(group)
erp_identity = _erp_section_identity(group)
if not wehago_identity or not erp_identity:
continue
candidates.append(
{
"group": group,
"wehago_identity": wehago_identity,
"erp_identity": erp_identity,
"wehago_capacity": _section_vat_exception_capacity(group, side="wehago"),
"rank": (1 if manual_match else 0, *_recheck_group_rank_key(group)),
"manual_match": manual_match,
}
)
candidates.sort(key=lambda item: item["rank"], reverse=True)
promoted: set[int] = set()
for candidate in candidates:
wehago_identity = candidate["wehago_identity"]
erp_identity = candidate["erp_identity"]
wehago_capacity = int(candidate["wehago_capacity"] or 1)
if used_wehago.get(wehago_identity, 0) >= wehago_capacity:
continue
promoted.add(id(candidate["group"]))
used_wehago[wehago_identity] = used_wehago.get(wehago_identity, 0) + 1
return promoted
def _search_text(summary: dict[str, Any], rows: list[dict[str, Any]]) -> str:
parts = [
summary.get("voucher_no"),
summary.get("draft_no"),
summary.get("ledger_accounts"),
summary.get("voucher_accounts"),
summary.get("ledger_vendors"),
summary.get("voucher_vendors"),
summary.get("review_reason"),
]
for row in rows[:20]:
parts.extend([row.get("ledger_desc"), row.get("voucher_desc")])
return " ".join(clean(part) for part in parts if clean(part))
def _insert_group(
conn: sqlite3.Connection,
*,
signature: str,
status_key: str,
group_index: int,
group: dict[str, Any],
promoted: bool = False,
split_change_keys: set[str] | None = None,
omit_split_rows: bool = False,
) -> None:
summary = dict(group["summary"])
rows = [dict(row) for row in group.get("rows") or []]
split_change_keys = split_change_keys or set()
if promoted and omit_split_rows:
rows = [row for row in rows if _row_change_key(row, "split") not in split_change_keys]
summary.update(
{
"start_year": TARGET_START_YEAR,
"end_year": TARGET_END_YEAR,
"status_key": status_key,
"signature": signature,
"group_index": group_index,
}
)
if promoted:
summary["review_reason"] = clean(summary.get("review_reason")) or "RECHECK_PROMOTED_BY_USER_RULE"
summary["search_text"] = _search_text(summary, rows)
values = [summary.get(column, "") for column in GROUP_COLUMNS]
conn.execute(
f"""
INSERT INTO wehago_compare_query_groups ({', '.join(GROUP_COLUMNS)}, created_at, updated_at)
VALUES ({', '.join('?' for _ in GROUP_COLUMNS)}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""",
values,
)
for row_index, row in enumerate(rows):
split_row = promoted and _row_change_key(row, "split") in split_change_keys
row.update(
{
"start_year": TARGET_START_YEAR,
"end_year": TARGET_END_YEAR,
"status_key": status_key,
"signature": signature,
"group_index": group_index,
"row_index": row_index,
}
)
if promoted:
if split_row:
row["status_label"] = "Unmatched"
row["review_reason"] = "MANUAL_SPLIT_FROM_RECHECK"
row["proof_date"] = ""
row["draft_no"] = ""
row["voucher_account_name"] = ""
row["voucher_vendor"] = ""
row["voucher_debit"] = 0
row["voucher_credit"] = 0
row["voucher_desc"] = ""
row["voucher_row_key"] = ""
row["match_identity_key"] = ""
else:
row["status_label"] = "Matched"
row["review_reason"] = clean(row.get("review_reason")) or "RECHECK_PROMOTED_BY_USER_RULE"
values = [row.get(column, "") for column in ROW_COLUMNS]
conn.execute(
f"""
INSERT INTO wehago_compare_query_rows ({', '.join(ROW_COLUMNS)}, created_at, updated_at)
VALUES ({', '.join('?' for _ in ROW_COLUMNS)}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""",
values,
)
def main() -> None:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
source_signature = _latest_query_projection_signature(conn)
source_kind = "query_projection"
if not source_signature:
source_signature = _snapshot_signature(conn)
source_kind = "export_cache"
target_signature = f"{QUERY_PROJECTION_VERSION}|snapshot-recheck-promote|{source_signature}"
groups = (
_load_groups_from_query_projection(conn, source_signature)
if source_kind == "query_projection"
else _load_groups_from_export_cache(conn, source_signature)
)
groups["voucher_recheck"] = [
_merge_internal_recheck_pairs(group)
for group in groups.get("voucher_recheck", [])
]
review_keys, match_change_keys, split_change_keys = _load_manual_change_keys(conn)
promoted_ids = _select_promotions(groups, review_keys, match_change_keys)
conn.execute("BEGIN")
try:
conn.execute(
"DELETE FROM wehago_compare_query_rows WHERE start_year = ? AND end_year = ? AND signature = ?",
(TARGET_START_YEAR, TARGET_END_YEAR, target_signature),
)
conn.execute(
"DELETE FROM wehago_compare_query_groups WHERE start_year = ? AND end_year = ? AND signature = ?",
(TARGET_START_YEAR, TARGET_END_YEAR, target_signature),
)
max_group_index = {
status_key: max([int(group["source_group_index"]) for group in status_groups] or [0])
for status_key, status_groups in groups.items()
}
for status_key, status_groups in groups.items():
for group in status_groups:
if status_key == "voucher_recheck" and id(group) in promoted_ids:
continue
_insert_group(
conn,
signature=target_signature,
status_key=status_key,
group_index=int(group["source_group_index"]),
group=group,
)
for group in groups.get("voucher_recheck", []):
if id(group) not in promoted_ids:
continue
max_group_index["voucher_matched"] = max_group_index.get("voucher_matched", 0) + 1
_insert_group(
conn,
signature=target_signature,
status_key="voucher_matched",
group_index=max_group_index["voucher_matched"],
group=group,
promoted=True,
split_change_keys=split_change_keys,
)
max_group_index["erp_voucher_matched"] = max_group_index.get("erp_voucher_matched", 0) + 1
_insert_group(
conn,
signature=target_signature,
status_key="erp_voucher_matched",
group_index=max_group_index["erp_voucher_matched"],
group=group,
promoted=True,
split_change_keys=split_change_keys,
omit_split_rows=True,
)
conn.execute(
"""
INSERT INTO wehago_action_history (action_type, payload_json, created_at)
VALUES ('auto_recheck_promote', ?, CURRENT_TIMESTAMP)
""",
(
f'{{"count": {len(promoted_ids)}, "start_year": {TARGET_START_YEAR}, '
f'"manual_review_keys": {len(review_keys)}, "match_change_keys": {len(match_change_keys)}, '
f'"split_change_keys": {len(split_change_keys)}, '
f'"source_kind": "{source_kind}", '
f'"end_year": {TARGET_END_YEAR}, "signature": "{target_signature}", '
f'"created_at": "{datetime.now().isoformat(timespec="seconds")}"}}',
),
)
conn.commit()
except Exception:
conn.rollback()
raise
counts = conn.execute(
"""
SELECT status_key, COUNT(*)
FROM wehago_compare_query_groups
WHERE signature = ?
AND fiscal_year BETWEEN ? AND ?
GROUP BY status_key
ORDER BY status_key
""",
(target_signature, TARGET_START_YEAR, TARGET_END_YEAR),
).fetchall()
print(
{
"source_signature": source_signature,
"source_kind": source_kind,
"target_signature": target_signature,
"promoted": len(promoted_ids),
"counts": {str(row[0]): int(row[1]) for row in counts},
}
)
if __name__ == "__main__":
main()