HEL-176: 公司端转账往来明细筛选与导出
新增 events 组合筛选 + keyset 分页,以及仅导出已确认明细的 CSV(含审计); 与 B-44 ledger /events 按查询参数分发,不改库表与确认语义。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
cad12b3d28
commit
40f7a91a0f
@@ -155,10 +155,13 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
self._handle_admin_intercompany_evidence(int(admin_evidence.group(1)))
|
||||
return
|
||||
|
||||
# Company transfer-summary (HEL-169/HEL-175, eligible_intercompany_events)
|
||||
# Company transfer-summary (HEL-169/HEL-175/HEL-176)
|
||||
if path == "/api/company/intercompany/summary":
|
||||
self._handle_company_intercompany_summary(query)
|
||||
return
|
||||
if path == "/api/company/intercompany/export.csv":
|
||||
self._handle_company_intercompany_export_csv(query)
|
||||
return
|
||||
|
||||
# B-44 intercompany positions (company, own-company scope)
|
||||
if path == "/api/company/intercompany/balances":
|
||||
@@ -169,7 +172,12 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
self._handle_company_intercompany_pair(int(company_pair.group(1)), query)
|
||||
return
|
||||
if path == "/api/company/intercompany/events":
|
||||
self._handle_company_intercompany_events(query)
|
||||
# HEL-176 transfer-detail list shares this path with B-44 ledger
|
||||
# events; dispatch by distinctive query params (to/direction/…).
|
||||
if company_transfers.is_transfer_summary_events_query(query):
|
||||
self._handle_company_transfer_summary_events(query)
|
||||
else:
|
||||
self._handle_company_intercompany_events(query)
|
||||
return
|
||||
if path == "/api/company/manual-records":
|
||||
self._handle_company_manual_records(query)
|
||||
@@ -2889,6 +2897,111 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def _reject_forged_company_id(self, query: dict[str, list[str]]) -> bool:
|
||||
"""Return True when the handler already sent a 400 for company_id."""
|
||||
if (query.get("company_id") or [None])[0] is not None:
|
||||
self._send_json(
|
||||
400,
|
||||
{"status": "error", "message": "不允许传入 company_id 参数。"},
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_company_transfer_summary_events(
|
||||
self, query: dict[str, list[str]]
|
||||
) -> None:
|
||||
"""HEL-176 filtered transfer-detail list (keyset pagination)."""
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
user, company_id = self._company_intercompany_scope(connection)
|
||||
if company_id is None:
|
||||
return
|
||||
if self._reject_forged_company_id(query):
|
||||
return
|
||||
try:
|
||||
payload = company_transfers.company_intercompany_events(
|
||||
connection,
|
||||
company_id=int(company_id),
|
||||
from_=(query.get("from") or [None])[0],
|
||||
to=(query.get("to") or [None])[0],
|
||||
counterparty_id=(query.get("counterparty_id") or [None])[0],
|
||||
direction=(query.get("direction") or [None])[0],
|
||||
state=(query.get("state") or [None])[0],
|
||||
limit=(query.get("limit") or [None])[0],
|
||||
cursor=(query.get("cursor") or [None])[0],
|
||||
)
|
||||
except company_transfers.TransferSummaryInputError as exc:
|
||||
self._send_json(400, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
self._send_json(200, {"status": "ok", **payload})
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def _handle_company_intercompany_export_csv(
|
||||
self, query: dict[str, list[str]]
|
||||
) -> None:
|
||||
"""Confirmed-only CSV export; session company scope + audit (HEL-176)."""
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
user, company_id = self._company_intercompany_scope(connection)
|
||||
if company_id is None:
|
||||
return
|
||||
if self._reject_forged_company_id(query):
|
||||
return
|
||||
# Pending must never leave via export, even if a client sends state=.
|
||||
if (query.get("state") or [None])[0] not in (None, "", "confirmed"):
|
||||
self._send_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"message": "导出仅支持已确认明细,不允许导出待确认。",
|
||||
},
|
||||
)
|
||||
return
|
||||
try:
|
||||
items, meta = company_transfers.company_intercompany_export_rows(
|
||||
connection,
|
||||
company_id=int(company_id),
|
||||
from_=(query.get("from") or [None])[0],
|
||||
to=(query.get("to") or [None])[0],
|
||||
counterparty_id=(query.get("counterparty_id") or [None])[0],
|
||||
direction=(query.get("direction") or [None])[0],
|
||||
)
|
||||
except company_transfers.TransferSummaryInputError as exc:
|
||||
self._send_json(400, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
content = company_transfers.render_intercompany_export_csv(items)
|
||||
detail_parts = [
|
||||
f"rows:{meta['row_count']}",
|
||||
f"from:{meta['start']}",
|
||||
f"to:{meta['end']}",
|
||||
"state:confirmed",
|
||||
]
|
||||
if meta["counterparty_id"] is not None:
|
||||
detail_parts.append(f"counterparty_id:{meta['counterparty_id']}")
|
||||
if meta["direction"] is not None:
|
||||
detail_parts.append(f"direction:{meta['direction']}")
|
||||
auth.audit(
|
||||
connection,
|
||||
"export_intercompany_csv",
|
||||
actor=user,
|
||||
target=f"company:{company_id}",
|
||||
detail=";".join(detail_parts),
|
||||
ip=self._client_ip,
|
||||
)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/csv; charset=utf-8")
|
||||
self.send_header(
|
||||
"Content-Disposition",
|
||||
'attachment; filename="intercompany-export.csv"',
|
||||
)
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def _handle_company_intercompany_balances(self, query: dict[str, list[str]]) -> None:
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user