This commit is contained in:
admin
2026-01-15 10:56:05 +09:00
parent c753e89f49
commit 6c5e8593fb
6 changed files with 314 additions and 169 deletions

View File

@@ -16,8 +16,8 @@ app = FastAPI(
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], # 本地开发先用 * allow_origins=["*"],
allow_credentials=True, allow_credentials=False,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )

View File

@@ -92,6 +92,11 @@ def fetch_trial_balance(date_from: str, date_to: str):
closing = opening + debit - credit closing = opening + debit - credit
# 負債・資本は絶対値で表示(見やすくするため)
if acc["account_type"] in ("liability", "equity"):
opening = abs(opening)
closing = abs(closing)
account_data[code] = { account_data[code] = {
"account_id": aid, "account_id": aid,
"account_code": code, "account_code": code,

View File

@@ -38,7 +38,8 @@ class JournalEntryDeleteRequest(BaseModel):
def get_journal_entries( def get_journal_entries(
from_date: date = None, from_date: date = None,
to_date: date = None, to_date: date = None,
keyword: str = None keyword: str = None,
account_id: int = None
): ):
""" """
仕訳一覧を検索して取得 仕訳一覧を検索して取得
@@ -73,6 +74,14 @@ def get_journal_entries(
sql += " AND je.description LIKE %s" sql += " AND je.description LIKE %s"
params.append(f"%{keyword}%") params.append(f"%{keyword}%")
if account_id:
sql += """ AND je.journal_entry_id IN (
SELECT DISTINCT journal_entry_id
FROM journal_lines
WHERE account_id = %s
)"""
params.append(account_id)
sql += """ sql += """
GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year
ORDER BY je.entry_date DESC, je.journal_entry_id DESC ORDER BY je.entry_date DESC, je.journal_entry_id DESC
@@ -116,7 +125,9 @@ def get_journal_entry(journal_id: int):
a.account_code, a.account_code,
a.account_name, a.account_name,
jl.debit, jl.debit,
jl.credit jl.credit,
jl.tax_rate,
jl.tax_direction
FROM journal_lines jl FROM journal_lines jl
JOIN accounts a ON jl.account_id = a.account_id JOIN accounts a ON jl.account_id = a.account_id
WHERE jl.journal_entry_id = %s WHERE jl.journal_entry_id = %s
@@ -301,19 +312,35 @@ def create_journal_entry(req: JournalEntryRequest):
# 明細(税行を含む) # 明細(税行を含む)
for line in all_lines: for line in all_lines:
# tax_directionの値を変換: paid->INPUT, received->OUTPUT, none->NONE
tax_dir_db = None
if line.tax_direction:
if line.tax_direction == 'paid':
tax_dir_db = 'INPUT'
elif line.tax_direction == 'received':
tax_dir_db = 'OUTPUT'
elif line.tax_direction == 'none':
tax_dir_db = 'NONE'
else:
tax_dir_db = line.tax_direction.upper()
cur.execute(""" cur.execute("""
INSERT INTO journal_lines ( INSERT INTO journal_lines (
journal_entry_id, journal_entry_id,
account_id, account_id,
debit, debit,
credit credit,
tax_rate,
tax_direction
) )
VALUES (%s, %s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s)
""", ( """, (
entry_id, entry_id,
line.account_id, line.account_id,
line.debit, line.debit,
line.credit line.credit,
line.tax_rate,
tax_dir_db
)) ))
conn.commit() conn.commit()

View File

@@ -177,7 +177,12 @@
<input type="date" id="searchToDate" min="1900-01-01" max="2999-12-31" /> <input type="date" id="searchToDate" min="1900-01-01" max="2999-12-31" />
<label style="margin-left: 16px">摘要</label> <label style="margin-left: 16px">摘要</label>
<input type="text" id="searchKeyword" /> <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>
<button onclick="searchJournals()" style="margin-left: 8px">検索</button> <button onclick="searchJournals()" style="margin-left: 8px">検索</button>
</div> </div>
@@ -200,6 +205,17 @@
const API = "http://127.0.0.1:18080"; 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 = []; let accounts = [];
const ACCOUNT_GROUPS = [ const ACCOUNT_GROUPS = [
@@ -269,6 +285,7 @@
); );
setupTaxSelectors(); setupTaxSelectors();
setupSearchAccountSelector();
addRow(); addRow();
document.getElementById("entryDate").value = new Date() document.getElementById("entryDate").value = new Date()
@@ -283,6 +300,32 @@
init(); 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);
});
}
// ----------------------------- // -----------------------------
// 摘要履歴管理 // 摘要履歴管理
// ----------------------------- // -----------------------------
@@ -363,12 +406,12 @@
${buildAccountOptions()} ${buildAccountOptions()}
</select> </select>
</td> </td>
<td><input type="number" class="debit right" min="0" ${ <td><input type="text" class="debit right" ${
isTaxRow ? "readonly" : "" isTaxRow ? "readonly" : ""
}></td> } oninput="formatNumberInput(this)"></td>
<td><input type="number" class="credit right" min="0" ${ <td><input type="text" class="credit right" ${
isTaxRow ? "readonly" : "" isTaxRow ? "readonly" : ""
}></td> } oninput="formatNumberInput(this)"></td>
<td> <td>
<select class="taxType" ${isTaxRow ? "disabled" : ""}> <select class="taxType" ${isTaxRow ? "disabled" : ""}>
<option value="none">対象外</option> <option value="none">対象外</option>
@@ -436,8 +479,12 @@
const taxType = row.querySelector(".taxType").value; const taxType = row.querySelector(".taxType").value;
const taxDir = row.querySelector(".taxDirection").value; const taxDir = row.querySelector(".taxDirection").value;
const debit = Number(row.querySelector(".debit").value || 0); const debit = Number(
const credit = Number(row.querySelector(".credit").value || 0); row.querySelector(".debit").value.replace(/,/g, "") || 0
);
const credit = Number(
row.querySelector(".credit").value.replace(/,/g, "") || 0
);
if (taxType === "none" || taxDir === "none") return; if (taxType === "none" || taxDir === "none") return;
@@ -503,8 +550,11 @@
// 登録(税行自動生成版) // 登録(税行自動生成版)
// ----------------------------- // -----------------------------
async function submitJournal() { async function submitJournal() {
try {
const entryDate = document.getElementById("entryDate").value; const entryDate = document.getElementById("entryDate").value;
const description = document.getElementById("description").value.trim(); const description = document
.getElementById("description")
.value.trim();
if (!entryDate) { if (!entryDate) {
alert("仕訳日を入力してください"); alert("仕訳日を入力してください");
@@ -519,8 +569,12 @@
if (row.classList.contains("tax-row")) return; // 税行は無視 if (row.classList.contains("tax-row")) return; // 税行は無視
const accountId = Number(row.querySelector(".account").value); const accountId = Number(row.querySelector(".account").value);
const debit = Number(row.querySelector(".debit").value || 0); const debit = Number(
const credit = Number(row.querySelector(".credit").value || 0); row.querySelector(".debit").value.replace(/,/g, "") || 0
);
const credit = Number(
row.querySelector(".credit").value.replace(/,/g, "") || 0
);
const taxType = row.querySelector(".taxType").value; const taxType = row.querySelector(".taxType").value;
const taxDirection = row.querySelector(".taxDirection").value; const taxDirection = row.querySelector(".taxDirection").value;
@@ -550,6 +604,36 @@
return; 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 = { const payload = {
entry_date: entryDate, entry_date: entryDate,
description, description,
@@ -557,10 +641,17 @@
tax_paid_account_id: tax_paid_account_id:
Number(document.getElementById("taxPaidSelect").value) || null, Number(document.getElementById("taxPaidSelect").value) || null,
tax_received_account_id: tax_received_account_id:
Number(document.getElementById("taxReceivedSelect").value) || null, Number(document.getElementById("taxReceivedSelect").value) ||
null,
rounding_mode: document.getElementById("roundingMode").value, 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`, { const res = await fetch(`${API}/journal-entries`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -582,7 +673,22 @@
saveDescriptionToHistory(description); saveDescriptionToHistory(description);
alert(`登録完了ID=${result.journal_entry_id}`); alert(`登録完了ID=${result.journal_entry_id}`);
location.reload();
// フォームをクリア
document.getElementById("description").value = "";
const tbody = document.querySelector("#linesTable tbody");
tbody.innerHTML = "";
rowSeq = 0;
addRow();
// 仕訳検索を自動実行して最新の登録状況を表示
searchJournals();
} catch (error) {
console.error("送信エラー:", error);
alert(
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`
);
}
} }
const API_BASE = "http://127.0.0.1:18080"; const API_BASE = "http://127.0.0.1:18080";
@@ -763,6 +869,7 @@
const from = document.getElementById("searchFromDate").value; const from = document.getElementById("searchFromDate").value;
const to = document.getElementById("searchToDate").value; const to = document.getElementById("searchToDate").value;
const key = document.getElementById("searchKeyword").value; const key = document.getElementById("searchKeyword").value;
const accountId = document.getElementById("searchAccountId").value;
// 検索条件を保存 // 検索条件を保存
localStorage.setItem( localStorage.setItem(
@@ -771,6 +878,7 @@
fromDate: from, fromDate: from,
toDate: to, toDate: to,
keyword: key, keyword: key,
accountId: accountId,
}) })
); );
@@ -778,6 +886,7 @@
if (from) qs.append("from_date", from); if (from) qs.append("from_date", from);
if (to) qs.append("to_date", to); if (to) qs.append("to_date", to);
if (key) qs.append("keyword", key); if (key) qs.append("keyword", key);
if (accountId) qs.append("account_id", accountId);
const res = await fetch(`${API}/journal-entries?${qs.toString()}`); const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
const data = await res.json(); const data = await res.json();
@@ -793,11 +902,15 @@
<td style="text-align:right">${e.debit_total.toLocaleString()}</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:right">${e.credit_total.toLocaleString()}</td>
<td> <td>
<button onclick="viewJournal(${e.journal_entry_id})">表示</button> <button onclick="viewJournal(${
e.journal_entry_id
})">表示</button>
<button onclick="copyJournal(${ <button onclick="copyJournal(${
e.journal_entry_id e.journal_entry_id
})" style="color: green;">複製</button> })" style="color: green;">複製</button>
<button onclick="reverseAndEdit(${e.journal_entry_id})">修正</button> <button onclick="reverseAndEdit(${
e.journal_entry_id
})">修正</button>
<button onclick="deleteJournalEntry(${ <button onclick="deleteJournalEntry(${
e.journal_entry_id e.journal_entry_id
})" style="color: red;">削除</button> })" style="color: red;">削除</button>
@@ -824,10 +937,8 @@
const data = await res.json(); const data = await res.json();
// 日付は今日の日付をデフォルトに(必要に応じて変更可能) // 日付をコピー元の仕訳日に設定
document.getElementById("entryDate").value = new Date() document.getElementById("entryDate").value = data.entry_date;
.toISOString()
.slice(0, 10);
// 摘要をコピー // 摘要をコピー
document.getElementById("description").value = data.description || ""; document.getElementById("description").value = data.description || "";
@@ -837,7 +948,8 @@
tbody.innerHTML = ""; tbody.innerHTML = "";
rowSeq = 0; rowSeq = 0;
// 仕訳明細をコピー // 仕訳明細をすべてコピー(税行も含む)
// 税の自動計算機能は実行しない(税区分・税方向を「対象外」に設定)
if (data.lines && data.lines.length > 0) { if (data.lines && data.lines.length > 0) {
data.lines.forEach((line) => { data.lines.forEach((line) => {
const tr = addRow(); const tr = addRow();
@@ -848,26 +960,25 @@
accountSelect.value = line.account_id; accountSelect.value = line.account_id;
} }
// 借方・貸方金額を設定 // 借方・貸方金額を設定(カンマフォーマット付き)
const debitInput = tr.querySelector(".debit"); const debitInput = tr.querySelector(".debit");
const creditInput = tr.querySelector(".credit"); const creditInput = tr.querySelector(".credit");
if (debitInput && line.debit) { if (debitInput && line.debit) {
debitInput.value = line.debit; debitInput.value = Number(line.debit).toLocaleString();
} }
if (creditInput && line.credit) { if (creditInput && line.credit) {
creditInput.value = line.credit; creditInput.value = Number(line.credit).toLocaleString();
} }
// 税区分を設定 // 税区分・税方向は「対象外」に設定(自動計算を防ぐため)
const taxTypeSelect = tr.querySelector(".taxType"); const taxTypeSelect = tr.querySelector(".taxType");
const taxDirSelect = tr.querySelector(".taxDirection"); const taxDirSelect = tr.querySelector(".taxDirection");
if (line.tax_rate && taxTypeSelect) { if (taxTypeSelect) {
taxTypeSelect.value = String(line.tax_rate); taxTypeSelect.value = "none"; // 対象外
} }
if (taxDirSelect) {
if (line.tax_direction && taxDirSelect) { taxDirSelect.value = "none"; // 対象外
taxDirSelect.value = line.tax_direction;
} }
}); });
} }
@@ -875,8 +986,12 @@
// ページ上部の仕訳入力フォームにスクロール // ページ上部の仕訳入力フォームにスクロール
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
console.log("コピーされたデータ:", data); // デバッグ用
console.log(
`${data.lines.length}行をコピーしました。税区分・税方向はすべて「対象外」に設定されています。`
);
alert( alert(
"仕訳を複製しました。日付や金額などを修正して登録してください。" `仕訳を複製しました${data.lines.length}行)。\n税の自動計算を防ぐため、すべての行の税区分・税方向を「対象外」に設定しています。\n必要に応じて修正してから登録してください。`
); );
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
@@ -962,7 +1077,7 @@
searchJournals(); // リストを再読み込み searchJournals(); // リストを再読み込み
} }
// ページロード後に前回の検索条件を復元 // ページロード後に検索条件を設定
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
// デフォルト期間を設定5/31決算6月1日現在 // デフォルト期間を設定5/31決算6月1日現在
const today = new Date(); const today = new Date();
@@ -981,29 +1096,23 @@
const defaultFromDate = `${fiscalStartYear}-06-01`; const defaultFromDate = `${fiscalStartYear}-06-01`;
const defaultToDate = today.toISOString().slice(0, 10); const defaultToDate = today.toISOString().slice(0, 10);
// 常にデフォルト期間を設定(会計年度に合わせるため)
document.getElementById("searchFromDate").value = defaultFromDate;
document.getElementById("searchToDate").value = defaultToDate;
// 摘要と科目のみ前回の検索条件を復元
const savedConditions = localStorage.getItem("journalSearchConditions"); const savedConditions = localStorage.getItem("journalSearchConditions");
if (savedConditions) { if (savedConditions) {
const conditions = JSON.parse(savedConditions); const conditions = JSON.parse(savedConditions);
if (conditions.fromDate)
document.getElementById("searchFromDate").value =
conditions.fromDate;
else
document.getElementById("searchFromDate").value = defaultFromDate;
if (conditions.toDate)
document.getElementById("searchToDate").value = conditions.toDate;
else document.getElementById("searchToDate").value = defaultToDate;
if (conditions.keyword) if (conditions.keyword)
document.getElementById("searchKeyword").value = conditions.keyword; document.getElementById("searchKeyword").value = conditions.keyword;
if (conditions.accountId)
// 自動的に検索を実行 document.getElementById("searchAccountId").value =
searchJournals(); conditions.accountId;
} else {
// 保存された条件がない場合はデフォルト値を設定
document.getElementById("searchFromDate").value = defaultFromDate;
document.getElementById("searchToDate").value = defaultToDate;
} }
// ページロード時に自動的に検索を実行して最新データを表示
searchJournals();
}); });
</script> </script>
</body> </body>

View File

@@ -128,7 +128,7 @@
} }
function goBack() { function goBack() {
window.location.href = "journal-list.html"; window.location.href = "journal-entry.html";
} }
loadJournalEntry(); loadJournalEntry();

View File

@@ -6,12 +6,14 @@
<style> <style>
body { body {
font-family: 'MS Gothic', 'Meiryo', sans-serif; font-family: 'MS Gothic', 'Meiryo', sans-serif;
margin: 24px; margin: 24px auto;
font-size: 13px; font-size: 15px;
max-width: 1400px;
} }
h2 { h2 {
text-align: center; text-align: center;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 20px;
} }
.search-form { .search-form {
text-align: center; text-align: center;
@@ -24,13 +26,14 @@
.search-form label { .search-form label {
margin-right: 5px; margin-right: 5px;
font-weight: bold; font-weight: bold;
font-size: 14px;
} }
.search-form input { .search-form input {
margin-right: 15px; margin-right: 15px;
padding: 5px 10px; padding: 5px 10px;
border: 1px solid #999; border: 1px solid #999;
border-radius: 3px; border-radius: 3px;
font-size: 13px; font-size: 14px;
} }
.search-form button { .search-form button {
padding: 6px 20px; padding: 6px 20px;
@@ -40,7 +43,7 @@
border: none; border: none;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 14px;
} }
.search-form button:hover { .search-form button:hover {
background: #45a049; background: #45a049;
@@ -54,23 +57,24 @@
.period-info { .period-info {
text-align: center; text-align: center;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 12px; font-size: 14px;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
font-size: 14px;
} }
th, th,
td { td {
border: 1px solid #999; border: 1px solid #999;
padding: 5px 8px; padding: 8px 10px;
text-align: right; text-align: right;
} }
th { th {
background: #e8e8e8; background: #e8e8e8;
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 14px;
} }
td.left { td.left {
text-align: left; text-align: left;