desktop修正

This commit is contained in:
admin
2025-12-14 23:54:48 +09:00
parent f9f5eec35e
commit 74dcb88fe6
25 changed files with 1068 additions and 157 deletions

View File

@@ -1,10 +1,13 @@
const API_BASE = "http://localhost:18080";
const API_BASE = "http://127.0.0.1:18080";
async function apiGet(path) {
const res = await fetch(`${API_BASE}${path}`);
async function apiGet(path, params = {}) {
const query = new URLSearchParams(params).toString();
const url = `${API_BASE}${path}?${query}`;
const res = await fetch(url);
if (!res.ok) {
const text = await res.text();
throw new Error(text || "APIエラー");
const data = await res.json();
throw new Error(data.detail || "APIエラー");
}
return res.json();
}

65
frontend/js/journal.js Normal file
View File

@@ -0,0 +1,65 @@
function addLine() {
const tbody = document.querySelector("#lines-table tbody");
const tr = document.createElement("tr");
tr.innerHTML = `
<td><input type="number" class="account-id" /></td>
<td><input type="number" class="debit" value="0" /></td>
<td><input type="number" class="credit" value="0" /></td>
<td><button onclick="this.closest('tr').remove()">削除</button></td>
`;
tbody.appendChild(tr);
}
async function submitJournal() {
const date = document.getElementById("journal-date").value;
const desc = document.getElementById("description").value;
if (!date) {
alert("日付を入力してください");
return;
}
const lines = [];
document.querySelectorAll("#lines-table tbody tr").forEach((tr) => {
const accountId = tr.querySelector(".account-id").value;
const debit = tr.querySelector(".debit").value || 0;
const credit = tr.querySelector(".credit").value || 0;
if (!accountId) return;
lines.push({
account_id: Number(accountId),
debit: Number(debit),
credit: Number(credit),
});
});
if (lines.length < 2) {
alert("仕訳明細は2行以上必要です");
return;
}
try {
await fetch("http://127.0.0.1:18080/journals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
journal_date: date,
description: desc,
lines: lines,
}),
}).then(async (res) => {
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "登録失敗");
}
});
alert("仕訳を登録しました");
location.href = "index.html";
} catch (e) {
alert(e.message);
}
}

37
frontend/js/ledger.js Normal file
View File

@@ -0,0 +1,37 @@
async function loadLedger() {
const params = new URLSearchParams(location.search);
const accountId = params.get("account_id");
const from = params.get("from");
const to = params.get("to");
try {
const data = await apiGet("/general-ledger", {
account_id: accountId,
date_from: from,
date_to: to,
});
document.getElementById(
"account-title"
).innerText = `${data.account.account_code} ${data.account.account_name}`;
const tbody = document.getElementById("ledger-body");
tbody.innerHTML = "";
data.lines.forEach((line) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${line.journal_date}</td>
<td>${line.description}</td>
<td>${line.debit}</td>
<td>${line.credit}</td>
<td>${line.balance}</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
alert(e.message);
}
}
loadLedger();

View File

@@ -0,0 +1,152 @@
let accounts = [];
let retainedEarnings = null;
let isLocked = false;
function formatNumber(n) {
return Number(n || 0).toLocaleString();
}
function recalcTotals() {
let debit = 0;
let credit = 0;
// 先算:不含繰越利益
accounts.forEach((a) => {
if (a.account_name === "繰越利益") return;
debit += Number(a.opening_debit || 0);
credit += Number(a.opening_credit || 0);
});
const diff = debit - credit;
// 自动计算 繰越利益
if (retainedEarnings) {
if (diff > 0) {
retainedEarnings.opening_debit = 0;
retainedEarnings.opening_credit = diff;
} else {
retainedEarnings.opening_debit = -diff;
retainedEarnings.opening_credit = 0;
}
}
// 再算一次(包含繰越利益)
let finalDebit = 0;
let finalCredit = 0;
accounts.forEach((a) => {
finalDebit += Number(a.opening_debit || 0);
finalCredit += Number(a.opening_credit || 0);
});
document.getElementById("totalDebit").innerText = formatNumber(finalDebit);
document.getElementById("totalCredit").innerText = formatNumber(finalCredit);
document.getElementById("diff").innerText = formatNumber(
finalDebit - finalCredit
);
}
async function loadOpeningBalances() {
const year = document.getElementById("fiscalYear").value;
const data = await apiGet("/opening-balances", { fiscal_year: year });
accounts = data;
retainedEarnings = accounts.find((a) => a.account_name === "繰越利益");
const tbody = document.querySelector("#balanceTable tbody");
tbody.innerHTML = "";
GROUPS.forEach((group) => {
// 分组标题行
const headerTr = document.createElement("tr");
headerTr.innerHTML = `
<td colspan="4" style="background:#ddd; font-weight:bold;">
${group.label}
</td>
`;
tbody.appendChild(headerTr);
accounts.forEach((a, i) => {
if (a.account_type !== group.key) return;
const isRetained = a.account_name === "繰越利益";
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${a.account_code}</td>
<td>${a.account_name}</td>
<td>
<input type="number" value="${a.opening_debit}"
${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_debit=this.value; recalcTotals();">
</td>
<td>
<input type="number" value="${a.opening_credit}"
${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_credit=this.value; recalcTotals();">
</td>
`;
tbody.appendChild(tr);
});
});
// 年度锁定チェック
fetch(
`http://127.0.0.1:18080/opening-balances/lock-status?fiscal_year=${year}`
)
.then((res) => res.json())
.then((data) => {
isLocked = data.is_locked;
document.querySelector(
"button[onclick='saveOpeningBalances()']"
).disabled = isLocked;
if (isLocked) {
alert("この会計年度の期首残高は確定されています。");
}
});
recalcTotals();
}
async function saveOpeningBalances() {
if (isLocked) {
alert("この会計年度の期首残高は確定されているため、保存できません。");
return;
}
const year = Number(document.getElementById("fiscalYear").value);
const balances = accounts
// ❗ 排除 繰越利益
.filter(
(a) =>
a.account_name !== "繰越利益" &&
(Number(a.opening_debit) !== 0 || Number(a.opening_credit) !== 0)
)
.map((a) => ({
account_id: a.account_id,
debit: Number(a.opening_debit || 0),
credit: Number(a.opening_credit || 0),
}));
try {
await fetch("http://127.0.0.1:18080/opening-balances", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fiscal_year: year,
balances: balances,
}),
}).then(async (res) => {
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail);
}
});
alert("期首残高を保存しました");
} catch (e) {
alert(e.message);
}
}

View File

@@ -1,4 +1,4 @@
document.getElementById("load").onclick = async () => {
async function loadTrialBalance() {
const from = document.getElementById("from").value;
const to = document.getElementById("to").value;
@@ -8,22 +8,33 @@ document.getElementById("load").onclick = async () => {
}
try {
const data = await apiGet(`/trial-balance?date_from=${from}&date_to=${to}`);
const tbody = document.getElementById("tb-body");
const data = await apiGet("/trial-balance", {
date_from: from,
date_to: to,
});
const tbody = document.querySelector("#trial-table tbody");
tbody.innerHTML = "";
data.accounts.forEach((a) => {
data.accounts.forEach((acc) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${a.account_name}</td>
<td style="text-align:right">${a.opening_balance}</td>
<td style="text-align:right">${a.debit}</td>
<td style="text-align:right">${a.credit}</td>
<td style="text-align:right">${a.closing_balance}</td>
<td>${acc.account_code}</td>
<td>
<a href="ledger.html?account_id=${acc.account_id}&from=${from}&to=${to}">
${acc.account_name}
</a>
</td>
<td>${acc.opening_balance}</td>
<td>${acc.debit}</td>
<td>${acc.credit}</td>
<td>${acc.closing_balance}</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
alert(e.message);
}
};
}