519 lines
15 KiB
HTML
519 lines
15 KiB
HTML
<!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;
|
||
}
|
||
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;
|
||
}
|
||
.error {
|
||
color: #c00;
|
||
margin-top: 8px;
|
||
}
|
||
.ok {
|
||
color: #090;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* 账户搜索下拉框相关样式 */
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.account-option:hover,
|
||
.account-option.selected {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.account-option-strong {
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.account-option-light {
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h2>修正仕訳入力</h2>
|
||
|
||
<label>仕訳日</label>
|
||
<input type="date" id="entryDate" min="1900-01-01" max="2099-12-31" />
|
||
|
||
<label>摘要</label>
|
||
<input type="text" id="description" style="width: 60%" />
|
||
|
||
<table id="linesTable">
|
||
<thead>
|
||
<tr>
|
||
<th>科目</th>
|
||
<th>借方</th>
|
||
<th>貸方</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<th>合計</th>
|
||
<th id="debitTotal" class="right">0</th>
|
||
<th id="creditTotal" class="right">0</th>
|
||
<th></th>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
|
||
<div style="margin-top: 12px">
|
||
<button onclick="addRow()">+ 行追加</button>
|
||
<button onclick="submitEntry()">修正版仕訳を登録</button>
|
||
<button onclick="cancelEdit()" style="background-color: #f0f0f0">
|
||
キャンセル
|
||
</button>
|
||
</div>
|
||
|
||
<div id="result"></div>
|
||
|
||
<script>
|
||
const API = "";
|
||
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",
|
||
],
|
||
},
|
||
];
|
||
|
||
// ========================================
|
||
// 账户搜索功能
|
||
// ========================================
|
||
function setupAccountSearch(inputElement, row) {
|
||
const container = inputElement.closest(".account-search-container");
|
||
const dropdown = container.querySelector(".account-dropdown");
|
||
|
||
inputElement.addEventListener("input", (e) => {
|
||
const searchText = e.target.value.toLowerCase().trim();
|
||
|
||
if (!searchText) {
|
||
dropdown.classList.remove("active");
|
||
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);
|
||
});
|
||
|
||
// 生成下拉建议
|
||
dropdown.innerHTML = "";
|
||
if (filtered.length === 0) {
|
||
dropdown.innerHTML = `<div style="padding: 8px 12px; color: #999;">該当する科目がありません</div>`;
|
||
} else {
|
||
filtered.slice(0, 20).forEach((acc) => {
|
||
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("click", () => {
|
||
inputElement.value = `${acc.account_code} ${acc.account_name}`;
|
||
inputElement.dataset.accountId = acc.account_id;
|
||
dropdown.classList.remove("active");
|
||
});
|
||
dropdown.appendChild(option);
|
||
});
|
||
}
|
||
|
||
dropdown.classList.add("active");
|
||
});
|
||
|
||
// クリック以外の場所のドロップダウンを非表示
|
||
document.addEventListener("click", (e) => {
|
||
if (!container.contains(e.target)) {
|
||
dropdown.classList.remove("active");
|
||
}
|
||
});
|
||
|
||
// フォーカス時にドロップダウンを表示(既存値がある場合)
|
||
inputElement.addEventListener("focus", () => {
|
||
if (inputElement.value) {
|
||
dropdown.classList.add("active");
|
||
}
|
||
});
|
||
}
|
||
|
||
// 科目選択肢をグループ分けして構築
|
||
function buildAccountOptions() {
|
||
let html = "";
|
||
|
||
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;
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 初期化
|
||
// ------------------------------------
|
||
async function init() {
|
||
// 科目取得
|
||
const res = await fetch(`${API}/accounts`);
|
||
const data = await res.json();
|
||
accounts = data.items ?? data;
|
||
|
||
// 元仕訳取得
|
||
const src = localStorage.getItem("editSource");
|
||
if (!src) {
|
||
showError("修正元の仕訳データが見つかりません");
|
||
return;
|
||
}
|
||
const srcData = JSON.parse(src);
|
||
|
||
// ヘッダ反映
|
||
document.getElementById("entryDate").value = srcData.entry_date;
|
||
document.getElementById("description").value =
|
||
`【修正後】${srcData.description}`;
|
||
|
||
// 明細反映
|
||
srcData.lines.forEach((l) => addRow(l));
|
||
|
||
recalc();
|
||
}
|
||
init();
|
||
|
||
// ------------------------------------
|
||
// 行操作
|
||
// ------------------------------------
|
||
function addRow(line = null) {
|
||
const tbody = document.querySelector("#linesTable tbody");
|
||
const tr = document.createElement("tr");
|
||
|
||
tr.innerHTML = `
|
||
<td>
|
||
<div class="account-search-container">
|
||
<input
|
||
type="text"
|
||
class="account-search-input account"
|
||
placeholder="科目を入力・検索"
|
||
autocomplete="off" />
|
||
<div class="account-dropdown"></div>
|
||
</div>
|
||
</td>
|
||
<td><input type="number" min="0"></td>
|
||
<td><input type="number" min="0"></td>
|
||
<td><button onclick="removeRow(this)">削除</button></td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
|
||
// Setup account search for this row
|
||
const accountInput = tr.querySelector(".account-search-input");
|
||
setupAccountSearch(accountInput, tr);
|
||
|
||
if (line) {
|
||
// Find the account and populate the input
|
||
const account = accounts.find(a => a.account_id === line.account_id);
|
||
if (account) {
|
||
accountInput.value = `${account.account_code} ${account.account_name}`;
|
||
accountInput.dataset.accountId = account.account_id;
|
||
}
|
||
tr.querySelectorAll("input")[1].value = line.debit || "";
|
||
tr.querySelectorAll("input")[2].value = line.credit || "";
|
||
}
|
||
|
||
// 入力時に合計を自動更新
|
||
tr.querySelectorAll("input").forEach((input) => {
|
||
input.addEventListener("input", recalc);
|
||
input.addEventListener("change", recalc);
|
||
});
|
||
}
|
||
|
||
function removeRow(btn) {
|
||
btn.closest("tr").remove();
|
||
recalc();
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 合計再計算
|
||
// ------------------------------------
|
||
function recalc() {
|
||
let d = 0,
|
||
c = 0;
|
||
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
|
||
d += Number(tr.querySelectorAll("input")[0].value || 0);
|
||
c += Number(tr.querySelectorAll("input")[1].value || 0);
|
||
});
|
||
document.getElementById("debitTotal").textContent = d.toLocaleString();
|
||
document.getElementById("creditTotal").textContent = c.toLocaleString();
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 登録
|
||
// ------------------------------------
|
||
async function submitEntry() {
|
||
const result = document.getElementById("result");
|
||
result.textContent = "";
|
||
result.className = "";
|
||
|
||
const entryDate = document.getElementById("entryDate").value;
|
||
const desc = document.getElementById("description").value;
|
||
|
||
if (!entryDate || !desc) {
|
||
showError("日付と摘要は必須です");
|
||
return;
|
||
}
|
||
|
||
const lines = [];
|
||
let d = 0,
|
||
c = 0;
|
||
|
||
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
|
||
const accountInput = tr.querySelector(".account-search-input");
|
||
const accountId = Number(accountInput.dataset.accountId || 0);
|
||
const debit = Number(tr.querySelectorAll("input")[1].value || 0);
|
||
const credit = Number(tr.querySelectorAll("input")[2].value || 0);
|
||
if (debit === 0 && credit === 0) return;
|
||
lines.push({ account_id: accountId, debit, credit });
|
||
d += debit;
|
||
c += credit;
|
||
});
|
||
|
||
if (lines.length < 2) return showError("仕訳行は2行以上必要です");
|
||
if (d !== c) return showError("借方合計と貸方合計が一致していません");
|
||
|
||
// 源仕訳IDを親IDとして取得(修正の場合)
|
||
const src = localStorage.getItem("editSource");
|
||
const srcData = src ? JSON.parse(src) : null;
|
||
// 【修正仕訳】の original_entry_id は、修正元の仕訳ID
|
||
// 如果当前是修正版本,传递它的 original_entry_id(真正的原始版本)
|
||
// 如果当前是原始版本,传递它自己的 journal_entry_id
|
||
const originalEntryId =
|
||
srcData?.original_entry_id || srcData?.journal_entry_id || null;
|
||
|
||
const payload = {
|
||
entry_date: entryDate,
|
||
description: desc,
|
||
lines,
|
||
tax_paid_account_id: null,
|
||
tax_received_account_id: null,
|
||
rounding_mode: "floor",
|
||
original_entry_id: originalEntryId,
|
||
};
|
||
|
||
try {
|
||
const res = await fetch(`${API}/journal-entries`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) return showError(data.detail || "登録失敗");
|
||
|
||
// 登録成功 - 親窓口の検索を再実行してから画面を閉じる
|
||
alert(`登録しました(ID: ${data.journal_entry_id})`);
|
||
localStorage.removeItem("editSource");
|
||
|
||
// 親窓口(列表页)の searchJournals()を呼び出す
|
||
if (window.opener && window.opener.searchJournals) {
|
||
window.opener.searchJournals();
|
||
}
|
||
|
||
window.close();
|
||
} catch {
|
||
showError("通信エラー");
|
||
}
|
||
}
|
||
|
||
function showError(msg) {
|
||
const r = document.getElementById("result");
|
||
r.textContent = msg;
|
||
r.className = "error";
|
||
}
|
||
function showOk(msg) {
|
||
const r = document.getElementById("result");
|
||
r.textContent = msg;
|
||
r.className = "ok";
|
||
}
|
||
|
||
function cancelEdit() {
|
||
localStorage.removeItem("editSource");
|
||
window.close();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|