Polish project detail modals and data flows
This commit is contained in:
@@ -2,6 +2,7 @@ import os
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import date, datetime
|
||||
from functools import lru_cache
|
||||
@@ -21,6 +22,11 @@ from sqlalchemy import create_engine, event, text
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_HEALTH_PAYLOAD_CACHE: dict[str, Any] = {
|
||||
"expires_at": 0.0,
|
||||
"payload": None,
|
||||
}
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
@@ -636,6 +642,27 @@ def init_db() -> None:
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS project_analysis_settings (
|
||||
support_dept_code TEXT NOT NULL PRIMARY KEY,
|
||||
detail_note TEXT DEFAULT '',
|
||||
inactive_related_codes_json TEXT DEFAULT '[]',
|
||||
labor_joint_exempt INTEGER DEFAULT 0,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_project_analysis_settings_code
|
||||
ON project_analysis_settings (support_dept_code)
|
||||
"""
|
||||
)
|
||||
)
|
||||
page_state_columns_before = {
|
||||
row[1]
|
||||
for row in conn.execute(text("PRAGMA table_info(project_page_state)")).fetchall()
|
||||
@@ -3339,6 +3366,16 @@ def normalize_account_display(account_code: Any, account_name: Any) -> tuple[str
|
||||
return normalized_code, normalized_name, label
|
||||
|
||||
|
||||
def build_transaction_posting_display(voucher_number: Any, posting_date: Any) -> str:
|
||||
year, month, day = extract_period(normalize_text(voucher_number), normalize_text(posting_date))
|
||||
if year and month and day:
|
||||
return f"{year:04d}-{month:02d}-{day:02d}"
|
||||
parsed_date = normalize_date_text(posting_date)
|
||||
if re.match(r"^\d{4}-\d{2}-\d{2}$", parsed_date):
|
||||
return parsed_date
|
||||
return "-"
|
||||
|
||||
|
||||
def get_data_version() -> str:
|
||||
with engine.begin() as conn:
|
||||
transaction_updated = conn.execute(text("SELECT MAX(updated_at) FROM transactions")).scalar()
|
||||
@@ -3358,12 +3395,20 @@ def get_data_version() -> str:
|
||||
return max((version for version in versions if version), default="")
|
||||
|
||||
|
||||
def build_health_payload() -> dict[str, str]:
|
||||
return {
|
||||
def build_health_payload(force: bool = False) -> dict[str, str]:
|
||||
now = time.monotonic()
|
||||
cached_payload = _HEALTH_PAYLOAD_CACHE.get("payload")
|
||||
if not force and cached_payload and now < float(_HEALTH_PAYLOAD_CACHE.get("expires_at") or 0.0):
|
||||
return dict(cached_payload)
|
||||
|
||||
payload = {
|
||||
"status": "ok",
|
||||
"server_time": datetime.now().isoformat(timespec="seconds"),
|
||||
"data_version": get_data_version(),
|
||||
}
|
||||
_HEALTH_PAYLOAD_CACHE["payload"] = payload
|
||||
_HEALTH_PAYLOAD_CACHE["expires_at"] = now + 2.0
|
||||
return dict(payload)
|
||||
|
||||
|
||||
def check_record_revision(conn: Any, table_name: str, key_column: str, key_value: Any, edit_revision: str) -> None:
|
||||
@@ -3953,6 +3998,83 @@ def save_project_comparison_note(support_dept_code: str | None, item_key: str |
|
||||
)
|
||||
|
||||
|
||||
def get_project_analysis_settings_map() -> dict[str, dict[str, object]]:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
support_dept_code,
|
||||
COALESCE(detail_note, '') AS detail_note,
|
||||
COALESCE(inactive_related_codes_json, '[]') AS inactive_related_codes_json,
|
||||
COALESCE(labor_joint_exempt, 0) AS labor_joint_exempt
|
||||
FROM project_analysis_settings
|
||||
WHERE COALESCE(support_dept_code, '') <> ''
|
||||
"""
|
||||
)
|
||||
).mappings().all()
|
||||
result: dict[str, dict[str, object]] = {}
|
||||
for row in rows:
|
||||
code = normalize_text(row.get("support_dept_code"))
|
||||
if not code:
|
||||
continue
|
||||
try:
|
||||
inactive_codes = json.loads(row.get("inactive_related_codes_json") or "[]")
|
||||
except Exception:
|
||||
inactive_codes = []
|
||||
result[code] = {
|
||||
"detail_note": normalize_text(row.get("detail_note")),
|
||||
"inactive_related_codes": [
|
||||
normalize_text(value) for value in (inactive_codes or []) if normalize_text(value)
|
||||
],
|
||||
"labor_joint_exempt": bool(row.get("labor_joint_exempt")),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def save_project_analysis_settings(
|
||||
support_dept_code: str | None,
|
||||
detail_note: str | None = None,
|
||||
inactive_related_codes: list[str] | None = None,
|
||||
labor_joint_exempt: bool | None = None,
|
||||
) -> None:
|
||||
code = normalize_text(support_dept_code)
|
||||
if not code:
|
||||
return
|
||||
current = get_project_analysis_settings_map().get(code, {})
|
||||
next_detail_note = normalize_text(detail_note) if detail_note is not None else normalize_text(current.get("detail_note"))
|
||||
current_inactive = current.get("inactive_related_codes", [])
|
||||
next_inactive_related_codes = [
|
||||
normalize_text(value)
|
||||
for value in (inactive_related_codes if inactive_related_codes is not None else current_inactive)
|
||||
if normalize_text(value)
|
||||
]
|
||||
next_labor_joint_exempt = bool(labor_joint_exempt) if labor_joint_exempt is not None else bool(current.get("labor_joint_exempt"))
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO project_analysis_settings (
|
||||
support_dept_code, detail_note, inactive_related_codes_json, labor_joint_exempt, updated_at
|
||||
) VALUES (
|
||||
:support_dept_code, :detail_note, :inactive_related_codes_json, :labor_joint_exempt, CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT(support_dept_code) DO UPDATE SET
|
||||
detail_note = excluded.detail_note,
|
||||
inactive_related_codes_json = excluded.inactive_related_codes_json,
|
||||
labor_joint_exempt = excluded.labor_joint_exempt,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""
|
||||
),
|
||||
{
|
||||
"support_dept_code": code,
|
||||
"detail_note": next_detail_note,
|
||||
"inactive_related_codes_json": json.dumps(next_inactive_related_codes, ensure_ascii=False),
|
||||
"labor_joint_exempt": 1 if next_labor_joint_exempt else 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_project_status_for_edit(support_dept_code: str | None) -> dict[str, Any]:
|
||||
contract_info_map = get_project_contract_info_map()
|
||||
billing_summary_map = get_project_billing_summary_map()
|
||||
@@ -4931,12 +5053,128 @@ def get_project_account_breakdowns(selected_year: int | None) -> dict[str, dict[
|
||||
normalized_result[code] = {}
|
||||
for kind, entries in buckets.items():
|
||||
normalized_result[code][kind] = [
|
||||
{"label": label, "amount": amount}
|
||||
{
|
||||
"label": label,
|
||||
"amount": amount,
|
||||
"account_code": label.split(" · ", 1)[0] if " · " in label else "",
|
||||
"account_name": label.split(" · ", 1)[1] if " · " in label else label,
|
||||
}
|
||||
for label, amount in sorted(entries.items(), key=lambda item: item[1], reverse=True)
|
||||
]
|
||||
return normalized_result
|
||||
|
||||
|
||||
def parse_support_dept_codes_param(raw_codes: Any, fallback_code: Any = "") -> list[str]:
|
||||
values: list[str] = []
|
||||
for chunk in re.split(r"[\s,]+", normalize_text(raw_codes)):
|
||||
normalized = normalize_text(chunk)
|
||||
if normalized and normalized not in values:
|
||||
values.append(normalized)
|
||||
normalized_fallback = normalize_text(fallback_code)
|
||||
if normalized_fallback and normalized_fallback not in values:
|
||||
values.insert(0, normalized_fallback)
|
||||
return values
|
||||
|
||||
|
||||
def build_in_clause(prefix: str, values: list[str]) -> tuple[str, dict[str, Any]]:
|
||||
params: dict[str, Any] = {}
|
||||
placeholders: list[str] = []
|
||||
for index, value in enumerate(values):
|
||||
key = f"{prefix}_{index}"
|
||||
placeholders.append(f":{key}")
|
||||
params[key] = value
|
||||
return ", ".join(placeholders), params
|
||||
|
||||
|
||||
def fetch_project_expense_transaction_rows(
|
||||
codes: list[str],
|
||||
expense_group: str = "",
|
||||
account_label: str = "",
|
||||
) -> list[dict[str, Any]]:
|
||||
normalized_codes = [normalize_text(code) for code in codes if normalize_text(code)]
|
||||
if not normalized_codes:
|
||||
return []
|
||||
in_clause, code_params = build_in_clause("project_code", normalized_codes)
|
||||
query = text(
|
||||
f"""
|
||||
SELECT
|
||||
COALESCE(voucher_number, '') AS voucher_number,
|
||||
COALESCE(posting_date, '') AS posting_date,
|
||||
COALESCE(partner_name, '') AS partner_name,
|
||||
COALESCE(partner_code, '') AS partner_code,
|
||||
COALESCE(cost_dept_name, '') AS cost_dept_name,
|
||||
COALESCE(support_dept_code, '') AS support_dept_code,
|
||||
COALESCE(support_dept_name, '') AS support_dept_name,
|
||||
COALESCE(account_code, '') AS account_code,
|
||||
COALESCE(account_name, '') AS account_name,
|
||||
amount
|
||||
FROM transactions
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
AND accounting_category = '원가'
|
||||
ORDER BY posting_date DESC, voucher_number DESC, partner_name, cost_dept_name, account_code
|
||||
"""
|
||||
)
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(query, code_params).mappings().all()
|
||||
normalized_group = normalize_text(expense_group).lower()
|
||||
normalized_account_label = normalize_text(account_label)
|
||||
result: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
normalized_code, normalized_name, normalized_label = normalize_account_display(
|
||||
row["account_code"],
|
||||
row["account_name"],
|
||||
)
|
||||
is_design_outsource = "기술협력비" in normalized_label or "기술협력비" in normalized_name
|
||||
if normalized_group == "outsource" and not is_design_outsource:
|
||||
continue
|
||||
if normalized_group == "overhead" and is_design_outsource:
|
||||
continue
|
||||
if normalized_account_label and normalized_label != normalized_account_label:
|
||||
continue
|
||||
result.append(
|
||||
{
|
||||
"posting_date": build_transaction_posting_display(row["voucher_number"], row["posting_date"]),
|
||||
"voucher_number": normalize_text(row["voucher_number"]),
|
||||
"partner_name": normalize_text(row["partner_name"]),
|
||||
"partner_code": normalize_text(row["partner_code"]),
|
||||
"cost_dept_name": normalize_text(row["cost_dept_name"]),
|
||||
"support_dept_code": normalize_text(row["support_dept_code"]),
|
||||
"support_dept_name": normalize_text(row["support_dept_name"]),
|
||||
"account_code": normalized_code,
|
||||
"account_name": normalized_name,
|
||||
"amount": int(round(float(row["amount"] or 0))),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_project_expense_date_range(codes: list[str]) -> tuple[str, str]:
|
||||
normalized_codes = [normalize_text(code) for code in (codes or []) if normalize_text(code)]
|
||||
if not normalized_codes:
|
||||
return "", ""
|
||||
in_clause, code_params = build_in_clause("project_code", normalized_codes)
|
||||
query = text(
|
||||
f"""
|
||||
SELECT
|
||||
COALESCE(voucher_number, '') AS voucher_number,
|
||||
COALESCE(posting_date, '') AS posting_date
|
||||
FROM transactions
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
AND accounting_category = '원가'
|
||||
"""
|
||||
)
|
||||
dates: list[str] = []
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(query, code_params).mappings().all()
|
||||
for row in rows:
|
||||
display = build_transaction_posting_display(row["voucher_number"], row["posting_date"])
|
||||
if re.match(r"^\d{4}-\d{2}-\d{2}$", display):
|
||||
dates.append(display)
|
||||
if not dates:
|
||||
return "", ""
|
||||
return min(dates), max(dates)
|
||||
|
||||
|
||||
def get_recent_transactions(limit: int = 50) -> list[dict[str, Any]]:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
@@ -5862,6 +6100,7 @@ def render_projects_page(
|
||||
"project_account_breakdowns": get_project_account_breakdowns(selected_year),
|
||||
"project_status_rows": get_project_status_rows(),
|
||||
"project_comparison_notes": get_project_comparison_notes_map(),
|
||||
"project_analysis_settings": get_project_analysis_settings_map(),
|
||||
"project_edit": get_project_status_for_edit(edit_code),
|
||||
"project_focus_code": normalize_text(focus_code),
|
||||
"project_page_state": get_project_page_state(),
|
||||
@@ -6049,6 +6288,329 @@ async def project_comparison_note_save(request: Request):
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.post("/projects/analysis-settings")
|
||||
async def project_analysis_settings_save(request: Request):
|
||||
try:
|
||||
payload = await request.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("잘못된 프로젝트 상세 설정 형식입니다.")
|
||||
inactive_related_codes = payload.get("inactive_related_codes")
|
||||
if inactive_related_codes is not None and not isinstance(inactive_related_codes, list):
|
||||
raise ValueError("제외 연관 프로젝트 형식이 올바르지 않습니다.")
|
||||
save_project_analysis_settings(
|
||||
payload.get("support_dept_code"),
|
||||
detail_note=payload.get("detail_note") if "detail_note" in payload else None,
|
||||
inactive_related_codes=inactive_related_codes if isinstance(inactive_related_codes, list) else None,
|
||||
labor_joint_exempt=payload.get("labor_joint_exempt") if "labor_joint_exempt" in payload else None,
|
||||
)
|
||||
return JSONResponse(content={"status": "ok"})
|
||||
except Exception as exc:
|
||||
logger.exception("프로젝트 상세 설정 저장 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/projects/uncontracted-detail-transactions")
|
||||
async def project_uncontracted_detail_transactions(
|
||||
support_dept_code: str = "",
|
||||
kind: str = "expense",
|
||||
detail_type: str = "",
|
||||
year: int | None = None,
|
||||
month: int | None = None,
|
||||
category: str | None = None,
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
):
|
||||
try:
|
||||
normalized_code = normalize_text(support_dept_code)
|
||||
if not normalized_code:
|
||||
raise ValueError("프로젝트 코드가 필요합니다.")
|
||||
normalized_kind = normalize_text(kind).lower()
|
||||
if normalized_kind not in {"expense", "revenue"}:
|
||||
raise ValueError("조회 종류가 올바르지 않습니다.")
|
||||
normalized_detail_type = normalize_text(detail_type).lower()
|
||||
if normalized_detail_type not in {"year", "month", "category"}:
|
||||
raise ValueError("세부 조회 형식이 올바르지 않습니다.")
|
||||
|
||||
filters = ["support_dept_code = :support_dept_code"]
|
||||
params: dict[str, Any] = {"support_dept_code": normalized_code}
|
||||
|
||||
if normalized_kind == "expense":
|
||||
filters.append("accounting_category IN ('원가', '판관비')")
|
||||
else:
|
||||
filters.append(REVENUE_SQL)
|
||||
|
||||
if normalized_detail_type == "month":
|
||||
if not year or not month:
|
||||
raise ValueError("월별 세부 조회에는 연도와 월이 필요합니다.")
|
||||
filters.append("year = :year")
|
||||
filters.append("month = :month")
|
||||
params["year"] = int(year)
|
||||
params["month"] = int(month)
|
||||
elif normalized_detail_type == "year":
|
||||
if not year:
|
||||
raise ValueError("연도별 세부 조회에는 연도가 필요합니다.")
|
||||
filters.append("year = :year")
|
||||
params["year"] = int(year)
|
||||
else:
|
||||
# category 상세는 프로젝트 생성 시기와 무관하게 선택된 연도 구간 안의 발생 전표를 모두 보여준다.
|
||||
if start_year:
|
||||
filters.append("year >= :start_year")
|
||||
params["start_year"] = int(start_year)
|
||||
if end_year:
|
||||
filters.append("year <= :end_year")
|
||||
params["end_year"] = int(end_year)
|
||||
if category:
|
||||
params["category"] = normalize_text(category)
|
||||
|
||||
query = text(
|
||||
f"""
|
||||
SELECT
|
||||
COALESCE(voucher_number, '') AS voucher_number,
|
||||
COALESCE(posting_date, '') AS posting_date,
|
||||
COALESCE(partner_name, '') AS partner_name,
|
||||
COALESCE(partner_code, '') AS partner_code,
|
||||
COALESCE(cost_dept_name, '') AS cost_dept_name,
|
||||
COALESCE(support_dept_code, '') AS support_dept_code,
|
||||
COALESCE(support_dept_name, '') AS support_dept_name,
|
||||
COALESCE(account_code, '') AS account_code,
|
||||
COALESCE(account_name, '') AS account_name,
|
||||
amount
|
||||
FROM transactions
|
||||
WHERE {' AND '.join(filters)}
|
||||
ORDER BY posting_date DESC, voucher_number DESC, partner_name, cost_dept_name, account_code
|
||||
"""
|
||||
)
|
||||
|
||||
with engine.begin() as conn:
|
||||
rows = [
|
||||
{
|
||||
"posting_date": build_transaction_posting_display(row["voucher_number"], row["posting_date"]),
|
||||
"voucher_number": normalize_text(row["voucher_number"]),
|
||||
"partner_name": normalize_text(row["partner_name"]),
|
||||
"partner_code": normalize_text(row["partner_code"]),
|
||||
"cost_dept_name": normalize_text(row["cost_dept_name"]),
|
||||
"support_dept_code": normalize_text(row["support_dept_code"]),
|
||||
"support_dept_name": normalize_text(row["support_dept_name"]),
|
||||
"account_code": normalize_text(row["account_code"]),
|
||||
"account_name": normalize_text(row["account_name"]),
|
||||
"amount": int(round(float(row["amount"] or 0))),
|
||||
}
|
||||
for row in conn.execute(query, params).mappings()
|
||||
]
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"rows": rows,
|
||||
"total_amount": sum(int(row["amount"] or 0) for row in rows),
|
||||
"support_dept_code": normalized_code,
|
||||
"kind": normalized_kind,
|
||||
"detail_type": normalized_detail_type,
|
||||
"category": normalize_text(category),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("미계약 세부 거래내역 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/projects/uncontracted-vendor-transactions")
|
||||
async def project_uncontracted_vendor_transactions(
|
||||
partner_code: str = "",
|
||||
partner_name: str = "",
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
):
|
||||
try:
|
||||
normalized_partner_code = normalize_text(partner_code)
|
||||
normalized_partner_name = normalize_text(partner_name)
|
||||
if not normalized_partner_code and not normalized_partner_name:
|
||||
raise ValueError("거래처 정보가 필요합니다.")
|
||||
|
||||
filters = ["accounting_category IN ('원가', '판관비')"]
|
||||
params: dict[str, Any] = {}
|
||||
|
||||
if start_year:
|
||||
filters.append("year >= :start_year")
|
||||
params["start_year"] = int(start_year)
|
||||
if end_year:
|
||||
filters.append("year <= :end_year")
|
||||
params["end_year"] = int(end_year)
|
||||
if normalized_partner_code:
|
||||
filters.append("COALESCE(partner_code, '') = :partner_code")
|
||||
params["partner_code"] = normalized_partner_code
|
||||
else:
|
||||
filters.append("COALESCE(partner_name, '') = :partner_name")
|
||||
params["partner_name"] = normalized_partner_name
|
||||
|
||||
query = text(
|
||||
f"""
|
||||
SELECT
|
||||
COALESCE(voucher_number, '') AS voucher_number,
|
||||
COALESCE(posting_date, '') AS posting_date,
|
||||
COALESCE(partner_name, '') AS partner_name,
|
||||
COALESCE(partner_code, '') AS partner_code,
|
||||
COALESCE(cost_dept_name, '') AS cost_dept_name,
|
||||
COALESCE(support_dept_code, '') AS support_dept_code,
|
||||
COALESCE(support_dept_name, '') AS support_dept_name,
|
||||
COALESCE(account_code, '') AS account_code,
|
||||
COALESCE(account_name, '') AS account_name,
|
||||
amount
|
||||
FROM transactions
|
||||
WHERE {' AND '.join(filters)}
|
||||
ORDER BY posting_date DESC, voucher_number DESC, cost_dept_name, account_code
|
||||
"""
|
||||
)
|
||||
|
||||
with engine.begin() as conn:
|
||||
rows = [
|
||||
{
|
||||
"posting_date": build_transaction_posting_display(row["voucher_number"], row["posting_date"]),
|
||||
"voucher_number": normalize_text(row["voucher_number"]),
|
||||
"partner_name": normalize_text(row["partner_name"]),
|
||||
"partner_code": normalize_text(row["partner_code"]),
|
||||
"cost_dept_name": normalize_text(row["cost_dept_name"]),
|
||||
"support_dept_code": normalize_text(row["support_dept_code"]),
|
||||
"support_dept_name": normalize_text(row["support_dept_name"]),
|
||||
"account_code": normalize_text(row["account_code"]),
|
||||
"account_name": normalize_text(row["account_name"]),
|
||||
"amount": int(round(float(row["amount"] or 0))),
|
||||
}
|
||||
for row in conn.execute(query, params).mappings()
|
||||
]
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"rows": rows,
|
||||
"total_amount": sum(int(row["amount"] or 0) for row in rows),
|
||||
"partner_code": normalized_partner_code,
|
||||
"partner_name": normalized_partner_name,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("거래처별 미계약 비용 상세 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/projects/comparison-actual-transactions")
|
||||
async def project_comparison_actual_transactions(
|
||||
support_dept_code: str = "",
|
||||
codes: str = "",
|
||||
group: str = "",
|
||||
account_label: str = "",
|
||||
):
|
||||
try:
|
||||
normalized_codes = parse_support_dept_codes_param(codes, support_dept_code)
|
||||
if not normalized_codes:
|
||||
raise ValueError("프로젝트 코드가 필요합니다.")
|
||||
rows = fetch_project_expense_transaction_rows(
|
||||
normalized_codes,
|
||||
expense_group=group,
|
||||
account_label=account_label,
|
||||
)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"rows": rows,
|
||||
"total_amount": sum(int(row["amount"] or 0) for row in rows),
|
||||
"support_dept_code": normalize_text(support_dept_code),
|
||||
"codes": normalized_codes,
|
||||
"group": normalize_text(group).lower(),
|
||||
"account_label": normalize_text(account_label),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("비교 실제 집행 세부 거래내역 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/projects/comparison-vendor-transactions")
|
||||
async def project_comparison_vendor_transactions(
|
||||
support_dept_code: str = "",
|
||||
codes: str = "",
|
||||
partner_code: str = "",
|
||||
partner_name: str = "",
|
||||
start_date: str = "",
|
||||
end_date: str = "",
|
||||
):
|
||||
try:
|
||||
normalized_codes = parse_support_dept_codes_param(codes, support_dept_code)
|
||||
if not normalized_codes:
|
||||
raise ValueError("프로젝트 코드가 필요합니다.")
|
||||
normalized_partner_code = normalize_text(partner_code)
|
||||
normalized_partner_name = normalize_text(partner_name)
|
||||
if not normalized_partner_code and not normalized_partner_name:
|
||||
raise ValueError("거래처 정보가 필요합니다.")
|
||||
project_start_date, project_end_date = get_project_expense_date_range(normalized_codes)
|
||||
effective_start_date = normalize_text(start_date) or project_start_date
|
||||
effective_end_date = normalize_text(end_date) or project_end_date
|
||||
|
||||
filters = ["accounting_category IN ('원가', '판관비')"]
|
||||
params: dict[str, Any] = {}
|
||||
if effective_start_date:
|
||||
filters.append("COALESCE(posting_date, '') >= :start_date")
|
||||
params["start_date"] = effective_start_date
|
||||
if effective_end_date:
|
||||
filters.append("COALESCE(posting_date, '') <= :end_date")
|
||||
params["end_date"] = effective_end_date
|
||||
if normalized_partner_code:
|
||||
filters.append("COALESCE(partner_code, '') = :partner_code")
|
||||
params["partner_code"] = normalized_partner_code
|
||||
else:
|
||||
filters.append("COALESCE(partner_name, '') = :partner_name")
|
||||
params["partner_name"] = normalized_partner_name
|
||||
|
||||
query = text(
|
||||
f"""
|
||||
SELECT
|
||||
COALESCE(voucher_number, '') AS voucher_number,
|
||||
COALESCE(posting_date, '') AS posting_date,
|
||||
COALESCE(partner_name, '') AS partner_name,
|
||||
COALESCE(partner_code, '') AS partner_code,
|
||||
COALESCE(cost_dept_name, '') AS cost_dept_name,
|
||||
COALESCE(support_dept_code, '') AS support_dept_code,
|
||||
COALESCE(support_dept_name, '') AS support_dept_name,
|
||||
COALESCE(account_code, '') AS account_code,
|
||||
COALESCE(account_name, '') AS account_name,
|
||||
amount
|
||||
FROM transactions
|
||||
WHERE {' AND '.join(filters)}
|
||||
ORDER BY posting_date DESC, voucher_number DESC, cost_dept_name, account_code
|
||||
"""
|
||||
)
|
||||
with engine.begin() as conn:
|
||||
filtered_rows = [
|
||||
{
|
||||
"posting_date": build_transaction_posting_display(row["voucher_number"], row["posting_date"]),
|
||||
"voucher_number": normalize_text(row["voucher_number"]),
|
||||
"partner_name": normalize_text(row["partner_name"]),
|
||||
"partner_code": normalize_text(row["partner_code"]),
|
||||
"cost_dept_name": normalize_text(row["cost_dept_name"]),
|
||||
"support_dept_code": normalize_text(row["support_dept_code"]),
|
||||
"support_dept_name": normalize_text(row["support_dept_name"]),
|
||||
"account_code": normalize_text(row["account_code"]),
|
||||
"account_name": normalize_text(row["account_name"]),
|
||||
"amount": int(round(float(row["amount"] or 0))),
|
||||
}
|
||||
for row in conn.execute(query, params).mappings()
|
||||
]
|
||||
return JSONResponse(
|
||||
content={
|
||||
"rows": filtered_rows,
|
||||
"total_amount": sum(int(row["amount"] or 0) for row in filtered_rows),
|
||||
"support_dept_code": normalize_text(support_dept_code),
|
||||
"codes": normalized_codes,
|
||||
"partner_code": normalized_partner_code,
|
||||
"partner_name": normalized_partner_name,
|
||||
"start_date": effective_start_date,
|
||||
"end_date": effective_end_date,
|
||||
"project_start_date": project_start_date,
|
||||
"project_end_date": project_end_date,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("비교 거래처 세부 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/annual-summary")
|
||||
async def annual_summary(request: Request):
|
||||
try:
|
||||
@@ -6125,7 +6687,7 @@ async def save_project_json(request: Request):
|
||||
form_data = parse_project_form(await request.body())
|
||||
save_project_status(form_data)
|
||||
code = normalize_text(form_data.get("support_dept_code"))
|
||||
health_payload = build_health_payload()
|
||||
health_payload = build_health_payload(force=True)
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user