锁账后写保护与闭期补录留痕;重开须审批并按版本链再结;列表/导出改走真实 API。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
173 lines
6.4 KiB
Python
173 lines
6.4 KiB
Python
"""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,
|
|
}
|