This commit is contained in:
admin
2026-01-12 20:59:52 +09:00
parent 76d8e7622e
commit 59e8e8625d
7 changed files with 840 additions and 314 deletions

View File

@@ -85,7 +85,7 @@ app.include_router(cash.router)
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
app.mount( app.mount(
"/", "/static",
StaticFiles(directory="app/static", html=True), StaticFiles(directory="app/static", html=True),
name="static", name="static",
) )

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from app.core.database import get_connection from app.core.database import get_connection
from pydantic import BaseModel from pydantic import BaseModel
from typing import List from typing import List, Optional
from decimal import Decimal from decimal import Decimal
from datetime import date from datetime import date
@@ -12,12 +12,17 @@ class JournalLine(BaseModel):
account_id: int account_id: int
debit: Decimal = Decimal("0") debit: Decimal = Decimal("0")
credit: Decimal = Decimal("0") credit: Decimal = Decimal("0")
tax_rate: Optional[int] = None # 10 or 8 or None
tax_direction: Optional[str] = None # "paid" or "received" or None
class JournalEntryRequest(BaseModel): class JournalEntryRequest(BaseModel):
entry_date: date entry_date: date
description: str description: str
lines: List[JournalLine] lines: List[JournalLine]
tax_paid_account_id: Optional[int] = None # 仮払消費税等 勘定
tax_received_account_id: Optional[int] = None # 仮受消費税等 勘定
rounding_mode: str = "round" # "round", "floor", or "ceil"
class JournalEntryUpdateRequest(BaseModel): class JournalEntryUpdateRequest(BaseModel):
description: str description: str
@@ -28,10 +33,183 @@ class JournalEntryUpdateRequest(BaseModel):
class JournalEntryDeleteRequest(BaseModel): class JournalEntryDeleteRequest(BaseModel):
deleted_reason: str deleted_reason: str
@router.get("", summary="仕訳一覧取得")
def get_journal_entries(
from_date: date = None,
to_date: date = None,
keyword: str = None
):
"""
仕訳一覧を検索して取得
"""
with get_connection() as conn:
with conn.cursor() as cur:
# クエリ構築
sql = """
SELECT
je.journal_entry_id,
je.entry_date,
je.description,
je.fiscal_year,
COALESCE(SUM(jl.debit), 0) as debit_total,
COALESCE(SUM(jl.credit), 0) as credit_total
FROM journal_entries je
LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id
WHERE je.is_deleted = false
"""
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 LIKE %s"
params.append(f"%{keyword}%")
sql += """
GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year
ORDER BY je.entry_date DESC, je.journal_entry_id DESC
"""
cur.execute(sql, params)
rows = cur.fetchall()
return [dict(row) for row in rows]
@router.get("/{journal_id}", summary="仕訳詳細取得")
def get_journal_entry(journal_id: int):
"""
指定された仕訳の詳細を取得
"""
with get_connection() as conn:
with conn.cursor() as cur:
# ヘッダー取得
cur.execute("""
SELECT
journal_entry_id,
entry_date,
description,
fiscal_year,
version,
is_deleted
FROM journal_entries
WHERE journal_entry_id = %s
""", (journal_id,))
header = cur.fetchone()
if not header:
raise HTTPException(status_code=404, detail="仕訳が存在しません")
# 明細取得
cur.execute("""
SELECT
jl.journal_line_id,
jl.account_id,
a.account_code,
a.account_name,
jl.debit,
jl.credit
FROM journal_lines jl
JOIN accounts a ON jl.account_id = a.account_id
WHERE jl.journal_entry_id = %s
ORDER BY jl.journal_line_id
""", (journal_id,))
lines = cur.fetchall()
return {
**dict(header),
"lines": [dict(line) for line in lines]
}
@router.post("/{journal_id}/reverse", summary="逆仕訳作成")
def create_reverse_entry(journal_id: int):
"""
指定された仕訳の逆仕訳を作成
"""
with get_connection() as conn:
with conn.cursor() as cur:
# 元仕訳取得
cur.execute("""
SELECT entry_date, description, fiscal_year
FROM journal_entries
WHERE journal_entry_id = %s AND is_deleted = false
""", (journal_id,))
header = cur.fetchone()
if not header:
raise HTTPException(status_code=404, detail="仕訳が存在しません")
# 元仕訳の明細取得
cur.execute("""
SELECT account_id, debit, credit
FROM journal_lines
WHERE journal_entry_id = %s
""", (journal_id,))
lines = cur.fetchall()
# 逆仕訳ヘッダー作成
cur.execute("""
INSERT INTO journal_entries (
entry_date,
description,
fiscal_year,
version,
is_deleted,
created_by
)
VALUES (%s, %s, %s, 1, false, %s)
RETURNING journal_entry_id
""", (
header["entry_date"],
f"[逆仕訳] {header['description']}",
header["fiscal_year"],
"system"
))
new_entry_id = cur.fetchone()["journal_entry_id"]
# 逆仕訳明細作成(借方と貸方を入れ替え)
for line in lines:
cur.execute("""
INSERT INTO journal_lines (
journal_entry_id,
account_id,
debit,
credit
)
VALUES (%s, %s, %s, %s)
""", (
new_entry_id,
line["account_id"],
line["credit"], # 借方と貸方を入れ替え
line["debit"]
))
conn.commit()
return {
"status": "ok",
"reversed_journal_entry_id": new_entry_id,
"original_journal_entry_id": journal_id
}
@router.post("", summary="仕訳登録") @router.post("", summary="仕訳登録")
def create_journal_entry(req: JournalEntryRequest): def create_journal_entry(req: JournalEntryRequest):
from app.utils.month_lock import is_month_locked from app.utils.month_lock import is_month_locked
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN, ROUND_UP
if is_month_locked(req.entry_date): if is_month_locked(req.entry_date):
raise HTTPException( raise HTTPException(
@@ -42,9 +220,57 @@ def create_journal_entry(req: JournalEntryRequest):
if not req.lines: if not req.lines:
raise HTTPException(status_code=400, detail="仕訳明細不能为空") raise HTTPException(status_code=400, detail="仕訳明細不能为空")
# (可选)简单的借贷平衡校验 # 税額計算と税行生成
total_debit = sum(l.debit for l in req.lines) all_lines = []
total_credit = sum(l.credit for l in req.lines) tax_aggregation = {} # {"paid": Decimal, "received": Decimal}
# 端数処理モード設定
rounding_map = {
"round": ROUND_HALF_UP,
"floor": ROUND_DOWN,
"ceil": ROUND_UP
}
rounding = rounding_map.get(req.rounding_mode, ROUND_HALF_UP)
for line in req.lines:
all_lines.append(line)
# 税対象行の場合、税額を計算
if line.tax_rate and line.tax_direction:
amount = line.debit if line.debit > 0 else line.credit
rate = Decimal(str(line.tax_rate)) / Decimal("100") # 10% -> 0.10
# 入力額は税抜額として、税額 = 税抜額 × 税率
raw_tax = amount * rate
tax_amount = raw_tax.quantize(Decimal("1"), rounding=rounding)
# 税方向ごとに集計
if line.tax_direction not in tax_aggregation:
tax_aggregation[line.tax_direction] = Decimal("0")
tax_aggregation[line.tax_direction] += tax_amount
# 集計した税額で税行を追加
if "paid" in tax_aggregation and tax_aggregation["paid"] > 0:
if not req.tax_paid_account_id:
raise HTTPException(status_code=400, detail="仮払消費税等 勘定を選択してください")
all_lines.append(JournalLine(
account_id=req.tax_paid_account_id,
debit=tax_aggregation["paid"],
credit=Decimal("0")
))
if "received" in tax_aggregation and tax_aggregation["received"] > 0:
if not req.tax_received_account_id:
raise HTTPException(status_code=400, detail="仮受消費税等 勘定を選択してください")
all_lines.append(JournalLine(
account_id=req.tax_received_account_id,
debit=Decimal("0"),
credit=tax_aggregation["received"]
))
# 借貸平衡校験
total_debit = sum(l.debit for l in all_lines)
total_credit = sum(l.credit for l in all_lines)
if total_debit != total_credit: if total_debit != total_credit:
raise HTTPException(status_code=400, detail="借方和贷方不平衡") raise HTTPException(status_code=400, detail="借方和贷方不平衡")
@@ -55,7 +281,7 @@ def create_journal_entry(req: JournalEntryRequest):
# 仕訳ヘッダ # 仕訳ヘッダ
cur.execute(""" cur.execute("""
INSERT INTO journal_entries ( INSERT INTO journal_entries (
journal_date, entry_date,
description, description,
fiscal_year, fiscal_year,
version, version,
@@ -63,21 +289,21 @@ def create_journal_entry(req: JournalEntryRequest):
created_by created_by
) )
VALUES (%s, %s, %s, 1, false, %s) VALUES (%s, %s, %s, 1, false, %s)
RETURNING journal_id RETURNING journal_entry_id
""", ( """, (
req.entry_date, req.entry_date,
req.description, req.description,
fiscal_year, fiscal_year,
"system" # 以后可以换成登录用户 "system"
)) ))
entry_id = cur.fetchone()["journal_id"] entry_id = cur.fetchone()["journal_entry_id"]
# 明細 # 明細(税行を含む)
for line in req.lines: for line in all_lines:
cur.execute(""" cur.execute("""
INSERT INTO journal_lines ( INSERT INTO journal_lines (
journal_id, journal_entry_id,
account_id, account_id,
debit, debit,
credit credit
@@ -179,7 +405,7 @@ def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest):
@router.delete("/{journal_id}", summary="仕訳除(软删除)") @router.delete("/{journal_id}", summary="仕訳除(ソフト削除)")
def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest): def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
with get_connection() as conn: with get_connection() as conn:
@@ -187,21 +413,17 @@ def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
cur.execute(""" cur.execute("""
UPDATE journal_entries UPDATE journal_entries
SET is_deleted = true, SET is_deleted = true,
deleted_at = NOW(), updated_at = NOW()
deleted_reason = %s WHERE journal_entry_id = %s
WHERE journal_id = %s
AND is_deleted = false AND is_deleted = false
RETURNING journal_id RETURNING journal_entry_id
""", ( """, (journal_id,))
req.deleted_reason,
journal_id
))
row = cur.fetchone() row = cur.fetchone()
if not row: if not row:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="仕訳存在,或已被删除" detail="仕訳存在しないか、既に削除されています"
) )
conn.commit() conn.commit()

View File

@@ -1,30 +1,55 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>修正仕訳入力</title> <title>修正仕訳入力</title>
<style> <style>
body { font-family: sans-serif; margin: 24px; } body {
table { border-collapse: collapse; width: 100%; margin-top: 12px; } font-family: sans-serif;
th, td { border: 1px solid #ccc; padding: 6px; text-align: center; } margin: 24px;
th { background: #f5f5f5; } }
input, select, button { padding: 6px; } table {
.right { text-align: right; } border-collapse: collapse;
.error { color: #c00; margin-top: 8px; } width: 100%;
.ok { color: #090; margin-top: 8px; } margin-top: 12px;
</style> }
</head> th,
<body> 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;
}
</style>
</head>
<body>
<h2>修正仕訳入力</h2>
<h2>修正仕訳入力</h2> <label>仕訳日</label>
<input type="date" id="entryDate" />
<label>仕訳日</label> <label>摘要</label>
<input type="date" id="entryDate"> <input type="text" id="description" style="width: 60%" />
<label>摘要</label> <table id="linesTable">
<input type="text" id="description" style="width:60%">
<table id="linesTable">
<thead> <thead>
<tr> <tr>
<th>科目</th> <th>科目</th>
@@ -42,24 +67,30 @@
<th></th> <th></th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
<button onclick="addRow()"> 行追加</button> <div style="margin-top: 12px">
<button onclick="submitEntry()">修正版仕訳を登録</button> <button onclick="addRow()"> 行追加</button>
<button onclick="submitEntry()">修正版仕訳を登録</button>
<button onclick="cancelEdit()" style="background-color: #f0f0f0">
キャンセル
</button>
</div>
<div id="result"></div> <div id="result"></div>
<script> <script>
const API = "http://127.0.0.1:18080"; const API = "http://127.0.0.1:18080";
let accounts = []; let accounts = [];
// ------------------------------------ // ------------------------------------
// 初期化 // 初期化
// ------------------------------------ // ------------------------------------
async function init() { async function init() {
// 科目取得 // 科目取得
const res = await fetch(`${API}/accounts`); const res = await fetch(`${API}/accounts`);
accounts = await res.json(); const data = await res.json();
accounts = data.items ?? data;
// 元仕訳取得 // 元仕訳取得
const src = localStorage.getItem("editSource"); const src = localStorage.getItem("editSource");
@@ -67,32 +98,37 @@ async function init() {
showError("修正元の仕訳データが見つかりません"); showError("修正元の仕訳データが見つかりません");
return; return;
} }
const data = JSON.parse(src); const srcData = JSON.parse(src);
// ヘッダ反映 // ヘッダ反映
document.getElementById("entryDate").value = data.entry_date; document.getElementById("entryDate").value = srcData.entry_date;
document.getElementById("description").value = `【修正後】${data.description}`; document.getElementById(
"description"
).value = `【修正後】${srcData.description}`;
// 明細反映 // 明細反映
data.lines.forEach(l => addRow(l)); srcData.lines.forEach((l) => addRow(l));
recalc(); recalc();
} }
init(); init();
// ------------------------------------ // ------------------------------------
// 行操作 // 行操作
// ------------------------------------ // ------------------------------------
function addRow(line = null) { function addRow(line = null) {
const tbody = document.querySelector("#linesTable tbody"); const tbody = document.querySelector("#linesTable tbody");
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.innerHTML = ` tr.innerHTML = `
<td> <td>
<select> <select>
${accounts.map(a => ${accounts
.map(
(a) =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>` `<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
).join("")} )
.join("")}
</select> </select>
</td> </td>
<td><input type="number" min="0"></td> <td><input type="number" min="0"></td>
@@ -107,30 +143,31 @@ function addRow(line = null) {
tr.querySelectorAll("input")[0].value = line.debit || ""; tr.querySelectorAll("input")[0].value = line.debit || "";
tr.querySelectorAll("input")[1].value = line.credit || ""; tr.querySelectorAll("input")[1].value = line.credit || "";
} }
} }
function removeRow(btn) { function removeRow(btn) {
btn.closest("tr").remove(); btn.closest("tr").remove();
recalc(); recalc();
} }
// ------------------------------------ // ------------------------------------
// 合計再計算 // 合計再計算
// ------------------------------------ // ------------------------------------
function recalc() { function recalc() {
let d = 0, c = 0; let d = 0,
document.querySelectorAll("#linesTable tbody tr").forEach(tr => { c = 0;
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
d += Number(tr.querySelectorAll("input")[0].value || 0); d += Number(tr.querySelectorAll("input")[0].value || 0);
c += Number(tr.querySelectorAll("input")[1].value || 0); c += Number(tr.querySelectorAll("input")[1].value || 0);
}); });
document.getElementById("debitTotal").textContent = d.toLocaleString(); document.getElementById("debitTotal").textContent = d.toLocaleString();
document.getElementById("creditTotal").textContent = c.toLocaleString(); document.getElementById("creditTotal").textContent = c.toLocaleString();
} }
// ------------------------------------ // ------------------------------------
// 登録 // 登録
// ------------------------------------ // ------------------------------------
async function submitEntry() { async function submitEntry() {
const result = document.getElementById("result"); const result = document.getElementById("result");
result.textContent = ""; result.textContent = "";
result.className = ""; result.className = "";
@@ -144,9 +181,10 @@ async function submitEntry() {
} }
const lines = []; const lines = [];
let d = 0, c = 0; let d = 0,
c = 0;
document.querySelectorAll("#linesTable tbody tr").forEach(tr => { document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
const accountId = Number(tr.querySelector("select").value); const accountId = Number(tr.querySelector("select").value);
const debit = Number(tr.querySelectorAll("input")[0].value || 0); const debit = Number(tr.querySelectorAll("input")[0].value || 0);
const credit = Number(tr.querySelectorAll("input")[1].value || 0); const credit = Number(tr.querySelectorAll("input")[1].value || 0);
@@ -162,14 +200,17 @@ async function submitEntry() {
const payload = { const payload = {
entry_date: entryDate, entry_date: entryDate,
description: desc, description: desc,
lines lines,
tax_paid_account_id: null,
tax_received_account_id: null,
rounding_mode: "floor",
}; };
try { try {
const res = await fetch(`${API}/journal-entries`, { const res = await fetch(`${API}/journal-entries`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload) body: JSON.stringify(payload),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) return showError(data.detail || "登録失敗"); if (!res.ok) return showError(data.detail || "登録失敗");
@@ -178,19 +219,25 @@ async function submitEntry() {
} catch { } catch {
showError("通信エラー"); showError("通信エラー");
} }
} }
function showError(msg) { function showError(msg) {
const r = document.getElementById("result"); const r = document.getElementById("result");
r.textContent = msg; r.textContent = msg;
r.className = "error"; r.className = "error";
} }
function showOk(msg) { function showOk(msg) {
const r = document.getElementById("result"); const r = document.getElementById("result");
r.textContent = msg; r.textContent = msg;
r.className = "ok"; r.className = "ok";
} }
</script>
</body> function cancelEdit() {
if (confirm("編集をキャンセルして一覧画面に戻りますか?")) {
localStorage.removeItem("editSource");
window.location.href = "journal-list.html";
}
}
</script>
</body>
</html> </html>

View File

@@ -75,8 +75,8 @@
<div style="margin-bottom: 10px"> <div style="margin-bottom: 10px">
<label>消費税 端数処理:</label> <label>消費税 端数処理:</label>
<select id="roundingMode"> <select id="roundingMode">
<option value="round">四捨五入</option>
<option value="floor">切捨て</option> <option value="floor">切捨て</option>
<option value="round">四捨五入</option>
<option value="ceil">切上げ</option> <option value="ceil">切上げ</option>
</select> </select>
</div> </div>
@@ -104,7 +104,14 @@
<div style="margin-bottom: 8px"> <div style="margin-bottom: 8px">
<label>年度:</label> <label>年度:</label>
<input type="number" id="lockYear" value="2025" style="width: 80px" /> <input
type="number"
id="lockYear"
value="2025"
min="1900"
max="2999"
style="width: 80px"
/>
<label>月份:</label> <label>月份:</label>
<input <input
@@ -353,8 +360,8 @@
const rate = taxType === "8" ? 0.08 : 0.1; const rate = taxType === "8" ? 0.08 : 0.1;
const roundingMode = document.getElementById("roundingMode").value; const roundingMode = document.getElementById("roundingMode").value;
// 税込 → 税額逆算 // 税抜額 × 税率 = 税額
const rawTax = (totalAmount * rate) / (1 + rate); const rawTax = totalAmount * rate;
let taxAmount; let taxAmount;
switch (roundingMode) { switch (roundingMode) {
case "floor": case "floor":
@@ -406,7 +413,7 @@
} }
// ----------------------------- // -----------------------------
// 登録(最小可用版) // 登録(税行自動生成版)
// ----------------------------- // -----------------------------
async function submitJournal() { async function submitJournal() {
const entryDate = document.getElementById("entryDate").value; const entryDate = document.getElementById("entryDate").value;
@@ -417,45 +424,55 @@
return; return;
} }
// 明細収集 // 明細収集(税情報も含む)
const rows = document.querySelectorAll("#linesTable tbody tr"); const rows = document.querySelectorAll("#linesTable tbody tr");
const lines = []; const lines = [];
let totalDebit = 0,
totalCredit = 0;
rows.forEach((row) => { rows.forEach((row) => {
if (row.classList.contains("tax-row")) return; if (row.classList.contains("tax-row")) return; // 税行は無視
const accountId = Number(row.querySelector(".account").value); const accountId = Number(row.querySelector(".account").value);
const debit = Number(row.querySelector(".debit").value || 0); const debit = Number(row.querySelector(".debit").value || 0);
const credit = Number(row.querySelector(".credit").value || 0); const credit = Number(row.querySelector(".credit").value || 0);
const taxType = row.querySelector(".taxType").value;
const taxDirection = row.querySelector(".taxDirection").value;
if (debit > 0 && credit > 0) { if (debit > 0 && credit > 0) {
alert("同一行不能同时填写借方和贷方"); alert("同一行不能同时填写借方和贷方");
throw new Error("invalid line"); throw new Error("invalid line");
} }
if (debit === 0 && credit === 0) return; if (debit === 0 && credit === 0) return;
lines.push({ account_id: accountId, debit: debit, credit: credit }); const lineData = {
totalDebit += debit; account_id: accountId,
totalCredit += credit; debit: debit,
credit: credit,
};
// 税情報を追加
if (taxType !== "none" && taxDirection !== "none") {
lineData.tax_rate = parseInt(taxType);
lineData.tax_direction = taxDirection;
}
lines.push(lineData);
}); });
if (lines.length === 0) { if (lines.length === 0) {
alert("仕訳明細がありません"); alert("仕訳明細がありません");
return; return;
} }
if (lines.length < 2) {
alert("至少需要借方和贷方各一行");
return;
}
// 借貸一致チェック(最小)
if (totalDebit !== totalCredit) {
alert(
`借方(${totalDebit.toLocaleString()})と貸方(${totalCredit.toLocaleString()})が一致しません`
);
return;
}
const payload = { entry_date: entryDate, description, lines }; const payload = {
entry_date: entryDate,
description,
lines,
tax_paid_account_id:
Number(document.getElementById("taxPaidSelect").value) || null,
tax_received_account_id:
Number(document.getElementById("taxReceivedSelect").value) || null,
rounding_mode: document.getElementById("roundingMode").value,
};
const res = await fetch(`${API}/journal-entries`, { const res = await fetch(`${API}/journal-entries`, {
method: "POST", method: "POST",
@@ -464,7 +481,11 @@
}); });
if (!res.ok) { if (!res.ok) {
alert("登録に失敗しました"); const errorData = await res.json().catch(() => ({}));
const errorMsg =
errorData.detail || `登録に失敗しました (HTTP ${res.status})`;
console.error("Error:", errorData);
alert(errorMsg);
return; return;
} }

View File

@@ -1,28 +1,41 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>仕訳一覧</title> <title>仕訳一覧</title>
<style> <style>
body { font-family: sans-serif; margin: 24px; } body {
table { border-collapse: collapse; width: 100%; margin-top: 12px; } font-family: sans-serif;
th, td { border: 1px solid #ccc; padding: 6px; } margin: 24px;
th { background: #f5f5f5; } }
</style> table {
</head> border-collapse: collapse;
<body> width: 100%;
margin-top: 12px;
}
th,
td {
border: 1px solid #ccc;
padding: 6px;
}
th {
background: #f5f5f5;
}
</style>
</head>
<body>
<h2>仕訳一覧</h2>
<h2>仕訳一覧</h2> <label>期間</label>
<input type="date" id="fromDate" min="1900-01-01" max="2999-12-31" />
<input type="date" id="toDate" min="1900-01-01" max="2999-12-31" />
<label>期間</label> <label>摘要</label>
<input type="date" id="fromDate"> <input type="date" id="toDate"> <input type="text" id="keyword" />
<label>摘要</label> <button onclick="search()">検索</button>
<input type="text" id="keyword">
<button onclick="search()">検索</button> <table>
<table>
<thead> <thead>
<tr> <tr>
<th>日付</th> <th>日付</th>
@@ -33,16 +46,26 @@
</tr> </tr>
</thead> </thead>
<tbody id="list"></tbody> <tbody id="list"></tbody>
</table> </table>
<script> <script>
const API = "http://127.0.0.1:18080"; const API = "http://127.0.0.1:18080";
async function search() { async function search() {
const from = document.getElementById("fromDate").value; const from = document.getElementById("fromDate").value;
const to = document.getElementById("toDate").value; const to = document.getElementById("toDate").value;
const key = document.getElementById("keyword").value; const key = document.getElementById("keyword").value;
// 検索条件を保存
localStorage.setItem(
"journalSearchConditions",
JSON.stringify({
fromDate: from,
toDate: to,
keyword: key,
})
);
const qs = new URLSearchParams(); const qs = new URLSearchParams();
if (from) qs.append("from_date", from); if (from) qs.append("from_date", from);
if (to) qs.append("to_date", to); if (to) qs.append("to_date", to);
@@ -54,7 +77,7 @@ async function search() {
const tbody = document.getElementById("list"); const tbody = document.getElementById("list");
tbody.innerHTML = ""; tbody.innerHTML = "";
data.forEach(e => { data.forEach((e) => {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
tr.innerHTML = ` tr.innerHTML = `
<td>${e.entry_date}</td> <td>${e.entry_date}</td>
@@ -64,45 +87,113 @@ async function search() {
<td> <td>
<button onclick="view(${e.journal_entry_id})">表示</button> <button onclick="view(${e.journal_entry_id})">表示</button>
<button onclick="reverseAndEdit(${e.journal_entry_id})">修正</button> <button onclick="reverseAndEdit(${e.journal_entry_id})">修正</button>
<button onclick="deleteEntry(${
e.journal_entry_id
})" style="color: red;">削除</button>
</td> </td>
`; `;
tbody.appendChild(tr); tbody.appendChild(tr);
}); });
} }
function view(id) { function view(id) {
window.open(`journal-view.html?id=${id}`, "_blank"); window.open(`journal-view.html?id=${id}`, "_blank");
} }
// -------------------------------------------------- // --------------------------------------------------
// 修正仕訳:逆仕訳を作って修正画面へ遷移 // 修正仕訳:逆仕訳を作って修正画面へ遷移
// -------------------------------------------------- // --------------------------------------------------
async function reverseAndEdit(id) { async function reverseAndEdit(id) {
if (!confirm("この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?")) return; if (
!confirm(
"この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?"
)
)
return;
try {
// ① 逆仕訳を作成 // ① 逆仕訳を作成
const res = await fetch(`${API}/journal-entries/${id}/reverse`, { const res = await fetch(`${API}/journal-entries/${id}/reverse`, {
method: "POST" method: "POST",
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
alert(
data.detail || `逆仕訳の作成に失敗しました (HTTP ${res.status})`
);
return;
}
const data = await res.json();
alert(
`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`
);
// ② 元仕訳内容を取得して修正画面へ遷移
const srcRes = await fetch(`${API}/journal-entries/${id}`);
if (!srcRes.ok) {
alert("元仕訳の取得に失敗しました");
return;
}
const srcData = await srcRes.json();
localStorage.setItem("editSource", JSON.stringify(srcData));
// 修正画面を開く
window.open("journal-edit.html", "_blank");
} catch (error) {
console.error("Error:", error);
alert(`エラーが発生しました: ${error.message}`);
}
}
// --------------------------------------------------
// 仕訳削除
// --------------------------------------------------
async function deleteEntry(id) {
const reason = prompt("削除理由を入力してください:");
if (!reason || reason.trim() === "") {
alert("削除理由の入力が必要です");
return;
}
if (!confirm("この仕訳を削除してもよろしいですか?")) {
return;
}
const res = await fetch(`${API}/journal-entries/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ deleted_reason: reason }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
alert(data.detail || "逆仕訳の作成に失敗しました"); alert(data.detail || "削除に失敗しました");
return; return;
} }
alert(`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`); alert("削除しました");
search(); // リストを再読み込み
}
// ② 元仕訳内容を取得して修正画面へ遷移 // ページロード時に前回の検索条件を復元
const src = await fetch(`${API}/journal-entries/${id}`); window.addEventListener("DOMContentLoaded", () => {
const srcData = await src.json(); const savedConditions = localStorage.getItem("journalSearchConditions");
if (savedConditions) {
const conditions = JSON.parse(savedConditions);
if (conditions.fromDate)
document.getElementById("fromDate").value = conditions.fromDate;
if (conditions.toDate)
document.getElementById("toDate").value = conditions.toDate;
if (conditions.keyword)
document.getElementById("keyword").value = conditions.keyword;
localStorage.setItem("editSource", JSON.stringify(srcData)); // 自動的に検索を実行
search();
window.open("journal-edit.html", "_blank"); }
} });
</script>
</script> </body>
</body>
</html> </html>

137
frontend/journal-view.html Normal file
View File

@@ -0,0 +1,137 @@
<!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: 8px;
}
th {
background: #f5f5f5;
}
.info-row {
margin: 8px 0;
}
.info-label {
font-weight: bold;
min-width: 120px;
display: inline-block;
}
.total-row {
background: #f0f8ff;
font-weight: bold;
}
</style>
</head>
<body>
<h2>仕訳詳細</h2>
<div id="info"></div>
<table>
<thead>
<tr>
<th>科目コード</th>
<th>科目名</th>
<th>借方</th>
<th>貸方</th>
</tr>
</thead>
<tbody id="lines"></tbody>
</table>
<div style="margin-top: 16px">
<button onclick="window.close()">閉じる</button>
<button onclick="goBack()">一覧に戻る</button>
</div>
<script>
const API = "http://127.0.0.1:18080";
async function loadJournalEntry() {
const params = new URLSearchParams(window.location.search);
const id = params.get("id");
if (!id) {
alert("仕訳IDが指定されていません");
return;
}
const res = await fetch(`${API}/journal-entries/${id}`);
if (!res.ok) {
alert("仕訳の取得に失敗しました");
return;
}
const data = await res.json();
// ヘッダー情報
document.getElementById("info").innerHTML = `
<div class="info-row">
<span class="info-label">仕訳ID:</span> ${data.journal_entry_id}
</div>
<div class="info-row">
<span class="info-label">仕訳日:</span> ${data.entry_date}
</div>
<div class="info-row">
<span class="info-label">摘要:</span> ${data.description}
</div>
<div class="info-row">
<span class="info-label">会計年度:</span> ${data.fiscal_year}
</div>
`;
// 明細
const tbody = document.getElementById("lines");
let totalDebit = 0;
let totalCredit = 0;
data.lines.forEach((line) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${line.account_code}</td>
<td>${line.account_name}</td>
<td style="text-align:right">${
line.debit > 0 ? line.debit.toLocaleString() : ""
}</td>
<td style="text-align:right">${
line.credit > 0 ? line.credit.toLocaleString() : ""
}</td>
`;
tbody.appendChild(tr);
totalDebit += parseFloat(line.debit);
totalCredit += parseFloat(line.credit);
});
// 合計行
const totalRow = document.createElement("tr");
totalRow.className = "total-row";
totalRow.innerHTML = `
<td colspan="2" style="text-align:center">合計</td>
<td style="text-align:right">${totalDebit.toLocaleString()}</td>
<td style="text-align:right">${totalCredit.toLocaleString()}</td>
`;
tbody.appendChild(totalRow);
}
function goBack() {
window.location.href = "journal-list.html";
}
loadJournalEntry();
</script>
</body>
</html>

View File

@@ -30,6 +30,14 @@ DB_NAME=njts_acct
DB_USER=njts_app DB_USER=njts_app
DB_PASSWORD=njts_app2025 DB_PASSWORD=njts_app2025
----------------------
nano /volume1/docker/postgres/docker-compose.yml
DB重启
cd /volume1/docker/postgres
docker compose down
docker compose up -d
docker ps
---------------------- ----------------------
バージョン確認 バージョン確認
python --version python --version