feat: deliver schema 15 monthly settlement lifecycle
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from jarca_ledger_mcp.db import init_db
|
||||
from dashboard.page_views import monthly_settlement as view
|
||||
|
||||
|
||||
class _FakeStreamlit:
|
||||
def __init__(self, query=None):
|
||||
self.query_params = dict(query or {})
|
||||
self.session_state = {}
|
||||
self.dataframes = []
|
||||
self.errors = []
|
||||
self.headers = []
|
||||
self.titles = []
|
||||
self.buttons = []
|
||||
self.markdowns = []
|
||||
|
||||
def header(self, value):
|
||||
self.headers.append(value)
|
||||
|
||||
def title(self, value):
|
||||
self.titles.append(value)
|
||||
|
||||
def caption(self, value):
|
||||
pass
|
||||
|
||||
def markdown(self, value, **kwargs):
|
||||
self.markdowns.append(value)
|
||||
|
||||
def write(self, value):
|
||||
self.markdowns.append(str(value))
|
||||
|
||||
def selectbox(self, label, options, **kwargs):
|
||||
return options[0]
|
||||
|
||||
def info(self, value):
|
||||
pass
|
||||
|
||||
def error(self, value):
|
||||
self.errors.append(value)
|
||||
|
||||
def dataframe(self, value, **kwargs):
|
||||
self.dataframes.append((value, kwargs))
|
||||
|
||||
class _Column:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def button(self, label, **kwargs):
|
||||
return False
|
||||
|
||||
def popover(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def columns(self, spec):
|
||||
return [self._Column() for _ in spec]
|
||||
|
||||
def container(self, **kwargs):
|
||||
return self._Column()
|
||||
|
||||
def button(self, label, **kwargs):
|
||||
self.buttons.append((label, kwargs))
|
||||
return False
|
||||
|
||||
def popover(self, *args, **kwargs):
|
||||
return self._Column()
|
||||
|
||||
def rerun(self):
|
||||
return None
|
||||
|
||||
|
||||
def _fixture_db(tmp_path):
|
||||
path = tmp_path / "monthly-navigation.sqlite3"
|
||||
init_db(path)
|
||||
with sqlite3.connect(path) as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO monthly_settlement
|
||||
(id, month, basis_date, status, version, created_by, created_at, updated_by, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
("settlement-2026-09", "2026-09", "2026-09-30", "draft", 1, "test", "2026-09-30T00:00:00Z", "test", "2026-09-30T00:00:00Z"),
|
||||
)
|
||||
conn.commit()
|
||||
return path
|
||||
|
||||
|
||||
def test_month_text_link_uses_exact_canonical_id_and_direct_detail_reads_month(tmp_path, monkeypatch):
|
||||
db_path = _fixture_db(tmp_path)
|
||||
fake = _FakeStreamlit()
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {
|
||||
"success": True,
|
||||
"months": [{"settlement_id": "settlement-2026-09", "month": "2026-09", "status_label": "초안", "income": 100}],
|
||||
})
|
||||
|
||||
view._render_monthly_list(str(db_path))
|
||||
month_buttons = [item for item in fake.buttons if item[0] == "2026-09"]
|
||||
assert len(month_buttons) == 1
|
||||
assert month_buttons[0][1]["type"] == "tertiary"
|
||||
assert month_buttons[0][1]["on_click"] is view._set_monthly_detail
|
||||
assert month_buttons[0][1]["args"] == ("settlement-2026-09", "전체", "전체")
|
||||
assert "?page=monthly_settlement" not in "\n".join(fake.markdowns)
|
||||
month_buttons[0][1]["on_click"](*month_buttons[0][1]["args"])
|
||||
assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-09"}
|
||||
assert not fake.dataframes
|
||||
|
||||
detail_fake = _FakeStreamlit({"page": "monthly_settlement", "id": "settlement-2026-09"})
|
||||
monkeypatch.setattr(view, "st", detail_fake)
|
||||
calls = []
|
||||
monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: calls.append((db, month)))
|
||||
view.page_monthly_settlement(str(db_path))
|
||||
assert calls == [(str(db_path), "2026-09")]
|
||||
assert detail_fake.titles == ["월정산 상세 · 2026-09"]
|
||||
assert [label for label, _ in detail_fake.buttons] == ["← 월정산 목록", "검토승인"]
|
||||
|
||||
|
||||
def test_unregistered_month_selection_does_not_create_none_link(monkeypatch):
|
||||
fake = _FakeStreamlit()
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {
|
||||
"success": True,
|
||||
"months": [{"settlement_id": None, "month": "2026-08", "status_label": "정산 미생성·상세 선택 불가·현재집계(미확정)"}],
|
||||
})
|
||||
view._render_monthly_list("fixture.sqlite3")
|
||||
rendered = "\n".join(fake.markdowns)
|
||||
assert "2026-08" in rendered
|
||||
assert "[2026-08]" not in rendered
|
||||
assert "?page=monthly_settlement&id=None" not in rendered
|
||||
|
||||
|
||||
def test_direct_reload_unknown_id_does_not_fallback_to_month_or_list(tmp_path, monkeypatch):
|
||||
db_path = _fixture_db(tmp_path)
|
||||
fake = _FakeStreamlit({"page": "monthly_settlement", "id": "unknown-id", "month": "2026-09"})
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
calls = []
|
||||
monkeypatch.setattr(view, "_render_monthly_list", lambda db: calls.append(("list", db)))
|
||||
view.page_monthly_settlement(str(db_path))
|
||||
assert calls == []
|
||||
assert fake.errors == ["월정산을 찾을 수 없습니다."]
|
||||
|
||||
|
||||
def test_legacy_month_query_rewrites_to_canonical_id(tmp_path, monkeypatch):
|
||||
db_path = _fixture_db(tmp_path)
|
||||
fake = _FakeStreamlit({"page": "monthly_settlement", "month": "2026-09"})
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {
|
||||
"success": True,
|
||||
"months": [{"settlement_id": "settlement-2026-09", "month": "2026-09", "status_label": "초안"}],
|
||||
})
|
||||
monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: None)
|
||||
view.page_monthly_settlement(str(db_path))
|
||||
assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-09"}
|
||||
|
||||
|
||||
def test_back_to_list_uses_canonical_page_and_preserves_filters(monkeypatch):
|
||||
fake = _FakeStreamlit({"page": "monthly_settlement", "id": "settlement-2026-09", "year": "2026", "status": "미마감"})
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
view._clear_monthly_detail("2026", "미마감")
|
||||
assert fake.query_params == {"page": "monthly_settlement", "year": "2026", "status": "미마감"}
|
||||
assert fake.session_state["dashboard_page"] == "월정산"
|
||||
|
||||
|
||||
|
||||
|
||||
def test_add_button_opens_dialog_and_navigates_to_created_canonical_id(monkeypatch):
|
||||
class DialogFake(_FakeStreamlit):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.reruns = 0
|
||||
|
||||
def dialog(self, title):
|
||||
assert title == "월정산 추가"
|
||||
return lambda fn: fn
|
||||
|
||||
def text_input(self, label, **kwargs):
|
||||
assert label == "대상 월"
|
||||
return "2026-10"
|
||||
|
||||
def button(self, label, **kwargs):
|
||||
self.buttons.append((label, kwargs))
|
||||
return label in {"+", "추가"}
|
||||
|
||||
def rerun(self):
|
||||
self.reruns += 1
|
||||
|
||||
fake = DialogFake()
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
monkeypatch.setattr(view.overview_gateway, "list_monthly_settlement_overview", lambda **kwargs: {"success": True, "months": []})
|
||||
monkeypatch.setattr(view.settlement_gateway, "create_monthly_settlement", lambda month, **kwargs: {"success": True, "settlement_id": "settlement-2026-10"})
|
||||
|
||||
view._render_monthly_list("fixture.sqlite3")
|
||||
|
||||
assert fake.query_params == {"page": "monthly_settlement", "id": "settlement-2026-10"}
|
||||
assert fake.reruns == 1
|
||||
|
||||
|
||||
def test_month_link_target_contains_no_write_operation():
|
||||
source = view._render_monthly_list.__code__.co_names
|
||||
assert "create_settlement_draft" not in source
|
||||
assert "finalize_settlement_draft" not in source
|
||||
|
||||
|
||||
def test_back_button_single_click_clears_detail_query_and_reruns(monkeypatch):
|
||||
class ClickFake(_FakeStreamlit):
|
||||
def __init__(self):
|
||||
super().__init__({"page": "monthly_settlement", "id": "settlement-2026-09", "month": "2026-09", "year": "2026", "status": "미마감"})
|
||||
self.session_state.update({"dashboard_page": "월정산", "monthly_settlement_detail_id": "settlement-2026-09", "monthly_settlement_detail_month": "2026-09"})
|
||||
self.reruns = 0
|
||||
|
||||
def rerun(self):
|
||||
self.reruns += 1
|
||||
|
||||
fake = ClickFake()
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
|
||||
view._clear_monthly_detail("2026", "미마감")
|
||||
|
||||
assert fake.reruns == 1
|
||||
assert fake.query_params == {"page": "monthly_settlement", "year": "2026", "status": "미마감"}
|
||||
assert "monthly_settlement_detail_id" not in fake.session_state
|
||||
assert "monthly_settlement_detail_month" not in fake.session_state
|
||||
|
||||
|
||||
def test_next_rerun_without_detail_query_does_not_restore_detail(monkeypatch):
|
||||
fake = _FakeStreamlit({"page": "monthly_settlement", "year": "2026", "status": "미마감"})
|
||||
fake.session_state.update({"dashboard_page": "월정산"})
|
||||
monkeypatch.setattr(view, "st", fake)
|
||||
rendered = []
|
||||
monkeypatch.setattr(view, "_render_monthly_list", lambda db: rendered.append(db))
|
||||
monkeypatch.setattr(view, "_render_monthly_detail", lambda db, month: (_ for _ in ()).throw(AssertionError("detail restored")))
|
||||
|
||||
view.page_monthly_settlement("fixture.sqlite3")
|
||||
|
||||
assert rendered == ["fixture.sqlite3"]
|
||||
Reference in New Issue
Block a user