20251215滴修正

This commit is contained in:
FNOS-WIN11ENT\choshoukaku
2025-12-15 14:49:59 +09:00
parent 74dcb88fe6
commit b6751ced18
11 changed files with 851 additions and 46 deletions

View File

@@ -61,7 +61,8 @@ app.include_router(general_ledger_router)
from app.modules.opening_balances.lock_router import router as opening_balance_lock_router from app.modules.opening_balances.lock_router import router as opening_balance_lock_router
app.include_router(opening_balance_lock_router) app.include_router(opening_balance_lock_router)
from app.routers import journal_entries
app.include_router(journal_entries.router)

View File

@@ -0,0 +1,16 @@
from pydantic import BaseModel
from typing import List
from decimal import Decimal
from datetime import date
class JournalLine(BaseModel):
account_id: int
debit: Decimal = Decimal("0")
credit: Decimal = Decimal("0")
class JournalEntryRequest(BaseModel):
entry_date: date
description: str
lines: List[JournalLine]

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel, field_validator
from typing import List from typing import List, Optional, Tuple
from decimal import Decimal from decimal import Decimal, InvalidOperation
from app.core.database import get_connection from app.core.database import get_connection
@@ -15,13 +15,33 @@ class OpeningBalanceItem(BaseModel):
debit: Decimal = Decimal("0") debit: Decimal = Decimal("0")
credit: Decimal = Decimal("0") credit: Decimal = Decimal("0")
@field_validator("debit", "credit", mode="before")
@classmethod
def to_decimal(cls, v):
try:
d = Decimal(str(v))
except InvalidOperation:
raise ValueError("金額は数値で入力してください")
if d < 0:
raise ValueError("金額は0以上で入力してください")
return d
@field_validator("credit")
@classmethod
def exclusive_debit_credit(cls, v, info):
# 同時に > 0 は不可0/0 は許容:行スキップ用)
debit = info.data.get("debit", Decimal("0"))
if v > 0 and Decimal(debit) > 0:
raise ValueError("借方と貸方を同時に入力しないでください")
return v
class OpeningBalanceRequest(BaseModel): class OpeningBalanceRequest(BaseModel):
fiscal_year: int fiscal_year: int
balances: List[OpeningBalanceItem] balances: List[OpeningBalanceItem]
# ---------- GET期首残高取得 ---------- # ---------- GET期首残高取得BS のみ) ----------
@router.get("", summary="期首残高取得") @router.get("", summary="期首残高取得")
def get_opening_balances( def get_opening_balances(
@@ -41,68 +61,142 @@ def get_opening_balances(
ON o.account_id = a.account_id ON o.account_id = a.account_id
AND o.fiscal_year = %s AND o.fiscal_year = %s
WHERE a.is_active = true WHERE a.is_active = true
AND a.account_type IN ('asset','liability','equity')
ORDER BY a.account_code ORDER BY a.account_code
""", (fiscal_year,)) """, (fiscal_year,))
return cur.fetchall() return cur.fetchall()
# ---------- POST期首残高保存 ---------- # ---------- 内部 util繰越利益剰余金アカウント検索 ----------
def _find_retained_earnings(cur) -> Optional[Tuple[int, str]]:
"""
可能な名称で繰越利益(剰余金)科目を探す。
優先:'繰越利益剰余金''繰越利益'
戻り値: (account_id, account_name) or None
"""
cur.execute("""
SELECT account_id, account_name
FROM accounts
WHERE is_active = true
AND account_type = 'equity'
AND account_name IN ('繰越利益剰余金','繰越利益')
ORDER BY CASE account_name
WHEN '繰越利益剰余金' THEN 1
WHEN '繰越利益' THEN 2
ELSE 99
END
LIMIT 1
""")
row = cur.fetchone()
return (row["account_id"], row["account_name"]) if row else None
# ---------- POST期首残高保存BSのみ・繰越利益は自動計算 ----------
@router.post("", summary="期首残高保存") @router.post("", summary="期首残高保存")
def save_opening_balances(req: OpeningBalanceRequest): def save_opening_balances(req: OpeningBalanceRequest):
# 年度锁定チェック
cur.execute("""
SELECT is_locked
FROM fiscal_year_locks
WHERE fiscal_year = %s
""", (req.fiscal_year,))
row = cur.fetchone()
if row and row["is_locked"]:
raise HTTPException(
status_code=400,
detail="この会計年度の期首残高は既に確定されています。"
)
total_debit = Decimal("0")
total_credit = Decimal("0")
for b in req.balances:
total_debit += b.debit
total_credit += b.credit
if total_debit != total_credit:
raise HTTPException(
status_code=400,
detail="借方合計と貸方合計が一致していません。"
)
with get_connection() as conn, conn.cursor() as cur: with get_connection() as conn, conn.cursor() as cur:
# 年度ロック
# 既存データ削除(年度単位)
cur.execute(""" cur.execute("""
DELETE FROM opening_balances SELECT is_locked
FROM fiscal_year_locks
WHERE fiscal_year = %s WHERE fiscal_year = %s
""", (req.fiscal_year,)) """, (req.fiscal_year,))
row = cur.fetchone()
if row and row["is_locked"]:
raise HTTPException(status_code=400, detail="この会計年度の期首残高は既に確定されています。")
# 全 BS 科目取得(存在性/種別チェック用)
cur.execute("""
SELECT account_id, account_type, account_name
FROM accounts
WHERE is_active = true
""")
acc_map = {r["account_id"]: (r["account_type"], r["account_name"]) for r in cur.fetchall()}
# 入力の整形BSのみ採用・繰越利益は無視後で自動作成
bs_items: List[OpeningBalanceItem] = []
total_debit = Decimal("0")
total_credit = Decimal("0")
# 再登録
for b in req.balances: for b in req.balances:
# 科目存在
if b.account_id not in acc_map:
raise HTTPException(status_code=400, detail=f"存在しない科目IDです{b.account_id}")
acc_type, acc_name = acc_map[b.account_id]
# 繰越利益(剰余金)は前端から送られても無視(自動計算対象)
if acc_name in ("繰越利益剰余金", "繰越利益"):
continue
# BS 以外は不可
if acc_type not in ("asset", "liability", "equity"):
raise HTTPException(status_code=400, detail=f"BS科目のみ設定可能です{acc_name}")
# 0/0 はスキップ
if b.debit == 0 and b.credit == 0: if b.debit == 0 and b.credit == 0:
continue continue
# 集計(繰越利益を除く)
total_debit += b.debit
total_credit += b.credit
bs_items.append(b)
# 繰越利益(剰余金)を自動計算
diff = total_debit - total_credit # 借方合計 貸方合計
re_account = _find_retained_earnings(cur)
# 既存データ削除(年度単位)
cur.execute("DELETE FROM opening_balances WHERE fiscal_year = %s", (req.fiscal_year,))
# 再登録BS各行
for b in bs_items:
cur.execute(""" cur.execute("""
INSERT INTO opening_balances INSERT INTO opening_balances
(fiscal_year, account_id, opening_debit, opening_credit) (fiscal_year, account_id, opening_debit, opening_credit)
VALUES VALUES (%s, %s, %s, %s)
(%s, %s, %s, %s) """, (req.fiscal_year, b.account_id, b.debit, b.credit))
""", (
req.fiscal_year, # 自動:繰越利益(剰余金)
b.account_id, re_inserted = None
b.debit, if re_account:
b.credit re_id, re_name = re_account
)) if diff > 0:
# 借方が大 → その差額を「繰越利益(剰余金)の貸方」
cur.execute("""
INSERT INTO opening_balances (fiscal_year, account_id, opening_debit, opening_credit)
VALUES (%s, %s, %s, %s)
""", (req.fiscal_year, re_id, Decimal("0"), diff))
re_inserted = {"account_id": re_id, "name": re_name, "debit": "0", "credit": str(diff)}
elif diff < 0:
# 贷方が大 → その差額を「繰越利益(剰余金)の借方」
cur.execute("""
INSERT INTO opening_balances (fiscal_year, account_id, opening_debit, opening_credit)
VALUES (%s, %s, %s, %s)
""", (req.fiscal_year, re_id, -diff, Decimal("0")))
re_inserted = {"account_id": re_id, "name": re_name, "debit": str(-diff), "credit": "0"}
else:
# 完全一致 → 追加不要
pass
else:
# 繰越利益科目が登録されていない場合:差額が 0 でないなら警告
if diff != 0:
raise HTTPException(
status_code=400,
detail="差額がありますが『繰越利益(剰余金)』科目が見つかりません。先に科目マスタを登録してください。"
)
conn.commit() conn.commit()
return {"message": "期首残高を保存しました。"} return {
"message": "期首残高を保存しました。",
"totals": {
"debit": str(total_debit),
"credit": str(total_credit),
"diff_before_retained": str(diff)
},
"retained_earnings_auto": re_inserted
}

View File

View File

View File

@@ -0,0 +1,232 @@
from fastapi import APIRouter, HTTPException, Query
from typing import Optional
from decimal import Decimal
from datetime import date
from app.core.database import get_connection
from app.models.journal_entry import JournalEntryRequest
router = APIRouter(prefix="/journal-entries", tags=["仕訳"])
# -------------------------------------------------
# 仕訳登録
# -------------------------------------------------
@router.post("", summary="仕訳登録")
def create_journal_entry(req: JournalEntryRequest):
if len(req.lines) < 2:
raise HTTPException(status_code=400, detail="仕訳行は2行以上必要です")
if req.entry_date < date(2025, 6, 1):
raise HTTPException(status_code=400, detail="仕訳日は2025-06-01以降である必要があります")
debit_total = Decimal("0")
credit_total = Decimal("0")
for line in req.lines:
if line.debit < 0 or line.credit < 0:
raise HTTPException(status_code=400, detail="金額は正数で入力してください")
if line.debit > 0 and line.credit > 0:
raise HTTPException(status_code=400, detail="同一行で借方・貸方の同時入力は不可です")
debit_total += line.debit
credit_total += line.credit
if debit_total != credit_total:
raise HTTPException(
status_code=400,
detail=f"借方合計({debit_total})と貸方合計({credit_total})が一致しません"
)
with get_connection() as conn, conn.cursor() as cur:
# 期首ロック確認
cur.execute("""
SELECT is_locked
FROM opening_balance_locks
WHERE fiscal_year = 2025
""")
row = cur.fetchone()
if not row or not row[0]:
raise HTTPException(status_code=400, detail="期首残高が確定lockされていません")
# ヘッダ登録
cur.execute("""
INSERT INTO journal_entries (entry_date, description)
VALUES (%s, %s)
RETURNING journal_entry_id
""", (req.entry_date, req.description))
journal_entry_id = cur.fetchone()[0]
# 明細登録
for line in req.lines:
cur.execute("""
INSERT INTO journal_lines
(journal_entry_id, account_id, debit, credit)
VALUES (%s, %s, %s, %s)
""", (
journal_entry_id,
line.account_id,
line.debit,
line.credit
))
return {
"message": "仕訳を登録しました",
"journal_entry_id": journal_entry_id
}
# -------------------------------------------------
# 仕訳一覧取得
# -------------------------------------------------
@router.get("", summary="仕訳一覧取得")
def list_journal_entries(
from_date: Optional[str] = Query(None),
to_date: Optional[str] = Query(None),
keyword: Optional[str] = Query(None)
):
sql = """
SELECT
je.journal_entry_id,
je.entry_date,
je.description,
SUM(jl.debit) AS debit_total,
SUM(jl.credit) AS credit_total
FROM journal_entries je
JOIN journal_lines jl
ON jl.journal_entry_id = je.journal_entry_id
WHERE 1=1
"""
params = []
if from_date:
sql += " AND je.entry_date >= %s"
params.append(from_date)
if to_date:
sql += " AND je.entry_date <= %s"
params.append(to_date)
if keyword:
sql += " AND je.description ILIKE %s"
params.append(f"%{keyword}%")
sql += """
GROUP BY je.journal_entry_id, je.entry_date, je.description
ORDER BY je.entry_date DESC, je.journal_entry_id DESC
"""
with get_connection() as conn, conn.cursor() as cur:
cur.execute(sql, params)
rows = cur.fetchall()
return [
{
"journal_entry_id": r[0],
"entry_date": r[1],
"description": r[2],
"debit_total": r[3],
"credit_total": r[4],
}
for r in rows
]
# -------------------------------------------------
# 仕訳明細取得
# -------------------------------------------------
@router.get("/{journal_entry_id}", summary="仕訳明細取得")
def get_journal_entry(journal_entry_id: int):
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
SELECT entry_date, description
FROM journal_entries
WHERE journal_entry_id = %s
""", (journal_entry_id,))
header = cur.fetchone()
if not header:
raise HTTPException(status_code=404, detail="仕訳が存在しません")
cur.execute("""
SELECT
jl.account_id,
a.account_code,
a.account_name,
jl.debit,
jl.credit
FROM journal_lines jl
JOIN accounts a ON a.account_id = jl.account_id
WHERE jl.journal_entry_id = %s
""", (journal_entry_id,))
lines = cur.fetchall()
return {
"entry_date": header[0],
"description": header[1],
"lines": [
{
"account_id": l[0],
"account_code": l[1],
"account_name": l[2],
"debit": l[3],
"credit": l[4]
} for l in lines
]
}
# -------------------------------------------------
# 逆仕訳生成(修正仕訳)
# -------------------------------------------------
@router.post("/{journal_entry_id}/reverse", summary="逆仕訳生成")
def reverse_journal_entry(journal_entry_id: int):
with get_connection() as conn, conn.cursor() as cur:
# 元仕訳取得
cur.execute("""
SELECT entry_date, description
FROM journal_entries
WHERE journal_entry_id = %s
""", (journal_entry_id,))
header = cur.fetchone()
if not header:
raise HTTPException(status_code=404, detail="仕訳が存在しません")
entry_date, description = header
cur.execute("""
SELECT account_id, debit, credit
FROM journal_lines
WHERE journal_entry_id = %s
""", (journal_entry_id,))
lines = cur.fetchall()
if not lines:
raise HTTPException(status_code=400, detail="仕訳明細が存在しません")
# 逆仕訳ヘッダ
cur.execute("""
INSERT INTO journal_entries (entry_date, description)
VALUES (%s, %s)
RETURNING journal_entry_id
""", (entry_date, f"【修正】{description}"))
new_entry_id = cur.fetchone()[0]
# 逆仕訳明細(借贷反転)
for account_id, debit, credit in lines:
cur.execute("""
INSERT INTO journal_lines
(journal_entry_id, account_id, debit, credit)
VALUES (%s, %s, %s, %s)
""", (
new_entry_id,
account_id,
credit,
debit
))
return {
"message": "逆仕訳を作成しました",
"reversed_journal_entry_id": new_entry_id
}

View File

Binary file not shown.

View File

@@ -0,0 +1,41 @@
-- =========================
-- BS 科目マスタ3桁コード
-- =========================
-- 資産
INSERT INTO accounts (account_code, account_name, account_type) VALUES
('101', '現金', 'asset'),
('102', '普通預金', 'asset'),
('103', '当座預金', 'asset'),
('104', '定期預金', 'asset'),
('201', '売掛金', 'asset'),
('202', '未収入金', 'asset'),
('203', '仮払金', 'asset'),
('204', '前払費用', 'asset'),
('205', '立替金', 'asset'),
('301', '有価証券', 'asset'),
('401', '建物', 'asset'),
('402', '建物附属設備', 'asset'),
('403', '構築物', 'asset'),
('404', '機械装置', 'asset'),
('405', '車両運搬具', 'asset'),
('406', '工具器具備品', 'asset'),
('407', '土地', 'asset'),
('408', 'ソフトウェア', 'asset');
-- 負債
INSERT INTO accounts (account_code, account_name, account_type) VALUES
('501', '買掛金', 'liability'),
('502', '未払金', 'liability'),
('503', '未払費用', 'liability'),
('504', '未払法人税等', 'liability'),
('505', '未払消費税等', 'liability'),
('506', '預り金', 'liability'),
('507', '前受金', 'liability'),
('508', '短期借入金', 'liability'),
('509', '長期借入金', 'liability');
-- 純資産
INSERT INTO accounts (account_code, account_name, account_type) VALUES
('601', '資本金', 'equity'),
('602', '繰越利益剰余金', 'equity');

324
frontend/journal-entry.html Normal file
View File

@@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>仕訳入力(複数行・消費税自動)</title>
<style>
body { font-family: sans-serif; margin: 24px; }
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
th, td { border: 1px solid #ccc; padding: 6px; text-align: center; }
th { background: #f5f5f5; }
input, select, button { padding: 6px; }
.right { text-align: right; }
.error { color: #c00; margin-top: 8px; }
.ok { color: #090; margin-top: 8px; }
.row { display: flex; gap: 12px; align-items: center; margin: 8px 0; }
.row > * { flex: 1; }
</style>
</head>
<body>
<h2><div class="row">
<div>
<label>消費税 端数処理</label>
<select id="roundingMode">
<option value="round">四捨五入</option>
<option value="floor">切捨て</option>
<option value="ceil">切上げ</option>
</select>
</div>
</div></h2>
<div class="row">
<div>
<label>仕訳日</label>
<input type="date" id="entryDate" />
</div>
<div style="flex:2">
<label>摘要</label>
<input type="text" id="description" placeholder="例:売上計上/経費支払" style="width:100%" />
</div>
</div>
<div class="row">
<div>
<label>仮払消費税等 勘定</label>
<select id="taxDebitAccount"></select>
</div>
<div>
<label>仮受消費税等 勘定</label>
<select id="taxCreditAccount"></select>
</div>
</div>
<table id="linesTable">
<thead>
<tr>
<th>科目</th>
<th>借方</th>
<th>貸方</th>
<th>税区分</th>
<th>税方向</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<th>合計</th>
<th id="debitTotal" class="right">0</th>
<th id="creditTotal" class="right">0</th>
<th colspan="3"></th>
</tr>
</tfoot>
</table>
<button onclick="addRow()"> 行追加</button>
<button onclick="submitEntry()">登録</button>
<div id="result"></div>
<script>
const API_BASE = "http://127.0.0.1:18080";
let accounts = [];
const TAX_OPTIONS = [
{ value: "none", label: "非課税" },
{ value: "10-excl", label: "10%(税抜)" },
{ value: "10-incl", label: "10%(税込)" },
{ value: "8-excl", label: "8%(税抜)" },
{ value: "8-incl", label: "8%(税込)" }
];
const TAX_DIRECTIONS = [
{ value: "karibarai", label: "仮払(仕入・経費側)" },
{ value: "kariuke", label: "仮受(売上側)" }
];
// ------------------------
// 初期化
// ------------------------
async function init() {
const res = await fetch(`${API_BASE}/accounts`);
accounts = await res.json();
// 税勘定候補を抽出
const taxDebitSel = document.getElementById("taxDebitAccount");
const taxCreditSel = document.getElementById("taxCreditAccount");
const taxDebitCandidates = accounts.filter(a => a.account_name.includes("仮払消費税"));
const taxCreditCandidates = accounts.filter(a => a.account_name.includes("仮受消費税"));
// 仮払
fillSelect(taxDebitSel, taxDebitCandidates.length ? taxDebitCandidates : accounts);
// 仮受
fillSelect(taxCreditSel, taxCreditCandidates.length ? taxCreditCandidates : accounts);
addRow();
addRow();
recalc();
}
function fillSelect(sel, list) {
sel.innerHTML = "";
list.forEach(a => {
const opt = document.createElement("option");
opt.value = a.account_id;
opt.textContent = `${a.account_code} ${a.account_name}`;
sel.appendChild(opt);
});
}
// ------------------------
// 行追加・削除
// ------------------------
function addRow() {
const tbody = document.querySelector("#linesTable tbody");
const tr = document.createElement("tr");
tr.innerHTML = `
<td>
<select class="account">
${accounts.map(a =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
).join("")}
</select>
</td>
<td><input type="number" min="0" class="debit" oninput="onDebitInput(this)" /></td>
<td><input type="number" min="0" class="credit" oninput="onCreditInput(this)" /></td>
<td>
<select class="tax">
${TAX_OPTIONS.map(t => `<option value="${t.value}">${t.label}</option>`).join("")}
</select>
</td>
<td>
<select class="taxdir">
${TAX_DIRECTIONS.map(t => `<option value="${t.value}">${t.label}</option>`).join("")}
</select>
</td>
<td><button onclick="removeRow(this)">削除</button></td>
`;
tbody.appendChild(tr);
}
function removeRow(btn) {
btn.closest("tr").remove();
recalc();
}
function onDebitInput(input) {
const credit = input.closest("tr").querySelector(".credit");
if (input.value) credit.value = "";
recalc();
}
function onCreditInput(input) {
const debit = input.closest("tr").querySelector(".debit");
if (input.value) debit.value = "";
recalc();
}
// ------------------------
// 合計再計算(税は送信時に展開するので、ここは本体行のみ)
// ------------------------
function recalc() {
let debitTotal = 0;
let creditTotal = 0;
document.querySelectorAll("#linesTable tbody tr").forEach(tr => {
const d = Number(tr.querySelector(".debit").value || 0);
const c = Number(tr.querySelector(".credit").value || 0);
debitTotal += d;
creditTotal += c;
});
document.getElementById("debitTotal").textContent = debitTotal.toLocaleString();
document.getElementById("creditTotal").textContent = creditTotal.toLocaleString();
}
// ------------------------
// 税計算(四捨五入)
// ------------------------
function calcTaxSplit(amount, rate, inclusive) {
if (!rate) return { base: amount, tax: 0 };
if (!inclusive) {
// 税抜amount = 本体
const tax = applyRounding(amount * rate);
return { base: amount, tax };
} else {
// 税込amount = 総額
const base = applyRounding(amount / (1 + rate));
const tax = amount - base; // 差額は必ず一致させる
return { base, tax };
}
}
function parseTaxOption(opt) {
if (opt === "none") return { rate: 0, inclusive: false };
const [rateStr, mode] = opt.split("-");
const rate = rateStr === "10" ? 0.10 : 0.08;
return { rate, inclusive: mode === "incl" };
}
// ------------------------
// 送信(税行を自動付与)
// ------------------------
async function submitEntry() {
const result = document.getElementById("result");
result.textContent = "";
result.className = "";
const entryDate = document.getElementById("entryDate").value;
const desc = document.getElementById("description").value;
if (!entryDate) return showError("仕訳日を入力してください");
if (!desc) return showError("摘要を入力してください");
const taxDebitAccountId = Number(document.getElementById("taxDebitAccount").value);
const taxCreditAccountId = Number(document.getElementById("taxCreditAccount").value);
const baseLines = [];
const taxLines = [];
document.querySelectorAll("#linesTable tbody tr").forEach(tr => {
const accountId = Number(tr.querySelector(".account").value);
const debit = Number(tr.querySelector(".debit").value || 0);
const credit = Number(tr.querySelector(".credit").value || 0);
const taxOpt = tr.querySelector(".tax").value;
const taxDir = tr.querySelector(".taxdir").value;
// 空行は無視
if (debit === 0 && credit === 0) return;
const { rate, inclusive } = parseTaxOption(taxOpt);
if (debit > 0) {
const { base, tax } = calcTaxSplit(debit, rate, inclusive);
baseLines.push({ account_id: accountId, debit: base, credit: 0 });
if (tax > 0) {
if (taxDir === "karibarai") {
taxLines.push({ account_id: taxDebitAccountId, debit: tax, credit: 0 });
} else {
taxLines.push({ account_id: taxCreditAccountId, debit: 0, credit: tax });
}
}
} else {
const { base, tax } = calcTaxSplit(credit, rate, inclusive);
baseLines.push({ account_id: accountId, debit: 0, credit: base });
if (tax > 0) {
if (taxDir === "karibarai") {
taxLines.push({ account_id: taxDebitAccountId, debit: tax, credit: 0 });
} else {
taxLines.push({ account_id: taxCreditAccountId, debit: 0, credit: tax });
}
}
}
});
const lines = [...baseLines, ...taxLines];
if (lines.length < 2)
return showError("仕訳行は2行以上必要です");
// 借貸チェック(送信前に最終確認)
const tot = lines.reduce((s, l) => {
s.debit += Number(l.debit || 0);
s.credit += Number(l.credit || 0);
return s;
}, {debit:0, credit:0});
if (tot.debit !== tot.credit)
return showError(`借方合計(${tot.debit})と貸方合計(${tot.credit})が一致していません`);
const payload = { entry_date: entryDate, description: desc, lines };
try {
const res = await fetch(`${API_BASE}/journal-entries`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) return showError(data.detail || "登録失敗");
showOk(`登録完了ID: ${data.journal_entry_id}`);
} catch {
showError("通信エラー");
}
}
function showError(msg) {
const r = document.getElementById("result");
r.textContent = msg;
r.className = "error";
}
function showOk(msg) {
const r = document.getElementById("result");
r.textContent = msg;
r.className = "ok";
}
// 合計の初期表示用
document.addEventListener("input", () => recalc());
init();
</script>
</body>
</html>

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>仕訳一覧</title>
<style>
body { font-family: sans-serif; margin: 24px; }
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
th, td { border: 1px solid #ccc; padding: 6px; }
th { background: #f5f5f5; }
</style>
</head>
<body>
<h2>仕訳一覧</h2>
<label>期間</label>
<input type="date" id="fromDate"> <input type="date" id="toDate">
<label>摘要</label>
<input type="text" id="keyword">
<button onclick="search()">検索</button>
<table>
<thead>
<tr>
<th>日付</th>
<th>摘要</th>
<th>借方合計</th>
<th>贷方合计</th>
<th>操作</th>
</tr>
</thead>
<tbody id="list"></tbody>
</table>
<script>
const API = "http://127.0.0.1:18080";
async function search() {
const from = document.getElementById("fromDate").value;
const to = document.getElementById("toDate").value;
const key = document.getElementById("keyword").value;
const qs = new URLSearchParams();
if (from) qs.append("from_date", from);
if (to) qs.append("to_date", to);
if (key) qs.append("keyword", key);
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
const data = await res.json();
const tbody = document.getElementById("list");
tbody.innerHTML = "";
data.forEach(e => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${e.entry_date}</td>
<td>${e.description}</td>
<td style="text-align:right">${e.debit_total.toLocaleString()}</td>
<td style="text-align:right">${e.credit_total.toLocaleString()}</td>
<td>
<button onclick="view(${e.journal_entry_id})">表示</button>
<button onclick="reverse(${e.journal_entry_id})">修正</button>
</td>
`;
tbody.appendChild(tr);
});
}
function view(id) {
window.open(`journal-view.html?id=${id}`, "_blank");
}
async function reverse(id) {
if (!confirm("この仕訳の逆仕訳を作成します。よろしいですか?")) return;
const res = await fetch(`${API}/journal-entries/${id}/reverse`, {
method: "POST"
});
const data = await res.json();
if (!res.ok) {
alert(data.detail || "逆仕訳の作成に失敗しました");
return;
}
alert(`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`);
search();
}
</script>
</body>
</html>