修正
This commit is contained in:
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
137
frontend/journal-view.html
Normal 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>
|
||||||
8
メモ.txt
8
メモ.txt
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user