修正
This commit is contained in:
@@ -4,13 +4,15 @@ load_dotenv()
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
||||||
# FastAPI アプリケーション作成
|
# FastAPI アプリケーション作成
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="日本小規模企業向け会計システム"
|
title="日本小規模企業向け会計システム"
|
||||||
)
|
)
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
@@ -64,5 +66,7 @@ app.include_router(opening_balance_lock_router)
|
|||||||
from app.routers import journal_entries
|
from app.routers import journal_entries
|
||||||
app.include_router(journal_entries.router)
|
app.include_router(journal_entries.router)
|
||||||
|
|
||||||
|
from app.routers import month_locks
|
||||||
|
app.include_router(month_locks.router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +1,29 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
from app.core.database import get_connection
|
|
||||||
from app.modules.trial_balance.service import fetch_trial_balance
|
from app.modules.trial_balance.service import fetch_trial_balance
|
||||||
|
|
||||||
router = APIRouter(prefix="/trial-balance", tags=["試算表"])
|
router = APIRouter(prefix="/trial-balance", tags=["試算表"])
|
||||||
|
|
||||||
def D(x) -> Decimal:
|
|
||||||
return Decimal(str(x or 0))
|
|
||||||
|
|
||||||
@router.get("", summary="試算表(全科目)")
|
@router.get("", summary="試算表(全科目)")
|
||||||
def get_trial_balance(
|
def get_trial_balance(
|
||||||
fiscal_year: int = Query(..., description="会計年度(例: 2025)"),
|
fiscal_year: Optional[int] = Query(None, description="会計年度(例: 2025)"),
|
||||||
date_from: str = Query(..., description="YYYY-MM-DD"),
|
date_from: Optional[str] = Query(None, description="YYYY-MM-DD"),
|
||||||
date_to: str = Query(..., description="YYYY-MM-DD"),
|
date_to: Optional[str] = Query(None, description="YYYY-MM-DD"),
|
||||||
):
|
):
|
||||||
|
print("trial-balance NEW VERSION (optional params)")
|
||||||
|
|
||||||
|
# 👉 没传参数时,给默认值(开发期非常推荐)
|
||||||
|
if fiscal_year is None:
|
||||||
|
fiscal_year = 2025
|
||||||
|
if date_from is None:
|
||||||
|
date_from = "2025-01-01"
|
||||||
|
if date_to is None:
|
||||||
|
date_to = "2025-12-31"
|
||||||
|
|
||||||
if date_from > date_to:
|
if date_from > date_to:
|
||||||
raise HTTPException(status_code=400, detail="期間指定が不正です。")
|
raise HTTPException(status_code=400, detail="期間指定が不正です。")
|
||||||
|
|
||||||
result = fetch_trial_balance(date_from, date_to)
|
result = fetch_trial_balance(date_from, date_to)
|
||||||
|
|
||||||
return {
|
return result
|
||||||
"period": {
|
|
||||||
"from": date_from,
|
|
||||||
"to": date_to
|
|
||||||
},
|
|
||||||
**result
|
|
||||||
}
|
|
||||||
|
|
||||||
with get_connection() as conn, conn.cursor() as cur:
|
|
||||||
|
|
||||||
# 全科目取得
|
|
||||||
cur.execute("""
|
|
||||||
SELECT account_id, account_code, account_name, account_type
|
|
||||||
FROM accounts
|
|
||||||
WHERE is_active = true
|
|
||||||
ORDER BY account_code
|
|
||||||
""")
|
|
||||||
accounts = cur.fetchall()
|
|
||||||
|
|
||||||
result_accounts: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
total_opening = Decimal("0")
|
|
||||||
total_debit = Decimal("0")
|
|
||||||
total_credit = Decimal("0")
|
|
||||||
total_closing = Decimal("0")
|
|
||||||
|
|
||||||
for acc in accounts:
|
|
||||||
aid = acc["account_id"]
|
|
||||||
|
|
||||||
# 期首残高(opening_balances)
|
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
|
||||||
COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS opening
|
|
||||||
FROM opening_balances
|
|
||||||
WHERE fiscal_year = %s
|
|
||||||
AND account_id = %s
|
|
||||||
""", (fiscal_year, aid))
|
|
||||||
|
|
||||||
row = cur.fetchone()
|
|
||||||
opening = D(row["opening"]) if row else D(0)
|
|
||||||
|
|
||||||
|
|
||||||
# 当期
|
|
||||||
cur.execute("""
|
|
||||||
SELECT
|
|
||||||
COALESCE(SUM(l.debit), 0) AS debit,
|
|
||||||
COALESCE(SUM(l.credit), 0) AS credit
|
|
||||||
FROM journal_lines l
|
|
||||||
JOIN journal_entries j ON j.journal_id = l.journal_id
|
|
||||||
WHERE l.account_id = %s
|
|
||||||
AND j.journal_date BETWEEN %s AND %s
|
|
||||||
""", (aid, date_from, date_to))
|
|
||||||
row = cur.fetchone()
|
|
||||||
debit = D(row["debit"])
|
|
||||||
credit = D(row["credit"])
|
|
||||||
|
|
||||||
closing = opening + debit - credit
|
|
||||||
|
|
||||||
total_opening += opening
|
|
||||||
total_debit += debit
|
|
||||||
total_credit += credit
|
|
||||||
total_closing += closing
|
|
||||||
|
|
||||||
result_accounts.append({
|
|
||||||
"account_id": aid,
|
|
||||||
"account_code": acc["account_code"],
|
|
||||||
"account_name": acc["account_name"],
|
|
||||||
"account_type": acc["account_type"],
|
|
||||||
"opening_balance": str(opening),
|
|
||||||
"debit": str(debit),
|
|
||||||
"credit": str(credit),
|
|
||||||
"closing_balance": str(closing),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"period": {
|
|
||||||
"from": date_from,
|
|
||||||
"to": date_to
|
|
||||||
},
|
|
||||||
"totals": {
|
|
||||||
"opening_balance": str(total_opening),
|
|
||||||
"debit": str(total_debit),
|
|
||||||
"credit": str(total_credit),
|
|
||||||
"closing_balance": str(total_closing)
|
|
||||||
},
|
|
||||||
"accounts": result_accounts
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
|
|||||||
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
|
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
|
||||||
FROM journal_lines l
|
FROM journal_lines l
|
||||||
JOIN journal_entries j ON j.journal_id = l.journal_id
|
JOIN journal_entries j ON j.journal_id = l.journal_id
|
||||||
|
AND j.is_deleted = false
|
||||||
WHERE l.account_id = %s
|
WHERE l.account_id = %s
|
||||||
AND j.journal_date < %s
|
AND j.journal_date < %s
|
||||||
""", (aid, date_from))
|
""", (aid, date_from))
|
||||||
@@ -45,6 +46,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
|
|||||||
COALESCE(SUM(l.credit), 0) AS credit
|
COALESCE(SUM(l.credit), 0) AS credit
|
||||||
FROM journal_lines l
|
FROM journal_lines l
|
||||||
JOIN journal_entries j ON j.journal_id = l.journal_id
|
JOIN journal_entries j ON j.journal_id = l.journal_id
|
||||||
|
AND j.is_deleted = false
|
||||||
WHERE l.account_id = %s
|
WHERE l.account_id = %s
|
||||||
AND j.journal_date BETWEEN %s AND %s
|
AND j.journal_date BETWEEN %s AND %s
|
||||||
""", (aid, date_from, date_to))
|
""", (aid, date_from, date_to))
|
||||||
|
|||||||
@@ -19,24 +19,69 @@ class JournalEntryRequest(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
lines: List[JournalLine]
|
lines: List[JournalLine]
|
||||||
|
|
||||||
|
class JournalEntryUpdateRequest(BaseModel):
|
||||||
|
description: str
|
||||||
|
lines: List[JournalLine]
|
||||||
|
updated_reason: str
|
||||||
|
|
||||||
@router.post("")
|
|
||||||
|
class JournalEntryDeleteRequest(BaseModel):
|
||||||
|
deleted_reason: str
|
||||||
|
|
||||||
|
@router.post("", summary="仕訳登録")
|
||||||
def create_journal_entry(req: JournalEntryRequest):
|
def create_journal_entry(req: JournalEntryRequest):
|
||||||
|
|
||||||
|
from app.utils.month_lock import is_month_locked
|
||||||
|
|
||||||
|
if is_month_locked(req.entry_date):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="该月份已锁定,不能新增仕訳"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not req.lines:
|
||||||
|
raise HTTPException(status_code=400, detail="仕訳明細不能为空")
|
||||||
|
|
||||||
|
# (可选)简单的借贷平衡校验
|
||||||
|
total_debit = sum(l.debit for l in req.lines)
|
||||||
|
total_credit = sum(l.credit for l in req.lines)
|
||||||
|
if total_debit != total_credit:
|
||||||
|
raise HTTPException(status_code=400, detail="借方和贷方不平衡")
|
||||||
|
|
||||||
|
fiscal_year = req.entry_date.year
|
||||||
|
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
# 仕訳ヘッダ
|
# 仕訳ヘッダ
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO journal_entries (entry_date, description)
|
INSERT INTO journal_entries (
|
||||||
VALUES (%s, %s)
|
journal_date,
|
||||||
RETURNING id
|
description,
|
||||||
""", (req.entry_date, req.description))
|
fiscal_year,
|
||||||
entry_id = cur.fetchone()[0]
|
version,
|
||||||
|
is_deleted,
|
||||||
|
created_by
|
||||||
|
)
|
||||||
|
VALUES (%s, %s, %s, 1, false, %s)
|
||||||
|
RETURNING journal_id
|
||||||
|
""", (
|
||||||
|
req.entry_date,
|
||||||
|
req.description,
|
||||||
|
fiscal_year,
|
||||||
|
"system" # 以后可以换成登录用户
|
||||||
|
))
|
||||||
|
|
||||||
|
entry_id = cur.fetchone()["journal_id"]
|
||||||
|
|
||||||
# 明細
|
# 明細
|
||||||
for line in req.lines:
|
for line in req.lines:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO journal_lines
|
INSERT INTO journal_lines (
|
||||||
(journal_entry_id, account_id, debit, credit)
|
journal_id,
|
||||||
|
account_id,
|
||||||
|
debit,
|
||||||
|
credit
|
||||||
|
)
|
||||||
VALUES (%s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s)
|
||||||
""", (
|
""", (
|
||||||
entry_id,
|
entry_id,
|
||||||
@@ -47,4 +92,121 @@ def create_journal_entry(req: JournalEntryRequest):
|
|||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
return {"status": "ok", "journal_entry_id": entry_id}
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"journal_entry_id": entry_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{journal_id}")
|
||||||
|
def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest):
|
||||||
|
from app.utils.month_lock import is_month_locked
|
||||||
|
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
|
||||||
|
# ① 取得现有仕訳
|
||||||
|
cur.execute("""
|
||||||
|
SELECT journal_date, is_deleted, version
|
||||||
|
FROM journal_entries
|
||||||
|
WHERE journal_id = %s
|
||||||
|
""", (journal_id,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="仕訳が存在しません")
|
||||||
|
|
||||||
|
if row["is_deleted"]:
|
||||||
|
raise HTTPException(status_code=400, detail="削除済み仕訳は更新できません")
|
||||||
|
|
||||||
|
journal_date = row["journal_date"]
|
||||||
|
|
||||||
|
# 借贷平衡校验(update)
|
||||||
|
total_debit = sum(l.debit for l in req.lines)
|
||||||
|
total_credit = sum(l.credit for l in req.lines)
|
||||||
|
|
||||||
|
if total_debit != total_credit:
|
||||||
|
raise HTTPException(status_code=400, detail="借方和贷方不平衡")
|
||||||
|
|
||||||
|
|
||||||
|
# ② 月次锁定检查
|
||||||
|
if is_month_locked(journal_date):
|
||||||
|
raise HTTPException(status_code=400, detail="该月份已锁定,不能修改仕訳")
|
||||||
|
|
||||||
|
# ③ 更新 header
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE journal_entries
|
||||||
|
SET description = %s,
|
||||||
|
version = version + 1,
|
||||||
|
updated_at = NOW(),
|
||||||
|
updated_reason = %s
|
||||||
|
WHERE journal_id = %s
|
||||||
|
""", (
|
||||||
|
req.description,
|
||||||
|
req.updated_reason,
|
||||||
|
journal_id
|
||||||
|
))
|
||||||
|
|
||||||
|
# ④ 删除原有明细(物理删除,OK)
|
||||||
|
cur.execute("""
|
||||||
|
DELETE FROM journal_lines
|
||||||
|
WHERE journal_id = %s
|
||||||
|
""", (journal_id,))
|
||||||
|
|
||||||
|
# ⑤ 插入新明细
|
||||||
|
for line in req.lines:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO journal_lines (
|
||||||
|
journal_id,
|
||||||
|
account_id,
|
||||||
|
debit,
|
||||||
|
credit
|
||||||
|
) VALUES (%s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
journal_id,
|
||||||
|
line.account_id,
|
||||||
|
line.debit,
|
||||||
|
line.credit
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"journal_id": journal_id,
|
||||||
|
"message": "仕訳を更新しました"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{journal_id}", summary="仕訳删除(软删除)")
|
||||||
|
def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
|
||||||
|
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE journal_entries
|
||||||
|
SET is_deleted = true,
|
||||||
|
deleted_at = NOW(),
|
||||||
|
deleted_reason = %s
|
||||||
|
WHERE journal_id = %s
|
||||||
|
AND is_deleted = false
|
||||||
|
RETURNING journal_id
|
||||||
|
""", (
|
||||||
|
req.deleted_reason,
|
||||||
|
journal_id
|
||||||
|
))
|
||||||
|
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="仕訳不存在,或已被删除"
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "deleted",
|
||||||
|
"journal_entry_id": journal_id
|
||||||
|
}
|
||||||
|
|||||||
93
backend/app/routers/month_locks.py
Normal file
93
backend/app/routers/month_locks.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/month-locks", tags=["月次锁定"])
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- GET:月次锁定一覧 ----------
|
||||||
|
|
||||||
|
@router.get("", summary="月次锁定一覧取得")
|
||||||
|
def get_month_locks():
|
||||||
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
fiscal_year,
|
||||||
|
month,
|
||||||
|
is_locked,
|
||||||
|
locked_at,
|
||||||
|
locked_by
|
||||||
|
FROM month_locks
|
||||||
|
ORDER BY fiscal_year, month
|
||||||
|
""")
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- POST:锁定月份 ----------
|
||||||
|
|
||||||
|
@router.post("/lock", summary="锁定月份")
|
||||||
|
def lock_month(fiscal_year: int, month: int):
|
||||||
|
# 参数校验
|
||||||
|
if not (1 <= month <= 12):
|
||||||
|
raise HTTPException(status_code=400, detail="month 必须是 1~12")
|
||||||
|
|
||||||
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO month_locks (
|
||||||
|
fiscal_year,
|
||||||
|
month,
|
||||||
|
is_locked,
|
||||||
|
locked_at,
|
||||||
|
locked_by
|
||||||
|
)
|
||||||
|
VALUES (%s, %s, true, NOW(), %s)
|
||||||
|
ON CONFLICT (fiscal_year, month)
|
||||||
|
DO UPDATE
|
||||||
|
SET is_locked = true,
|
||||||
|
locked_at = NOW(),
|
||||||
|
locked_by = EXCLUDED.locked_by
|
||||||
|
""", (
|
||||||
|
fiscal_year,
|
||||||
|
month,
|
||||||
|
"system" # 将来可替换为登录用户
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "locked",
|
||||||
|
"fiscal_year": fiscal_year,
|
||||||
|
"month": month
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- POST:解锁月份 ----------
|
||||||
|
|
||||||
|
@router.post("/unlock", summary="解锁月份")
|
||||||
|
def unlock_month(fiscal_year: int, month: int):
|
||||||
|
# 参数校验
|
||||||
|
if not (1 <= month <= 12):
|
||||||
|
raise HTTPException(status_code=400, detail="month 必须是 1~12")
|
||||||
|
|
||||||
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE month_locks
|
||||||
|
SET is_locked = false
|
||||||
|
WHERE fiscal_year = %s
|
||||||
|
AND month = %s
|
||||||
|
""", (
|
||||||
|
fiscal_year,
|
||||||
|
month
|
||||||
|
))
|
||||||
|
|
||||||
|
if cur.rowcount == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="指定月份不存在"
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "unlocked",
|
||||||
|
"fiscal_year": fiscal_year,
|
||||||
|
"month": month
|
||||||
|
}
|
||||||
59
backend/app/routers/trial_balance.py
Normal file
59
backend/app/routers/trial_balance.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from fastapi import APIRouter, Query
|
||||||
|
from datetime import date
|
||||||
|
from typing import Optional
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/trial-balance", tags=["试算表"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", summary="试算表(全科目)")
|
||||||
|
def get_trial_balance(
|
||||||
|
fiscal_year: Optional[int] = Query(None, description="会计年度(可选)"),
|
||||||
|
date_from: Optional[date] = Query(None, description="开始日期(可选)"),
|
||||||
|
date_to: Optional[date] = Query(None, description="结束日期(可选)"),
|
||||||
|
):
|
||||||
|
print("### trial-balance OPTIONAL PARAMS ACTIVE ###")
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
a.account_id,
|
||||||
|
a.account_code,
|
||||||
|
a.account_name,
|
||||||
|
a.account_type,
|
||||||
|
COALESCE(SUM(l.debit), 0) AS debit,
|
||||||
|
COALESCE(SUM(l.credit), 0) AS credit
|
||||||
|
FROM journal_lines l
|
||||||
|
JOIN journal_entries e ON e.journal_id = l.journal_id
|
||||||
|
JOIN accounts a ON a.account_id = l.account_id
|
||||||
|
WHERE 1 = 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if fiscal_year is not None:
|
||||||
|
sql += " AND e.fiscal_year = %(fiscal_year)s"
|
||||||
|
params["fiscal_year"] = fiscal_year
|
||||||
|
|
||||||
|
if date_from is not None:
|
||||||
|
sql += " AND e.journal_date >= %(date_from)s"
|
||||||
|
params["date_from"] = date_from
|
||||||
|
|
||||||
|
if date_to is not None:
|
||||||
|
sql += " AND e.journal_date <= %(date_to)s"
|
||||||
|
params["date_to"] = date_to
|
||||||
|
|
||||||
|
sql += """
|
||||||
|
GROUP BY
|
||||||
|
a.account_id,
|
||||||
|
a.account_code,
|
||||||
|
a.account_name,
|
||||||
|
a.account_type
|
||||||
|
ORDER BY
|
||||||
|
a.account_code
|
||||||
|
"""
|
||||||
|
|
||||||
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute(sql, params)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
return rows
|
||||||
17
backend/app/utils/month_lock.py
Normal file
17
backend/app/utils/month_lock.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from datetime import date
|
||||||
|
from app.core.database import get_connection
|
||||||
|
|
||||||
|
def is_month_locked(target_date: date) -> bool:
|
||||||
|
fiscal_year = target_date.year
|
||||||
|
month = target_date.month
|
||||||
|
|
||||||
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT 1
|
||||||
|
FROM month_locks
|
||||||
|
WHERE fiscal_year = %s
|
||||||
|
AND month = %s
|
||||||
|
AND is_locked = true
|
||||||
|
""", (fiscal_year, month))
|
||||||
|
|
||||||
|
return cur.fetchone() is not None
|
||||||
47
backend/sql/schema.sql
Normal file
47
backend/sql/schema.sql
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION fn_check_month_locked(p_date date)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM month_locks
|
||||||
|
WHERE lock_month = date_trunc('month', p_date)
|
||||||
|
AND is_locked = true
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION
|
||||||
|
'该月份已锁定,不能操作该仕訳(%)',
|
||||||
|
to_char(p_date, 'YYYY-MM');
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION trg_journal_entries_month_lock()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF TG_OP = 'INSERT' THEN
|
||||||
|
PERFORM fn_check_month_locked(NEW.journal_date);
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_OP = 'UPDATE' THEN
|
||||||
|
PERFORM fn_check_month_locked(OLD.journal_date);
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
PERFORM fn_check_month_locked(OLD.journal_date);
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_journal_entries_month_lock
|
||||||
|
BEFORE INSERT OR UPDATE OR DELETE
|
||||||
|
ON journal_entries
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION trg_journal_entries_month_lock();
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>仕訳日</label>
|
<label>仕訳日</label>
|
||||||
<input type="date" id="entryDate" />
|
<input type="date" id="entryDate" onchange="onEntryDateChanged()"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -37,6 +37,14 @@
|
|||||||
<label>仮受消費税等 勘定</label>
|
<label>仮受消費税等 勘定</label>
|
||||||
<select id="taxReceivedSelect"></select>
|
<select id="taxReceivedSelect"></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label>消費税 端数処理:</label>
|
||||||
|
<select id="roundingMode">
|
||||||
|
<option value="round">四捨五入</option>
|
||||||
|
<option value="floor">切捨て</option>
|
||||||
|
<option value="ceil">切上げ</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table id="linesTable">
|
<table id="linesTable">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -57,7 +65,43 @@
|
|||||||
<button onclick="submitJournal()">登録</button>
|
<button onclick="submitJournal()">登録</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>月次锁定管理</h3>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 8px;">
|
||||||
|
<label>年度:</label>
|
||||||
|
<input type="number" id="lockYear" value="2025" style="width:80px;">
|
||||||
|
|
||||||
|
<label>月份:</label>
|
||||||
|
<input type="number" id="lockMonth" min="1" max="12" value="6" style="width:60px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="lockMonth()">🔒 锁定</button>
|
||||||
|
<button onclick="unlockMonth()">🔓 解锁</button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="lockResult" style="margin-top:10px;color:#333;"></div>
|
||||||
|
|
||||||
|
<div style="margin-top:12px;">
|
||||||
|
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
|
||||||
|
<pre id="lockList" style="background:#f7f7f7;padding:8px;"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="monthLockBanner"
|
||||||
|
style="display:none;
|
||||||
|
margin:8px 0;
|
||||||
|
padding:6px 10px;
|
||||||
|
background:#ffecec;
|
||||||
|
border:1px solid #ffaaaa;
|
||||||
|
color:#a00000;">
|
||||||
|
🔒 该月份已锁定,不能编辑仕訳
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
let rowSeq = 0; // 各行唯一ID
|
||||||
|
|
||||||
const API = "http://127.0.0.1:18080";
|
const API = "http://127.0.0.1:18080";
|
||||||
|
|
||||||
let accounts = [];
|
let accounts = [];
|
||||||
@@ -115,10 +159,16 @@ function setupTaxSelectors() {
|
|||||||
// -----------------------------
|
// -----------------------------
|
||||||
// 行追加
|
// 行追加
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
function addRow(isTaxRow = false, taxInfo = null) {
|
function addRow(isTaxRow = false, taxInfo = null, parentId = null) {
|
||||||
const tbody = document.querySelector("#linesTable tbody");
|
const tbody = document.querySelector("#linesTable tbody");
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
if (isTaxRow) tr.classList.add("tax-row");
|
|
||||||
|
if (!isTaxRow) {
|
||||||
|
tr.dataset.rowId = String(++rowSeq);
|
||||||
|
} else {
|
||||||
|
tr.classList.add("tax-row");
|
||||||
|
tr.dataset.parentId = parentId;
|
||||||
|
}
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>
|
<td>
|
||||||
@@ -149,26 +199,46 @@ function addRow(isTaxRow = false, taxInfo = null) {
|
|||||||
|
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
if (!isTaxRow) {
|
// 税行の場合,填值
|
||||||
tr.querySelector(".debit").addEventListener("input", () => handleTax(tr));
|
|
||||||
tr.querySelector(".credit").addEventListener("input", () => handleTax(tr));
|
|
||||||
tr.querySelector(".taxType").addEventListener("change", () => handleTax(tr));
|
|
||||||
tr.querySelector(".taxDirection").addEventListener("change", () => handleTax(tr));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTaxRow && taxInfo) {
|
if (isTaxRow && taxInfo) {
|
||||||
const acc = tr.querySelector(".account");
|
const acc = tr.querySelector(".account");
|
||||||
acc.value = taxInfo.accountId || "";
|
acc.value = taxInfo.accountId || "";
|
||||||
if (taxInfo.side === "debit") tr.querySelector(".debit").value = taxInfo.amount;
|
if (taxInfo.side === "debit") tr.querySelector(".debit").value = taxInfo.amount;
|
||||||
if (taxInfo.side === "credit") tr.querySelector(".credit").value = taxInfo.amount;
|
if (taxInfo.side === "credit") tr.querySelector(".credit").value = taxInfo.amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 普通行:绑定事件
|
||||||
|
if (!isTaxRow) {
|
||||||
|
const recalc = () => handleTax(tr);
|
||||||
|
tr.querySelector(".debit").addEventListener("input", recalc);
|
||||||
|
tr.querySelector(".credit").addEventListener("input", recalc);
|
||||||
|
tr.querySelector(".taxType").addEventListener("change", recalc);
|
||||||
|
tr.querySelector(".taxDirection").addEventListener("change", recalc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除该行的所有税行
|
||||||
|
function removeTaxRowsOf(row) {
|
||||||
|
const id = row.dataset.rowId;
|
||||||
|
if (!id) return;
|
||||||
|
document.querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`).forEach(r => r.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(btn) {
|
||||||
|
const row = btn.closest("tr");
|
||||||
|
removeTaxRowsOf(row);
|
||||||
|
row.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// 税行 自動生成/更新(10%/8%のみ、四捨五入)
|
// 税行 自動生成/更新(10%/8%のみ、四捨五入)
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
function handleTax(row) {
|
function handleTax(row) {
|
||||||
removeTaxRow(row);
|
// 清理旧税行
|
||||||
|
removeTaxRowsOf(row);
|
||||||
|
|
||||||
const taxType = row.querySelector(".taxType").value;
|
const taxType = row.querySelector(".taxType").value;
|
||||||
const taxDir = row.querySelector(".taxDirection").value;
|
const taxDir = row.querySelector(".taxDirection").value;
|
||||||
@@ -176,28 +246,48 @@ function handleTax(row) {
|
|||||||
const credit = Number(row.querySelector(".credit").value || 0);
|
const credit = Number(row.querySelector(".credit").value || 0);
|
||||||
|
|
||||||
if (taxType === "none" || taxDir === "none") return;
|
if (taxType === "none" || taxDir === "none") return;
|
||||||
const baseAmount = debit || credit;
|
|
||||||
if (!baseAmount) return;
|
const totalAmount = debit || credit;
|
||||||
|
if (!totalAmount) return;
|
||||||
|
|
||||||
const rate = taxType === "8" ? 0.08 : 0.10;
|
const rate = taxType === "8" ? 0.08 : 0.10;
|
||||||
const taxAmount = Math.round(baseAmount * rate);
|
const roundingMode = document.getElementById("roundingMode").value;
|
||||||
|
|
||||||
|
// 税込 → 税額逆算
|
||||||
|
const rawTax = totalAmount * rate / (1 + rate);
|
||||||
|
let taxAmount;
|
||||||
|
switch (roundingMode) {
|
||||||
|
case "floor": taxAmount = Math.floor(rawTax); break;
|
||||||
|
case "ceil": taxAmount = Math.ceil(rawTax); break;
|
||||||
|
default: taxAmount = Math.round(rawTax);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentId = row.dataset.rowId;
|
||||||
|
|
||||||
if (taxDir === "paid") {
|
if (taxDir === "paid") {
|
||||||
const taxAcc = document.getElementById("taxPaidSelect").value;
|
const taxAcc = document.getElementById("taxPaidSelect").value;
|
||||||
if (!taxAcc) { alert("仮払消費税等 勘定を選択してください"); return; }
|
if (!taxAcc) return alert("仮払消費税等 勘定を選択してください");
|
||||||
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "debit" });
|
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "debit" }, parentId);
|
||||||
}
|
}
|
||||||
if (taxDir === "received") {
|
if (taxDir === "received") {
|
||||||
const taxAcc = document.getElementById("taxReceivedSelect").value;
|
const taxAcc = document.getElementById("taxReceivedSelect").value;
|
||||||
if (!taxAcc) { alert("仮受消費税等 勘定を選択してください"); return; }
|
if (!taxAcc) return alert("仮受消費税等 勘定を選択してください");
|
||||||
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "credit" });
|
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "credit" }, parentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 税行は常に「直後の1行」を使う
|
// 税行は常に「直後の1行」を使う
|
||||||
function removeTaxRow(row) {
|
function removeTaxRow(row) {
|
||||||
const next = row.nextElementSibling;
|
const id = row.dataset.rowId;
|
||||||
if (next && next.classList.contains("tax-row")) next.remove();
|
if (!id) return;
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`)
|
||||||
|
.forEach(tr => tr.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRow(btn) {
|
function removeRow(btn) {
|
||||||
@@ -254,6 +344,137 @@ async function submitJournal() {
|
|||||||
alert(`登録完了(ID=${result.journal_entry_id})`);
|
alert(`登録完了(ID=${result.journal_entry_id})`);
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const API_BASE = "http://127.0.0.1:18080";
|
||||||
|
|
||||||
|
async function callApi(path) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, { method: "POST" });
|
||||||
|
const text = await res.text();
|
||||||
|
let data = {};
|
||||||
|
try { data = text ? JSON.parse(text) : {}; } catch { data = { detail: text }; }
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(data.detail || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e.message || "请求失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lockMonth() {
|
||||||
|
const year = document.getElementById("lockYear").value;
|
||||||
|
const month = document.getElementById("lockMonth").value;
|
||||||
|
|
||||||
|
const el = document.getElementById("lockResult");
|
||||||
|
el.innerText = "处理中...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callApi(`/month-locks/lock?fiscal_year=${year}&month=${month}`);
|
||||||
|
el.innerText = `✅ ${year}年${month}月 已锁定`;
|
||||||
|
await refreshMonthLocks(); // 可选:自动刷新列表
|
||||||
|
} catch (e) {
|
||||||
|
el.innerText = `❌ 错误:${e.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlockMonth() {
|
||||||
|
const year = document.getElementById("lockYear").value;
|
||||||
|
const month = document.getElementById("lockMonth").value;
|
||||||
|
|
||||||
|
const el = document.getElementById("lockResult");
|
||||||
|
el.innerText = "处理中...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callApi(`/month-locks/unlock?fiscal_year=${year}&month=${month}`);
|
||||||
|
el.innerText = `✅ ${year}年${month}月 已解锁`;
|
||||||
|
await refreshMonthLocks(); // 可选:自动刷新列表
|
||||||
|
} catch (e) {
|
||||||
|
el.innerText = `❌ 错误:${e.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function refreshMonthLocks() {
|
||||||
|
const el = document.getElementById("lockList");
|
||||||
|
el.textContent = "读取中...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/month-locks`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// 只显示 locked 的,避免太长
|
||||||
|
const locked = (data || []).filter(x => x.is_locked);
|
||||||
|
|
||||||
|
if (locked.length === 0) {
|
||||||
|
el.textContent = "(当前没有锁定月份)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.textContent = locked
|
||||||
|
.map(x => `${x.fiscal_year}-${String(x.month).padStart(2, "0")} locked_by=${x.locked_by ?? ""}`)
|
||||||
|
.join("\n");
|
||||||
|
} catch (e) {
|
||||||
|
el.textContent = `读取失败:${e.message || e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function checkMonthLockByDate(dateStr) {
|
||||||
|
if (!dateStr) return false;
|
||||||
|
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = d.getMonth() + 1;
|
||||||
|
|
||||||
|
const res = await fetch(`${API_BASE}/month-locks`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
return (data || []).some(
|
||||||
|
x => x.fiscal_year === year && x.month === month && x.is_locked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onEntryDateChanged() {
|
||||||
|
const dateStr = document.getElementById("entryDate").value;
|
||||||
|
const locked = await checkMonthLockByDate(dateStr);
|
||||||
|
|
||||||
|
const banner = document.getElementById("monthLockBanner");
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
banner.style.display = "block";
|
||||||
|
setJournalReadonly(true);
|
||||||
|
} else {
|
||||||
|
banner.style.display = "none";
|
||||||
|
setJournalReadonly(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setJournalReadonly(readonly) {
|
||||||
|
// 保存按钮
|
||||||
|
const saveBtn = document.getElementById("saveButton");
|
||||||
|
if (saveBtn) saveBtn.disabled = readonly;
|
||||||
|
|
||||||
|
// 行追加按钮
|
||||||
|
const addRowBtn = document.getElementById("addRowButton");
|
||||||
|
if (addRowBtn) addRowBtn.disabled = readonly;
|
||||||
|
|
||||||
|
// 所有“删除行”按钮
|
||||||
|
document.querySelectorAll(".delete-row-btn").forEach(btn => {
|
||||||
|
btn.disabled = readonly;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 所有仕訳输入项(金额 / 科目等)
|
||||||
|
document.querySelectorAll(".journal-input").forEach(input => {
|
||||||
|
input.disabled = readonly;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,40 +1,32 @@
|
|||||||
async function loadTrialBalance() {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const from = document.getElementById("from").value;
|
console.log("trial-balance.js 已执行");
|
||||||
const to = document.getElementById("to").value;
|
const API_URL = "http://127.0.0.1:18080/trial-balance";
|
||||||
|
|
||||||
if (!from || !to) {
|
fetch(API_URL)
|
||||||
alert("期間を指定してください");
|
.then(r => { if (!r.ok) throw new Error("API 返回错误: " + r.status); return r.json(); })
|
||||||
return;
|
.then(data => {
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
console.log("试算表 API 数据:", data);
|
||||||
const data = await apiGet("/trial-balance", {
|
const tbody = document.querySelector("#trialBalanceTable tbody");
|
||||||
date_from: from,
|
if (!tbody) { console.error("未找到 #trialBalanceTable tbody"); return; }
|
||||||
date_to: to,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tbody = document.querySelector("#trial-table tbody");
|
let totalDebit = 0, totalCredit = 0;
|
||||||
tbody.innerHTML = "";
|
data.accounts.forEach(row => {
|
||||||
|
const debit = Number(row.debit ?? 0);
|
||||||
|
const credit = Number(row.credit ?? 0);
|
||||||
|
totalDebit += debit; totalCredit += credit;
|
||||||
|
|
||||||
data.accounts.forEach((acc) => {
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${acc.account_code}</td>
|
<td class="left">${row.account_code}</td>
|
||||||
<td>
|
<td class="left">${row.account_name}</td>
|
||||||
<a href="ledger.html?account_id=${acc.account_id}&from=${from}&to=${to}">
|
<td>${debit.toLocaleString()}</td>
|
||||||
${acc.account_name}
|
<td>${credit.toLocaleString()}</td>`;
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>${acc.opening_balance}</td>
|
|
||||||
<td>${acc.debit}</td>
|
|
||||||
<td>${acc.credit}</td>
|
|
||||||
<td>${acc.closing_balance}</td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
alert(e.message);
|
document.getElementById("totalDebit").textContent = totalDebit.toLocaleString();
|
||||||
}
|
document.getElementById("totalCredit").textContent = totalCredit.toLocaleString();
|
||||||
}
|
})
|
||||||
|
.catch(err => { console.error("試算表读取失败:", err); alert("試算表读取失败,请查看控制台日志"); });
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,37 +1,56 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ja">
|
<html lang="ja">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>試算表</title>
|
<title>试算表</title>
|
||||||
<link rel="stylesheet" href="css/style.css" />
|
<style>
|
||||||
</head>
|
table {
|
||||||
<body>
|
border-collapse: collapse;
|
||||||
<h1>試算表</h1>
|
width: 100%;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
td.left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
tfoot td {
|
||||||
|
font-weight: bold;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
<label>
|
<h2>試算表</h2>
|
||||||
期首:
|
|
||||||
<input type="date" id="from" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
期末:
|
|
||||||
<input type="date" id="to" />
|
|
||||||
</label>
|
|
||||||
<button id="load">表示</button>
|
|
||||||
|
|
||||||
<table border="1">
|
<table id="trialBalanceTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>科目</th>
|
<th>科目代码</th>
|
||||||
<th>期首</th>
|
<th>科目名称</th>
|
||||||
<th>借方</th>
|
<th>借方合计</th>
|
||||||
<th>貸方</th>
|
<th>贷方合计</th>
|
||||||
<th>期末</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="tb-body"></tbody>
|
<tbody>
|
||||||
</table>
|
<!-- JS 动态插入 -->
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">合计</td>
|
||||||
|
<td id="totalDebit">0</td>
|
||||||
|
<td id="totalCredit">0</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
<script src="js/api.js"></script>
|
<script src="./js/trial-balance.js"></script>
|
||||||
<script src="js/trial-balance.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
14
メモ.txt
14
メモ.txt
@@ -105,3 +105,17 @@ pip install psycopg[binary]==3.2.3
|
|||||||
|
|
||||||
|
|
||||||
C:\workspace\njts-accounting-core\backend uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
C:\workspace\njts-accounting-core\backend uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
version = 1:初始输入
|
||||||
|
|
||||||
|
version >= 2:你改过
|
||||||
|
|
||||||
|
updated_at:最后一次修改时间
|
||||||
|
|
||||||
|
updated_reason:
|
||||||
|
|
||||||
|
可空
|
||||||
|
|
||||||
|
例如:金额修正、补录摘要、对账调整
|
||||||
Reference in New Issue
Block a user