修正
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if calc_income_tax:
|
||||||
income_tax = PayrollCalculationService.calculate_income_tax(
|
income_tax = PayrollCalculationService.calculate_income_tax(
|
||||||
taxable_income,
|
taxable_income,
|
||||||
dependents_count,
|
dependents_count,
|
||||||
payment_date
|
payment_date
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
income_tax = Decimal("0")
|
||||||
|
|
||||||
# 総控除額
|
# 総控除額
|
||||||
total_deduction = (
|
total_deduction = (
|
||||||
|
|||||||
@@ -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,6 +68,7 @@ def create_salary_setting(setting: schemas.SalarySettingCreate):
|
|||||||
)
|
)
|
||||||
has_employment_insurance_column = cur.fetchone()
|
has_employment_insurance_column = cur.fetchone()
|
||||||
|
|
||||||
|
try:
|
||||||
if has_employment_insurance_column:
|
if has_employment_insurance_column:
|
||||||
# カラムが存在する場合
|
# カラムが存在する場合
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -106,6 +108,11 @@ def create_salary_setting(setting: schemas.SalarySettingCreate):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました")
|
print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました")
|
||||||
|
except UniqueViolation:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="この適用開始日はすでに登録されています。別の日付を指定してください。"
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
||||||
|
try:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *",
|
f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *",
|
||||||
values,
|
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()
|
||||||
|
|
||||||
|
|
||||||
# ========================================
|
# ========================================
|
||||||
# 社会保険料率管理
|
# 社会保険料率管理
|
||||||
# ========================================
|
# ========================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
`登録しました(ID: ${data.journal_entry_id})`,
|
||||||
|
"success",
|
||||||
|
() => {
|
||||||
localStorage.removeItem("editSource");
|
localStorage.removeItem("editSource");
|
||||||
|
|
||||||
// 親窓口(列表页)の searchJournals()を呼び出す
|
|
||||||
if (window.opener && window.opener.searchJournals) {
|
if (window.opener && window.opener.searchJournals) {
|
||||||
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>
|
||||||
|
|||||||
@@ -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, "&").replace(/</g, "<");
|
||||||
|
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;">×</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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,43 +935,47 @@
|
|||||||
`${API_BASE}/payroll/calculation/?${params}`,
|
`${API_BASE}/payroll/calculation/?${params}`,
|
||||||
);
|
);
|
||||||
const payrolls = await response.json();
|
const payrolls = await response.json();
|
||||||
const html = payrolls
|
|
||||||
|
if (!payrolls || payrolls.length === 0) {
|
||||||
|
document.getElementById("payrollList").innerHTML =
|
||||||
|
"<p>給与データがありません</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 月別にグループ化
|
||||||
|
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) => {
|
.map((p) => {
|
||||||
const emp = employeeMap[p.employee_id];
|
const emp = employeeMap[p.employee_id];
|
||||||
const empLabel = emp
|
const empLabel = emp
|
||||||
? `従業員ID: ${p.employee_id} | ${emp.employee_code} - ${emp.name}`
|
? `${emp.employee_code} - ${emp.name}`
|
||||||
: `従業員ID: ${p.employee_id}`;
|
: `従業員ID: ${p.employee_id}`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="payroll-item" style="display:flex; justify-content:space-between; align-items:flex-start;">
|
<div class="payroll-item" style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||||||
<div style="flex: 1; cursor: pointer;" onclick="viewPayroll(${
|
<div style="flex:1; cursor:pointer;" onclick="viewPayroll(${p.payroll_id})">
|
||||||
p.payroll_id
|
|
||||||
})">
|
|
||||||
<div>
|
<div>
|
||||||
<strong>${p.payroll_year}年${
|
<strong>${empLabel}</strong>
|
||||||
p.payroll_month
|
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
|
||||||
}月</strong>
|
<span style="margin-left:8px; font-size:12px; color:#666;">支給日: ${p.payment_date}</span>
|
||||||
<span class="status-badge status-${
|
|
||||||
p.status
|
|
||||||
}">${getStatusLabel(p.status)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style="margin-top:4px;">
|
||||||
${empLabel} | 支給日: ${p.payment_date}
|
<strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong>
|
||||||
</div>
|
<span style="color:#666; font-size:13px;">(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()})</span>
|
||||||
<div>
|
|
||||||
<strong>差引支給額: ¥${Number(
|
|
||||||
p.net_payment,
|
|
||||||
).toLocaleString()}</strong>
|
|
||||||
(総支給: ¥${Number(
|
|
||||||
p.total_payment,
|
|
||||||
).toLocaleString()} - 控除: ¥${Number(
|
|
||||||
p.total_deduction,
|
|
||||||
).toLocaleString()})
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-danger btn-small" onclick="deletePayroll(${
|
<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;">
|
||||||
p.payroll_id
|
|
||||||
}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; background-color: #dc3545; color: #fff; border: none;">
|
|
||||||
削除
|
削除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -871,8 +983,18 @@
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
document.getElementById("payrollList").innerHTML =
|
return `
|
||||||
html || "<p>給与データがありません</p>";
|
<div style="margin-bottom:20px;">
|
||||||
|
<div style="background:#e8f0fe; border-left:4px solid #007bff; padding:8px 14px; font-weight:bold; font-size:15px; margin-bottom:6px;">
|
||||||
|
${Number(y)}年 ${Number(m)}月
|
||||||
|
</div>
|
||||||
|
${items}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
document.getElementById("payrollList").innerHTML = html;
|
||||||
} 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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user