修正
This commit is contained in:
@@ -7,6 +7,26 @@ from datetime import date
|
|||||||
|
|
||||||
router = APIRouter(prefix="/journal-entries", tags=["仕訳"])
|
router = APIRouter(prefix="/journal-entries", tags=["仕訳"])
|
||||||
|
|
||||||
|
# journal_lines に line_description カラムが存在するかキャッシュ
|
||||||
|
_line_description_column_exists: Optional[bool] = None
|
||||||
|
|
||||||
|
def _check_line_description_column() -> bool:
|
||||||
|
"""journal_lines テーブルに line_description カラムが存在するか確認(キャッシュあり)"""
|
||||||
|
global _line_description_column_exists
|
||||||
|
if _line_description_column_exists is None:
|
||||||
|
try:
|
||||||
|
with get_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'journal_lines'
|
||||||
|
AND column_name = 'line_description'
|
||||||
|
""")
|
||||||
|
_line_description_column_exists = cur.fetchone() is not None
|
||||||
|
except Exception:
|
||||||
|
_line_description_column_exists = False
|
||||||
|
return _line_description_column_exists
|
||||||
|
|
||||||
|
|
||||||
class JournalLine(BaseModel):
|
class JournalLine(BaseModel):
|
||||||
account_id: int
|
account_id: int
|
||||||
@@ -14,6 +34,7 @@ class JournalLine(BaseModel):
|
|||||||
credit: Decimal = Decimal("0")
|
credit: Decimal = Decimal("0")
|
||||||
tax_rate: Optional[int] = None # 10 or 8 or None
|
tax_rate: Optional[int] = None # 10 or 8 or None
|
||||||
tax_direction: Optional[str] = None # "paid" or "received" or None
|
tax_direction: Optional[str] = None # "paid" or "received" or None
|
||||||
|
line_description: Optional[str] = None # 行摘要(任意)
|
||||||
|
|
||||||
|
|
||||||
class JournalEntryRequest(BaseModel):
|
class JournalEntryRequest(BaseModel):
|
||||||
@@ -205,7 +226,9 @@ def get_journal_entry(journal_id: int):
|
|||||||
raise HTTPException(status_code=404, detail="仕訳が存在しません")
|
raise HTTPException(status_code=404, detail="仕訳が存在しません")
|
||||||
|
|
||||||
# 明細取得
|
# 明細取得
|
||||||
cur.execute("""
|
has_line_desc = _check_line_description_column()
|
||||||
|
line_desc_col = ", jl.line_description" if has_line_desc else ""
|
||||||
|
cur.execute(f"""
|
||||||
SELECT
|
SELECT
|
||||||
jl.journal_line_id,
|
jl.journal_line_id,
|
||||||
jl.account_id,
|
jl.account_id,
|
||||||
@@ -215,6 +238,7 @@ def get_journal_entry(journal_id: int):
|
|||||||
jl.credit,
|
jl.credit,
|
||||||
jl.tax_rate,
|
jl.tax_rate,
|
||||||
jl.tax_direction
|
jl.tax_direction
|
||||||
|
{line_desc_col}
|
||||||
FROM journal_lines jl
|
FROM journal_lines jl
|
||||||
JOIN accounts a ON jl.account_id = a.account_id
|
JOIN accounts a ON jl.account_id = a.account_id
|
||||||
WHERE jl.journal_entry_id = %s
|
WHERE jl.journal_entry_id = %s
|
||||||
@@ -493,6 +517,7 @@ def create_journal_entry(req: JournalEntryRequest):
|
|||||||
entry_id = cur.fetchone()["journal_entry_id"]
|
entry_id = cur.fetchone()["journal_entry_id"]
|
||||||
|
|
||||||
# ④ 插入交易明細
|
# ④ 插入交易明細
|
||||||
|
has_line_desc = _check_line_description_column()
|
||||||
for line in all_lines:
|
for line in all_lines:
|
||||||
# 轉換 tax_direction
|
# 轉換 tax_direction
|
||||||
tax_dir_db = None
|
tax_dir_db = None
|
||||||
@@ -504,24 +529,46 @@ def create_journal_entry(req: JournalEntryRequest):
|
|||||||
else:
|
else:
|
||||||
tax_dir_db = line.tax_direction.upper()
|
tax_dir_db = line.tax_direction.upper()
|
||||||
|
|
||||||
cur.execute("""
|
if has_line_desc:
|
||||||
INSERT INTO journal_lines (
|
cur.execute("""
|
||||||
journal_entry_id,
|
INSERT INTO journal_lines (
|
||||||
account_id,
|
journal_entry_id,
|
||||||
debit,
|
account_id,
|
||||||
credit,
|
debit,
|
||||||
tax_rate,
|
credit,
|
||||||
tax_direction
|
tax_rate,
|
||||||
)
|
tax_direction,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s)
|
line_description
|
||||||
""", (
|
)
|
||||||
entry_id,
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||||
line.account_id,
|
""", (
|
||||||
line.debit,
|
entry_id,
|
||||||
line.credit,
|
line.account_id,
|
||||||
line.tax_rate,
|
line.debit,
|
||||||
tax_dir_db
|
line.credit,
|
||||||
))
|
line.tax_rate,
|
||||||
|
tax_dir_db,
|
||||||
|
line.line_description or None
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO journal_lines (
|
||||||
|
journal_entry_id,
|
||||||
|
account_id,
|
||||||
|
debit,
|
||||||
|
credit,
|
||||||
|
tax_rate,
|
||||||
|
tax_direction
|
||||||
|
)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
entry_id,
|
||||||
|
line.account_id,
|
||||||
|
line.debit,
|
||||||
|
line.credit,
|
||||||
|
line.tax_rate,
|
||||||
|
tax_dir_db
|
||||||
|
))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -543,6 +590,7 @@ def create_journal_entry(req: JournalEntryRequest):
|
|||||||
entry_id = cur.fetchone()["journal_entry_id"]
|
entry_id = cur.fetchone()["journal_entry_id"]
|
||||||
|
|
||||||
# 插入交易明細
|
# 插入交易明細
|
||||||
|
has_line_desc = _check_line_description_column()
|
||||||
for line in all_lines:
|
for line in all_lines:
|
||||||
# 轉換 tax_direction
|
# 轉換 tax_direction
|
||||||
tax_dir_db = None
|
tax_dir_db = None
|
||||||
@@ -554,19 +602,35 @@ def create_journal_entry(req: JournalEntryRequest):
|
|||||||
else:
|
else:
|
||||||
tax_dir_db = line.tax_direction.upper()
|
tax_dir_db = line.tax_direction.upper()
|
||||||
|
|
||||||
cur.execute("""
|
if has_line_desc:
|
||||||
INSERT INTO journal_lines (
|
cur.execute("""
|
||||||
journal_entry_id, account_id, debit, credit,
|
INSERT INTO journal_lines (
|
||||||
tax_rate, tax_direction
|
journal_entry_id, account_id, debit, credit,
|
||||||
) VALUES (%s, %s, %s, %s, %s, %s)
|
tax_rate, tax_direction, line_description
|
||||||
""", (
|
) VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||||
entry_id,
|
""", (
|
||||||
line.account_id,
|
entry_id,
|
||||||
line.debit,
|
line.account_id,
|
||||||
line.credit,
|
line.debit,
|
||||||
line.tax_rate,
|
line.credit,
|
||||||
tax_dir_db
|
line.tax_rate,
|
||||||
))
|
tax_dir_db,
|
||||||
|
line.line_description or None
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO journal_lines (
|
||||||
|
journal_entry_id, account_id, debit, credit,
|
||||||
|
tax_rate, tax_direction
|
||||||
|
) VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
""", (
|
||||||
|
entry_id,
|
||||||
|
line.account_id,
|
||||||
|
line.debit,
|
||||||
|
line.credit,
|
||||||
|
line.tax_rate,
|
||||||
|
tax_dir_db
|
||||||
|
))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|||||||
24
backend/sql/add_line_description.sql
Normal file
24
backend/sql/add_line_description.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- マイグレーション: journal_lines テーブルに line_description カラム追加
|
||||||
|
-- 実行方法: admin または postgres 超级用户で実行
|
||||||
|
-- psql -h 192.168.0.61 -p 55432 -U admin -d njts_acct -f add_line_description.sql
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'journal_lines' AND column_name = 'line_description'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE journal_lines ADD COLUMN line_description TEXT;
|
||||||
|
RAISE NOTICE '✓ line_description カラムを journal_lines に追加しました';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✓ line_description カラムはすでに存在しています';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 確認
|
||||||
|
SELECT column_name, data_type, is_nullable
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'journal_lines'
|
||||||
|
ORDER BY ordinal_position;
|
||||||
@@ -28,6 +28,10 @@
|
|||||||
button {
|
button {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
#linesTable td input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.right {
|
.right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -122,10 +126,11 @@
|
|||||||
<table id="linesTable">
|
<table id="linesTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>科目</th>
|
<th style="width: 220px">科目</th>
|
||||||
<th>借方</th>
|
<th style="width: 120px">借方</th>
|
||||||
<th>貸方</th>
|
<th style="width: 120px">貸方</th>
|
||||||
<th>操作</th>
|
<th style="width: 180px">摘要(行)</th>
|
||||||
|
<th style="width: 60px">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
@@ -135,6 +140,7 @@
|
|||||||
<th id="debitTotal" class="right">0</th>
|
<th id="debitTotal" class="right">0</th>
|
||||||
<th id="creditTotal" class="right">0</th>
|
<th id="creditTotal" class="right">0</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
@@ -277,7 +283,11 @@
|
|||||||
filtered = accounts.filter((acc) => {
|
filtered = accounts.filter((acc) => {
|
||||||
const code = acc.account_code.toLowerCase();
|
const code = acc.account_code.toLowerCase();
|
||||||
const name = acc.account_name.toLowerCase();
|
const name = acc.account_name.toLowerCase();
|
||||||
return code.includes(searchText) || name.includes(searchText);
|
return (
|
||||||
|
code.includes(searchText) ||
|
||||||
|
name.includes(searchText) ||
|
||||||
|
`${code} ${name}`.includes(searchText)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,8 +436,9 @@
|
|||||||
<div class="account-dropdown"></div>
|
<div class="account-dropdown"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="number" min="0"></td>
|
<td><input type="number" min="0" style="text-align:right"></td>
|
||||||
<td><input type="number" min="0"></td>
|
<td><input type="number" min="0" style="text-align:right"></td>
|
||||||
|
<td><input type="text" class="line-memo-input" placeholder="行摘要(任意)"></td>
|
||||||
<td><button onclick="removeRow(this)">削除</button></td>
|
<td><button onclick="removeRow(this)">削除</button></td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -453,6 +464,9 @@
|
|||||||
const creditVal = Number(line.credit) || 0;
|
const creditVal = Number(line.credit) || 0;
|
||||||
numberInputs[0].value = debitVal > 0 ? String(debitVal) : "";
|
numberInputs[0].value = debitVal > 0 ? String(debitVal) : "";
|
||||||
numberInputs[1].value = creditVal > 0 ? String(creditVal) : "";
|
numberInputs[1].value = creditVal > 0 ? String(creditVal) : "";
|
||||||
|
// 摘要(行)を設定
|
||||||
|
const memoInput = tr.querySelector(".line-memo-input");
|
||||||
|
if (memoInput) memoInput.value = line.line_description || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 入力時に合計を自動更新
|
// 入力時に合計を自動更新
|
||||||
@@ -540,8 +554,15 @@
|
|||||||
const numberInputs = tr.querySelectorAll("input[type='number']");
|
const numberInputs = tr.querySelectorAll("input[type='number']");
|
||||||
const debit = Number(numberInputs[0]?.value || 0);
|
const debit = Number(numberInputs[0]?.value || 0);
|
||||||
const credit = Number(numberInputs[1]?.value || 0);
|
const credit = Number(numberInputs[1]?.value || 0);
|
||||||
|
const lineDesc =
|
||||||
|
tr.querySelector(".line-memo-input")?.value.trim() || undefined;
|
||||||
if (debit === 0 && credit === 0) return;
|
if (debit === 0 && credit === 0) return;
|
||||||
lines.push({ account_id: accountId, debit, credit });
|
lines.push({
|
||||||
|
account_id: accountId,
|
||||||
|
debit,
|
||||||
|
credit,
|
||||||
|
...(lineDesc ? { line_description: lineDesc } : {}),
|
||||||
|
});
|
||||||
d += debit;
|
d += debit;
|
||||||
c += credit;
|
c += credit;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -342,14 +342,20 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>摘要</label>
|
<label>摘要</label>
|
||||||
<input
|
<div
|
||||||
type="text"
|
class="account-search-container"
|
||||||
id="description"
|
style="flex: 3; min-width: 600px; max-width: 900px; position: relative"
|
||||||
list="descriptionHistory"
|
>
|
||||||
style="flex: 3; min-width: 600px"
|
<input
|
||||||
placeholder="例:売上入金/経費支払 など"
|
type="text"
|
||||||
/>
|
id="description"
|
||||||
<datalist id="descriptionHistory"></datalist>
|
class="account-search-input"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="例:売上入金/経費支払 など"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div class="account-dropdown" id="descriptionDropdown"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -406,6 +412,14 @@
|
|||||||
<option value="8r">8%(軽減)</option>
|
<option value="8r">8%(軽減)</option>
|
||||||
<option value="5">5%</option>
|
<option value="5">5%</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label style="margin-left: 16px; white-space: nowrap"
|
||||||
|
>小数点以下端数処理</label
|
||||||
|
>
|
||||||
|
<select id="entryRoundingMode" style="width: 120px" onchange="calcTax()">
|
||||||
|
<option value="floor" selected>切り捨て</option>
|
||||||
|
<option value="round">四捨五入</option>
|
||||||
|
<option value="ceil">切り上げ</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -453,6 +467,257 @@
|
|||||||
|
|
||||||
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
||||||
|
|
||||||
|
<!-- ========================================
|
||||||
|
仕訳詳細入力(多行対応)
|
||||||
|
======================================== -->
|
||||||
|
<details
|
||||||
|
style="margin: 16px 0"
|
||||||
|
open
|
||||||
|
id="detailSection"
|
||||||
|
ontoggle="
|
||||||
|
document.getElementById('detailSummaryText').textContent = this.open
|
||||||
|
? '▼ 仕訳詳細入力(複数行・借貸直接入力)'
|
||||||
|
: '▶ 仕訳詳細入力(複数行・借貸直接入力)'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<summary
|
||||||
|
style="
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 12px;
|
||||||
|
background: #eaf0fb;
|
||||||
|
border: 1px solid #c8d8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
list-style: none;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span id="detailSummaryText"
|
||||||
|
>▼ 仕訳詳細入力(複数行・借貸直接入力)</span
|
||||||
|
>
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
background: #f9fbff;
|
||||||
|
border: 1px solid #c8d8f0;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- 仕訳日 -->
|
||||||
|
<div class="row" style="margin-bottom: 10px">
|
||||||
|
<label style="min-width: 80px">仕訳日</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
id="detailEntryDate"
|
||||||
|
min="1900-01-01"
|
||||||
|
max="2099-12-31"
|
||||||
|
style="padding: 6px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 摘要 -->
|
||||||
|
<div class="row" style="margin-bottom: 10px">
|
||||||
|
<label style="min-width: 80px">摘要</label>
|
||||||
|
<div
|
||||||
|
class="account-search-container"
|
||||||
|
style="
|
||||||
|
flex: 3;
|
||||||
|
min-width: 600px;
|
||||||
|
max-width: 900px;
|
||||||
|
position: relative;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="detailDescription"
|
||||||
|
class="account-search-input"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="例:売上入金/経費支払 など"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div class="account-dropdown" id="detailDescriptionDropdown"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 明細テーブル -->
|
||||||
|
<table
|
||||||
|
id="detailLinesTable"
|
||||||
|
style="border-collapse: collapse; width: 100%; margin-top: 8px"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #eaf0fb">
|
||||||
|
<th
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
width: 24%;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
科目
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
width: 22%;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
借方金額
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
width: 22%;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
貸方金額
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
width: 14%;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
摘要(行)
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 4px;
|
||||||
|
width: 6%;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="detailLinesTbody"></tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr style="background: #f5f5f5; font-weight: bold">
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
合計
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-align: right;
|
||||||
|
"
|
||||||
|
id="detailDebitTotal"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-align: right;
|
||||||
|
"
|
||||||
|
id="detailCreditTotal"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
colspan="2"
|
||||||
|
style="
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 7px 10px;
|
||||||
|
text-align: center;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span id="detailBalanceStatus" style="font-size: 13px"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ボタン類 -->
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onclick="detailAddLine()"
|
||||||
|
style="
|
||||||
|
padding: 7px 18px;
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
+ 行追加
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick="submitDetailJournal()"
|
||||||
|
style="
|
||||||
|
padding: 8px 28px;
|
||||||
|
font-size: 15px;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
登録
|
||||||
|
</button>
|
||||||
|
<label
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<input type="checkbox" id="detailKeepAfterSubmit" />
|
||||||
|
<span style="font-size: 14px">登録後に明細を保持する</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onclick="detailClearAll()"
|
||||||
|
style="
|
||||||
|
padding: 7px 14px;
|
||||||
|
background: #fff;
|
||||||
|
color: #dc3545;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: auto;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
🗑 クリア
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
||||||
|
|
||||||
<!-- 月次锁定管理(折叠式) -->
|
<!-- 月次锁定管理(折叠式) -->
|
||||||
<details style="margin: 16px 0">
|
<details style="margin: 16px 0">
|
||||||
<summary
|
<summary
|
||||||
@@ -722,6 +987,8 @@
|
|||||||
.value.replace(/,/g, "");
|
.value.replace(/,/g, "");
|
||||||
const amount = Number(amountStr) || 0;
|
const amount = Number(amountStr) || 0;
|
||||||
const rateVal = document.getElementById("entryTaxRate").value;
|
const rateVal = document.getElementById("entryTaxRate").value;
|
||||||
|
const roundingMode =
|
||||||
|
document.getElementById("entryRoundingMode")?.value || "floor";
|
||||||
|
|
||||||
if (amount === 0 || rateVal === "0") {
|
if (amount === 0 || rateVal === "0") {
|
||||||
document.getElementById("entryTaxAmount").value = "";
|
document.getElementById("entryTaxAmount").value = "";
|
||||||
@@ -730,8 +997,17 @@
|
|||||||
|
|
||||||
// 税率を数値に変換(8r = 軽減8%)
|
// 税率を数値に変換(8r = 軽減8%)
|
||||||
const rate = rateVal === "8r" ? 8 : Number(rateVal);
|
const rate = rateVal === "8r" ? 8 : Number(rateVal);
|
||||||
// 税込金額から消費税額を計算(切上げ)
|
const rawTax = (amount * rate) / (100 + rate);
|
||||||
const taxAmount = Math.ceil((amount * rate) / (100 + rate));
|
|
||||||
|
// 端数処理
|
||||||
|
const roundFn =
|
||||||
|
roundingMode === "ceil"
|
||||||
|
? Math.ceil
|
||||||
|
: roundingMode === "round"
|
||||||
|
? Math.round
|
||||||
|
: Math.floor;
|
||||||
|
const taxAmount = roundFn(rawTax);
|
||||||
|
|
||||||
document.getElementById("entryTaxAmount").value =
|
document.getElementById("entryTaxAmount").value =
|
||||||
taxAmount.toLocaleString();
|
taxAmount.toLocaleString();
|
||||||
}
|
}
|
||||||
@@ -820,7 +1096,11 @@
|
|||||||
const filtered = accounts.filter((acc) => {
|
const filtered = accounts.filter((acc) => {
|
||||||
const code = acc.account_code.toLowerCase();
|
const code = acc.account_code.toLowerCase();
|
||||||
const name = acc.account_name.toLowerCase();
|
const name = acc.account_name.toLowerCase();
|
||||||
return code.includes(searchText) || name.includes(searchText);
|
return (
|
||||||
|
code.includes(searchText) ||
|
||||||
|
name.includes(searchText) ||
|
||||||
|
`${code} ${name}`.includes(searchText)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderDropdown(filtered, true);
|
renderDropdown(filtered, true);
|
||||||
@@ -895,6 +1175,17 @@
|
|||||||
// 摘要履歴を読み込み
|
// 摘要履歴を読み込み
|
||||||
loadDescriptionHistory();
|
loadDescriptionHistory();
|
||||||
|
|
||||||
|
// 摘要検索ドロップダウンをセットアップ
|
||||||
|
setupDescriptionSearch();
|
||||||
|
|
||||||
|
// 詳細入力セクション初期化
|
||||||
|
setupDetailDescriptionSearch();
|
||||||
|
detailClearAll();
|
||||||
|
// 今日の日付をデフォルト設定
|
||||||
|
document.getElementById("detailEntryDate").value = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
console.log("✓ init() 完成");
|
console.log("✓ init() 完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,7 +1297,11 @@
|
|||||||
const filtered = accounts.filter((acc) => {
|
const filtered = accounts.filter((acc) => {
|
||||||
const code = acc.account_code.toLowerCase();
|
const code = acc.account_code.toLowerCase();
|
||||||
const name = acc.account_name.toLowerCase();
|
const name = acc.account_name.toLowerCase();
|
||||||
return code.includes(searchText) || name.includes(searchText);
|
return (
|
||||||
|
code.includes(searchText) ||
|
||||||
|
name.includes(searchText) ||
|
||||||
|
`${code} ${name}`.includes(searchText)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderSearchDropdown(filtered, true);
|
renderSearchDropdown(filtered, true);
|
||||||
@@ -1043,17 +1338,7 @@
|
|||||||
// 摘要履歴管理
|
// 摘要履歴管理
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
function loadDescriptionHistory() {
|
function loadDescriptionHistory() {
|
||||||
const history = JSON.parse(
|
// 旧datalist方式の互換用に残す(現在はカスタムドロップダウンで管理)
|
||||||
localStorage.getItem("descriptionHistory") || "[]",
|
|
||||||
);
|
|
||||||
const datalist = document.getElementById("descriptionHistory");
|
|
||||||
datalist.innerHTML = "";
|
|
||||||
|
|
||||||
history.forEach((desc) => {
|
|
||||||
const option = document.createElement("option");
|
|
||||||
option.value = desc;
|
|
||||||
datalist.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDescriptionToHistory(description) {
|
function saveDescriptionToHistory(description) {
|
||||||
@@ -1067,22 +1352,564 @@
|
|||||||
history = history.filter((d) => d !== description);
|
history = history.filter((d) => d !== description);
|
||||||
history.unshift(description);
|
history.unshift(description);
|
||||||
|
|
||||||
// 最大100件まで保存
|
// 最大500件まで保存
|
||||||
if (history.length > 100) {
|
if (history.length > 500) {
|
||||||
history = history.slice(0, 100);
|
history = history.slice(0, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem("descriptionHistory", JSON.stringify(history));
|
localStorage.setItem("descriptionHistory", JSON.stringify(history));
|
||||||
loadDescriptionHistory();
|
}
|
||||||
|
|
||||||
|
// 摘要検索ドロップダウン
|
||||||
|
function setupDescriptionSearch() {
|
||||||
|
const input = document.getElementById("description");
|
||||||
|
const dropdown = document.getElementById("descriptionDropdown");
|
||||||
|
if (!input || !dropdown) return;
|
||||||
|
|
||||||
|
let debounceTimer = null;
|
||||||
|
|
||||||
|
function makeSectionLabel(text) {
|
||||||
|
const lbl = document.createElement("div");
|
||||||
|
lbl.style.cssText =
|
||||||
|
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||||||
|
lbl.textContent = text;
|
||||||
|
return lbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOption(text, onSelect) {
|
||||||
|
const opt = document.createElement("div");
|
||||||
|
opt.className = "account-option";
|
||||||
|
opt.style.cssText = "font-size:14px;";
|
||||||
|
opt.textContent = text;
|
||||||
|
opt.addEventListener("mousedown", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
input.value = text;
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
if (onSelect) onSelect();
|
||||||
|
});
|
||||||
|
dropdown.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDropdown(historyItems, apiItems) {
|
||||||
|
dropdown.innerHTML = "";
|
||||||
|
|
||||||
|
// 充てる候補なし
|
||||||
|
if (historyItems.length === 0 && apiItems.length === 0) {
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyItems.length > 0) {
|
||||||
|
dropdown.appendChild(makeSectionLabel("🗒️ 入力履歴"));
|
||||||
|
historyItems.slice(0, 20).forEach((t) => addOption(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiItems.length > 0) {
|
||||||
|
const sep = document.createElement("div");
|
||||||
|
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||||||
|
if (historyItems.length > 0) dropdown.appendChild(sep);
|
||||||
|
dropdown.appendChild(makeSectionLabel("📝 過去の仕訳から"));
|
||||||
|
apiItems.slice(0, 20).forEach((t) => addOption(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSearch(text) {
|
||||||
|
const history = JSON.parse(
|
||||||
|
localStorage.getItem("descriptionHistory") || "[]",
|
||||||
|
);
|
||||||
|
const historyFiltered = text
|
||||||
|
? history.filter((d) =>
|
||||||
|
d.toLowerCase().includes(text.toLowerCase()),
|
||||||
|
)
|
||||||
|
: history.slice(0, 30);
|
||||||
|
|
||||||
|
// APIから過去仕訳の摘要を検索
|
||||||
|
let apiItems = [];
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (text) qs.append("keyword", text);
|
||||||
|
qs.append("limit", "50");
|
||||||
|
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const entries = data.items ?? data;
|
||||||
|
const historySet = new Set(historyFiltered);
|
||||||
|
const seen = new Set();
|
||||||
|
apiItems = entries
|
||||||
|
.map((e) => e.description)
|
||||||
|
.filter(
|
||||||
|
(d) => d && !historySet.has(d) && !seen.has(d) && seen.add(d),
|
||||||
|
)
|
||||||
|
.slice(0, 20);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// APIエラーは無視
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDropdown(historyFiltered, apiItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => doSearch(input.value.trim()), 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("focus", () => {
|
||||||
|
doSearch(input.value.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("click", () => {
|
||||||
|
if (!dropdown.classList.contains("active")) {
|
||||||
|
doSearch(input.value.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!input.parentElement.contains(e.target)) {
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enterキー・Tabキーでドロップダウンを閉じる
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === "Tab") {
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
// (setupTaxSelectors, addRow, handleTax, updateTotals は新UIでは不要)
|
// (setupTaxSelectors, addRow, handleTax, updateTotals は新UIでは不要)
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
// -----------------------------
|
// ============================================================
|
||||||
// 登録(簡易入力版)
|
// 仕訳詳細入力(多行対応)
|
||||||
// -----------------------------
|
// ============================================================
|
||||||
|
let detailRowSeq = 0;
|
||||||
|
|
||||||
|
function detailAddLine() {
|
||||||
|
const tbody = document.getElementById("detailLinesTbody");
|
||||||
|
const rowId = ++detailRowSeq;
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.id = `detailRow_${rowId}`;
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||||||
|
<div class="account-search-container" style="position:relative;">
|
||||||
|
<input type="text" class="account-search-input detail-account-input"
|
||||||
|
data-account-id="" autocomplete="off"
|
||||||
|
placeholder="科目を検索" style="width:100%;font-size:13px;" />
|
||||||
|
<div class="account-dropdown"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||||||
|
<input type="text" class="detail-debit right" inputmode="numeric"
|
||||||
|
style="width:100%;text-align:right;padding:5px;box-sizing:border-box;ime-mode:inactive;"
|
||||||
|
placeholder="0" oninput="formatNumberInput(this);detailUpdateTotals();" />
|
||||||
|
</td>
|
||||||
|
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||||||
|
<input type="text" class="detail-credit right" inputmode="numeric"
|
||||||
|
style="width:100%;text-align:right;padding:5px;box-sizing:border-box;ime-mode:inactive;"
|
||||||
|
placeholder="0" oninput="formatNumberInput(this);detailUpdateTotals();" />
|
||||||
|
</td>
|
||||||
|
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||||||
|
<input type="text" class="detail-line-memo"
|
||||||
|
style="width:100%;padding:5px;box-sizing:border-box;font-size:12px;"
|
||||||
|
placeholder="行摘要(任意)" />
|
||||||
|
</td>
|
||||||
|
<td style="border:1px solid #ccc;padding:4px 2px;text-align:center;">
|
||||||
|
<button onclick="detailRemoveLine(${rowId})"
|
||||||
|
style="padding:4px 10px;background:#dc3545;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px;">削除</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
|
// 科目検索セットアップ
|
||||||
|
const input = tr.querySelector(".detail-account-input");
|
||||||
|
setupDetailAccountSearch(input);
|
||||||
|
|
||||||
|
detailUpdateTotals();
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailRemoveLine(rowId) {
|
||||||
|
const tr = document.getElementById(`detailRow_${rowId}`);
|
||||||
|
if (tr) tr.remove();
|
||||||
|
detailUpdateTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailGetNumber(input) {
|
||||||
|
return Number((input.value || "0").replace(/,/g, "")) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailUpdateTotals() {
|
||||||
|
const tbody = document.getElementById("detailLinesTbody");
|
||||||
|
let debitSum = 0,
|
||||||
|
creditSum = 0;
|
||||||
|
tbody.querySelectorAll("tr").forEach((tr) => {
|
||||||
|
debitSum += detailGetNumber(tr.querySelector(".detail-debit"));
|
||||||
|
creditSum += detailGetNumber(tr.querySelector(".detail-credit"));
|
||||||
|
});
|
||||||
|
|
||||||
|
const fmt = (n) => n.toLocaleString();
|
||||||
|
document.getElementById("detailDebitTotal").textContent = fmt(debitSum);
|
||||||
|
document.getElementById("detailCreditTotal").textContent =
|
||||||
|
fmt(creditSum);
|
||||||
|
|
||||||
|
const el = document.getElementById("detailBalanceStatus");
|
||||||
|
if (debitSum === 0 && creditSum === 0) {
|
||||||
|
el.textContent = "";
|
||||||
|
} else if (debitSum === creditSum) {
|
||||||
|
el.textContent = "✅ 貸借一致";
|
||||||
|
el.style.color = "#28a745";
|
||||||
|
} else {
|
||||||
|
const diff = Math.abs(debitSum - creditSum);
|
||||||
|
el.textContent = `⚠ 差額 ${diff.toLocaleString()} 円`;
|
||||||
|
el.style.color = "#dc3545";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupDetailAccountSearch(inputElement) {
|
||||||
|
const container = inputElement.closest(".account-search-container");
|
||||||
|
const dropdown = container.querySelector(".account-dropdown");
|
||||||
|
|
||||||
|
function makeLbl(text) {
|
||||||
|
const lbl = document.createElement("div");
|
||||||
|
lbl.style.cssText =
|
||||||
|
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||||||
|
lbl.textContent = text;
|
||||||
|
return lbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOpt(acc) {
|
||||||
|
const opt = document.createElement("div");
|
||||||
|
opt.className = "account-option";
|
||||||
|
opt.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
|
||||||
|
opt.addEventListener("mousedown", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
inputElement.value = `${acc.account_code} ${acc.account_name}`;
|
||||||
|
inputElement.dataset.accountId = acc.account_id;
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
saveAccountToRecent(acc.account_id);
|
||||||
|
});
|
||||||
|
dropdown.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDropdown(list, isFiltered) {
|
||||||
|
dropdown.innerHTML = "";
|
||||||
|
if (list.length === 0) {
|
||||||
|
dropdown.innerHTML =
|
||||||
|
'<div style="padding:8px 12px;color:#999;">該当する科目がありません</div>';
|
||||||
|
dropdown.classList.add("active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isFiltered) {
|
||||||
|
list
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.account_code.localeCompare(b.account_code))
|
||||||
|
.slice(0, 50)
|
||||||
|
.forEach(appendOpt);
|
||||||
|
} else {
|
||||||
|
const recentIds = JSON.parse(
|
||||||
|
localStorage.getItem("recentAccounts") || "[]",
|
||||||
|
);
|
||||||
|
const recentItems = recentIds
|
||||||
|
.map((id) => list.find((a) => a.account_id === id))
|
||||||
|
.filter(Boolean);
|
||||||
|
const recentSet = new Set(recentIds);
|
||||||
|
const otherItems = list
|
||||||
|
.filter((a) => !recentSet.has(a.account_id))
|
||||||
|
.sort((a, b) => a.account_code.localeCompare(b.account_code));
|
||||||
|
if (recentItems.length > 0) {
|
||||||
|
dropdown.appendChild(makeLbl("⭐ 最近使った科目"));
|
||||||
|
recentItems.forEach(appendOpt);
|
||||||
|
const sep = document.createElement("div");
|
||||||
|
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||||||
|
dropdown.appendChild(sep);
|
||||||
|
}
|
||||||
|
dropdown.appendChild(makeLbl("📋 すべての科目"));
|
||||||
|
otherItems.slice(0, 50).forEach(appendOpt);
|
||||||
|
}
|
||||||
|
dropdown.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
inputElement.addEventListener("input", (e) => {
|
||||||
|
const q = e.target.value.toLowerCase().trim();
|
||||||
|
inputElement.dataset.accountId = "";
|
||||||
|
const filtered = q
|
||||||
|
? accounts.filter(
|
||||||
|
(a) =>
|
||||||
|
a.account_code.toLowerCase().includes(q) ||
|
||||||
|
a.account_name.toLowerCase().includes(q) ||
|
||||||
|
`${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes(
|
||||||
|
q,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: accounts;
|
||||||
|
renderDropdown(filtered, !!q);
|
||||||
|
});
|
||||||
|
|
||||||
|
inputElement.addEventListener("focus", () => {
|
||||||
|
const q = inputElement.value.toLowerCase().trim();
|
||||||
|
renderDropdown(
|
||||||
|
q
|
||||||
|
? accounts.filter(
|
||||||
|
(a) =>
|
||||||
|
a.account_code.toLowerCase().includes(q) ||
|
||||||
|
a.account_name.toLowerCase().includes(q) ||
|
||||||
|
`${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes(
|
||||||
|
q,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: accounts,
|
||||||
|
!!q,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
inputElement.addEventListener("click", () => {
|
||||||
|
if (!dropdown.classList.contains("active")) {
|
||||||
|
const q = inputElement.value.toLowerCase().trim();
|
||||||
|
renderDropdown(
|
||||||
|
q
|
||||||
|
? accounts.filter(
|
||||||
|
(a) =>
|
||||||
|
a.account_code.toLowerCase().includes(q) ||
|
||||||
|
a.account_name.toLowerCase().includes(q) ||
|
||||||
|
`${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes(
|
||||||
|
q,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: accounts,
|
||||||
|
!!q,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!container.contains(e.target))
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 詳細入力の摘要オートコンプリート
|
||||||
|
function setupDetailDescriptionSearch() {
|
||||||
|
const input = document.getElementById("detailDescription");
|
||||||
|
const dropdown = document.getElementById("detailDescriptionDropdown");
|
||||||
|
if (!input || !dropdown) return;
|
||||||
|
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
function makeSectionLabel(text) {
|
||||||
|
const lbl = document.createElement("div");
|
||||||
|
lbl.style.cssText =
|
||||||
|
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||||||
|
lbl.textContent = text;
|
||||||
|
return lbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOpt(text) {
|
||||||
|
const opt = document.createElement("div");
|
||||||
|
opt.className = "account-option";
|
||||||
|
opt.style.fontSize = "14px";
|
||||||
|
opt.textContent = text;
|
||||||
|
opt.addEventListener("mousedown", (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
input.value = text;
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
});
|
||||||
|
dropdown.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doSearch(text) {
|
||||||
|
const history = JSON.parse(
|
||||||
|
localStorage.getItem("descriptionHistory") || "[]",
|
||||||
|
);
|
||||||
|
const histFiltered = text
|
||||||
|
? history.filter((d) =>
|
||||||
|
d.toLowerCase().includes(text.toLowerCase()),
|
||||||
|
)
|
||||||
|
: history.slice(0, 30);
|
||||||
|
let apiItems = [];
|
||||||
|
try {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (text) qs.append("keyword", text);
|
||||||
|
qs.append("limit", "50");
|
||||||
|
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const entries = data.items ?? data;
|
||||||
|
const hSet = new Set(histFiltered);
|
||||||
|
const seen = new Set();
|
||||||
|
apiItems = entries
|
||||||
|
.map((e) => e.description)
|
||||||
|
.filter((d) => d && !hSet.has(d) && !seen.has(d) && seen.add(d))
|
||||||
|
.slice(0, 20);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
dropdown.innerHTML = "";
|
||||||
|
if (histFiltered.length === 0 && apiItems.length === 0) {
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (histFiltered.length > 0) {
|
||||||
|
dropdown.appendChild(makeSectionLabel("🗒️ 入力履歴"));
|
||||||
|
histFiltered.slice(0, 20).forEach(addOpt);
|
||||||
|
}
|
||||||
|
if (apiItems.length > 0) {
|
||||||
|
if (histFiltered.length > 0) {
|
||||||
|
const s = document.createElement("div");
|
||||||
|
s.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||||||
|
dropdown.appendChild(s);
|
||||||
|
}
|
||||||
|
dropdown.appendChild(makeSectionLabel("📝 過去の仕訳から"));
|
||||||
|
apiItems.forEach(addOpt);
|
||||||
|
}
|
||||||
|
dropdown.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => doSearch(input.value.trim()), 250);
|
||||||
|
});
|
||||||
|
input.addEventListener("focus", () => doSearch(input.value.trim()));
|
||||||
|
input.addEventListener("click", () => {
|
||||||
|
if (!dropdown.classList.contains("active"))
|
||||||
|
doSearch(input.value.trim());
|
||||||
|
});
|
||||||
|
input.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === "Tab")
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
});
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!input.parentElement.contains(e.target))
|
||||||
|
dropdown.classList.remove("active");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDetailJournal() {
|
||||||
|
const dateInput = document.getElementById("detailEntryDate");
|
||||||
|
const entryDate = dateInput.value;
|
||||||
|
const description = document
|
||||||
|
.getElementById("detailDescription")
|
||||||
|
.value.trim();
|
||||||
|
|
||||||
|
if (dateInput.validity.badInput) {
|
||||||
|
alert(
|
||||||
|
"日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!entryDate) {
|
||||||
|
alert("仕訳日を入力してください");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!description) {
|
||||||
|
alert("摘要を入力してください");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
let debitSum = 0,
|
||||||
|
creditSum = 0;
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
document.querySelectorAll("#detailLinesTbody tr").forEach((tr) => {
|
||||||
|
const accountInput = tr.querySelector(".detail-account-input");
|
||||||
|
const accountId = Number(accountInput?.dataset.accountId || 0);
|
||||||
|
const debit = detailGetNumber(tr.querySelector(".detail-debit"));
|
||||||
|
const credit = detailGetNumber(tr.querySelector(".detail-credit"));
|
||||||
|
|
||||||
|
if (debit === 0 && credit === 0) return; // 空行スキップ
|
||||||
|
|
||||||
|
if (!accountId) {
|
||||||
|
alert("科目が選択されていない行があります");
|
||||||
|
valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (debit > 0 && credit > 0) {
|
||||||
|
alert("1行に借方と貸方の両方を入力することはできません");
|
||||||
|
valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineDesc =
|
||||||
|
tr.querySelector(".detail-line-memo")?.value.trim() || null;
|
||||||
|
lines.push({
|
||||||
|
account_id: accountId,
|
||||||
|
debit,
|
||||||
|
credit,
|
||||||
|
line_description: lineDesc || undefined,
|
||||||
|
});
|
||||||
|
debitSum += debit;
|
||||||
|
creditSum += credit;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) return;
|
||||||
|
if (lines.length < 2) {
|
||||||
|
alert("仕訳明細は2行以上必要です");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (debitSum !== creditSum) {
|
||||||
|
alert(
|
||||||
|
`貸借が一致していません(差額:${Math.abs(debitSum - creditSum).toLocaleString()} 円)`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
entry_date: entryDate,
|
||||||
|
description,
|
||||||
|
lines,
|
||||||
|
tax_paid_account_id:
|
||||||
|
taxPaidAccounts.length > 0 ? taxPaidAccounts[0].account_id : null,
|
||||||
|
tax_received_account_id:
|
||||||
|
taxReceivedAccounts.length > 0
|
||||||
|
? taxReceivedAccounts[0].account_id
|
||||||
|
: null,
|
||||||
|
rounding_mode: "floor",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/journal-entries`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json();
|
||||||
|
throw new Error(err.detail || "登録失敗");
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDescriptionToHistory(description);
|
||||||
|
alert("仕訳を登録しました");
|
||||||
|
|
||||||
|
const keep = document.getElementById("detailKeepAfterSubmit").checked;
|
||||||
|
if (!keep) {
|
||||||
|
detailClearAll();
|
||||||
|
}
|
||||||
|
// keep=true の場合は仕訳日・摘要・明細・金額すべてそのまま保持
|
||||||
|
|
||||||
|
searchJournals();
|
||||||
|
} catch (e) {
|
||||||
|
alert("登録エラー: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailClearAll() {
|
||||||
|
document.getElementById("detailLinesTbody").innerHTML = "";
|
||||||
|
document.getElementById("detailDescription").value = "";
|
||||||
|
document.getElementById("detailEntryDate").value = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10);
|
||||||
|
detailRowSeq = 0;
|
||||||
|
detailUpdateTotals();
|
||||||
|
// デフォルト2行追加
|
||||||
|
detailAddLine();
|
||||||
|
detailAddLine();
|
||||||
|
}
|
||||||
|
|
||||||
async function submitJournal() {
|
async function submitJournal() {
|
||||||
try {
|
try {
|
||||||
const entryDateInput = document.getElementById("entryDate");
|
const entryDateInput = document.getElementById("entryDate");
|
||||||
@@ -1216,7 +2043,8 @@
|
|||||||
taxReceivedAccounts.length > 0
|
taxReceivedAccounts.length > 0
|
||||||
? taxReceivedAccounts[0].account_id
|
? taxReceivedAccounts[0].account_id
|
||||||
: null,
|
: null,
|
||||||
rounding_mode: "floor",
|
rounding_mode:
|
||||||
|
document.getElementById("entryRoundingMode")?.value || "floor",
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("送信するデータ:", JSON.stringify(payload, null, 2));
|
console.log("送信するデータ:", JSON.stringify(payload, null, 2));
|
||||||
|
|||||||
@@ -44,10 +44,11 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>科目コード</th>
|
<th style="width: 80px">科目コード</th>
|
||||||
<th>科目名</th>
|
<th>科目名</th>
|
||||||
<th>借方</th>
|
<th>借方</th>
|
||||||
<th>貸方</th>
|
<th>貸方</th>
|
||||||
|
<th>摘要(行)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="lines"></tbody>
|
<tbody id="lines"></tbody>
|
||||||
@@ -123,6 +124,7 @@
|
|||||||
<td style="text-align:right">${
|
<td style="text-align:right">${
|
||||||
line.credit > 0 ? line.credit.toLocaleString() : ""
|
line.credit > 0 ? line.credit.toLocaleString() : ""
|
||||||
}</td>
|
}</td>
|
||||||
|
<td>${line.line_description || ""}</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
|
|
||||||
@@ -137,6 +139,7 @@
|
|||||||
<td colspan="2" style="text-align:center">合計</td>
|
<td colspan="2" style="text-align:center">合計</td>
|
||||||
<td style="text-align:right">${totalDebit.toLocaleString()}</td>
|
<td style="text-align:right">${totalDebit.toLocaleString()}</td>
|
||||||
<td style="text-align:right">${totalCredit.toLocaleString()}</td>
|
<td style="text-align:right">${totalCredit.toLocaleString()}</td>
|
||||||
|
<td></td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(totalRow);
|
tbody.appendChild(totalRow);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user