Files
njts-accounting-core/frontend/journal-entry.html
2026-03-03 00:01:51 +09:00

2233 lines
75 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>仕訳入力</title>
<style>
body {
font-family: sans-serif;
margin: 24px;
}
h2 {
margin: 0 0 12px;
}
table {
border-collapse: collapse;
width: 100%;
margin-top: 12px;
}
th,
td {
border: 1px solid #ccc;
padding: 6px;
text-align: center;
}
th {
background: #f5f5f5;
}
input,
select,
button {
padding: 6px;
}
.right {
text-align: right;
}
.row {
margin: 8px 0;
display: flex;
gap: 16px;
align-items: center;
}
.row label {
min-width: 120px;
}
.search-form {
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 20px;
margin: 16px 0;
}
.search-form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px 30px;
margin-bottom: 16px;
}
.search-form-row {
display: flex;
align-items: center;
gap: 12px;
}
.search-form-row label {
min-width: 100px;
font-weight: 500;
color: #333;
}
.search-form-row input,
.search-form-row select {
flex: 1;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.search-form-row input:focus,
.search-form-row select:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 4px rgba(76, 175, 80, 0.3);
}
.search-form-row-full {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 12px;
}
.search-buttons {
display: flex;
gap: 8px;
justify-content: flex-start;
margin-top: 8px;
}
.search-buttons button {
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.search-buttons button:first-child {
background: #4caf50;
color: white;
}
.search-buttons button:first-child:hover {
background: #45a049;
}
.search-buttons button:last-child {
background: white;
color: #2196f3;
border: 1px solid #2196f3;
}
.search-buttons button:last-child:hover {
background: #f0f8ff;
}
.tax-row {
background: #f0f8ff;
}
.print-header {
display: none;
}
.page-break {
page-break-before: always;
}
.page-number {
display: none;
}
@media print {
@page {
margin: 20mm 20mm 25mm 20mm;
@bottom-center {
content: "ページ " counter(page);
font-size: 0.75em;
}
}
body * {
visibility: hidden;
}
#printArea,
#printArea * {
visibility: visible;
}
#printArea {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
button,
.no-print {
display: none !important;
}
.print-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 0.9em;
padding-bottom: 4px;
}
.print-header-left {
text-align: left;
}
.print-header-right {
text-align: right;
}
.page-number {
display: none !important;
}
#printArea table {
font-size: 0.85em;
}
#printArea th,
#printArea td {
padding: 3px 4px;
}
#printArea h3 {
font-size: 1.1em;
margin-bottom: 6px;
}
thead {
display: table-header-group;
}
}
/* 账户搜索下拉框相关样式 */
.account-search-container {
position: relative;
width: 100%;
}
.account-search-input {
width: 100%;
padding: 6px !important;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.account-search-input:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 4px rgba(76, 175, 80, 0.3);
}
.account-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.account-dropdown.active {
display: block;
}
.account-option {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
text-align: left;
display: block;
width: 100%;
}
.account-option:hover,
.account-option.selected {
background-color: #e3f2fd;
}
.account-option-strong {
font-weight: bold;
color: #333;
display: inline;
margin-right: 6px;
}
.account-option-light {
color: #555;
font-size: 14px;
display: inline;
}
</style>
</head>
<body>
<button
onclick="location.href = 'index.html'"
class="back-button"
style="
margin: 0 0 20px 0;
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
"
>
← 首ページに戻る
</button>
<h2>仕訳入力</h2>
<div class="row" style="margin-bottom: 12px">
<label style="min-width: 80px">区分</label>
<label
style="
cursor: pointer;
font-weight: bold;
font-size: 16px;
display: flex;
align-items: center;
gap: 4px;
"
>
<input
type="radio"
name="entryType"
value="expense"
checked
style="transform: scale(1.3)"
/>
【支出】
</label>
<label
style="
cursor: pointer;
font-weight: bold;
font-size: 16px;
display: flex;
align-items: center;
gap: 4px;
margin-left: 16px;
"
>
<input
type="radio"
name="entryType"
value="income"
style="transform: scale(1.3)"
/>
【収入】
</label>
</div>
<div
style="
margin: -4px 0 12px 96px;
font-size: 13px;
color: #666;
line-height: 1.8;
"
>
<span style="margin-right: 24px">
【支出】 借方:<b>科目</b> / 貸方:<b>取引手段</b>
</span>
<span> 【収入】 借方:<b>取引手段</b> / 貸方:<b>科目</b> </span>
</div>
<div class="row">
<label>仕訳日</label>
<input
type="date"
id="entryDate"
min="1900-01-01"
max="2099-12-31"
onchange="onEntryDateChanged()"
/>
</div>
<div class="row">
<label>摘要</label>
<input
type="text"
id="description"
list="descriptionHistory"
style="flex: 3; min-width: 600px"
placeholder="例:売上入金/経費支払 など"
/>
<datalist id="descriptionHistory"></datalist>
</div>
<div class="row">
<label>科目</label>
<div class="account-search-container" style="flex: 2; max-width: 400px">
<input
type="text"
id="entryAccountInput"
class="account-search-input"
placeholder="科目を入力・検索"
autocomplete="off"
/>
<div class="account-dropdown"></div>
</div>
</div>
<div class="row">
<label>取引手段</label>
<div class="account-search-container" style="flex: 2; max-width: 400px">
<input
type="text"
id="entryMethodInput"
class="account-search-input"
placeholder="取引手段を入力・検索(例:現金、普通預金)"
autocomplete="off"
/>
<div class="account-dropdown"></div>
</div>
</div>
<div class="row">
<label>金額</label>
<input
type="text"
id="entryAmount"
class="right"
inputmode="numeric"
style="ime-mode: inactive; width: 200px"
placeholder="税込金額"
oninput="
formatNumberInput(this);
calcTax();
"
/>
<span style="margin-left: -8px"></span>
</div>
<div class="row">
<label>消費税率</label>
<select id="entryTaxRate" style="width: 160px" onchange="calcTax()">
<option value="0">対象外</option>
<option value="10" selected>10%</option>
<option value="8">8%</option>
<option value="8r">8%(軽減)</option>
<option value="5">5%</option>
</select>
</div>
<div class="row">
<label>うち消費税額</label>
<input
type="text"
id="entryTaxAmount"
class="right"
inputmode="numeric"
style="ime-mode: inactive; width: 200px"
placeholder="自動計算(手動変更可)"
oninput="formatNumberInput(this)"
/>
<span style="margin-left: -8px"></span>
</div>
<div style="margin-top: 14px; display: flex; gap: 8px; align-items: center">
<button
onclick="submitJournal()"
style="
padding: 8px 24px;
font-size: 15px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
"
>
登録
</button>
<label
style="
display: flex;
align-items: center;
gap: 6px;
margin-left: 20px;
cursor: pointer;
"
>
<input type="checkbox" id="keepDetailsAfterSubmit" />
<span style="font-size: 14px">登録後に明細を保持する</span>
</label>
</div>
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
<!-- 月次锁定管理(折叠式) -->
<details style="margin: 16px 0">
<summary
style="
cursor: pointer;
font-size: 18px;
font-weight: bold;
padding: 12px;
background: #f5f5f5;
border-radius: 4px;
user-select: none;
"
>
▶ 月次锁定管理
</summary>
<div
style="
padding: 16px;
background: #fafafa;
border-radius: 4px;
margin-top: 8px;
"
>
<div style="margin-bottom: 8px">
<label>年度:</label>
<input
type="number"
id="lockYear"
value="2025"
min="1900"
max="2999"
style="width: 80px"
/>
<label>月份:</label>
<input
type="number"
id="lockMonth"
min="1"
max="12"
value="6"
style="width: 60px"
/>
</div>
<button onclick="lockMonth()">🔒 锁定</button>
<button onclick="unlockMonth()">🔓 解锁</button>
<div id="lockResult" style="margin-top: 10px; color: #333"></div>
<div style="margin-top: 12px">
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
<pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre>
</div>
</div>
</details>
<div
id="monthLockBanner"
style="
display: none;
margin: 8px 0;
padding: 6px 10px;
background: #ffecec;
border: 1px solid #ffaaaa;
color: #a00000;
"
>
🔒 该月份已锁定,不能编辑仕訳
</div>
<!-- ========================================
仕訳検索セクションjournal-list.htmlから統合
======================================== -->
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
<h2>仕訳検索</h2>
<div class="search-form">
<div class="search-form-grid">
<div class="search-form-row">
<label>期間</label>
<div style="display: flex; gap: 8px; align-items: center; flex: 1">
<input
type="date"
id="searchFromDate"
min="1900-01-01"
max="2099-12-31"
/>
<span style="color: #999"></span>
<input
type="date"
id="searchToDate"
min="1900-01-01"
max="2099-12-31"
/>
</div>
</div>
<div class="search-form-row">
<label>科目</label>
<div class="account-search-container">
<input
type="text"
id="searchAccountInput"
class="account-search-input"
placeholder="科目を入力・検索"
autocomplete="off"
/>
<div class="account-dropdown"></div>
</div>
</div>
<div class="search-form-row" style="grid-column: 1 / -1">
<label>摘要</label>
<input type="text" id="searchKeyword" />
</div>
<div class="search-form-row">
<label>方向</label>
<select id="searchAccountSide">
<option value="">-- すべて --</option>
<option value="debit">借方</option>
<option value="credit">貸方</option>
</select>
</div>
<div class="search-form-row">
<label style="min-width: auto">
<input
type="checkbox"
id="searchIncludeHistory"
style="margin-right: 6px"
/>
修正履歴を含む
</label>
</div>
</div>
<div class="search-buttons">
<button onclick="searchJournals()">🔍 検索</button>
<button onclick="printSearchResults()">🖨️ 印刷</button>
</div>
</div>
<div id="printArea"></div>
<script>
const API = "";
// 千分位カンマフォーマット関数
function formatNumberInput(input) {
let value = input.value.replace(/,/g, ""); // 既存のカンマを削除
if (value === "" || isNaN(value)) {
input.value = "";
return;
}
// 数値をカンマ区切りに変換
input.value = Number(value).toLocaleString();
}
let accounts = [];
const ACCOUNT_GROUPS = [
// 資産の部
{
label: "現金・預金",
codes: ["101", "102", "103", "104", "105"],
},
{
label: "売上債権",
codes: ["201", "202"],
},
{
label: "有価証券",
codes: ["301"],
},
{
label: "棚卸資産",
codes: ["210"],
},
{
label: "他流動資産",
codes: ["203", "204", "205", "206", "207", "208"],
},
{
label: "有形固定資産",
codes: ["401", "402", "403", "404", "405", "406", "407"],
},
{
label: "無形固定資産",
codes: ["408"],
},
{
label: "投資その他資産",
codes: ["410"],
},
// 負債の部
{
label: "仕入債務",
codes: ["501"],
},
{
label: "その他流動負債",
codes: ["502", "503", "504", "505", "506", "507", "508"],
},
{
label: "固定負債",
codes: ["509"],
},
// 資本の部
{
label: "資本金",
codes: ["601"],
},
{
label: "利益剰余金",
codes: ["602"],
},
// 収益・費用
{
label: "収益",
codes: ["701", "702", "703", "704", "705"],
},
{
label: "費用",
codes: [
"801",
"802",
"803",
"804",
"805",
"806",
"807",
"808",
"809",
"810",
"811",
"812",
"813",
"814",
"815",
"816",
"817",
"818",
"819",
"820",
],
},
];
let taxPaidAccounts = [];
let taxReceivedAccounts = [];
let rowSeq = 0; // 行ID計数器
// ========================================
// (旧 setupAccountSearch は setupEntryAccountSearch に統合済)
// ========================================
// ========================================
// 消費税額 自動計算
// ========================================
function calcTax() {
const amountStr = document
.getElementById("entryAmount")
.value.replace(/,/g, "");
const amount = Number(amountStr) || 0;
const rateVal = document.getElementById("entryTaxRate").value;
if (amount === 0 || rateVal === "0") {
document.getElementById("entryTaxAmount").value = "";
return;
}
// 税率を数値に変換8r = 軽減8%
const rate = rateVal === "8r" ? 8 : Number(rateVal);
// 税込金額から消費税額を計算(切上げ)
const taxAmount = Math.ceil((amount * rate) / (100 + rate));
document.getElementById("entryTaxAmount").value =
taxAmount.toLocaleString();
}
// ========================================
// 入力フォーム用科目検索セットアップ
// ========================================
function setupEntryAccountSearch(inputElement) {
const container = inputElement.closest(".account-search-container");
const dropdown = container.querySelector(".account-dropdown");
function appendAccountOption(acc, onSelect) {
const option = document.createElement("div");
option.className = "account-option";
option.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
option.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);
if (onSelect) onSelect();
});
dropdown.appendChild(option);
}
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 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) {
// 検索中 → コード順で表示
const sorted = list
.slice()
.sort((a, b) => a.account_code.localeCompare(b.account_code));
sorted.slice(0, 50).forEach((acc) => appendAccountOption(acc));
} 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(makeSectionLabel("⭐ 最近使った科目"));
recentItems.forEach((acc) => appendAccountOption(acc));
const sep = document.createElement("div");
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
dropdown.appendChild(sep);
}
dropdown.appendChild(makeSectionLabel("📋 すべての科目"));
otherItems.slice(0, 50).forEach((acc) => appendAccountOption(acc));
}
dropdown.classList.add("active");
}
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
inputElement.dataset.accountId = "";
if (!searchText) {
renderDropdown(accounts, false);
return;
}
const filtered = accounts.filter((acc) => {
const code = acc.account_code.toLowerCase();
const name = acc.account_name.toLowerCase();
return code.includes(searchText) || name.includes(searchText);
});
renderDropdown(filtered, true);
});
document.addEventListener("click", (e) => {
if (!container.contains(e.target)) {
dropdown.classList.remove("active");
}
});
inputElement.addEventListener("focus", () => {
inputElement.dataset.accountId = inputElement.dataset.accountId || "";
const searchText = inputElement.value.toLowerCase().trim();
if (!searchText) {
renderDropdown(accounts, false);
} else {
inputElement.dispatchEvent(new Event("input"));
}
});
inputElement.addEventListener("click", () => {
if (!dropdown.classList.contains("active")) {
const searchText = inputElement.value.toLowerCase().trim();
if (!searchText) {
renderDropdown(accounts, false);
} else {
inputElement.dispatchEvent(new Event("input"));
}
}
});
}
// ========================================
// 初期化
// ========================================
async function init() {
console.log("🔧 init() 开始...");
const res = await fetch(`${API}/accounts`);
if (!res.ok) {
console.error("❌ API 错误:", res.status);
alert("科目一覧の取得に失敗しました");
return;
}
const data = await res.json();
console.log("✓ 获取到科目数据:", data);
accounts = data.items ?? data;
console.log("✓ accounts 已赋值,共", accounts.length, "个科目");
// 仮払消費税等・仮受消費税等を自動検出
taxPaidAccounts = accounts.filter((a) =>
a.account_name.includes("仮払消費税"),
);
taxReceivedAccounts = accounts.filter((a) =>
a.account_name.includes("仮受消費税"),
);
// 科目・取引手段の検索セットアップ
setupEntryAccountSearch(document.getElementById("entryAccountInput"));
setupEntryAccountSearch(document.getElementById("entryMethodInput"));
setupSearchAccountSelector();
document.getElementById("entryDate").value = new Date()
.toISOString()
.slice(0, 10);
await onEntryDateChanged();
// 摘要履歴を読み込み
loadDescriptionHistory();
console.log("✓ init() 完成");
}
init();
// -----------------------------
// 検索用科目セレクタ
// -----------------------------
function setupSearchAccountSelector() {
// 新的可搜索输入框实现
const inputElement = document.getElementById("searchAccountInput");
if (!inputElement) {
console.log("searchAccountInput 不存在");
return;
}
const container = inputElement.closest(".account-search-container");
if (!container) return;
const dropdown = container.querySelector(".account-dropdown");
function appendSearchOption(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");
performSearch();
});
dropdown.appendChild(opt);
}
function makeSearchSectionLabel(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 renderSearchDropdown(list, isFiltered) {
dropdown.innerHTML = "";
// 「すべて」選択肢
const all = document.createElement("div");
all.className = "account-option";
all.innerHTML = `<span class="account-option-strong">-- すべて --</span>`;
all.addEventListener("mousedown", (ev) => {
ev.preventDefault();
inputElement.value = "";
inputElement.dataset.accountId = "";
dropdown.classList.remove("active");
performSearch();
});
dropdown.appendChild(all);
if (list.length === 0) {
const none = document.createElement("div");
none.style.cssText = "padding:8px 12px;color:#999;";
none.textContent = "該当する科目がありません";
dropdown.appendChild(none);
dropdown.classList.add("active");
return;
}
if (isFiltered) {
const sorted = list
.slice()
.sort((a, b) => a.account_code.localeCompare(b.account_code));
sorted.slice(0, 50).forEach(appendSearchOption);
} 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(makeSearchSectionLabel("⭐ 最近使った科目"));
recentItems.forEach(appendSearchOption);
const sep = document.createElement("div");
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
dropdown.appendChild(sep);
}
dropdown.appendChild(makeSearchSectionLabel("📋 すべての科目"));
otherItems.slice(0, 50).forEach(appendSearchOption);
}
dropdown.classList.add("active");
}
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
inputElement.dataset.accountId = "";
if (!searchText) {
renderSearchDropdown(accounts, false);
return;
}
const filtered = accounts.filter((acc) => {
const code = acc.account_code.toLowerCase();
const name = acc.account_name.toLowerCase();
return code.includes(searchText) || name.includes(searchText);
});
renderSearchDropdown(filtered, true);
});
document.addEventListener("click", (e) => {
if (!container.contains(e.target)) {
dropdown.classList.remove("active");
}
});
inputElement.addEventListener("focus", () => {
const searchText = inputElement.value.toLowerCase().trim();
if (!searchText) {
renderSearchDropdown(accounts, false);
} else {
inputElement.dispatchEvent(new Event("input"));
}
});
inputElement.addEventListener("click", () => {
if (!dropdown.classList.contains("active")) {
const searchText = inputElement.value.toLowerCase().trim();
if (!searchText) {
renderSearchDropdown(accounts, false);
} else {
inputElement.dispatchEvent(new Event("input"));
}
}
});
}
// -----------------------------
// 摘要履歴管理
// -----------------------------
function loadDescriptionHistory() {
const history = JSON.parse(
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) {
if (!description || description.trim() === "") return;
let history = JSON.parse(
localStorage.getItem("descriptionHistory") || "[]",
);
// 重複を避け、先頭に追加
history = history.filter((d) => d !== description);
history.unshift(description);
// 最大100件まで保存
if (history.length > 100) {
history = history.slice(0, 100);
}
localStorage.setItem("descriptionHistory", JSON.stringify(history));
loadDescriptionHistory();
}
// -----------------------------
// (setupTaxSelectors, addRow, handleTax, updateTotals は新UIでは不要)
// -----------------------------
// -----------------------------
// 登録(簡易入力版)
// -----------------------------
async function submitJournal() {
try {
const entryDateInput = document.getElementById("entryDate");
const entryDate = entryDateInput.value;
const description = document
.getElementById("description")
.value.trim();
const entryType = document.querySelector(
'input[name="entryType"]:checked',
).value;
if (entryDateInput.validity.badInput) {
alert(
"日付が正しくありません。存在しない日付が入力されています11月31日は無効です",
);
return;
}
if (!entryDate) {
alert("仕訳日を入力してください");
return;
}
const accountId = Number(
document.getElementById("entryAccountInput").dataset.accountId || 0,
);
const methodId = Number(
document.getElementById("entryMethodInput").dataset.accountId || 0,
);
const amount = Number(
document.getElementById("entryAmount").value.replace(/,/g, "") || 0,
);
const taxRateVal = document.getElementById("entryTaxRate").value;
const taxAmount = Number(
document.getElementById("entryTaxAmount").value.replace(/,/g, "") ||
0,
);
if (!accountId) {
alert("科目を選択してください");
return;
}
if (!methodId) {
alert("取引手段を選択してください");
return;
}
if (amount <= 0) {
alert("金額を入力してください");
return;
}
const netAmount = amount - taxAmount; // 税抜金額
const lines = [];
// 消費税率を数値に変換(バックエンド送信用)
const taxRateNum =
taxRateVal === "8r"
? 8
: taxRateVal === "0"
? null
: Number(taxRateVal);
if (entryType === "expense") {
// ===== 支出 =====
// 借方: 科目(税抜金額)
lines.push({
account_id: accountId,
debit: netAmount,
credit: 0,
});
// 借方: 仮払消費税等(税額)※税額がある場合のみ
if (taxAmount > 0) {
const taxPaidAcc =
taxPaidAccounts.length > 0 ? taxPaidAccounts[0] : null;
if (!taxPaidAcc) {
alert("仮払消費税等の勘定科目が見つかりません");
return;
}
lines.push({
account_id: taxPaidAcc.account_id,
debit: taxAmount,
credit: 0,
});
}
// 貸方: 取引手段(税込金額)
lines.push({
account_id: methodId,
debit: 0,
credit: amount,
});
} else {
// ===== 収入 =====
// 借方: 取引手段(税込金額)
lines.push({
account_id: methodId,
debit: amount,
credit: 0,
});
// 貸方: 科目(税抜金額)
lines.push({
account_id: accountId,
debit: 0,
credit: netAmount,
});
// 貸方: 仮受消費税等(税額)※税額がある場合のみ
if (taxAmount > 0) {
const taxReceivedAcc =
taxReceivedAccounts.length > 0 ? taxReceivedAccounts[0] : null;
if (!taxReceivedAcc) {
alert("仮受消費税等の勘定科目が見つかりません");
return;
}
lines.push({
account_id: taxReceivedAcc.account_id,
debit: 0,
credit: taxAmount,
});
}
}
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",
};
console.log("送信するデータ:", JSON.stringify(payload, null, 2));
const res = await fetch(`${API}/journal-entries`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
const errorMsg =
errorData.detail || `登録に失敗しました (HTTP ${res.status})`;
console.error("Error:", errorData);
alert(errorMsg);
return;
}
const result = await res.json();
// 摘要を履歴に保存
saveDescriptionToHistory(description);
alert(`登録完了ID=${result.journal_entry_id}`);
// 「登録後に明細を保持する」チェックボックスを確認
const keepDetails = document.getElementById(
"keepDetailsAfterSubmit",
).checked;
if (!keepDetails) {
// フォームをクリア
document.getElementById("description").value = "";
document.getElementById("entryAccountInput").value = "";
document.getElementById("entryAccountInput").dataset.accountId = "";
document.getElementById("entryMethodInput").value = "";
document.getElementById("entryMethodInput").dataset.accountId = "";
document.getElementById("entryAmount").value = "";
document.getElementById("entryTaxRate").value = "10";
document.getElementById("entryTaxAmount").value = "";
}
// 仕訳検索を自動実行して最新の登録状況を表示
searchJournals();
} catch (error) {
console.error("送信エラー:", error);
alert(
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`,
);
}
}
const API_BASE = "";
async function callApi(path) {
try {
const res = await fetch(`${API_BASE}${path}`, { method: "POST" });
const text = await res.text();
let data = {};
try {
data = text ? JSON.parse(text) : {};
} catch {
data = { detail: text };
}
if (!res.ok) {
throw new Error(data.detail || `HTTP ${res.status}`);
}
return data;
} catch (e) {
throw new Error(e.message || "请求失败");
}
}
async function lockMonth() {
const year = document.getElementById("lockYear").value;
const month = document.getElementById("lockMonth").value;
const el = document.getElementById("lockResult");
el.innerText = "处理中...";
try {
await callApi(`/month-locks/lock?fiscal_year=${year}&month=${month}`);
el.innerText = `${year}${month}月 已锁定`;
await refreshMonthLocks(); // 可选:自动刷新列表
} catch (e) {
el.innerText = `❌ 错误:${e.message}`;
}
}
async function unlockMonth() {
const year = document.getElementById("lockYear").value;
const month = document.getElementById("lockMonth").value;
const el = document.getElementById("lockResult");
el.innerText = "处理中...";
try {
await callApi(
`/month-locks/unlock?fiscal_year=${year}&month=${month}`,
);
el.innerText = `${year}${month}月 已解锁`;
await refreshMonthLocks(); // 可选:自动刷新列表
} catch (e) {
el.innerText = `❌ 错误:${e.message}`;
}
}
async function refreshMonthLocks() {
const el = document.getElementById("lockList");
el.textContent = "读取中...";
try {
const res = await fetch(`${API_BASE}/month-locks`);
const data = await res.json();
// 只显示 locked 的,避免太长
const locked = (data || []).filter((x) => x.is_locked);
if (locked.length === 0) {
el.textContent = "(当前没有锁定月份)";
return;
}
el.textContent = locked
.map(
(x) =>
`${x.fiscal_year}-${String(x.month).padStart(
2,
"0",
)} locked_by=${x.locked_by ?? ""}`,
)
.join("\n");
} catch (e) {
el.textContent = `读取失败:${e.message || e}`;
}
}
async function checkMonthLockByDate(dateStr) {
if (!dateStr) return false;
const d = new Date(dateStr);
const year = d.getFullYear();
const month = d.getMonth() + 1;
const res = await fetch(`${API_BASE}/month-locks`);
const data = await res.json();
return (data || []).some(
(x) => x.fiscal_year === year && x.month === month && x.is_locked,
);
}
async function onEntryDateChanged() {
const dateStr = document.getElementById("entryDate").value;
const locked = await checkMonthLockByDate(dateStr);
const banner = document.getElementById("monthLockBanner");
if (locked) {
banner.style.display = "block";
setJournalReadonly(true);
} else {
banner.style.display = "none";
setJournalReadonly(false);
}
}
function setJournalReadonly(readonly) {
// 保存按钮
const saveBtn = document.getElementById("saveButton");
if (saveBtn) saveBtn.disabled = readonly;
// 行追加按钮
const addRowBtn = document.getElementById("addRowButton");
if (addRowBtn) addRowBtn.disabled = readonly;
// 所有“删除行”按钮
document.querySelectorAll(".delete-row-btn").forEach((btn) => {
btn.disabled = readonly;
});
// 所有仕訳输入项(金额 / 科目等)
document.querySelectorAll(".journal-input").forEach((input) => {
input.disabled = readonly;
});
}
function buildAccountOptions() {
let html = "";
// 最近使った科目を表示
const recentAccountIds = JSON.parse(
localStorage.getItem("recentAccounts") || "[]",
);
if (recentAccountIds.length > 0) {
html += `<optgroup label="⭐ 最近使った科目">`;
recentAccountIds.forEach((accountId) => {
const account = accounts.find((a) => a.account_id === accountId);
if (account) {
html += `<option value="${account.account_id}">
${account.account_code} ${account.account_name}
</option>`;
}
});
html += `</optgroup>`;
}
// 已分组的科目
ACCOUNT_GROUPS.forEach((group) => {
html += `<optgroup label="${group.label}">`;
accounts
.filter((a) => group.codes.includes(a.account_code))
.forEach((a) => {
html += `<option value="${a.account_id}">
${a.account_code} ${a.account_name}
</option>`;
});
html += `</optgroup>`;
});
// 未分组的其他科目
const groupedCodes = ACCOUNT_GROUPS.flatMap((g) => g.codes);
const others = accounts.filter(
(a) => !groupedCodes.includes(a.account_code),
);
if (others.length > 0) {
html += `<optgroup label="📚 その他">`;
others.forEach((a) => {
html += `<option value="${a.account_id}">
${a.account_code} ${a.account_name}
</option>`;
});
html += `</optgroup>`;
}
return html;
}
// 科目選択履歴を保存
function saveAccountToRecent(accountId) {
if (!accountId) return;
let recentAccounts = JSON.parse(
localStorage.getItem("recentAccounts") || "[]",
);
// 重複を除去して先頭に追加
recentAccounts = recentAccounts.filter((id) => id !== accountId);
recentAccounts.unshift(accountId);
// 最大20件まで保持
if (recentAccounts.length > 20) {
recentAccounts = recentAccounts.slice(0, 20);
}
localStorage.setItem("recentAccounts", JSON.stringify(recentAccounts));
}
// ========================================
// 仕訳検索機能journal-list.htmlから統合
// ========================================
// 検索結果を印刷(検索を実行してから印刷)
async function printSearchResults() {
try {
// まず検索を実行
const from = document.getElementById("searchFromDate").value;
const to = document.getElementById("searchToDate").value;
const key = document.getElementById("searchKeyword").value;
const accountInput = document.getElementById("searchAccountInput");
const accountId = accountInput ? accountInput.dataset.accountId : "";
const accountSide =
document.getElementById("searchAccountSide").value;
const includeHistory = document.getElementById(
"searchIncludeHistory",
).checked;
const qs = new URLSearchParams();
if (from) qs.append("from_date", from);
if (to) qs.append("to_date", to);
if (key) qs.append("keyword", key);
if (accountId) qs.append("account_id", accountId);
if (accountSide) qs.append("account_side", accountSide);
if (includeHistory) qs.append("include_history", "true");
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
if (!res.ok) {
throw new Error(`API error: ${res.status} ${res.statusText}`);
}
let data = await res.json();
if (!Array.isArray(data)) {
console.warn("⚠️ API返り値は配列ではありません:", data);
data = [];
}
// 印刷用に昇順でソート
const printData = [...data].sort((a, b) => {
if (a.entry_date !== b.entry_date) {
return a.entry_date.localeCompare(b.entry_date);
}
return a.journal_entry_id - b.journal_entry_id;
});
const printArea = document.getElementById("printArea");
printArea.innerHTML = "";
// 月ごとにデータをグループ化
const monthGroups = {};
printData.forEach((e) => {
const entryMonth = e.entry_date.substring(0, 7);
if (!monthGroups[entryMonth]) {
monthGroups[entryMonth] = [];
}
monthGroups[entryMonth].push(e);
});
// 検索条件を生成
const accountName = accountInput ? accountInput.value : "";
const sideSelect = document.getElementById("searchAccountSide");
const sideName =
sideSelect.options[sideSelect.selectedIndex]?.text || "";
let conditions = [];
if (from || to) {
conditions.push(`期間: ${from || ""} ${to || ""}`);
}
if (key) {
conditions.push(`摘要: ${key}`);
}
if (accountName && accountName !== "-- すべて --") {
conditions.push(`科目: ${accountName}`);
}
if (sideName && sideName !== "-- すべて --") {
conditions.push(`方向: ${sideName}`);
}
if (includeHistory) {
conditions.push(`修正履歴を含む`);
}
const conditionsText =
conditions.length > 0 ? conditions.join(" | ") : "検索条件: すべて";
// 月ごとにテーブルを生成(印刷用)
Object.keys(monthGroups)
.sort()
.forEach((month, index) => {
const monthData = monthGroups[month];
const monthDisplay = new Date(month + "-01").toLocaleDateString(
"ja-JP",
{
year: "numeric",
month: "long",
},
);
const section = document.createElement("div");
if (index > 0) {
section.className = "page-break";
}
section.innerHTML = `
<table style="margin-bottom: 20px;">
<thead>
<tr>
<th colspan="6" style="text-align: center; font-size: 1.2em; padding: 8px; border: none; background: transparent;">仕訳検索結果</th>
</tr>
<tr>
<th colspan="3" style="text-align: left; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${conditionsText}</th>
<th colspan="3" style="text-align: right; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${monthDisplay}</th>
</tr>
<tr>
<th style="white-space: nowrap;">日付</th>
<th>摘要</th>
<th style="white-space: nowrap;">借方合計</th>
<th style="white-space: nowrap;">貸方合計</th>
<th>税率</th>
<th style="white-space: nowrap;">操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="page-number">ページ ${index + 1}</div>
`;
const tbody = section.querySelector("tbody");
let monthDebitTotal = 0;
let monthCreditTotal = 0;
monthData.forEach((e) => {
monthDebitTotal += e.debit_total;
monthCreditTotal += e.credit_total;
// 修正版本の表示revised_count を使用)
// includeHistory に関係なく、修正版本は常に表示
const versionInfo =
e.revision_count && e.revision_count > 1
? ` (v${e.revision_count})`
: "";
const tr = document.createElement("tr");
tr.innerHTML = `
<td style="white-space: nowrap;">${e.entry_date}</td>
<td style="text-align:left">${e.description}${versionInfo}</td>
<td style="text-align:right; white-space: nowrap;">${e.debit_total.toLocaleString()}</td>
<td style="text-align:right; white-space: nowrap;">${e.credit_total.toLocaleString()}</td>
<td style="text-align:center">${
e.tax_rates ? e.tax_rates + "%" : ""
}</td>
<td style="white-space: nowrap;">
<button onclick="viewJournal(${
e.journal_entry_id
})">表示</button>
<button onclick="copyJournal(${
e.journal_entry_id
})" style="color: green;">複製</button>
<button onclick="reverseAndEdit(${
e.journal_entry_id
})">修正</button>
<button onclick="deleteJournalEntry(${
e.journal_entry_id
})" style="color: red;">削除</button>
</td>
`;
tbody.appendChild(tr);
});
// 月合計行を追加
const totalRow = document.createElement("tr");
totalRow.style.fontWeight = "bold";
totalRow.style.backgroundColor = "#f0f0f0";
totalRow.innerHTML = `
<td colspan="2" style="text-align:right; white-space: nowrap;">合計:</td>
<td style="text-align:right; white-space: nowrap;">${monthDebitTotal.toLocaleString()}</td>
<td style="text-align:right; white-space: nowrap;">${monthCreditTotal.toLocaleString()}</td>
<td colspan="2"></td>
`;
tbody.appendChild(totalRow);
printArea.appendChild(section);
});
// 少し待ってから印刷(データの表示を待つ)
setTimeout(() => {
window.print();
}, 100);
} catch (err) {
console.error("❌ 印刷用検索エラー:", err);
const printArea = document.getElementById("printArea");
if (printArea) {
printArea.innerHTML = `
<div style="padding: 20px; color: red; border: 2px solid red;">
<h3>❌ 検索に失敗しました</h3>
<p><strong>エラー内容:</strong> ${err.message}</p>
<p style="font-size: 0.9em; color: #666;">
F12 で開発者ツールを確認して、コンソールログを確認してください。
</p>
</div>
`;
}
alert(`検索に失敗しました: ${err.message}`);
}
}
// 搜索函数(在选择搜索科目时被触发)
function performSearch() {
searchJournals();
}
// 检索结果を表に表示
async function searchJournals() {
try {
const from = document.getElementById("searchFromDate").value;
const to = document.getElementById("searchToDate").value;
const key = document.getElementById("searchKeyword").value;
const accountInput = document.getElementById("searchAccountInput");
const accountId = accountInput ? accountInput.dataset.accountId : "";
const accountSide =
document.getElementById("searchAccountSide").value;
const includeHistory = document.getElementById(
"searchIncludeHistory",
).checked;
// 検索条件を保存
localStorage.setItem(
"journalSearchConditions",
JSON.stringify({
fromDate: from,
toDate: to,
keyword: key,
accountId: accountId,
accountSide: accountSide,
includeHistory: includeHistory,
}),
);
const qs = new URLSearchParams();
if (from) qs.append("from_date", from);
if (to) qs.append("to_date", to);
if (key) qs.append("keyword", key);
if (accountId) qs.append("account_id", accountId);
if (accountSide) qs.append("account_side", accountSide);
if (includeHistory) qs.append("include_history", "true");
console.log(
"🔍 検索 URL:",
`${API}/journal-entries?${qs.toString()}`,
);
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
// ❌ API レスポンス確認
if (!res.ok) {
throw new Error(`API error: ${res.status} ${res.statusText}`);
}
let data = await res.json();
// 返されたデータが配列か確認
if (!Array.isArray(data)) {
console.warn("⚠️ API返り値は配列ではありません:", data);
data = [];
}
console.log("✓ 検索結果:", data.length, "件");
const printArea = document.getElementById("printArea");
printArea.innerHTML = "";
// 月ごとにデータをグループ化
const monthGroups = {};
data.forEach((e) => {
const entryMonth = e.entry_date.substring(0, 7); // YYYY-MM
if (!monthGroups[entryMonth]) {
monthGroups[entryMonth] = [];
}
monthGroups[entryMonth].push(e);
});
// 検索条件を生成
const conditionsText = (() => {
let conditions = [];
if (from || to) {
conditions.push(`期間: ${from || ""} ${to || ""}`);
}
if (key) {
conditions.push(`摘要: ${key}`);
}
const accountInput = document.getElementById("searchAccountInput");
const accountName = accountInput ? accountInput.value : "";
if (accountName && accountName.trim() !== "") {
conditions.push(`科目: ${accountName}`);
}
const sideSelect = document.getElementById("searchAccountSide");
const sideName = sideSelect
? sideSelect.options[sideSelect.selectedIndex]?.text
: "";
if (sideName && sideName !== "-- すべて --") {
conditions.push(`方向: ${sideName}`);
}
if (includeHistory) {
conditions.push(`修正履歴を含む`);
}
return conditions.length > 0
? conditions.join(" | ")
: "検索条件: すべて";
})();
// 月ごとにテーブルを生成(画面表示は降順)
Object.keys(monthGroups)
.sort((a, b) => b.localeCompare(a)) // 月を降順で表示
.forEach((month, index) => {
const monthData = monthGroups[month];
const monthDisplay = new Date(month + "-01").toLocaleDateString(
"ja-JP",
{
year: "numeric",
month: "long",
},
);
const section = document.createElement("div");
if (index > 0) {
section.className = "page-break";
}
section.innerHTML = `
<table style="margin-bottom: 20px;">
<thead>
<tr>
<th colspan="6" style="text-align: center; font-size: 1.2em; padding: 8px; border: none; background: transparent;">仕訳検索結果</th>
</tr>
<tr>
<th colspan="3" style="text-align: left; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${conditionsText}</th>
<th colspan="3" style="text-align: right; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${monthDisplay}</th>
</tr>
<tr>
<th style="white-space: nowrap;">日付</th>
<th>摘要</th>
<th style="white-space: nowrap;">借方合計</th>
<th style="white-space: nowrap;">貸方合計</th>
<th>税率</th>
<th style="white-space: nowrap;">操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="page-number">ページ ${index + 1}</div>
`;
const tbody = section.querySelector("tbody");
let monthDebitTotal = 0;
let monthCreditTotal = 0;
// 月内のデータも降順で表示(画面表示用)
monthData.forEach((e) => {
monthDebitTotal += e.debit_total;
monthCreditTotal += e.credit_total;
// 修正版本の表示revision_count を使用)
// includeHistory に関係なく、修正版本は常に表示
const versionInfo =
e.revision_count && e.revision_count > 1
? ` (v${e.revision_count})`
: "";
const tr = document.createElement("tr");
tr.innerHTML = `
<td style="white-space: nowrap;">${e.entry_date}</td>
<td style="text-align:left">${e.description}${versionInfo}</td>
<td style="text-align:right; white-space: nowrap;">${e.debit_total.toLocaleString()}</td>
<td style="text-align:right; white-space: nowrap;">${e.credit_total.toLocaleString()}</td>
<td style="text-align:center">${
e.tax_rates ? e.tax_rates + "%" : ""
}</td>
<td style="white-space: nowrap;">
<button onclick="viewJournal(${
e.journal_entry_id
})">表示</button>
<button onclick="copyJournal(${
e.journal_entry_id
})" style="color: green;">複製</button>
<button onclick="reverseAndEdit(${
e.journal_entry_id
})">修正</button>
<button onclick="deleteJournalEntry(${
e.journal_entry_id
})" style="color: red;">削除</button>
</td>
`;
tbody.appendChild(tr);
});
// 月合計行を追加
const totalRow = document.createElement("tr");
totalRow.style.fontWeight = "bold";
totalRow.style.backgroundColor = "#f0f0f0";
totalRow.innerHTML = `
<td colspan="2" style="text-align:right; white-space: nowrap;">合計:</td>
<td style="text-align:right; white-space: nowrap;">${monthDebitTotal.toLocaleString()}</td>
<td style="text-align:right; white-space: nowrap;">${monthCreditTotal.toLocaleString()}</td>
<td colspan="2"></td>
`;
tbody.appendChild(totalRow);
printArea.appendChild(section);
});
} catch (err) {
console.error("❌ 検索エラー:", err);
const printArea = document.getElementById("printArea");
if (printArea) {
printArea.innerHTML = `
<div style="padding: 20px; color: red; border: 2px solid red;">
<h3>❌ 検索に失敗しました</h3>
<p><strong>エラー内容:</strong> ${err.message}</p>
<p style="font-size: 0.9em; color: #666;">
F12 で開発者ツールを確認して、コンソールログを確認してください。
</p>
</div>
`;
}
alert(`検索に失敗しました: ${err.message}`);
}
}
function viewJournal(id) {
window.open(`journal-view.html?id=${id}`, "_blank");
}
// --------------------------------------------------
// 仕訳複製:既存の仕訳を入力フォームにコピー
// --------------------------------------------------
async function copyJournal(id) {
try {
const res = await fetch(`${API}/journal-entries/${id}`);
if (!res.ok) {
alert("仕訳の取得に失敗しました");
return;
}
const data = await res.json();
// 日付をコピー
document.getElementById("entryDate").value = data.entry_date;
// 摘要をコピー
document.getElementById("description").value = data.description || "";
// 新UIでは簡易的にフォームに反映最初の借方行の科目 + 最初の貸方行を取引手段に設定)
if (data.lines && data.lines.length > 0) {
// 借方と貸方を分類
const debitLines = data.lines.filter((l) => Number(l.debit) > 0);
const creditLines = data.lines.filter((l) => Number(l.credit) > 0);
// 仮払/仮受消費税行を除外して主要な科目を特定
const mainDebit = debitLines.find((l) => {
const acc = accounts.find((a) => a.account_id === l.account_id);
return (
acc &&
!acc.account_name.includes("仮払消費税") &&
!acc.account_name.includes("仮受消費税")
);
});
const mainCredit = creditLines.find((l) => {
const acc = accounts.find((a) => a.account_id === l.account_id);
return (
acc &&
!acc.account_name.includes("仮払消費税") &&
!acc.account_name.includes("仮受消費税")
);
});
// 税額行を検出
const taxDebitLine = debitLines.find((l) => {
const acc = accounts.find((a) => a.account_id === l.account_id);
return acc && acc.account_name.includes("仮払消費税");
});
const taxCreditLine = creditLines.find((l) => {
const acc = accounts.find((a) => a.account_id === l.account_id);
return acc && acc.account_name.includes("仮受消費税");
});
if (mainDebit && mainCredit) {
// 支出パターン: 借方=科目, 貸方=取引手段
if (taxDebitLine) {
// 支出(仮払消費税あり)
document.querySelector(
'input[name="entryType"][value="expense"]',
).checked = true;
const kamokuAcc = accounts.find(
(a) => a.account_id === mainDebit.account_id,
);
const methodAcc = accounts.find(
(a) => a.account_id === mainCredit.account_id,
);
if (kamokuAcc) {
document.getElementById("entryAccountInput").value =
`${kamokuAcc.account_code} ${kamokuAcc.account_name}`;
document.getElementById(
"entryAccountInput",
).dataset.accountId = kamokuAcc.account_id;
}
if (methodAcc) {
document.getElementById("entryMethodInput").value =
`${methodAcc.account_code} ${methodAcc.account_name}`;
document.getElementById(
"entryMethodInput",
).dataset.accountId = methodAcc.account_id;
}
const totalAmount = Number(mainCredit.credit);
document.getElementById("entryAmount").value =
totalAmount.toLocaleString();
document.getElementById("entryTaxAmount").value = Number(
taxDebitLine.debit,
).toLocaleString();
} else if (taxCreditLine) {
// 収入(仮受消費税あり)
document.querySelector(
'input[name="entryType"][value="income"]',
).checked = true;
const kamokuAcc = accounts.find(
(a) => a.account_id === mainCredit.account_id,
);
const methodAcc = accounts.find(
(a) => a.account_id === mainDebit.account_id,
);
if (kamokuAcc) {
document.getElementById("entryAccountInput").value =
`${kamokuAcc.account_code} ${kamokuAcc.account_name}`;
document.getElementById(
"entryAccountInput",
).dataset.accountId = kamokuAcc.account_id;
}
if (methodAcc) {
document.getElementById("entryMethodInput").value =
`${methodAcc.account_code} ${methodAcc.account_name}`;
document.getElementById(
"entryMethodInput",
).dataset.accountId = methodAcc.account_id;
}
const totalAmount = Number(mainDebit.debit);
document.getElementById("entryAmount").value =
totalAmount.toLocaleString();
document.getElementById("entryTaxAmount").value = Number(
taxCreditLine.credit,
).toLocaleString();
} else {
// 消費税なし - 借方科目が費用/資産 → 支出、それ以外は収入
const kamokuAccD = accounts.find(
(a) => a.account_id === mainDebit.account_id,
);
const isExpense =
kamokuAccD &&
(kamokuAccD.account_code.startsWith("8") ||
kamokuAccD.account_code.startsWith("1") ||
kamokuAccD.account_code.startsWith("2") ||
kamokuAccD.account_code.startsWith("3") ||
kamokuAccD.account_code.startsWith("4"));
if (isExpense) {
document.querySelector(
'input[name="entryType"][value="expense"]',
).checked = true;
document.getElementById("entryAccountInput").value =
`${kamokuAccD.account_code} ${kamokuAccD.account_name}`;
document.getElementById(
"entryAccountInput",
).dataset.accountId = kamokuAccD.account_id;
const methodAcc = accounts.find(
(a) => a.account_id === mainCredit.account_id,
);
if (methodAcc) {
document.getElementById("entryMethodInput").value =
`${methodAcc.account_code} ${methodAcc.account_name}`;
document.getElementById(
"entryMethodInput",
).dataset.accountId = methodAcc.account_id;
}
document.getElementById("entryAmount").value = Number(
mainDebit.debit,
).toLocaleString();
} else {
document.querySelector(
'input[name="entryType"][value="income"]',
).checked = true;
const kamokuAccC = accounts.find(
(a) => a.account_id === mainCredit.account_id,
);
if (kamokuAccC) {
document.getElementById("entryAccountInput").value =
`${kamokuAccC.account_code} ${kamokuAccC.account_name}`;
document.getElementById(
"entryAccountInput",
).dataset.accountId = kamokuAccC.account_id;
}
document.getElementById("entryMethodInput").value =
`${kamokuAccD.account_code} ${kamokuAccD.account_name}`;
document.getElementById(
"entryMethodInput",
).dataset.accountId = kamokuAccD.account_id;
document.getElementById("entryAmount").value = Number(
mainDebit.debit,
).toLocaleString();
}
document.getElementById("entryTaxRate").value = "0";
document.getElementById("entryTaxAmount").value = "";
}
}
}
// ページ上部の仕訳入力フォームにスクロール
window.scrollTo({ top: 0, behavior: "smooth" });
const dateInput = document.getElementById("entryDate");
dateInput.focus();
alert(
`仕訳を複製しました。\n\n` +
`※ このコピーは新規仕訳として独立した記録になります。\n` +
`※ 日付と摘要を必要に応じて修正してから登録してください。`,
);
} catch (error) {
console.error("Error:", error);
alert(`エラーが発生しました: ${error.message}`);
}
}
// --------------------------------------------------
// 修正仕訳:逆仕訳を作って修正画面へ遷移
// --------------------------------------------------
async function reverseAndEdit(id) {
try {
// 获取元仕訳内容
const srcRes = await fetch(`${API}/journal-entries/${id}`);
if (!srcRes.ok) {
return;
}
const srcData = await srcRes.json();
localStorage.setItem("editSource", JSON.stringify(srcData));
// 打开修正画面
window.open("journal-edit.html", "_blank");
} catch (error) {
console.error("Error:", error);
}
}
// --------------------------------------------------
// 仕訳削除
// --------------------------------------------------
async function deleteJournalEntry(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();
if (!res.ok) {
alert(data.detail || "削除に失敗しました");
return;
}
alert("削除しました");
searchJournals(); // リストを再読み込み
}
// ページロード後に検索条件を設定
window.addEventListener("DOMContentLoaded", async () => {
// デフォルト期間を設定5/31決算6月1日現在
const today = new Date();
const currentYear = today.getFullYear();
const currentMonth = today.getMonth() + 1; // 1-12
let fiscalStartYear;
if (currentMonth >= 6) {
// 6月以降当年6月1日から
fiscalStartYear = currentYear;
} else {
// 1-5月前年6月1日から
fiscalStartYear = currentYear - 1;
}
const defaultFromDate = `${fiscalStartYear}-06-01`;
const defaultToDate = today.toISOString().slice(0, 10);
// 前回の検索条件を復元
const savedConditions = localStorage.getItem("journalSearchConditions");
if (savedConditions) {
try {
const conditions = JSON.parse(savedConditions);
// 期間を復元
document.getElementById("searchFromDate").value =
conditions.fromDate || defaultFromDate;
document.getElementById("searchToDate").value =
conditions.toDate || defaultToDate;
// 摘要、科目、方向を復元
if (conditions.keyword !== undefined && conditions.keyword !== null)
document.getElementById("searchKeyword").value =
conditions.keyword;
if (
conditions.accountId !== undefined &&
conditions.accountId !== null
) {
const accountInput =
document.getElementById("searchAccountInput");
if (accountInput && accounts && accounts.length > 0) {
const account = accounts.find(
(a) => a.account_id === conditions.accountId,
);
if (account) {
accountInput.value = `${account.account_code} ${account.account_name}`;
accountInput.dataset.accountId = account.account_id;
}
}
}
if (
conditions.accountSide !== undefined &&
conditions.accountSide !== null
)
document.getElementById("searchAccountSide").value =
conditions.accountSide;
if (
conditions.includeHistory !== undefined &&
conditions.includeHistory !== null
)
document.getElementById("searchIncludeHistory").checked =
conditions.includeHistory;
} catch (e) {
// パース失敗時はデフォルト期間を設定
document.getElementById("searchFromDate").value = defaultFromDate;
document.getElementById("searchToDate").value = defaultToDate;
}
} else {
// 保存された条件がない場合はデフォルト期間を設定
document.getElementById("searchFromDate").value = defaultFromDate;
document.getElementById("searchToDate").value = defaultToDate;
}
// 検索条件が復元されたので、自動的に検索を実行
console.log("🔍 自動検索を実行します...");
await searchJournals();
});
</script>
</body>
</html>