1649 lines
54 KiB
HTML
1649 lines
54 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<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;
|
||
}
|
||
.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;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h2>仕訳入力</h2>
|
||
|
||
<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>
|
||
<select id="taxPaidSelect"></select>
|
||
|
||
<label>仮受消費税等 勘定</label>
|
||
<select id="taxReceivedSelect"></select>
|
||
</div>
|
||
<div style="margin-bottom: 10px">
|
||
<label>消費税 端数処理:</label>
|
||
<select id="roundingMode">
|
||
<option value="floor">切捨て</option>
|
||
<option value="round">四捨五入</option>
|
||
<option value="ceil">切上げ</option>
|
||
</select>
|
||
</div>
|
||
|
||
<table id="linesTable">
|
||
<thead>
|
||
<tr>
|
||
<th>科目</th>
|
||
<th>借方</th>
|
||
<th>貸方</th>
|
||
<th>税区分</th>
|
||
<th>税方向</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
|
||
<div
|
||
style="
|
||
margin-top: 10px;
|
||
padding: 10px;
|
||
background: #f9f9f9;
|
||
border: 1px solid #ddd;
|
||
display: flex;
|
||
gap: 40px;
|
||
font-weight: bold;
|
||
"
|
||
>
|
||
<div>
|
||
<label>借方合計:</label>
|
||
<span id="totalDebit" style="color: #0066cc; font-size: 16px">0</span>
|
||
円
|
||
</div>
|
||
<div>
|
||
<label>貸方合計:</label>
|
||
<span id="totalCredit" style="color: #cc0000; font-size: 16px">0</span>
|
||
円
|
||
</div>
|
||
<div
|
||
id="balanceStatus"
|
||
style="margin-left: auto; padding: 4px 12px; border-radius: 4px"
|
||
></div>
|
||
</div>
|
||
|
||
<div style="margin-top: 10px; display: flex; gap: 8px">
|
||
<button onclick="addRow()">+ 行追加</button>
|
||
<button onclick="submitJournal()">登録</button>
|
||
</div>
|
||
|
||
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
||
|
||
<h3>月次锁定管理</h3>
|
||
|
||
<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
|
||
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 style="margin: 16px 0">
|
||
<label>期間</label>
|
||
<input
|
||
type="date"
|
||
id="searchFromDate"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
~
|
||
<input type="date" id="searchToDate" min="1900-01-01" max="2099-12-31" />
|
||
|
||
<label style="margin-left: 16px">摘要</label>
|
||
<input type="text" id="searchKeyword" style="min-width: 600px" />
|
||
|
||
<label style="margin-left: 16px">科目</label>
|
||
<select id="searchAccountId" style="min-width: 200px">
|
||
<option value="">-- すべて --</option>
|
||
</select>
|
||
|
||
<label style="margin-left: 16px">
|
||
<input type="checkbox" id="searchIncludeHistory" />
|
||
全検索(修正履歴を含む)
|
||
</label>
|
||
|
||
<button onclick="searchJournals()" style="margin-left: 8px">検索</button>
|
||
<button
|
||
onclick="printSearchResults()"
|
||
style="margin-left: 8px; color: blue"
|
||
>
|
||
🖨️ 印刷
|
||
</button>
|
||
</div>
|
||
|
||
<div id="printArea"></div>
|
||
|
||
<script>
|
||
let rowSeq = 0; // 各行唯一ID
|
||
|
||
const API = "http://127.0.0.1:18080";
|
||
|
||
// 千分位カンマフォーマット関数
|
||
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", "203", "204", "205", "210"],
|
||
},
|
||
{
|
||
label: "🏗️ 固定資産",
|
||
codes: [
|
||
"301",
|
||
"401",
|
||
"402",
|
||
"403",
|
||
"404",
|
||
"405",
|
||
"406",
|
||
"407",
|
||
"408",
|
||
],
|
||
},
|
||
{
|
||
label: "💳 負債",
|
||
codes: [
|
||
"501",
|
||
"502",
|
||
"503",
|
||
"504",
|
||
"505",
|
||
"506",
|
||
"507",
|
||
"508",
|
||
"509",
|
||
],
|
||
},
|
||
{
|
||
label: "🏢 純資産",
|
||
codes: ["601", "602"],
|
||
},
|
||
{
|
||
label: "📈 収益",
|
||
codes: ["701", "702", "703", "704", "705"],
|
||
},
|
||
{
|
||
label: "📉 費用",
|
||
codes: [
|
||
"801",
|
||
"802",
|
||
"803",
|
||
"804",
|
||
"805",
|
||
"806",
|
||
"807",
|
||
"808",
|
||
"809",
|
||
"810",
|
||
],
|
||
},
|
||
];
|
||
|
||
let taxPaidAccounts = [];
|
||
let taxReceivedAccounts = [];
|
||
|
||
// -----------------------------
|
||
// 初期化
|
||
// -----------------------------
|
||
async function init() {
|
||
const res = await fetch(`${API}/accounts`);
|
||
if (!res.ok) {
|
||
alert("科目一覧の取得に失敗しました");
|
||
return;
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
// 🔴 ここ重要:items がある場合と無い場合の両対応
|
||
accounts = data.items ?? data;
|
||
|
||
// 仮払消費税等(あなたの DB では 505 は「未払消費税等」)
|
||
taxPaidAccounts = accounts.filter((a) =>
|
||
a.account_name.includes("仮払")
|
||
);
|
||
|
||
// 仮受消費税等
|
||
taxReceivedAccounts = accounts.filter((a) =>
|
||
a.account_name.includes("仮受")
|
||
);
|
||
|
||
setupTaxSelectors();
|
||
setupSearchAccountSelector();
|
||
addRow();
|
||
|
||
document.getElementById("entryDate").value = new Date()
|
||
.toISOString()
|
||
.slice(0, 10);
|
||
|
||
await onEntryDateChanged();
|
||
|
||
// 摘要履歴を読み込み
|
||
loadDescriptionHistory();
|
||
|
||
// 各フィールドの変更時に合計を更新
|
||
document
|
||
.getElementById("entryDate")
|
||
.addEventListener("change", updateTotals);
|
||
document
|
||
.getElementById("description")
|
||
.addEventListener("input", updateTotals);
|
||
document
|
||
.getElementById("taxPaidSelect")
|
||
.addEventListener("change", updateTotals);
|
||
document
|
||
.getElementById("taxReceivedSelect")
|
||
.addEventListener("change", updateTotals);
|
||
document
|
||
.getElementById("roundingMode")
|
||
.addEventListener("change", () => {
|
||
// 端数処理変更時は全ての税行を再計算
|
||
const rows = document.querySelectorAll(
|
||
"#linesTable tbody tr:not(.tax-row)"
|
||
);
|
||
rows.forEach((row) => handleTax(row));
|
||
});
|
||
}
|
||
|
||
init();
|
||
|
||
// -----------------------------
|
||
// 検索用科目セレクタ
|
||
// -----------------------------
|
||
function setupSearchAccountSelector() {
|
||
const select = document.getElementById("searchAccountId");
|
||
select.innerHTML = '<option value="">-- すべて --</option>';
|
||
|
||
ACCOUNT_GROUPS.forEach((group) => {
|
||
const optgroup = document.createElement("optgroup");
|
||
optgroup.label = group.label;
|
||
|
||
const groupAccounts = accounts.filter((a) =>
|
||
group.codes.includes(a.account_code)
|
||
);
|
||
|
||
groupAccounts.forEach((acc) => {
|
||
const option = document.createElement("option");
|
||
option.value = acc.account_id;
|
||
option.textContent = `${acc.account_code} ${acc.account_name}`;
|
||
optgroup.appendChild(option);
|
||
});
|
||
|
||
select.appendChild(optgroup);
|
||
});
|
||
}
|
||
|
||
// -----------------------------
|
||
// 摘要履歴管理
|
||
// -----------------------------
|
||
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();
|
||
}
|
||
|
||
// -----------------------------
|
||
// 仮払/仮受 セレクタ
|
||
// -----------------------------
|
||
function setupTaxSelectors() {
|
||
document.getElementById("taxPaidSelect").innerHTML =
|
||
`<option value="">-- 選択してください --</option>` +
|
||
taxPaidAccounts
|
||
.map(
|
||
(a) =>
|
||
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
|
||
)
|
||
.join("");
|
||
|
||
document.getElementById("taxReceivedSelect").innerHTML =
|
||
`<option value="">-- 選択してください --</option>` +
|
||
taxReceivedAccounts
|
||
.map(
|
||
(a) =>
|
||
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
// -----------------------------
|
||
// 行追加
|
||
// -----------------------------
|
||
function addRow(isTaxRow = false, taxInfo = null, parentId = null) {
|
||
const tbody = document.querySelector("#linesTable tbody");
|
||
const tr = document.createElement("tr");
|
||
|
||
if (!isTaxRow) {
|
||
tr.dataset.rowId = String(++rowSeq);
|
||
} else {
|
||
tr.classList.add("tax-row");
|
||
tr.dataset.parentId = parentId;
|
||
}
|
||
|
||
tr.innerHTML = `
|
||
<td>
|
||
<select class="account" ${isTaxRow ? "disabled" : ""}>
|
||
${buildAccountOptions()}
|
||
</select>
|
||
</td>
|
||
<td><input type="text" class="debit right" inputmode="numeric" style="ime-mode: inactive;" ${
|
||
isTaxRow ? "readonly" : ""
|
||
} oninput="formatNumberInput(this)"></td>
|
||
<td><input type="text" class="credit right" inputmode="numeric" style="ime-mode: inactive;" ${
|
||
isTaxRow ? "readonly" : ""
|
||
} oninput="formatNumberInput(this)"></td>
|
||
<td>
|
||
<select class="taxType" ${isTaxRow ? "disabled" : ""}>
|
||
<option value="none">対象外</option>
|
||
<option value="10">10%</option>
|
||
<option value="8">8%</option>
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<select class="taxDirection" ${isTaxRow ? "disabled" : ""}>
|
||
<option value="none">対象外</option>
|
||
<option value="paid">仮払(仕入・経費)</option>
|
||
<option value="received">仮受(売上)</option>
|
||
</select>
|
||
</td>
|
||
<td>${
|
||
isTaxRow ? "" : `<button onclick="removeRow(this)">削除</button>`
|
||
}</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
|
||
// 税行の場合,填值
|
||
if (isTaxRow && taxInfo) {
|
||
const acc = tr.querySelector(".account");
|
||
acc.value = taxInfo.accountId || "";
|
||
if (taxInfo.side === "debit")
|
||
tr.querySelector(".debit").value = taxInfo.amount;
|
||
if (taxInfo.side === "credit")
|
||
tr.querySelector(".credit").value = taxInfo.amount;
|
||
}
|
||
|
||
// 普通行:绑定事件
|
||
if (!isTaxRow) {
|
||
const recalc = () => handleTax(tr);
|
||
const debitInput = tr.querySelector(".debit");
|
||
const creditInput = tr.querySelector(".credit");
|
||
const accountSelect = tr.querySelector(".account");
|
||
|
||
debitInput.addEventListener("input", recalc);
|
||
creditInput.addEventListener("input", recalc);
|
||
|
||
// blur(フォーカス離脱)時に合計を更新
|
||
debitInput.addEventListener("blur", updateTotals);
|
||
creditInput.addEventListener("blur", updateTotals);
|
||
|
||
// 科目選択時に履歴を保存
|
||
accountSelect.addEventListener("change", () => {
|
||
saveAccountToRecent(parseInt(accountSelect.value));
|
||
});
|
||
|
||
tr.querySelector(".taxType").addEventListener("change", recalc);
|
||
tr.querySelector(".taxDirection").addEventListener("change", recalc);
|
||
}
|
||
|
||
return tr;
|
||
}
|
||
|
||
// 删除该行的所有税行
|
||
function removeTaxRowsOf(row) {
|
||
const id = row.dataset.rowId;
|
||
if (!id) return;
|
||
document
|
||
.querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`)
|
||
.forEach((r) => r.remove());
|
||
}
|
||
|
||
function removeRow(btn) {
|
||
const row = btn.closest("tr");
|
||
removeTaxRowsOf(row);
|
||
row.remove();
|
||
updateTotals(); // 削除後に合計を更新
|
||
}
|
||
|
||
// -----------------------------
|
||
// 借方・貸方合計を計算して表示
|
||
// -----------------------------
|
||
function updateTotals() {
|
||
let totalDebit = 0;
|
||
let totalCredit = 0;
|
||
|
||
const rows = document.querySelectorAll("#linesTable tbody tr");
|
||
rows.forEach((row) => {
|
||
const debitValue =
|
||
row.querySelector(".debit")?.value.replace(/,/g, "") || "0";
|
||
const creditValue =
|
||
row.querySelector(".credit")?.value.replace(/,/g, "") || "0";
|
||
|
||
totalDebit += Number(debitValue) || 0;
|
||
totalCredit += Number(creditValue) || 0;
|
||
});
|
||
|
||
// 表示を更新
|
||
document.getElementById("totalDebit").textContent =
|
||
totalDebit.toLocaleString();
|
||
document.getElementById("totalCredit").textContent =
|
||
totalCredit.toLocaleString();
|
||
|
||
// 貸借一致チェック
|
||
const balanceStatus = document.getElementById("balanceStatus");
|
||
if (totalDebit === totalCredit && totalDebit > 0) {
|
||
balanceStatus.textContent = "✓ 貸借一致";
|
||
balanceStatus.style.background = "#d4edda";
|
||
balanceStatus.style.color = "#155724";
|
||
} else if (totalDebit === 0 && totalCredit === 0) {
|
||
balanceStatus.textContent = "";
|
||
balanceStatus.style.background = "";
|
||
} else {
|
||
balanceStatus.textContent = `✗ 差額: ${Math.abs(
|
||
totalDebit - totalCredit
|
||
).toLocaleString()}`;
|
||
balanceStatus.style.background = "#f8d7da";
|
||
balanceStatus.style.color = "#721c24";
|
||
}
|
||
}
|
||
|
||
// -----------------------------
|
||
// 税行 自動生成/更新(10%/8%のみ、四捨五入)
|
||
// -----------------------------
|
||
function handleTax(row) {
|
||
// 清理旧税行
|
||
removeTaxRowsOf(row);
|
||
|
||
const taxType = row.querySelector(".taxType").value;
|
||
const taxDir = row.querySelector(".taxDirection").value;
|
||
const debit = Number(
|
||
row.querySelector(".debit").value.replace(/,/g, "") || 0
|
||
);
|
||
const credit = Number(
|
||
row.querySelector(".credit").value.replace(/,/g, "") || 0
|
||
);
|
||
|
||
if (taxType === "none" || taxDir === "none") {
|
||
updateTotals(); // 税なしでも合計更新
|
||
return;
|
||
}
|
||
|
||
const totalAmount = debit || credit;
|
||
if (!totalAmount) {
|
||
updateTotals();
|
||
return;
|
||
}
|
||
|
||
const rate = taxType === "8" ? 0.08 : 0.1;
|
||
const roundingMode = document.getElementById("roundingMode").value;
|
||
|
||
// 税抜額 × 税率 = 税額
|
||
const rawTax = totalAmount * rate;
|
||
let taxAmount;
|
||
switch (roundingMode) {
|
||
case "floor":
|
||
taxAmount = Math.floor(rawTax);
|
||
break;
|
||
case "ceil":
|
||
taxAmount = Math.ceil(rawTax);
|
||
break;
|
||
default:
|
||
taxAmount = Math.round(rawTax);
|
||
}
|
||
|
||
const parentId = row.dataset.rowId;
|
||
|
||
if (taxDir === "paid") {
|
||
const taxAcc = document.getElementById("taxPaidSelect").value;
|
||
if (!taxAcc) {
|
||
updateTotals();
|
||
return alert("仮払消費税等 勘定を選択してください");
|
||
}
|
||
addRow(
|
||
true,
|
||
{ accountId: taxAcc, amount: taxAmount, side: "debit" },
|
||
parentId
|
||
);
|
||
}
|
||
if (taxDir === "received") {
|
||
const taxAcc = document.getElementById("taxReceivedSelect").value;
|
||
if (!taxAcc) {
|
||
updateTotals();
|
||
return alert("仮受消費税等 勘定を選択してください");
|
||
}
|
||
addRow(
|
||
true,
|
||
{ accountId: taxAcc, amount: taxAmount, side: "credit" },
|
||
parentId
|
||
);
|
||
}
|
||
|
||
updateTotals(); // 税行追加後に合計更新
|
||
}
|
||
|
||
// 税行は常に「直後の1行」を使う
|
||
function removeTaxRow(row) {
|
||
const id = row.dataset.rowId;
|
||
if (!id) return;
|
||
|
||
document
|
||
.querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`)
|
||
.forEach((tr) => tr.remove());
|
||
}
|
||
|
||
function removeRow(btn) {
|
||
const row = btn.closest("tr");
|
||
removeTaxRow(row);
|
||
row.remove();
|
||
}
|
||
|
||
// -----------------------------
|
||
// 登録(税行自動生成版)
|
||
// -----------------------------
|
||
async function submitJournal() {
|
||
try {
|
||
const entryDate = document.getElementById("entryDate").value;
|
||
const description = document
|
||
.getElementById("description")
|
||
.value.trim();
|
||
|
||
if (!entryDate) {
|
||
alert("仕訳日を入力してください");
|
||
return;
|
||
}
|
||
|
||
// 明細収集(税情報も含む)
|
||
const rows = document.querySelectorAll("#linesTable tbody tr");
|
||
const lines = [];
|
||
|
||
rows.forEach((row) => {
|
||
if (row.classList.contains("tax-row")) return; // 税行は無視
|
||
|
||
const accountId = Number(row.querySelector(".account").value);
|
||
const debit = Number(
|
||
row.querySelector(".debit").value.replace(/,/g, "") || 0
|
||
);
|
||
const credit = Number(
|
||
row.querySelector(".credit").value.replace(/,/g, "") || 0
|
||
);
|
||
const taxType = row.querySelector(".taxType").value;
|
||
const taxDirection = row.querySelector(".taxDirection").value;
|
||
|
||
if (debit > 0 && credit > 0) {
|
||
alert("同一行不能同时填写借方和贷方");
|
||
throw new Error("invalid line");
|
||
}
|
||
if (debit === 0 && credit === 0) return;
|
||
|
||
const lineData = {
|
||
account_id: accountId,
|
||
debit: debit,
|
||
credit: credit,
|
||
};
|
||
|
||
// 税情報を追加
|
||
if (taxType !== "none" && taxDirection !== "none") {
|
||
lineData.tax_rate = parseInt(taxType);
|
||
lineData.tax_direction = taxDirection;
|
||
}
|
||
|
||
lines.push(lineData);
|
||
});
|
||
|
||
if (lines.length === 0) {
|
||
alert("仕訳明細がありません");
|
||
return;
|
||
}
|
||
|
||
// 税科目(仮払消費税等・仮受消費税等)が存在するかチェック
|
||
const hasTaxAccount = lines.some((line) => {
|
||
const account = accounts.find(
|
||
(a) => a.account_id === line.account_id
|
||
);
|
||
return (
|
||
account &&
|
||
(account.account_name.includes("仮払消費税") ||
|
||
account.account_name.includes("仮受消費税"))
|
||
);
|
||
});
|
||
|
||
// 税科目が存在し、かつ税情報を持つ行がある場合は警告
|
||
const hasTaxInfo = lines.some(
|
||
(line) => line.tax_rate != null && line.tax_direction != null
|
||
);
|
||
|
||
if (hasTaxAccount && hasTaxInfo) {
|
||
if (
|
||
!confirm(
|
||
"警告:仮払消費税等・仮受消費税等の行がすでに存在していますが、他の行に税区分・税方向が設定されています。\n" +
|
||
"このまま登録すると税が二重計算され、借貸不平衡になります。\n\n" +
|
||
"税区分・税方向をすべて「対象外」に設定してから登録することをお勧めします。\n\n" +
|
||
"このまま登録しますか?"
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
const payload = {
|
||
entry_date: entryDate,
|
||
description,
|
||
lines,
|
||
tax_paid_account_id:
|
||
Number(document.getElementById("taxPaidSelect").value) || null,
|
||
tax_received_account_id:
|
||
Number(document.getElementById("taxReceivedSelect").value) ||
|
||
null,
|
||
rounding_mode: document.getElementById("roundingMode").value,
|
||
};
|
||
|
||
console.log("送信するデータ:", JSON.stringify(payload, null, 2));
|
||
console.log("明細行数:", lines.length);
|
||
lines.forEach((line, index) => {
|
||
console.log(`行${index + 1}:`, line);
|
||
});
|
||
|
||
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})`);
|
||
|
||
// フォームをクリア
|
||
document.getElementById("description").value = "";
|
||
const tbody = document.querySelector("#linesTable tbody");
|
||
tbody.innerHTML = "";
|
||
rowSeq = 0;
|
||
addRow();
|
||
|
||
// 借方合計・貸方合計を0にリセット
|
||
document.getElementById("totalDebit").textContent = "0";
|
||
document.getElementById("totalCredit").textContent = "0";
|
||
document.getElementById("balanceStatus").textContent = "";
|
||
document.getElementById("balanceStatus").style.background = "";
|
||
|
||
// 仕訳検索を自動実行して最新の登録状況を表示
|
||
searchJournals();
|
||
} catch (error) {
|
||
console.error("送信エラー:", error);
|
||
alert(
|
||
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`
|
||
);
|
||
}
|
||
}
|
||
|
||
const API_BASE = "http://127.0.0.1:18080";
|
||
|
||
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() {
|
||
// まず検索を実行
|
||
const from = document.getElementById("searchFromDate").value;
|
||
const to = document.getElementById("searchToDate").value;
|
||
const key = document.getElementById("searchKeyword").value;
|
||
const accountId = document.getElementById("searchAccountId").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 (includeHistory) qs.append("include_history", "true");
|
||
|
||
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||
const data = await res.json();
|
||
|
||
// 印刷用に昇順でソート
|
||
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 accountSelect = document.getElementById("searchAccountId");
|
||
const accountName =
|
||
accountSelect.options[accountSelect.selectedIndex]?.text || "";
|
||
let conditions = [];
|
||
if (from || to) {
|
||
conditions.push(`期間: ${from || ""} ~ ${to || ""}`);
|
||
}
|
||
if (key) {
|
||
conditions.push(`摘要: ${key}`);
|
||
}
|
||
if (accountName && accountName !== "-- すべて --") {
|
||
conditions.push(`科目: ${accountName}`);
|
||
}
|
||
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>日付</th>
|
||
<th>摘要</th>
|
||
<th>借方合計</th>
|
||
<th>貸方合計</th>
|
||
<th>税率</th>
|
||
<th>操作</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;
|
||
|
||
const versionInfo =
|
||
includeHistory && e.version > 1 ? ` (v${e.version})` : "";
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td>${e.entry_date}</td>
|
||
<td style="text-align:left">${e.description}${versionInfo}</td>
|
||
<td style="text-align:right">${e.debit_total.toLocaleString()}</td>
|
||
<td style="text-align:right">${e.credit_total.toLocaleString()}</td>
|
||
<td style="text-align:center">${
|
||
e.tax_rates ? e.tax_rates + "%" : ""
|
||
}</td>
|
||
<td>
|
||
<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">合計:</td>
|
||
<td style="text-align:right">${monthDebitTotal.toLocaleString()}</td>
|
||
<td style="text-align:right">${monthCreditTotal.toLocaleString()}</td>
|
||
<td colspan="2"></td>
|
||
`;
|
||
tbody.appendChild(totalRow);
|
||
|
||
printArea.appendChild(section);
|
||
});
|
||
|
||
// 少し待ってから印刷(データの表示を待つ)
|
||
setTimeout(() => {
|
||
window.print();
|
||
}, 100);
|
||
}
|
||
|
||
async function searchJournals() {
|
||
const from = document.getElementById("searchFromDate").value;
|
||
const to = document.getElementById("searchToDate").value;
|
||
const key = document.getElementById("searchKeyword").value;
|
||
const accountId = document.getElementById("searchAccountId").value;
|
||
const includeHistory = document.getElementById(
|
||
"searchIncludeHistory"
|
||
).checked;
|
||
|
||
// 検索条件を保存
|
||
localStorage.setItem(
|
||
"journalSearchConditions",
|
||
JSON.stringify({
|
||
fromDate: from,
|
||
toDate: to,
|
||
keyword: key,
|
||
accountId: accountId,
|
||
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 (includeHistory) qs.append("include_history", "true");
|
||
|
||
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||
const data = await res.json();
|
||
|
||
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 accountSelect = document.getElementById("searchAccountId");
|
||
const accountName =
|
||
accountSelect.options[accountSelect.selectedIndex]?.text || "";
|
||
if (accountName && accountName !== "-- すべて --") {
|
||
conditions.push(`科目: ${accountName}`);
|
||
}
|
||
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>日付</th>
|
||
<th>摘要</th>
|
||
<th>借方合計</th>
|
||
<th>貸方合計</th>
|
||
<th>税率</th>
|
||
<th>操作</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;
|
||
|
||
const versionInfo =
|
||
includeHistory && e.version > 1 ? ` (v${e.version})` : "";
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td>${e.entry_date}</td>
|
||
<td style="text-align:left">${e.description}${versionInfo}</td>
|
||
<td style="text-align:right">${e.debit_total.toLocaleString()}</td>
|
||
<td style="text-align:right">${e.credit_total.toLocaleString()}</td>
|
||
<td style="text-align:center">${
|
||
e.tax_rates ? e.tax_rates + "%" : ""
|
||
}</td>
|
||
<td>
|
||
<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">合計:</td>
|
||
<td style="text-align:right">${monthDebitTotal.toLocaleString()}</td>
|
||
<td style="text-align:right">${monthCreditTotal.toLocaleString()}</td>
|
||
<td colspan="2"></td>
|
||
`;
|
||
tbody.appendChild(totalRow);
|
||
|
||
printArea.appendChild(section);
|
||
});
|
||
}
|
||
|
||
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 || "";
|
||
|
||
// 既存の行をすべてクリア(税行も含む)
|
||
const tbody = document.querySelector("#linesTable tbody");
|
||
tbody.innerHTML = "";
|
||
rowSeq = 0;
|
||
|
||
// 仕訳明細をすべてコピー(税行も含む)
|
||
// 税の自動計算機能は実行しない(税区分・税方向を「対象外」に設定)
|
||
if (data.lines && data.lines.length > 0) {
|
||
data.lines.forEach((line) => {
|
||
const tr = addRow();
|
||
|
||
// 科目を設定
|
||
const accountSelect = tr.querySelector(".account");
|
||
if (accountSelect) {
|
||
accountSelect.value = line.account_id;
|
||
}
|
||
|
||
// 借方・貸方金額を設定(カンマフォーマット付き)
|
||
const debitInput = tr.querySelector(".debit");
|
||
const creditInput = tr.querySelector(".credit");
|
||
if (debitInput && line.debit) {
|
||
debitInput.value = Number(line.debit).toLocaleString();
|
||
}
|
||
if (creditInput && line.credit) {
|
||
creditInput.value = Number(line.credit).toLocaleString();
|
||
}
|
||
|
||
// 税区分・税方向は「対象外」に設定(自動計算を防ぐため)
|
||
const taxTypeSelect = tr.querySelector(".taxType");
|
||
const taxDirSelect = tr.querySelector(".taxDirection");
|
||
|
||
if (taxTypeSelect) {
|
||
taxTypeSelect.value = "none"; // 対象外
|
||
}
|
||
if (taxDirSelect) {
|
||
taxDirSelect.value = "none"; // 対象外
|
||
}
|
||
});
|
||
}
|
||
|
||
// ページ上部の仕訳入力フォームにスクロール
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
|
||
console.log("コピーされたデータ:", data); // デバッグ用
|
||
console.log(
|
||
`${data.lines.length}行をコピーしました。税区分・税方向はすべて「対象外」に設定されています。`
|
||
);
|
||
alert(
|
||
`仕訳を複製しました(${data.lines.length}行)。\n税の自動計算を防ぐため、すべての行の税区分・税方向を「対象外」に設定しています。\n必要に応じて修正してから登録してください。`
|
||
);
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
alert(`エラーが発生しました: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 修正仕訳:逆仕訳を作って修正画面へ遷移
|
||
// --------------------------------------------------
|
||
async function reverseAndEdit(id) {
|
||
if (
|
||
!confirm(
|
||
"この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?"
|
||
)
|
||
)
|
||
return;
|
||
|
||
try {
|
||
// ① 逆仕訳を作成
|
||
const res = await fetch(`${API}/journal-entries/${id}/reverse`, {
|
||
method: "POST",
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const data = await res.json().catch(() => ({}));
|
||
alert(
|
||
data.detail || `逆仕訳の作成に失敗しました (HTTP ${res.status})`
|
||
);
|
||
return;
|
||
}
|
||
|
||
const data = await res.json();
|
||
alert(
|
||
`逆仕訳を作成しました(ID: ${data.reversed_journal_entry_id})`
|
||
);
|
||
|
||
// ② 元仕訳内容を取得して修正画面へ遷移
|
||
const srcRes = await fetch(`${API}/journal-entries/${id}`);
|
||
if (!srcRes.ok) {
|
||
alert("元仕訳の取得に失敗しました");
|
||
return;
|
||
}
|
||
|
||
const srcData = await srcRes.json();
|
||
// 逆仕訳IDを追加して保存(【修正後】仕訳のparent_entry_idとして使用)
|
||
srcData.reversed_journal_entry_id = data.reversed_journal_entry_id;
|
||
localStorage.setItem("editSource", JSON.stringify(srcData));
|
||
|
||
// 修正画面を開く
|
||
window.open("journal-edit.html", "_blank");
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
alert(`エラーが発生しました: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 仕訳削除
|
||
// --------------------------------------------------
|
||
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", () => {
|
||
// デフォルト期間を設定(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);
|
||
|
||
// 常にデフォルト期間を設定(会計年度に合わせるため)
|
||
document.getElementById("searchFromDate").value = defaultFromDate;
|
||
document.getElementById("searchToDate").value = defaultToDate;
|
||
|
||
// 摘要と科目のみ前回の検索条件を復元
|
||
const savedConditions = localStorage.getItem("journalSearchConditions");
|
||
if (savedConditions) {
|
||
const conditions = JSON.parse(savedConditions);
|
||
if (conditions.keyword)
|
||
document.getElementById("searchKeyword").value = conditions.keyword;
|
||
if (conditions.accountId)
|
||
document.getElementById("searchAccountId").value =
|
||
conditions.accountId;
|
||
if (conditions.includeHistory)
|
||
document.getElementById("searchIncludeHistory").checked =
|
||
conditions.includeHistory;
|
||
}
|
||
|
||
// ページロード時に自動的に検索を実行して最新データを表示
|
||
searchJournals();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|