From 1fe073066e086cc7f76ee7092b300d3b9b8c1bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=85=BE=E8=AE=AFWorkBuddy?= Date: Thu, 20 Aug 2026 00:37:27 +0800 Subject: [PATCH] =?UTF-8?q?B-44:=20=E8=BF=94=E4=BF=AE=E2=80=94=E2=80=94?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E7=9B=AE=E5=BD=95=E8=A7=86=E8=A7=89=E3=80=81?= =?UTF-8?q?=E5=85=AB=E5=88=97=E4=BA=8B=E4=BB=B6=E8=A1=A8=E3=80=81=E6=8A=BD?= =?UTF-8?q?=E5=B1=89=E9=94=AE=E7=9B=98=E4=B8=8E=E5=AE=A1=E6=A0=B8=E4=B8=89?= =?UTF-8?q?=E9=A1=B9=E5=86=B3=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- server.py | 32 +- src/bank_importer/manual_records.py | 7 +- src/bank_importer/positions.py | 105 ++++- src/bank_importer/subjects.py | 90 ++++ tests/b44_ui_check.js | 45 ++ tests/test_b44_frontend.py | 94 ++++ tests/test_positions.py | 66 +++ tests/test_positions_api.py | 39 ++ web/admin.html | 10 +- web/app.js | 693 +++++++++++++++++++--------- web/company.html | 2 +- web/styles.css | 101 +++- 12 files changed, 1018 insertions(+), 266 deletions(-) create mode 100644 tests/b44_ui_check.js create mode 100644 tests/test_b44_frontend.py diff --git a/server.py b/server.py index 3ad02b8..71110e9 100644 --- a/server.py +++ b/server.py @@ -2305,16 +2305,28 @@ class AppHandler(SimpleHTTPRequestHandler): ) return try: - payload = subjects.confirm_subject( - connection, - event_id, - perspective_company_id=perspective_company_id, - subject_code=str(data.get("subject_code") or ""), - reason=str(data.get("reason") or ""), - expected_revision=expected_revision, - request_key=str(data.get("request_key") or "") or None, - actor=user, - ) + action = str(data.get("action") or "confirm") + if action in ("return", "exception"): + payload = subjects.park_subject( + connection, + event_id, + disposition=action, + reason=str(data.get("reason") or ""), + expected_revision=expected_revision, + request_key=str(data.get("request_key") or "") or None, + actor=user, + ) + else: + payload = subjects.confirm_subject( + connection, + event_id, + perspective_company_id=perspective_company_id, + subject_code=str(data.get("subject_code") or ""), + reason=str(data.get("reason") or ""), + expected_revision=expected_revision, + request_key=str(data.get("request_key") or "") or None, + actor=user, + ) except subjects.SubjectConflictError as exc: self._send_json(409, {"status": "error", "message": str(exc)}) return diff --git a/src/bank_importer/manual_records.py b/src/bank_importer/manual_records.py index f8eaca4..bb81059 100644 --- a/src/bank_importer/manual_records.py +++ b/src/bank_importer/manual_records.py @@ -683,12 +683,14 @@ def list_records( SELECT m.*, d.id AS decision_id, d.state AS state, d.revision AS decision_revision, d.action AS action, d.reason AS decision_reason, d.actor_username AS decision_actor, d.created_at AS decision_at, - c.name AS company_name, cc.name AS counterparty_company_name + c.name AS company_name, cc.name AS counterparty_company_name, + u.username AS submitted_by_username FROM manual_records m JOIN current_manual_record_decisions c ON c.record_id = m.id JOIN manual_record_decisions d ON d.id = c.decision_id LEFT JOIN companies c ON c.id = m.company_id LEFT JOIN companies cc ON cc.id = m.counterparty_company_id + LEFT JOIN users u ON u.id = m.submitted_by {where} ORDER BY m.id DESC LIMIT ? @@ -752,6 +754,7 @@ def _record_payload(connection: sqlite3.Connection, record_id: int, *, idempoten def _row_payload(connection: sqlite3.Connection, row: sqlite3.Row) -> dict[str, object]: + evidence = json.loads(row["evidence_json"] or "{}") if row["evidence_json"] else {} return { "id": row["id"], "company_id": row["company_id"], @@ -772,6 +775,8 @@ def _row_payload(connection: sqlite3.Connection, row: sqlite3.Row) -> dict[str, "request_key": row["request_key"], "supersedes_record_id": row["supersedes_record_id"], "submitted_by": row["submitted_by"], + "submitted_by_username": row["submitted_by_username"] if "submitted_by_username" in row.keys() else None, + "attachment_name": evidence.get("attachment_name"), "created_at": row["created_at"], "state": row["state"], "decision_id": row["decision_id"], diff --git a/src/bank_importer/positions.py b/src/bank_importer/positions.py index 3016a3e..9c484b9 100644 --- a/src/bank_importer/positions.py +++ b/src/bank_importer/positions.py @@ -832,9 +832,106 @@ def event_payload( if int(event["payer_company_id"]) == int(viewer_company_id) else event["payer_company_name"] ) + item.update(_event_line_display(connection, event, viewer_company_id)) return item +_REPAY_MARKERS = ("还款", "归还借款", "归还往来款") + + +def bank_short_name(name: str | None) -> str: + text = str(name or "").strip() + if text.startswith("中国"): + text = text[2:] + if text.endswith("银行"): + text = text[:-2] + return text or "银行" + + +def _account_chip(visibility: str, bank_name: str | None, account: str | None) -> dict[str, object]: + if visibility == "missing": + return {"visibility": "missing", "label": None} + if visibility == "masked": + return {"visibility": "masked", "label": "按对方授权不可见"} + number = str(account or "") + tail = number[-4:] if number else "" + short = bank_short_name(bank_name) + label = f"{short} {tail}".strip() if tail else short + return {"visibility": "visible", "label": label} + + +def _event_line_display( + connection: sqlite3.Connection, + event: sqlite3.Row, + viewer_company_id: int | None, +) -> dict[str, object]: + """Account chips, summary and repayment flag for the event table.""" + missing = _account_chip("missing", None, None) + payer_chip, payee_chip = missing, missing + summary = None + texts: list[str] = [] + sides = _event_source_sides(connection, event["ledger_event_id"]) + for side in sides: + company_id = side["company_id"] + own = viewer_company_id is None or int(company_id) == int(viewer_company_id) + visibility = "visible" if own else "masked" + chip = _account_chip(visibility, side.get("bank_name"), side.get("own_account")) + if int(company_id) == int(event["payer_company_id"]): + payer_chip = chip + elif int(company_id) == int(event["payee_company_id"]): + payee_chip = chip + if own or viewer_company_id is None: + if side.get("summary"): + texts.append(str(side["summary"])) + if side.get("reason"): + texts.append(str(side["reason"])) + if texts: + summary = texts[0] + blob = " ".join(texts) + is_repayment = any(marker in blob for marker in _REPAY_MARKERS) + return { + "payer_account": payer_chip, + "payee_account": payee_chip, + "summary": summary, + "is_repayment": is_repayment, + } + + +def _event_source_sides( + connection: sqlite3.Connection, ledger_event_id: int +) -> list[dict[str, object]]: + rows = connection.execute( + """ + SELECT b.company_id AS company_id, s.bank_name AS bank_name, + r.own_account AS own_account, r.summary AS summary, + r.purpose AS purpose, NULL AS reason + FROM ledger_event_bank_sources bs + JOIN current_transfer_decisions cur ON cur.event_id = bs.bank_event_id + JOIN transfer_decision_observations o ON o.decision_id = cur.decision_id + JOIN source_rows r ON r.id = o.source_row_id + JOIN sheet_batches s ON s.id = r.sheet_batch_id + JOIN import_batches b ON b.id = s.import_batch_id + WHERE bs.ledger_event_id = ? + """, + (ledger_event_id,), + ).fetchall() + if rows: + return [_row(row) for row in rows] + manuals = connection.execute( + """ + SELECT m.company_id AS company_id, ba.bank_name AS bank_name, + ba.account_number AS own_account, m.summary AS summary, + NULL AS purpose, m.reason AS reason + FROM ledger_event_manual_sources ms + JOIN manual_records m ON m.id = ms.manual_record_id + LEFT JOIN bank_accounts ba ON ba.id = m.bank_account_id + WHERE ms.ledger_event_id = ? + """, + (ledger_event_id,), + ).fetchall() + return [_row(row) for row in manuals] + + _DETAIL_SELECT = """ SELECT p.ledger_event_id, p.id AS ledger_revision_id, p.effective_at, p.amount, p.amount_scale, p.currency, p.payer_company_id, p.payee_company_id, @@ -939,7 +1036,7 @@ def subject_review_queue( SELECT r.ledger_event_id, r.effective_at, r.amount, r.currency, r.payer_company_id, r.payee_company_id, cpayer.name AS payer_company_name, cpayee.name AS payee_company_name, - r.id AS revision_id + r.id AS revision_id, r.evidence_json AS evidence_json FROM current_ledger_event_revisions cur JOIN ledger_event_revisions r ON r.id = cur.revision_id JOIN companies cpayer ON cpayer.id = r.payer_company_id @@ -957,6 +1054,9 @@ def subject_review_queue( cursor_date, cursor_id = None, None filtered = [] for row in rows: + evidence = json.loads(row["evidence_json"] or "{}") if row["evidence_json"] else {} + if evidence.get("admin_disposition") == "exception": + continue if cursor_date is not None: if row["effective_at"] > cursor_date or ( row["effective_at"] == cursor_date and row["ledger_event_id"] >= cursor_id @@ -982,6 +1082,7 @@ def subject_review_queue( """, (row["ledger_event_id"],), ).fetchall() + display = _event_line_display(connection, row, None) items.append( { "ledger_event_id": row["ledger_event_id"], @@ -993,6 +1094,8 @@ def subject_review_queue( "payer_company_name": row["payer_company_name"], "payee_company_id": row["payee_company_id"], "payee_company_name": row["payee_company_name"], + "summary": display.get("summary"), + "is_repayment": display.get("is_repayment"), "suggestions": [ { "suggested_perspective_company_id": sug["suggested_perspective_company_id"], diff --git a/src/bank_importer/subjects.py b/src/bank_importer/subjects.py index 2312af8..b891331 100644 --- a/src/bank_importer/subjects.py +++ b/src/bank_importer/subjects.py @@ -281,6 +281,96 @@ def confirm_subject( return _revision_payload(connection, row) +def park_subject( + connection: sqlite3.Connection, + ledger_event_id: int, + *, + disposition: str, + reason: str, + expected_revision: int | None, + request_key: str | None, + actor: sqlite3.Row, +) -> dict[str, object]: + """Record 退回/转异常 without confirming a statutory subject. + + The event stays ``pending_subject`` so it never enters confirmed balances. + ``exception`` is hidden from the active review queue; ``return`` remains + visible so the company can supplement materials. + """ + from .ledger_events import append_revision, current_revision + + if disposition not in ("return", "exception"): + raise SubjectInputError("科目处理只能是退回或转异常。") + reason = (reason or "").strip() + if not reason: + raise SubjectInputError("必须填写处理依据。") + + began = False + if not connection.in_transaction: + connection.execute("BEGIN IMMEDIATE") + began = True + try: + if request_key: + existing = connection.execute( + """ + SELECT * FROM ledger_event_revisions + WHERE ledger_event_id = ? AND idempotency_key = ? + ORDER BY id LIMIT 1 + """, + (ledger_event_id, request_key), + ).fetchone() + if existing is not None: + if began: + connection.commit() + return _revision_payload(connection, existing) + + current = current_revision(connection, ledger_event_id) + if current is None: + raise SubjectConflictError("该事件不存在或没有当前修订。") + if current["state"] != "pending_subject": + raise SubjectConflictError("只有待确认科目的事件可以退回或转异常。") + if expected_revision is not None and int(expected_revision) not in ( + current["id"], current["revision"], + ): + raise SubjectConflictError("事件已发生变更,请刷新后重试。") + evidence = json.loads(current["evidence_json"] or "{}") if current["evidence_json"] else {} + evidence["admin_disposition"] = disposition + revision_id = append_revision( + connection, + ledger_event_id, + state="pending_subject", + effective_at=current["effective_at"], + amount=current["amount"], + currency=current["currency"], + payer_company_id=current["payer_company_id"], + payee_company_id=current["payee_company_id"], + perspective_company_id=None, + subject_code=None, + source_kind=current["source_kind"], + source_revision_token=current["source_revision_token"], + posting_kind=current["posting_kind"], + reverses_ledger_event_id=current["reverses_ledger_event_id"], + adjusts_ledger_event_id=current["adjusts_ledger_event_id"], + rule_version=current["rule_version"] or SUBJECT_RULE_VERSION, + evidence_json=json.dumps(evidence, ensure_ascii=False), + idempotency_key=request_key, + actor=actor, + reason=reason, + supersedes_revision_id=current["id"], + ) + row = connection.execute( + "SELECT * FROM ledger_event_revisions WHERE id = ?", (revision_id,) + ).fetchone() + except Exception: + if began: + connection.rollback() + raise + else: + if began: + connection.commit() + return _revision_payload(connection, row) + + def _revision_payload(connection: sqlite3.Connection, revision: sqlite3.Row) -> dict[str, object]: company = connection.execute( "SELECT name FROM companies WHERE id = ?", (revision["perspective_company_id"],) diff --git a/tests/b44_ui_check.js b/tests/b44_ui_check.js new file mode 100644 index 0000000..dd658a6 --- /dev/null +++ b/tests/b44_ui_check.js @@ -0,0 +1,45 @@ +"use strict"; + +const path = require("path"); +const assert = require("assert"); +const ui = require(path.join(__dirname, "..", "web", "app.js")); + +assert.strictEqual(ui.fmtAbsMoney(-1280), "1,280.00"); +assert.strictEqual(ui.fmtAbsMoney(1280), "1,280.00"); +assert.strictEqual(ui.fmtAbsMoney("60.5"), "60.50"); + +assert.strictEqual(ui.eventIsNegative({ posting_kind: "reversal" }), true); +assert.strictEqual(ui.eventIsNegative({ posting_kind: "normal", is_repayment: true }), true); +assert.strictEqual(ui.eventIsNegative({ posting_kind: "normal", is_repayment: false }), false); + +assert.strictEqual(ui.cycleTab(0, 4, false), 1); +assert.strictEqual(ui.cycleTab(3, 4, false), 0); +assert.strictEqual(ui.cycleTab(0, 4, true), 3); +assert.strictEqual(ui.cycleTab(2, 5, true), 1); +assert.strictEqual(ui.cycleTab(0, 0, false), 0); + +assert.strictEqual(ui.drawerEscAction(1), "close"); +assert.strictEqual(ui.drawerEscAction(0), "close"); +assert.strictEqual(ui.drawerEscAction(2), "back"); +assert.strictEqual(ui.drawerEscAction(3), "back"); + +const abs = ui.amountWithCurrency(1280, "CNY"); +assert.ok(abs.includes("CNY")); +assert.ok(abs.includes("1,280.00")); +assert.ok(!abs.includes("+")); +assert.ok(!abs.includes("−")); + +const repay = ui.amountWithCurrency(3200000, "CNY", { signed: true, negative: true }); +assert.ok(repay.includes("−")); +assert.ok(!repay.includes("+")); +assert.ok(repay.includes("3,200,000.00")); + +assert.strictEqual(ui.resultDirection(10).label, "应收"); +assert.strictEqual(ui.resultDirection(-10).label, "应付"); +assert.strictEqual(ui.resultDirection(0).label, "持平"); + +assert.ok(ui.accountCell({ visibility: "visible", label: "中信 5316" }).includes("中信 5316")); +assert.ok(ui.accountCell({ visibility: "masked" }).includes("按对方授权不可见")); +assert.ok(ui.accountCell({ visibility: "missing" }).includes("源行缺失")); + +console.log("b44_ui_check ok"); diff --git a/tests/test_b44_frontend.py b/tests/test_b44_frontend.py new file mode 100644 index 0000000..516b1cb --- /dev/null +++ b/tests/test_b44_frontend.py @@ -0,0 +1,94 @@ +"""Static + Node checks for the B-44 visual rework. + +Covers unique ids, nav copy, drawer/directory breakpoints, eight-column +event table, and the exported keyboard/amount helpers. +""" + +from __future__ import annotations + +from pathlib import Path +import re +import subprocess +import unittest + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" + + +class B44FrontendContractTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.admin = (WEB / "admin.html").read_text(encoding="utf-8") + cls.company = (WEB / "company.html").read_text(encoding="utf-8") + cls.css = (WEB / "styles.css").read_text(encoding="utf-8") + cls.js = (WEB / "app.js").read_text(encoding="utf-8") + + def test_admin_ids_are_unique(self) -> None: + self.assertEqual(1, self.admin.count('id="companyLedgers"')) + self.assertEqual(1, self.admin.count('id="balanceLedgers"')) + self.assertIn('data-view="pair"', self.admin) + self.assertRegex(self.admin, r'data-view="pair"[^>]*>[\s\S]*?往来查询') + self.assertIn("

往来查询

", self.admin) + self.assertIn("转为异常后,该记录暂不纳入余额计算", self.admin) + self.assertIn('id="auditExceptionNote"', self.admin) + + def test_company_balance_groups_and_nav(self) -> None: + self.assertIn('id="companyBalanceGroups"', self.company) + self.assertNotIn('id="companyBalanceLine"', self.company) + self.assertRegex(self.company, r'data-view="balances"[^>]*>[\s\S]*?往来余额') + self.assertIn("

往来余额

", self.company) + + def test_css_directory_and_drawer_breakpoints(self) -> None: + self.assertIn("@media (max-width: 768px)", self.css) + self.assertIn("@media (max-width: 375px)", self.css) + self.assertIn("@media (max-width: 1179px)", self.css) + self.assertIn("@media (max-width: 767px)", self.css) + self.assertIn(".drawer {", self.css) + self.assertIn("width: 640px", self.css) + self.assertIn(".drawer { width: 480px; }", self.css) + self.assertIn(".drawer { width: 100%; }", self.css) + self.assertIn(".drawer .pair-balance-line.is-six", self.css) + self.assertIn("repeat(3, 1fr)", self.css) + self.assertIn(".drawer .event-table { min-width: 860px; }", self.css) + self.assertIn(".company-row.is-balance-counterparty", self.css) + self.assertIn(".ledger-hide-md", self.css) + + def test_js_selectors_and_event_table(self) -> None: + self.assertIn('loadAdminBalances($("#balanceLedgers"))', self.js) + self.assertNotIn('loadAdminBalances($("#companyLedgers"))', self.js) + self.assertIn("交易日期", self.js) + self.assertIn("本方账户", self.js) + self.assertIn("对方账户", self.js) + self.assertIn("摘要", self.js) + self.assertIn("匹配状态", self.js) + self.assertIn("function eventTableHead()", self.js) + self.assertIn("colspan=\"8\"", self.js) + self.assertIn("function cycleTab(", self.js) + self.assertIn("function drawerEscAction(", self.js) + self.assertIn("function eventIsNegative(", self.js) + self.assertIn("fmtAbsMoney", self.js) + self.assertIn("is-balance-counterparty", self.js) + heads = re.search( + r"function eventTableHead\(\) \{\s*return `([^`]+)`", + self.js, + ) + self.assertIsNotNone(heads) + markup = heads.group(1) + self.assertNotIn("来源", markup) + for label in ("交易日期", "方向", "科目", "本方账户", "对方账户", "摘要", "匹配状态", "金额"): + self.assertIn(label, markup) + self.assertEqual(8, len(re.findall(r" None: + result = subprocess.run( + ["node", str(ROOT / "tests" / "b44_ui_check.js")], + capture_output=True, + text=True, + cwd=str(ROOT), + ) + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("b44_ui_check ok", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_positions.py b/tests/test_positions.py index 54cbf56..3fb42f5 100644 --- a/tests/test_positions.py +++ b/tests/test_positions.py @@ -359,6 +359,72 @@ class PaginationTests(LedgerBase): {(self.company_a, "CNY"), (self.company_b, "CNY")}, seen ) + def test_event_payload_account_chip_and_repayment_flag(self) -> None: + self.pair( + self.company_a, self.company_b, "40.00", + summary="归还往来款", purpose="还款", + ) + ledger_events.reconcile_bank_events(self.connection, actor=self.admin) + event_id = int(self.ledger_events()[0]["id"]) + detail = positions.event_detail(self.connection, event_id) + event = detail["event"] + self.assertEqual("visible", event["payer_account"]["visibility"]) + self.assertEqual("测试 0001", event["payer_account"]["label"]) + self.assertEqual("visible", event["payee_account"]["visibility"]) + self.assertEqual("测试 0002", event["payee_account"]["label"]) + self.assertEqual("归还往来款", event["summary"]) + self.assertTrue(event["is_repayment"]) + + company_view = positions.event_payload( + self.connection, + self.connection.execute( + positions._DETAIL_SELECT + " WHERE p.ledger_event_id = ?", + (event_id,), + ).fetchone(), + viewer_company_id=self.company_a, + ) + self.assertEqual("visible", company_view["payer_account"]["visibility"]) + self.assertEqual("masked", company_view["payee_account"]["visibility"]) + self.assertEqual("按对方授权不可见", company_view["payee_account"]["label"]) + + def test_park_subject_exception_hides_from_review_queue(self) -> None: + self.pair(self.company_a, self.company_b, "100.00") + ledger_events.reconcile_bank_events(self.connection, actor=self.admin) + event_id = int(self.ledger_events()[0]["id"]) + current = self.current(event_id) + subjects.park_subject( + self.connection, event_id, + disposition="exception", reason="转异常核查", + expected_revision=current["id"], request_key="park-exc-1", + actor=self.admin, + ) + queue = positions.subject_review_queue( + self.connection, from_="2026-01-01", cutoff="2026-07-31", + ) + self.assertFalse( + any(item["ledger_event_id"] == event_id for item in queue["items"]) + ) + revision = self.current(event_id) + self.assertEqual("pending_subject", revision["state"]) + + def test_park_subject_return_stays_in_review_queue(self) -> None: + self.pair(self.company_a, self.company_b, "80.00") + ledger_events.reconcile_bank_events(self.connection, actor=self.admin) + event_id = int(self.ledger_events()[0]["id"]) + current = self.current(event_id) + subjects.park_subject( + self.connection, event_id, + disposition="return", reason="退回补充摘要", + expected_revision=current["id"], request_key="park-ret-1", + actor=self.admin, + ) + queue = positions.subject_review_queue( + self.connection, from_="2026-01-01", cutoff="2026-07-31", + ) + self.assertTrue( + any(item["ledger_event_id"] == event_id for item in queue["items"]) + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_positions_api.py b/tests/test_positions_api.py index 6ce2cfc..58ba707 100644 --- a/tests/test_positions_api.py +++ b/tests/test_positions_api.py @@ -550,6 +550,45 @@ class IntercompanyApiTests(unittest.TestCase): self.assertEqual("2026-06-15", reversals[0]["effective_at"][:10]) self.assertEqual(event_id, reversals[0]["reverses_ledger_event_id"]) + def test_admin_events_include_account_chips(self) -> None: + pending = self._fresh_pair("100.00") + revision = self._confirm(pending["ledger_event_id"]) + status, _, data = self.admin.get( + "/api/admin/intercompany/events?from=2026-01-01&cutoff=2026-12-31" + ) + self.assertEqual(200, status, data) + event = next( + item for item in as_json(data)["items"] + if item["ledger_event_id"] == revision["ledger_event_id"] + ) + self.assertIn("payer_account", event) + self.assertIn("payee_account", event) + self.assertEqual("visible", event["payer_account"]["visibility"]) + self.assertTrue(event["payer_account"]["label"]) + self.assertIn("summary", event) + self.assertIn("is_repayment", event) + + def test_subject_exception_drops_from_review_queue(self) -> None: + pending = self._fresh_pair("77.00") + status, _, data = self.admin.post_json( + f"/api/admin/intercompany/events/{pending['ledger_event_id']}/subject-decisions", + { + "perspective_company_id": pending["payer_company_id"], + "action": "exception", + "reason": "转异常待核查", + "expected_revision": pending["revision_id"], + "request_key": "park-exc-api", + }, + ) + self.assertEqual(200, status, data) + self.assertEqual("pending_subject", as_json(data)["revision"]["state"]) + status, _, data = self.admin.get( + "/api/admin/subject-reviews?from=2026-01-01&cutoff=2026-12-31" + ) + self.assertEqual(200, status, data) + ids = [item["ledger_event_id"] for item in as_json(data)["items"]] + self.assertNotIn(pending["ledger_event_id"], ids) + if __name__ == "__main__": unittest.main() diff --git a/web/admin.html b/web/admin.html index 53bb995..3c84c21 100644 --- a/web/admin.html +++ b/web/admin.html @@ -22,7 +22,7 @@
金牛集团总账管理端