This commit is contained in:
admin
2026-03-04 23:20:42 +09:00
parent 99d16d8219
commit 31a64d5b59
11 changed files with 674 additions and 201 deletions

View File

@@ -51,7 +51,9 @@ def calculate_payroll(request: schemas.PayrollCalculationRequest):
payment_date=request.payment_date, payment_date=request.payment_date,
other_allowance=request.other_allowance, other_allowance=request.other_allowance,
resident_tax=request.resident_tax, resident_tax=request.resident_tax,
other_deduction=request.other_deduction other_deduction=request.other_deduction,
calc_income_tax=request.calc_income_tax,
calc_social_insurance=request.calc_social_insurance
) )
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@@ -174,8 +176,10 @@ def update_payroll(payroll_id: int, request: schemas.MonthlyPayrollUpdate):
@router.post("/{payroll_id}/recalculate", response_model=schemas.MonthlyPayroll) @router.post("/{payroll_id}/recalculate", response_model=schemas.MonthlyPayroll)
def recalculate_payroll(payroll_id: int): def recalculate_payroll(payroll_id: int, request: schemas.RecalculateRequest = None):
"""給与を再計算""" """給与を再計算"""
if request is None:
request = schemas.RecalculateRequest()
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
# 既存データを取得 # 既存データを取得
@@ -204,7 +208,9 @@ def recalculate_payroll(payroll_id: int):
other_allowance=existing["other_allowance"], other_allowance=existing["other_allowance"],
resident_tax=existing["resident_tax"], resident_tax=existing["resident_tax"],
other_deduction=existing["other_deduction"], other_deduction=existing["other_deduction"],
calculated_by="recalculated" calculated_by="recalculated",
calc_income_tax=request.calc_income_tax,
calc_social_insurance=request.calc_social_insurance
) )
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))

View File

@@ -123,6 +123,14 @@ class PayrollCalculationRequest(BaseModel):
other_allowance: Decimal = Decimal("0") other_allowance: Decimal = Decimal("0")
resident_tax: Decimal = Decimal("0") resident_tax: Decimal = Decimal("0")
other_deduction: Decimal = Decimal("0") other_deduction: Decimal = Decimal("0")
calc_income_tax: bool = True
calc_social_insurance: bool = True
class RecalculateRequest(BaseModel):
"""再計算リクエスト"""
calc_income_tax: bool = True
calc_social_insurance: bool = True
class PayrollApprovalRequest(BaseModel): class PayrollApprovalRequest(BaseModel):

View File

@@ -231,7 +231,9 @@ class PayrollCalculationService:
other_allowance: Decimal = Decimal("0"), other_allowance: Decimal = Decimal("0"),
resident_tax: Decimal = Decimal("0"), resident_tax: Decimal = Decimal("0"),
other_deduction: Decimal = Decimal("0"), other_deduction: Decimal = Decimal("0"),
calculated_by: str = "system" calculated_by: str = "system",
calc_income_tax: bool = True,
calc_social_insurance: bool = True
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""給与を計算""" """給与を計算"""
@@ -378,7 +380,7 @@ class PayrollCalculationService:
health_insurance = ( health_insurance = (
health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"])) health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"]))
).quantize(Decimal("0.01")) ).quantize(Decimal("0.01"))
else: elif calc_social_insurance:
errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year}") errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year}")
# 介護保険40歳以上65歳未満が対象、標準報酬月額表から検索した月額で計算 # 介護保険40歳以上65歳未満が対象、標準報酬月額表から検索した月額で計算
@@ -391,7 +393,7 @@ class PayrollCalculationService:
).quantize(Decimal("0.01")) ).quantize(Decimal("0.01"))
print(f"[DEBUG] 介護保険計算: {health_standard_salary} × {care_insurance_rate['employee_rate']}% = {care_insurance}") print(f"[DEBUG] 介護保険計算: {health_standard_salary} × {care_insurance_rate['employee_rate']}% = {care_insurance}")
else: else:
errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year}") errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year}") if calc_social_insurance else None
# 厚生年金(上限適用後の標準報酬月額で計算) # 厚生年金(上限適用後の標準報酬月額で計算)
pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date) pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date)
@@ -401,7 +403,7 @@ class PayrollCalculationService:
pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"])) pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"]))
).quantize(Decimal("0.01")) ).quantize(Decimal("0.01"))
print(f"[DEBUG] 厚生年金計算: {pension_standard_salary} × {pension_insurance_rate['employee_rate']}% = {pension_insurance}") print(f"[DEBUG] 厚生年金計算: {pension_standard_salary} × {pension_insurance_rate['employee_rate']}% = {pension_insurance}")
else: elif calc_social_insurance:
errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year}") errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year}")
# 雇用保険 # 雇用保険
@@ -417,9 +419,16 @@ class PayrollCalculationService:
).quantize(Decimal("0.01")) ).quantize(Decimal("0.01"))
elif not is_employment_insurance_eligible: elif not is_employment_insurance_eligible:
print(f"[DEBUG] この従業員は雇用保険非対象です") print(f"[DEBUG] この従業員は雇用保険非対象です")
elif not employment_insurance_rate: elif not employment_insurance_rate and calc_social_insurance:
errors.append(f"雇用保険料率が見つかりません(年度: {payment_date.year}") errors.append(f"雇用保険料率が見つかりません(年度: {payment_date.year}")
# 社会保険計算スキップ時は全て。0にする
if not calc_social_insurance:
health_insurance = Decimal("0")
care_insurance = Decimal("0")
pension_insurance = Decimal("0")
employment_insurance = Decimal("0")
# エラーがある場合は、エラーメッセージを返す # エラーがある場合は、エラーメッセージを返す
if errors: if errors:
raise ValueError(f"給与計算に必要なデータが不足しています:\n" + "\n".join(errors)) raise ValueError(f"給与計算に必要なデータが不足しています:\n" + "\n".join(errors))
@@ -441,11 +450,14 @@ class PayrollCalculationService:
employment_insurance employment_insurance
) )
income_tax = PayrollCalculationService.calculate_income_tax( if calc_income_tax:
taxable_income, income_tax = PayrollCalculationService.calculate_income_tax(
dependents_count, taxable_income,
payment_date dependents_count,
) payment_date
)
else:
income_tax = Decimal("0")
# 総控除額 # 総控除額
total_deduction = ( total_deduction = (

View File

@@ -12,6 +12,7 @@ import openpyxl
import xlrd import xlrd
from . import schemas from . import schemas
from ...core.database import get_connection from ...core.database import get_connection
from psycopg.errors import UniqueViolation
router = APIRouter(prefix="/payroll/settings", tags=["Payroll - Settings"]) router = APIRouter(prefix="/payroll/settings", tags=["Payroll - Settings"])
@@ -67,45 +68,51 @@ def create_salary_setting(setting: schemas.SalarySettingCreate):
) )
has_employment_insurance_column = cur.fetchone() has_employment_insurance_column = cur.fetchone()
if has_employment_insurance_column: try:
# カラムが存在する場合 if has_employment_insurance_column:
cur.execute( # カラムが存在する場合
""" cur.execute(
INSERT INTO salary_settings ( """
employee_id, base_salary, hourly_rate, employment_type, INSERT INTO salary_settings (
payment_type, commute_allowance, other_allowance, employee_id, base_salary, hourly_rate, employment_type,
employment_insurance_eligible, payment_type, commute_allowance, other_allowance,
valid_from, valid_to employment_insurance_eligible,
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) valid_from, valid_to
RETURNING * ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", RETURNING *
( """,
setting.employee_id, setting.base_salary, setting.hourly_rate, (
setting.employment_type, setting.payment_type, setting.employee_id, setting.base_salary, setting.hourly_rate,
setting.commute_allowance, setting.other_allowance, setting.employment_type, setting.payment_type,
setting.employment_insurance_eligible, setting.commute_allowance, setting.other_allowance,
setting.valid_from, setting.valid_to setting.employment_insurance_eligible,
), setting.valid_from, setting.valid_to
),
)
else:
# カラムが存在しない場合(互換性維持)
cur.execute(
"""
INSERT INTO salary_settings (
employee_id, base_salary, hourly_rate, employment_type,
payment_type, commute_allowance, other_allowance,
valid_from, valid_to
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
setting.employee_id, setting.base_salary, setting.hourly_rate,
setting.employment_type, setting.payment_type,
setting.commute_allowance, setting.other_allowance,
setting.valid_from, setting.valid_to
),
)
print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました")
except UniqueViolation:
raise HTTPException(
status_code=409,
detail="この適用開始日はすでに登録されています。別の日付を指定してください。"
) )
else:
# カラムが存在しない場合(互換性維持)
cur.execute(
"""
INSERT INTO salary_settings (
employee_id, base_salary, hourly_rate, employment_type,
payment_type, commute_allowance, other_allowance,
valid_from, valid_to
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
setting.employee_id, setting.base_salary, setting.hourly_rate,
setting.employment_type, setting.payment_type,
setting.commute_allowance, setting.other_allowance,
setting.valid_from, setting.valid_to
),
)
print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました")
result = cur.fetchone() result = cur.fetchone()
conn.commit() conn.commit()
@@ -184,10 +191,16 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate)
with get_connection() as conn: with get_connection() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( try:
f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *", cur.execute(
values, f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *",
) values,
)
except UniqueViolation:
raise HTTPException(
status_code=409,
detail="この適用開始日はすでに登録されています。別の日付を指定してください。"
)
result = cur.fetchone() result = cur.fetchone()
if not result: if not result:
raise HTTPException(status_code=404, detail="給与設定が見つかりません") raise HTTPException(status_code=404, detail="給与設定が見つかりません")
@@ -195,6 +208,21 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate)
return result return result
@router.delete("/salary/{setting_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_salary_setting(setting_id: int):
"""給与設定を削除"""
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"DELETE FROM salary_settings WHERE setting_id = %s RETURNING setting_id",
(setting_id,),
)
result = cur.fetchone()
if not result:
raise HTTPException(status_code=404, detail="給与設定が見つかりません")
conn.commit()
# ======================================== # ========================================
# 社会保険料率管理 # 社会保険料率管理
# ======================================== # ========================================

View File

@@ -32,6 +32,7 @@ class VoucherDataSummary(BaseModel):
employee_id: int employee_id: int
employee_code: str employee_code: str
employee_name: str employee_name: str
employment_type: Optional[str] = None
has_salary_data: bool has_salary_data: bool
has_bonus_data: bool has_bonus_data: bool
salary_count: int = 0 salary_count: int = 0

View File

@@ -310,6 +310,17 @@ def build_voucher_summary(
emp_name = emp_row.get("name") emp_name = emp_row.get("name")
else: else:
emp_code, emp_name = emp_row emp_code, emp_name = emp_row
# 雇用形態を最新の給与設定から取得
cur.execute(
"SELECT employment_type FROM salary_settings WHERE employee_id = %s ORDER BY valid_from DESC LIMIT 1",
(emp_id,)
)
ss_row = cur.fetchone()
if ss_row:
employment_type = ss_row.get("employment_type") if isinstance(ss_row, dict) else ss_row[0]
else:
employment_type = None
except Exception as e: except Exception as e:
logger.error(f"従業員情報取得エラー: {str(e)}") logger.error(f"従業員情報取得エラー: {str(e)}")
continue continue
@@ -333,6 +344,7 @@ def build_voucher_summary(
employee_id=emp_id, employee_id=emp_id,
employee_code=emp_code, employee_code=emp_code,
employee_name=emp_name, employee_name=emp_name,
employment_type=employment_type,
has_salary_data=len(salary_list) > 0, has_salary_data=len(salary_list) > 0,
has_bonus_data=len(bonus_list) > 0, has_bonus_data=len(bonus_list) > 0,
salary_count=len(salary_list), salary_count=len(salary_list),

View File

@@ -129,6 +129,7 @@
<th style="width: 220px">科目</th> <th style="width: 220px">科目</th>
<th style="width: 120px">借方</th> <th style="width: 120px">借方</th>
<th style="width: 120px">貸方</th> <th style="width: 120px">貸方</th>
<th style="width: 90px">税率</th>
<th style="width: 180px">摘要(行)</th> <th style="width: 180px">摘要(行)</th>
<th style="width: 60px">操作</th> <th style="width: 60px">操作</th>
</tr> </tr>
@@ -141,6 +142,7 @@
<th id="creditTotal" class="right">0</th> <th id="creditTotal" class="right">0</th>
<th></th> <th></th>
<th></th> <th></th>
<th></th>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@@ -159,6 +161,43 @@
const API = ""; const API = "";
let accounts = []; let accounts = [];
// カスタムアラートモーダル
function showAlert(msg, type, callback) {
const configs = {
success: {
icon: "✔",
iconColor: "#22a84b",
bg: "#f0faf4",
borderTop: "#22a84b",
},
error: {
icon: "✖",
iconColor: "#e53935",
bg: "#fff5f5",
borderTop: "#e53935",
},
warning: {
icon: "⚠",
iconColor: "#e67e00",
bg: "#fffbf0",
borderTop: "#f0a500",
},
};
const c = configs[type] || configs.warning;
const overlay = document.getElementById("alertModalOverlay");
document.getElementById("alertModalIcon").innerHTML =
`<span style="font-size:52px;line-height:1;color:${c.iconColor};">${c.icon}</span>`;
document.getElementById("alertModalMsg").textContent = msg;
const box = document.getElementById("alertModalBox");
box.style.borderTop = `5px solid ${c.borderTop}`;
box.style.background = c.bg;
overlay.style.display = "flex";
document.getElementById("alertModalOkBtn").onclick = function () {
overlay.style.display = "none";
if (callback) callback();
};
}
// 科目グループ定義(仕訳入力と同期) // 科目グループ定義(仕訳入力と同期)
const ACCOUNT_GROUPS = [ const ACCOUNT_GROUPS = [
// 資産の部 // 資産の部
@@ -438,6 +477,14 @@
</td> </td>
<td><input type="number" min="0" style="text-align:right"></td> <td><input type="number" min="0" style="text-align:right"></td>
<td><input type="number" min="0" style="text-align:right"></td> <td><input type="number" min="0" style="text-align:right"></td>
<td>
<select class="tax-rate-select" style="width:100%;padding:4px;">
<option value="">—</option>
<option value="10">10%</option>
<option value="8">8%(軽減)</option>
<option value="0">0%(非課税)</option>
</select>
</td>
<td><input type="text" class="line-memo-input" placeholder="行摘要(任意)"></td> <td><input type="text" class="line-memo-input" placeholder="行摘要(任意)"></td>
<td><button onclick="removeRow(this)">削除</button></td> <td><button onclick="removeRow(this)">削除</button></td>
`; `;
@@ -464,6 +511,11 @@
const creditVal = Number(line.credit) || 0; const creditVal = Number(line.credit) || 0;
numberInputs[0].value = debitVal > 0 ? String(debitVal) : ""; numberInputs[0].value = debitVal > 0 ? String(debitVal) : "";
numberInputs[1].value = creditVal > 0 ? String(creditVal) : ""; numberInputs[1].value = creditVal > 0 ? String(creditVal) : "";
// 税率を設定
if (line.tax_rate != null) {
const sel = tr.querySelector(".tax-rate-select");
sel.value = String(line.tax_rate);
}
// 摘要(行)を設定 // 摘要(行)を設定
const memoInput = tr.querySelector(".line-memo-input"); const memoInput = tr.querySelector(".line-memo-input");
if (memoInput) memoInput.value = line.line_description || ""; if (memoInput) memoInput.value = line.line_description || "";
@@ -556,11 +608,17 @@
const credit = Number(numberInputs[1]?.value || 0); const credit = Number(numberInputs[1]?.value || 0);
const lineDesc = const lineDesc =
tr.querySelector(".line-memo-input")?.value.trim() || undefined; tr.querySelector(".line-memo-input")?.value.trim() || undefined;
const taxRateRaw = tr.querySelector(".tax-rate-select")?.value;
const taxRate =
taxRateRaw !== "" && taxRateRaw != null
? Number(taxRateRaw)
: undefined;
if (debit === 0 && credit === 0) return; if (debit === 0 && credit === 0) return;
lines.push({ lines.push({
account_id: accountId, account_id: accountId,
debit, debit,
credit, credit,
...(taxRate !== undefined ? { tax_rate: taxRate } : {}),
...(lineDesc ? { line_description: lineDesc } : {}), ...(lineDesc ? { line_description: lineDesc } : {}),
}); });
d += debit; d += debit;
@@ -599,15 +657,17 @@
if (!res.ok) return showError(data.detail || "登録失敗"); if (!res.ok) return showError(data.detail || "登録失敗");
// 登録成功 - 親窓口の検索を再実行してから画面を閉じる // 登録成功 - 親窓口の検索を再実行してから画面を閉じる
alert(`登録しましたID: ${data.journal_entry_id}`); showAlert(
localStorage.removeItem("editSource"); `登録しましたID: ${data.journal_entry_id}`,
"success",
// 親窓口(列表页)の searchJournalsを呼び出す () => {
if (window.opener && window.opener.searchJournals) { localStorage.removeItem("editSource");
window.opener.searchJournals(); if (window.opener && window.opener.searchJournals) {
} window.opener.searchJournals();
}
window.close(); window.close();
},
);
} catch { } catch {
showError("通信エラー"); showError("通信エラー");
} }
@@ -629,5 +689,57 @@
window.close(); window.close();
} }
</script> </script>
<!-- アラートモーダル -->
<div
id="alertModalOverlay"
style="
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 9999;
align-items: center;
justify-content: center;
"
>
<div
id="alertModalBox"
style="
background: #fff;
border-radius: 10px;
padding: 32px 36px;
min-width: 320px;
max-width: 480px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
text-align: center;
"
>
<div id="alertModalIcon" style="margin-bottom: 14px"></div>
<div
id="alertModalMsg"
style="
font-size: 15px;
color: #333;
margin-bottom: 26px;
white-space: pre-wrap;
"
></div>
<button
id="alertModalOkBtn"
style="
padding: 9px 36px;
border-radius: 6px;
border: none;
font-size: 14px;
cursor: pointer;
background: #4a90d9;
color: #fff;
font-weight: 600;
"
>
OK
</button>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,4 @@
<!doctype html> <!doctype html>
<html lang="ja"> <html lang="ja">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -129,7 +129,7 @@
} }
@media print { @media print {
@page { @page {
margin: 20mm 20mm 25mm 20mm; margin: 5mm 15mm 20mm 15mm;
@bottom-center { @bottom-center {
content: "ページ " counter(page); content: "ページ " counter(page);
font-size: 0.75em; font-size: 0.75em;
@@ -869,6 +869,40 @@
<script> <script>
const API = ""; const API = "";
// カスタム通知トースト
function showAlert(msg, type) {
const configs = {
success: {
icon: "✔",
iconColor: "#22a84b",
bg: "#f0faf4",
border: "#22a84b",
},
error: {
icon: "✖",
iconColor: "#e53935",
bg: "#fff5f5",
border: "#e53935",
},
warning: {
icon: "⚠",
iconColor: "#e67e00",
bg: "#fffbf0",
border: "#f0a500",
},
};
const c = configs[type] || configs.warning;
const container = document.getElementById("alertToastContainer");
const toast = document.createElement("div");
toast.style.cssText = `pointer-events:auto;display:flex;align-items:flex-start;gap:12px;background:${c.bg};border:1px solid ${c.border};border-left:5px solid ${c.border};border-radius:8px;padding:14px 16px;min-width:280px;max-width:420px;box-shadow:0 4px 16px rgba(0,0,0,0.14);animation:alertSlideIn 0.25s ease;`;
const msgEsc = String(msg).replace(/&/g, "&amp;").replace(/</g, "&lt;");
toast.innerHTML = `<span style="font-size:26px;color:${c.iconColor};line-height:1;flex-shrink:0;">${c.icon}</span><span style="font-size:14px;color:#333;flex:1;white-space:pre-wrap;margin-top:3px;">${msgEsc}</span><button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:#aaa;font-size:20px;line-height:1;padding:0 0 0 8px;flex-shrink:0;">&times;</button>`;
container.appendChild(toast);
setTimeout(() => {
if (toast.parentElement) toast.remove();
}, 4500);
}
// 千分位カンマフォーマット関数 // 千分位カンマフォーマット関数
function formatNumberInput(input) { function formatNumberInput(input) {
let value = input.value.replace(/,/g, ""); // 既存のカンマを削除 let value = input.value.replace(/,/g, ""); // 既存のカンマを削除
@@ -1142,7 +1176,7 @@
const res = await fetch(`${API}/accounts`); const res = await fetch(`${API}/accounts`);
if (!res.ok) { if (!res.ok) {
console.error("❌ API 错误:", res.status); console.error("❌ API 错误:", res.status);
alert("科目一覧の取得に失敗しました"); showAlert("科目一覧の取得に失敗しました", "error");
return; return;
} }
@@ -1796,17 +1830,18 @@
.value.trim(); .value.trim();
if (dateInput.validity.badInput) { if (dateInput.validity.badInput) {
alert( showAlert(
"日付が正しくありません。存在しない日付が入力されています11月31日は無効です", "日付が正しくありません。存在しない日付が入力されています11月31日は無効です",
"warning",
); );
return; return;
} }
if (!entryDate) { if (!entryDate) {
alert("仕訳日を入力してください"); showAlert("仕訳日を入力してください", "warning");
return; return;
} }
if (!description) { if (!description) {
alert("摘要を入力してください"); showAlert("摘要を入力してください", "warning");
return; return;
} }
@@ -1824,12 +1859,15 @@
if (debit === 0 && credit === 0) return; // 空行スキップ if (debit === 0 && credit === 0) return; // 空行スキップ
if (!accountId) { if (!accountId) {
alert("科目が選択されていない行があります"); showAlert("科目が選択されていない行があります", "warning");
valid = false; valid = false;
return; return;
} }
if (debit > 0 && credit > 0) { if (debit > 0 && credit > 0) {
alert("1行に借方と貸方の両方を入力することはできません"); showAlert(
"1行に借方と貸方の両方を入力することはできません",
"warning",
);
valid = false; valid = false;
return; return;
} }
@@ -1848,12 +1886,13 @@
if (!valid) return; if (!valid) return;
if (lines.length < 2) { if (lines.length < 2) {
alert("仕訳明細は2行以上必要です"); showAlert("仕訳明細は2行以上必要です", "warning");
return; return;
} }
if (debitSum !== creditSum) { if (debitSum !== creditSum) {
alert( showAlert(
`貸借が一致していません(差額:${Math.abs(debitSum - creditSum).toLocaleString()} 円)`, `貸借が一致していません(差額:${Math.abs(debitSum - creditSum).toLocaleString()} 円)`,
"warning",
); );
return; return;
} }
@@ -1883,7 +1922,7 @@
} }
saveDescriptionToHistory(description); saveDescriptionToHistory(description);
alert("仕訳を登録しました"); showAlert("仕訳を登録しました", "success");
const keep = document.getElementById("detailKeepAfterSubmit").checked; const keep = document.getElementById("detailKeepAfterSubmit").checked;
if (!keep) { if (!keep) {
@@ -1893,7 +1932,7 @@
searchJournals(); searchJournals();
} catch (e) { } catch (e) {
alert("登録エラー: " + e.message); showAlert("登録エラー: " + e.message, "error");
} }
} }
@@ -1922,13 +1961,14 @@
).value; ).value;
if (entryDateInput.validity.badInput) { if (entryDateInput.validity.badInput) {
alert( showAlert(
"日付が正しくありません。存在しない日付が入力されています11月31日は無効です", "日付が正しくありません。存在しない日付が入力されています11月31日は無効です",
"warning",
); );
return; return;
} }
if (!entryDate) { if (!entryDate) {
alert("仕訳日を入力してください"); showAlert("仕訳日を入力してください", "warning");
return; return;
} }
@@ -1948,15 +1988,15 @@
); );
if (!accountId) { if (!accountId) {
alert("科目を選択してください"); showAlert("科目を選択してください", "warning");
return; return;
} }
if (!methodId) { if (!methodId) {
alert("取引手段を選択してください"); showAlert("取引手段を選択してください", "warning");
return; return;
} }
if (amount <= 0) { if (amount <= 0) {
alert("金額を入力してください"); showAlert("金額を入力してください", "warning");
return; return;
} }
@@ -1978,6 +2018,7 @@
account_id: accountId, account_id: accountId,
debit: netAmount, debit: netAmount,
credit: 0, credit: 0,
...(taxRateNum !== null ? { tax_rate: taxRateNum } : {}),
}); });
// 借方: 仮払消費税等(税額)※税額がある場合のみ // 借方: 仮払消費税等(税額)※税額がある場合のみ
@@ -1985,7 +2026,7 @@
const taxPaidAcc = const taxPaidAcc =
taxPaidAccounts.length > 0 ? taxPaidAccounts[0] : null; taxPaidAccounts.length > 0 ? taxPaidAccounts[0] : null;
if (!taxPaidAcc) { if (!taxPaidAcc) {
alert("仮払消費税等の勘定科目が見つかりません"); showAlert("仮払消費税等の勘定科目が見つかりません", "error");
return; return;
} }
lines.push({ lines.push({
@@ -2015,6 +2056,7 @@
account_id: accountId, account_id: accountId,
debit: 0, debit: 0,
credit: netAmount, credit: netAmount,
...(taxRateNum !== null ? { tax_rate: taxRateNum } : {}),
}); });
// 貸方: 仮受消費税等(税額)※税額がある場合のみ // 貸方: 仮受消費税等(税額)※税額がある場合のみ
@@ -2022,7 +2064,7 @@
const taxReceivedAcc = const taxReceivedAcc =
taxReceivedAccounts.length > 0 ? taxReceivedAccounts[0] : null; taxReceivedAccounts.length > 0 ? taxReceivedAccounts[0] : null;
if (!taxReceivedAcc) { if (!taxReceivedAcc) {
alert("仮受消費税等の勘定科目が見つかりません"); showAlert("仮受消費税等の勘定科目が見つかりません", "error");
return; return;
} }
lines.push({ lines.push({
@@ -2060,7 +2102,7 @@
const errorMsg = const errorMsg =
errorData.detail || `登録に失敗しました (HTTP ${res.status})`; errorData.detail || `登録に失敗しました (HTTP ${res.status})`;
console.error("Error:", errorData); console.error("Error:", errorData);
alert(errorMsg); showAlert(errorMsg, "error");
return; return;
} }
@@ -2069,7 +2111,7 @@
// 摘要を履歴に保存 // 摘要を履歴に保存
saveDescriptionToHistory(description); saveDescriptionToHistory(description);
alert(`登録完了ID=${result.journal_entry_id}`); showAlert(`登録完了ID=${result.journal_entry_id}`, "success");
// 「登録後に明細を保持する」チェックボックスを確認 // 「登録後に明細を保持する」チェックボックスを確認
const keepDetails = document.getElementById( const keepDetails = document.getElementById(
@@ -2092,8 +2134,9 @@
searchJournals(); searchJournals();
} catch (error) { } catch (error) {
console.error("送信エラー:", error); console.error("送信エラー:", error);
alert( showAlert(
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`, `エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`,
"error",
); );
} }
} }
@@ -2387,6 +2430,16 @@
const conditionsText = const conditionsText =
conditions.length > 0 ? conditions.join(" | ") : "検索条件: すべて"; conditions.length > 0 ? conditions.join(" | ") : "検索条件: すべて";
// 印刷日時
const printedAt = new Date().toLocaleString("ja-JP", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
// 月ごとにテーブルを生成(印刷用) // 月ごとにテーブルを生成(印刷用)
Object.keys(monthGroups) Object.keys(monthGroups)
.sort() .sort()
@@ -2409,11 +2462,14 @@
<table style="margin-bottom: 20px;"> <table style="margin-bottom: 20px;">
<thead> <thead>
<tr> <tr>
<th colspan="6" style="text-align: center; font-size: 1.2em; padding: 8px; border: none; background: transparent;">仕訳検索結果</th> <th colspan="5" style="text-align: center; font-size: 1.2em; padding: 8px; border: none; background: transparent;">仕訳検索結果</th>
</tr>
<tr>
<th colspan="5" style="text-align: right; font-weight: normal; font-size: 0.85em; padding: 2px 4px; border: none; background: transparent;">印刷日時: ${printedAt}</th>
</tr> </tr>
<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: 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> <th colspan="2" style="text-align: right; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${monthDisplay}</th>
</tr> </tr>
<tr> <tr>
<th style="white-space: nowrap;">日付</th> <th style="white-space: nowrap;">日付</th>
@@ -2421,7 +2477,6 @@
<th style="white-space: nowrap;">借方合計</th> <th style="white-space: nowrap;">借方合計</th>
<th style="white-space: nowrap;">貸方合計</th> <th style="white-space: nowrap;">貸方合計</th>
<th>税率</th> <th>税率</th>
<th style="white-space: nowrap;">操作</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@@ -2452,20 +2507,6 @@
<td style="text-align:center">${ <td style="text-align:center">${
e.tax_rates ? e.tax_rates + "%" : "" e.tax_rates ? e.tax_rates + "%" : ""
}</td> }</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); tbody.appendChild(tr);
}); });
@@ -2478,7 +2519,7 @@
<td colspan="2" style="text-align:right; white-space: nowrap;">合計:</td> <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;">${monthDebitTotal.toLocaleString()}</td>
<td style="text-align:right; white-space: nowrap;">${monthCreditTotal.toLocaleString()}</td> <td style="text-align:right; white-space: nowrap;">${monthCreditTotal.toLocaleString()}</td>
<td colspan="2"></td> <td></td>
`; `;
tbody.appendChild(totalRow); tbody.appendChild(totalRow);
@@ -2502,7 +2543,7 @@
</div> </div>
`; `;
} }
alert(`検索に失敗しました: ${err.message}`); showAlert(`検索に失敗しました: ${err.message}`, "error");
} }
} }
@@ -2722,7 +2763,7 @@
</div> </div>
`; `;
} }
alert(`検索に失敗しました: ${err.message}`); showAlert(`検索に失敗しました: ${err.message}`, "error");
} }
} }
@@ -2737,7 +2778,7 @@
try { try {
const res = await fetch(`${API}/journal-entries/${id}`); const res = await fetch(`${API}/journal-entries/${id}`);
if (!res.ok) { if (!res.ok) {
alert("仕訳の取得に失敗しました"); showAlert("仕訳の取得に失敗しました", "error");
return; return;
} }
@@ -2916,14 +2957,13 @@
const dateInput = document.getElementById("entryDate"); const dateInput = document.getElementById("entryDate");
dateInput.focus(); dateInput.focus();
alert( showAlert(
`仕訳を複製しました。\n\n` + `仕訳を複製しました。\n\n※ このコピーは新規仕訳として独立した記録になります。\n※ 日付と摘要を必要に応じて修正してから登録してください。`,
`※ このコピーは新規仕訳として独立した記録になります。\n` + "success",
`※ 日付と摘要を必要に応じて修正してから登録してください。`,
); );
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
alert(`エラーが発生しました: ${error.message}`); showAlert(`エラーが発生しました: ${error.message}`, "error");
} }
} }
@@ -2954,7 +2994,7 @@
async function deleteJournalEntry(id) { async function deleteJournalEntry(id) {
const reason = prompt("削除理由を入力してください:"); const reason = prompt("削除理由を入力してください:");
if (!reason || reason.trim() === "") { if (!reason || reason.trim() === "") {
alert("削除理由の入力が必要です"); showAlert("削除理由の入力が必要です", "warning");
return; return;
} }
@@ -2970,11 +3010,11 @@
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
alert(data.detail || "削除に失敗しました"); showAlert(data.detail || "削除に失敗しました", "error");
return; return;
} }
alert("削除しました"); showAlert("削除しました", "success");
searchJournals(); // リストを再読み込み searchJournals(); // リストを再読み込み
} }
@@ -3056,5 +3096,30 @@
await searchJournals(); await searchJournals();
}); });
</script> </script>
<div
id="alertToastContainer"
style="
position: fixed;
top: 24px;
right: 24px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
"
></div>
<style>
@keyframes alertSlideIn {
from {
opacity: 0;
transform: translateX(40px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>
</body> </body>
</html> </html>

View File

@@ -48,6 +48,7 @@
<th>科目名</th> <th>科目名</th>
<th>借方</th> <th>借方</th>
<th>貸方</th> <th>貸方</th>
<th>税率</th>
<th>摘要(行)</th> <th>摘要(行)</th>
</tr> </tr>
</thead> </thead>
@@ -124,6 +125,7 @@
<td style="text-align:right">${ <td style="text-align:right">${
line.credit > 0 ? line.credit.toLocaleString() : "" line.credit > 0 ? line.credit.toLocaleString() : ""
}</td> }</td>
<td style="text-align:center">${line.tax_rate != null ? line.tax_rate + "%" : ""}</td>
<td>${line.line_description || ""}</td> <td>${line.line_description || ""}</td>
`; `;
tbody.appendChild(tr); tbody.appendChild(tr);
@@ -140,6 +142,7 @@
<td style="text-align:right">${totalDebit.toLocaleString()}</td> <td style="text-align:right">${totalDebit.toLocaleString()}</td>
<td style="text-align:right">${totalCredit.toLocaleString()}</td> <td style="text-align:right">${totalCredit.toLocaleString()}</td>
<td></td> <td></td>
<td></td>
`; `;
tbody.appendChild(totalRow); tbody.appendChild(totalRow);
} }

View File

@@ -195,14 +195,22 @@
<div id="tab-salary" class="tab-content-calc active"> <div id="tab-salary" class="tab-content-calc active">
<div class="filters"> <div class="filters">
<label> <label>
対象年: 対象年:
<input <input
type="month" type="number"
id="filterYearMonth" id="filterYear"
style="width: 160px" style="width: 90px"
placeholder="YYYY-MM" value="2026"
min="2000"
max="2099"
/> />
</label> </label>
<label>
従業員:
<select id="filterEmployee" style="min-width: 200px">
<option value="">全員</option>
</select>
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button> <button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()"> <button class="btn btn-success" onclick="showCalculateForm()">
新規計算 新規計算
@@ -373,6 +381,34 @@
</div> </div>
</div> </div>
<div class="detail-section">
<h3>計算オプション</h3>
<div style="display: flex; gap: 30px; padding: 8px 0">
<label
style="
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
"
>
<input type="checkbox" id="calcIncomeTax" checked />
所得税を計算する
</label>
<label
style="
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
"
>
<input type="checkbox" id="calcSocialInsurance" checked />
社会保険を計算する
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">計算実行</button> <button type="submit" class="btn btn-primary">計算実行</button>
<button <button
type="button" type="button"
@@ -566,11 +602,66 @@
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>開始年月 *</label> <label>開始年月 *</label>
<input type="month" id="voucherStartYearMonth" required /> <div style="display: flex; gap: 6px; align-items: center">
<input
type="text"
id="voucherStartYear"
style="width: 90px"
placeholder="年"
maxlength="4"
pattern="[0-9]{4}"
inputmode="numeric"
required
/>
<span style="color: #666"></span>
<select id="voucherStartMonth" required>
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select>
<span style="color: #666"></span>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>終了年月</label> <label>終了年月</label>
<input type="month" id="voucherEndYearMonth" /> <div style="display: flex; gap: 6px; align-items: center">
<input
type="text"
id="voucherEndYear"
style="width: 90px"
placeholder="年"
maxlength="4"
pattern="[0-9]{4}"
inputmode="numeric"
/>
<span style="color: #666"></span>
<select id="voucherEndMonth">
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
</select>
<span style="color: #666"></span>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label style="color: #999; font-size: 12px"> <label style="color: #999; font-size: 12px">
@@ -799,24 +890,41 @@
const bonusSelect = document.getElementById("bonusEmployeeSelect"); const bonusSelect = document.getElementById("bonusEmployeeSelect");
if (bonusSelect) bonusSelect.innerHTML = optionsHtml; if (bonusSelect) bonusSelect.innerHTML = optionsHtml;
// 検索用フィルタのドロップダウンも更新
const filterSelect = document.getElementById("filterEmployee");
if (filterSelect) {
const currentVal = filterSelect.value;
filterSelect.innerHTML =
'<option value="">全員</option>' +
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`,
)
.join("");
filterSelect.value = currentVal;
}
return employees; return employees;
} }
async function loadPayrolls() { async function loadPayrolls() {
const ym = document.getElementById("filterYearMonth").value; // 前回の給与明細をクリア
let year = null; const detailDiv = document.getElementById("payrollDetail");
let month = null; if (detailDiv) {
if (ym) { detailDiv.style.display = "none";
const parts = ym.split("-"); detailDiv.innerHTML = "";
if (parts.length === 2) {
year = Number(parts[0]);
month = Number(parts[1]);
}
} }
const year =
Number(document.getElementById("filterYear")?.value) || null;
const employeeId =
document.getElementById("filterEmployee")?.value || null;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (year) params.append("payroll_year", year); if (year) params.append("payroll_year", year);
if (month) params.append("payroll_month", month); if (employeeId) params.append("employee_id", employeeId);
// employeeMap が空なら読み込む // employeeMap が空なら読み込む
if (!employeeMap || Object.keys(employeeMap).length === 0) { if (!employeeMap || Object.keys(employeeMap).length === 0) {
await loadEmployees(); await loadEmployees();
@@ -827,52 +935,66 @@
`${API_BASE}/payroll/calculation/?${params}`, `${API_BASE}/payroll/calculation/?${params}`,
); );
const payrolls = await response.json(); const payrolls = await response.json();
const html = payrolls
.map((p) => { if (!payrolls || payrolls.length === 0) {
const emp = employeeMap[p.employee_id]; document.getElementById("payrollList").innerHTML =
const empLabel = emp "<p>給与データがありません</p>";
? `従業員ID: ${p.employee_id} | ${emp.employee_code} - ${emp.name}` return;
: `従業員ID: ${p.employee_id}`; }
// 月別にグループ化
const byMonth = {};
payrolls.forEach((p) => {
const key = `${p.payroll_year}-${String(p.payroll_month).padStart(2, "0")}`;
if (!byMonth[key]) byMonth[key] = [];
byMonth[key].push(p);
});
// 月順にソート
const sortedMonths = Object.keys(byMonth).sort();
const html = sortedMonths
.map((monthKey) => {
const [y, m] = monthKey.split("-");
const items = byMonth[monthKey]
.map((p) => {
const emp = employeeMap[p.employee_id];
const empLabel = emp
? `${emp.employee_code} - ${emp.name}`
: `従業員ID: ${p.employee_id}`;
return `
<div class="payroll-item" style="display:flex; justify-content:space-between; align-items:flex-start;">
<div style="flex:1; cursor:pointer;" onclick="viewPayroll(${p.payroll_id})">
<div>
<strong>${empLabel}</strong>
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
<span style="margin-left:8px; font-size:12px; color:#666;">支給日: ${p.payment_date}</span>
</div>
<div style="margin-top:4px;">
<strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong>
<span style="color:#666; font-size:13px;">(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()}</span>
</div>
</div>
<button class="btn btn-danger btn-small" onclick="deletePayroll(${p.payroll_id}); event.stopPropagation();" style="margin-left:10px; white-space:nowrap; background-color:#dc3545; color:#fff; border:none;">
削除
</button>
</div>
`;
})
.join("");
return ` return `
<div class="payroll-item" style="display: flex; justify-content: space-between; align-items: flex-start;"> <div style="margin-bottom:20px;">
<div style="flex: 1; cursor: pointer;" onclick="viewPayroll(${ <div style="background:#e8f0fe; border-left:4px solid #007bff; padding:8px 14px; font-weight:bold; font-size:15px; margin-bottom:6px;">
p.payroll_id ${Number(y)}${Number(m)}
})"> </div>
<div> ${items}
<strong>${p.payroll_year}${ </div>
p.payroll_month `;
}月</strong>
<span class="status-badge status-${
p.status
}">${getStatusLabel(p.status)}</span>
</div>
<div>
${empLabel} | 支給日: ${p.payment_date}
</div>
<div>
<strong>差引支給額: ¥${Number(
p.net_payment,
).toLocaleString()}</strong>
(総支給: ¥${Number(
p.total_payment,
).toLocaleString()} - 控除: ¥${Number(
p.total_deduction,
).toLocaleString()})
</div>
</div>
<button class="btn btn-danger btn-small" onclick="deletePayroll(${
p.payroll_id
}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; background-color: #dc3545; color: #fff; border: none;">
削除
</button>
</div>
`;
}) })
.join(""); .join("");
document.getElementById("payrollList").innerHTML = document.getElementById("payrollList").innerHTML = html;
html || "<p>給与データがありません</p>";
} catch (error) { } catch (error) {
alert("給与一覧の取得に失敗しました"); alert("給与一覧の取得に失敗しました");
console.error(error); console.error(error);
@@ -927,6 +1049,11 @@
// ensure payroll_year/month are numbers // ensure payroll_year/month are numbers
data.payroll_year = valid.year; data.payroll_year = valid.year;
data.payroll_month = valid.month; data.payroll_month = valid.month;
// 計算オプションフラグ
data.calc_income_tax =
document.getElementById("calcIncomeTax")?.checked ?? true;
data.calc_social_insurance =
document.getElementById("calcSocialInsurance")?.checked ?? true;
try { try {
const response = await fetch( const response = await fetch(
@@ -942,6 +1069,7 @@
const result = await response.json(); const result = await response.json();
alert("給与計算が完了しました"); alert("給与計算が完了しました");
closeCalculateForm(); closeCalculateForm();
await loadPayrolls();
viewPayroll(result.payroll_id); viewPayroll(result.payroll_id);
} else { } else {
const error = await response.json(); const error = await response.json();
@@ -1188,6 +1316,20 @@
</div> </div>
</div> </div>
<div class="detail-section">
<h3>計算オプション</h3>
<div style="display:flex; gap:30px; padding:8px 0;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
<input type="checkbox" id="editCalcIncomeTax" checked />
所得税を計算する
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
<input type="checkbox" id="editCalcSocialInsurance" checked />
社会保険を計算する
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">保存して再計算</button> <button type="submit" class="btn btn-primary">保存して再計算</button>
<button type="button" class="btn btn-secondary" onclick="this.closest('div').remove()">キャンセル</button> <button type="button" class="btn btn-secondary" onclick="this.closest('div').remove()">キャンセル</button>
</form> </form>
@@ -1224,11 +1366,20 @@
); );
if (response.ok) { if (response.ok) {
// チェックボックスの値をフォーム削除前に取得
const calcIncomeTax =
document.getElementById("editCalcIncomeTax")?.checked ?? true;
const calcSocialInsurance =
document.getElementById("editCalcSocialInsurance")?.checked ??
true;
alert("給与データを更新しました"); alert("給与データを更新しました");
form.closest("div").remove(); form.closest("div").remove();
// 再計算 // 再計算
await recalculatePayroll(payrollId); await recalculatePayroll(payrollId, {
calc_income_tax: calcIncomeTax,
calc_social_insurance: calcSocialInsurance,
});
} else { } else {
const error = await response.json(); const error = await response.json();
alert("更新に失敗しました: " + error.detail); alert("更新に失敗しました: " + error.detail);
@@ -1266,14 +1417,17 @@
} }
} }
async function recalculatePayroll(payrollId) { async function recalculatePayroll(payrollId, calcOptions = null) {
if (!confirm("給与を再計算しますか?")) return; if (!confirm("給与を再計算しますか?")) return;
try { try {
const body = calcOptions || {};
const response = await fetch( const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}/recalculate`, `${API_BASE}/payroll/calculation/${payrollId}/recalculate`,
{ {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}, },
); );
@@ -1320,11 +1474,12 @@
} }
} }
// 初期化: 月ピッカーにセットして一覧を読み込む // 初期化: 年フィルタに現在年をセットして一覧を読み込む
const now = new Date(); const now = new Date();
const ym = const ym =
now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0"); now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
document.getElementById("filterYearMonth").value = ym; const filterYearEl = document.getElementById("filterYear");
if (filterYearEl) filterYearEl.value = now.getFullYear();
document.getElementById("filterBonusYearMonth").value = ym; document.getElementById("filterBonusYearMonth").value = ym;
loadPayrolls(); loadPayrolls();
@@ -1359,8 +1514,11 @@
now.getFullYear() + now.getFullYear() +
"-" + "-" +
String(now.getMonth() + 1).padStart(2, "0"); String(now.getMonth() + 1).padStart(2, "0");
document.getElementById("voucherStartYearMonth").value = ym; document.getElementById("voucherStartYear").value = now.getFullYear();
document.getElementById("voucherEndYearMonth").value = ""; document.getElementById("voucherStartMonth").value =
now.getMonth() + 1;
document.getElementById("voucherEndYear").value = "";
document.getElementById("voucherEndMonth").value = "";
} }
} }
@@ -1732,8 +1890,13 @@
async function printVouchers() { async function printVouchers() {
// 検証 // 検証
const startYM = document.getElementById("voucherStartYearMonth").value; const startYear = Number(
if (!startYM) { document.getElementById("voucherStartYear").value,
);
const startMonth = Number(
document.getElementById("voucherStartMonth").value,
);
if (!startYear || !startMonth) {
alert("開始年月を選択してください"); alert("開始年月を選択してください");
return; return;
} }
@@ -1745,11 +1908,10 @@
} }
// 年月を解析 // 年月を解析
const [startYear, startMonth] = startYM.split("-").map(Number); const endYearVal = document.getElementById("voucherEndYear").value;
const endYM = document.getElementById("voucherEndYearMonth").value; const endMonthVal = document.getElementById("voucherEndMonth").value;
const endParts = endYM ? endYM.split("-").map(Number) : null; const endYear = endYearVal ? Number(endYearVal) : null;
const endYear = endParts ? endParts[0] : null; const endMonth = endMonthVal ? Number(endMonthVal) : null;
const endMonth = endParts ? endParts[1] : null;
const voucherType = document.querySelector( const voucherType = document.querySelector(
"input[name='voucherType']:checked", "input[name='voucherType']:checked",
@@ -1900,6 +2062,7 @@
<div class="page-title">${salary.payroll_year}${String(salary.payroll_month).padStart(2, "0")}月 給与明細</div> <div class="page-title">${salary.payroll_year}${String(salary.payroll_month).padStart(2, "0")}月 給与明細</div>
<div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div> <div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div>
<div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${paymentDate}</div> <div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${paymentDate}</div>
${emp.employment_type ? `<div class="employee-info" style="color: #666; font-size: 12px;">雇用形態:${emp.employment_type}</div>` : ""}
<div class="section-title">勤怠情報</div> <div class="section-title">勤怠情報</div>
<table class="detail-table"> <table class="detail-table">
@@ -2013,6 +2176,7 @@
<div class="page-title">${bonus.bonus_year}${String(bonus.bonus_month).padStart(2, "0")}月 賞与明細${bonus.bonus_type ? ` (${bonus.bonus_type})` : ""}</div> <div class="page-title">${bonus.bonus_year}${String(bonus.bonus_month).padStart(2, "0")}月 賞与明細${bonus.bonus_type ? ` (${bonus.bonus_type})` : ""}</div>
<div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div> <div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div>
<div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${bonusPaymentDate}</div> <div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${bonusPaymentDate}</div>
${emp.employment_type ? `<div class="employee-info" style="color: #666; font-size: 12px;">雇用形態:${emp.employment_type}</div>` : ""}
<div class="section-title">支給項目</div> <div class="section-title">支給項目</div>
<table class="detail-table"> <table class="detail-table">
@@ -2103,8 +2267,13 @@
async function exportVouchersCSV() { async function exportVouchersCSV() {
// 検証 // 検証
const startYM = document.getElementById("voucherStartYearMonth").value; const startYear = Number(
if (!startYM) { document.getElementById("voucherStartYear").value,
);
const startMonth = Number(
document.getElementById("voucherStartMonth").value,
);
if (!startYear || !startMonth) {
alert("開始年月を選択してください"); alert("開始年月を選択してください");
return; return;
} }
@@ -2115,11 +2284,10 @@
return; return;
} }
const [startYear, startMonth] = startYM.split("-").map(Number); const endYearVal = document.getElementById("voucherEndYear").value;
const endYM = document.getElementById("voucherEndYearMonth").value; const endMonthVal = document.getElementById("voucherEndMonth").value;
const endParts = endYM ? endYM.split("-").map(Number) : null; const endYear = endYearVal ? Number(endYearVal) : null;
const endYear = endParts ? endParts[0] : null; const endMonth = endMonthVal ? Number(endMonthVal) : null;
const endMonth = endParts ? endParts[1] : null;
const voucherType = document.querySelector( const voucherType = document.querySelector(
"input[name='voucherType']:checked", "input[name='voucherType']:checked",
).value; ).value;

View File

@@ -326,10 +326,9 @@
: "✗ 非対象" : "✗ 非対象"
}</td> }</td>
<td>${s.valid_from} ${s.valid_to || "現在"}</td> <td>${s.valid_from} ${s.valid_to || "現在"}</td>
<td> <td style="white-space:nowrap;">
<button class="btn btn-primary" onclick="editSetting(${ <button class="btn btn-primary" onclick="editSetting(${s.setting_id})" style="padding: 5px 10px; font-size: 12px;">編集</button>
s.setting_id <button class="btn" onclick="deleteSetting(${s.setting_id}, '${s.employee_name}', '${s.valid_from}')" style="padding: 5px 10px; font-size: 12px; background:#dc3545; color:white; margin-left:4px;">削除</button>
})" style="padding: 5px 10px; font-size: 12px;">編集</button>
</td> </td>
</tr> </tr>
`, `,
@@ -390,6 +389,37 @@
} }
} }
async function deleteSetting(settingId, employeeName, validFrom) {
if (
!confirm(
`${employeeName}」の適用開始日 ${validFrom} の給与設定を削除しますか?\nこの操作は元に戻せません。`,
)
)
return;
try {
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${settingId}`,
{
method: "DELETE",
},
);
if (response.ok || response.status === 204) {
alert("給与設定を削除しました");
loadAllSalarySettings();
} else {
let errMsg = `HTTP ${response.status}`;
try {
const errBody = await response.json();
if (errBody.detail) errMsg = String(errBody.detail);
} catch (_) {}
alert("削除に失敗しました: " + errMsg);
}
} catch (error) {
alert("削除に失敗しました: " + (error.message || error));
console.error(error);
}
}
function showAddForm() { function showAddForm() {
document.getElementById("formTitle").textContent = "給与設定の追加"; document.getElementById("formTitle").textContent = "給与設定の追加";
document.getElementById("addForm").style.display = "block"; document.getElementById("addForm").style.display = "block";
@@ -416,8 +446,18 @@
const formData = new FormData(form); const formData = new FormData(form);
const settingId = document.getElementById("setting_id").value; const settingId = document.getElementById("setting_id").value;
// disabled な employee_id は FormData に含まれないため DOM から直接取得
const employeeIdRaw =
formData.get("employee_id") ||
document.getElementById("employee_id").value;
const employeeId = parseInt(employeeIdRaw);
if (!settingId && (!employeeId || isNaN(employeeId))) {
alert("従業員を選択してください");
return;
}
const data = { const data = {
employee_id: parseInt(formData.get("employee_id")),
base_salary: parseFloat(formData.get("base_salary")) || 0, base_salary: parseFloat(formData.get("base_salary")) || 0,
employment_type: formData.get("employment_type"), employment_type: formData.get("employment_type"),
payment_type: formData.get("payment_type"), payment_type: formData.get("payment_type"),
@@ -429,6 +469,11 @@
valid_from: formData.get("valid_from"), valid_from: formData.get("valid_from"),
}; };
// 新規登録のみ employee_id を含める
if (!settingId) {
data.employee_id = employeeId;
}
if (formData.get("hourly_rate")) { if (formData.get("hourly_rate")) {
data.hourly_rate = parseFloat(formData.get("hourly_rate")); data.hourly_rate = parseFloat(formData.get("hourly_rate"));
} }
@@ -465,11 +510,24 @@
hideAddForm(); hideAddForm();
loadAllSalarySettings(); loadAllSalarySettings();
} else { } else {
const error = await response.json(); let errMsg = `HTTP ${response.status}`;
alert("保存に失敗しました: " + error.detail); try {
const errBody = await response.json();
const detail = errBody.detail;
if (Array.isArray(detail)) {
errMsg = detail
.map((e) => e.msg || JSON.stringify(e))
.join(", ");
} else if (detail) {
errMsg = String(detail);
}
} catch (_) {
errMsg += ` ${await response.text().catch(() => "")}`;
}
alert("保存に失敗しました: " + errMsg);
} }
} catch (error) { } catch (error) {
alert("保存に失敗しました"); alert("保存に失敗しました: " + (error.message || error));
console.error(error); console.error(error);
} }
} }