"""Server-side flow listing and export from confirmed source rows. Lists and exports only cashier-confirmed worksheets. Match state is derived from current transfer decisions when a source row is claimed; otherwise the row is labelled 未归集. Company users only see their own company. """ from __future__ import annotations from decimal import Decimal, InvalidOperation import sqlite3 def _dec(value: object) -> Decimal: try: return Decimal(str(value or "0")) except (InvalidOperation, TypeError): return Decimal("0") def _direction(income: object, expense: object) -> tuple[str, Decimal]: income_d = _dec(income) expense_d = _dec(expense) if expense_d > 0 and income_d <= 0: return "付", expense_d return "收", income_d if income_d > 0 else expense_d def _status_label(classification: str | None, pairing: str | None, locked: int | None) -> tuple[str, str]: if classification in ("unresolved", "needs_review"): return "单边", "danger" if classification == "intercompany" and pairing == "paired": return "已归集", "success" if classification == "intercompany" and locked: return "已归集", "success" if classification == "intercompany": return "待确认", "warn" if classification == "same_company": return "同公司调拨", "muted" if classification == "external": return "未归集 · 外部", "muted" if classification: return "未归集", "muted" return "未归集", "muted" def list_flows( connection: sqlite3.Connection, *, company_id: int | None = None, bank: str | None = None, account: str | None = None, start: str | None = None, end: str | None = None, keyword: str | None = None, limit: int = 200, offset: int = 0, ) -> dict[str, object]: clauses = ["rv.review_status = 'confirmed'"] params: list[object] = [] if company_id is not None: clauses.append("b.company_id = ?") params.append(int(company_id)) if start: clauses.append("date(r.transaction_at) >= date(?)") params.append(start) if end: clauses.append("date(r.transaction_at) <= date(?)") params.append(end) if account: clauses.append("r.own_account LIKE ?") params.append(f"%{account}%") if bank: clauses.append("(COALESCE(ba.bank_name, '') LIKE ? OR r.own_name LIKE ?)") params.extend([f"%{bank}%", f"%{bank}%"]) if keyword: like = f"%{keyword}%" clauses.append( "(r.counterparty_name LIKE ? OR r.summary LIKE ? OR r.reference LIKE ? " "OR r.purpose LIKE ? OR c.name LIKE ?)" ) params.extend([like, like, like, like, like]) where = " AND ".join(clauses) limit = max(1, min(int(limit), 500)) offset = max(0, int(offset)) count_row = connection.execute( f""" SELECT COUNT(*) AS n FROM source_rows r JOIN sheet_batches s ON s.id = r.sheet_batch_id JOIN import_batches b ON b.id = s.import_batch_id JOIN sheet_reviews rv ON rv.sheet_batch_id = s.id JOIN companies c ON c.id = b.company_id LEFT JOIN bank_accounts ba ON ba.account_number = r.own_account WHERE {where} """, params, ).fetchone() rows = connection.execute( f""" SELECT r.id, r.transaction_at, r.income, r.expense, r.balance, r.own_account, r.own_name, r.counterparty_account, r.counterparty_name, r.counterparty_bank, r.summary, r.purpose, r.reference, r.currency, b.id AS batch_id, b.company_id, c.name AS company_name, COALESCE(ba.bank_name, '') AS bank_name, s.sheet_name, r.source_row, d.classification, d.pairing, d.locked, d.effective_at FROM source_rows r JOIN sheet_batches s ON s.id = r.sheet_batch_id JOIN import_batches b ON b.id = s.import_batch_id JOIN sheet_reviews rv ON rv.sheet_batch_id = s.id JOIN companies c ON c.id = b.company_id LEFT JOIN bank_accounts ba ON ba.account_number = r.own_account LEFT JOIN transfer_observation_claims toc ON toc.source_row_id = r.id LEFT JOIN transfer_match_decisions d ON d.id = toc.decision_id WHERE {where} ORDER BY r.transaction_at DESC, r.id DESC LIMIT ? OFFSET ? """, [*params, limit, offset], ).fetchall() items = [] inflow = Decimal("0") outflow = Decimal("0") for row in rows: direction, amount = _direction(row["income"], row["expense"]) if direction == "收": inflow += amount else: outflow += amount status, status_kind = _status_label( row["classification"], row["pairing"], row["locked"] ) tail = str(row["own_account"] or "")[-4:] items.append( { "id": int(row["id"]), "date": str(row["transaction_at"] or "")[:10], "time": str(row["transaction_at"] or ""), "company_id": int(row["company_id"]), "company": row["company_name"], "bank": row["bank_name"] or "", "account": row["own_account"], "account_label": ( f"{row['bank_name']} · 尾号 {tail}" if row["bank_name"] and tail else (row["own_account"] or "—") ), "own_name": row["own_name"], "direction": direction, "peer": row["counterparty_name"] or "—", "peer_account": row["counterparty_account"] or "—", "peer_bank": row["counterparty_bank"] or "—", "summary": row["summary"] or row["purpose"] or "—", "serial": row["reference"] or "—", "status": status, "status_kind": status_kind, "amount": str(amount), "balance": str(row["balance"] or ""), "currency": row["currency"] or "CNY", "batch_id": int(row["batch_id"]), "batch": f"IMP-{int(row['batch_id']):06d}", "locator": f"{row['sheet_name']}!R{row['source_row']}", "year_month": str(row["transaction_at"] or "")[:7], } ) return { "items": items, "total": int(count_row["n"]), "inflow": str(inflow), "outflow": str(outflow), "limit": limit, "offset": offset, }