From 86020ada5c36ddbe6978415954028866de232c18 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 21 Jan 2026 10:58:19 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/add_my_number.py | 36 + backend/app/main.py | 16 + backend/app/payroll/bonus/service.py | 23 +- backend/app/payroll/calculation/schemas.py | 1 + backend/app/payroll/calculation/service.py | 29 +- backend/app/payroll/employees/router.py | 15 +- backend/app/payroll/employees/schemas.py | 12 + backend/app/payroll/settings/router.py | 96 +- .../settings/standard_remuneration_router.py | 358 +++++ backend/app/routers/file_upload.py | 173 +++ backend/check_insurance_rates.py | 25 + backend/check_photo_data.py | 33 + backend/check_std_table.py | 51 + backend/create_std_remuneration_table.py | 53 + backend/create_std_table.py | 58 + backend/debug_care_insurance.py | 128 ++ backend/fix_photo_columns.py | 42 + backend/sql/add_employee_photos.sql | 15 + backend/sql/add_my_number_field.sql | 12 + backend/sql/create_standard_remuneration.sql | 32 + backend/sql/fix_photo_column_size.sql | 15 + backend/sql/insert_2025_insurance_rates.sql | 34 + backend/test_care_insurance.py | 94 ++ .../20190002_20260120_165205_10b99e35.png | Bin 0 -> 9291 bytes frontend/index.html | 4 +- frontend/journal-edit.html | 2 +- frontend/journal-entry.html | 6 +- frontend/journal-list.html | 4 +- frontend/journal.html | 2 +- frontend/pages/cash.html | 4 +- frontend/payroll-calculation.html | 199 ++- frontend/payroll-employees.html | 1232 ++++++++++++++++- frontend/payroll-salary-settings.html | 523 ++++--- frontend/payroll-settings.html | 449 +++++- frontend/trial-balance.html | 4 +- reports/tmp/r7ippan3.xlsx | Bin 0 -> 410136 bytes 36 files changed, 3417 insertions(+), 363 deletions(-) create mode 100644 backend/add_my_number.py create mode 100644 backend/app/payroll/settings/standard_remuneration_router.py create mode 100644 backend/app/routers/file_upload.py create mode 100644 backend/check_insurance_rates.py create mode 100644 backend/check_photo_data.py create mode 100644 backend/check_std_table.py create mode 100644 backend/create_std_remuneration_table.py create mode 100644 backend/create_std_table.py create mode 100644 backend/debug_care_insurance.py create mode 100644 backend/fix_photo_columns.py create mode 100644 backend/sql/add_employee_photos.sql create mode 100644 backend/sql/add_my_number_field.sql create mode 100644 backend/sql/create_standard_remuneration.sql create mode 100644 backend/sql/fix_photo_column_size.sql create mode 100644 backend/sql/insert_2025_insurance_rates.sql create mode 100644 backend/test_care_insurance.py create mode 100644 backend/uploads/employees/20190002/20190002_20260120_165205_10b99e35.png create mode 100644 reports/tmp/r7ippan3.xlsx diff --git a/backend/add_my_number.py b/backend/add_my_number.py new file mode 100644 index 0000000..5ade2f2 --- /dev/null +++ b/backend/add_my_number.py @@ -0,0 +1,36 @@ +import psycopg +from psycopg.rows import dict_row + +# データベース接続 +conn = psycopg.connect( + host="192.168.0.61", + port=55432, + dbname="njts_acct", + user="njts_app", + password="njts_app2025", + row_factory=dict_row +) + +# SQLファイルを読み込んで実行 +with open('sql/add_my_number_field.sql', encoding='utf-8') as f: + sql = f.read() + +cur = conn.cursor() +cur.execute(sql) +conn.commit() + +print("マイナンバーフィールドの追加が完了しました") + +# 確認 +cur.execute(""" + SELECT column_name, data_type, character_maximum_length + FROM information_schema.columns + WHERE table_name = 'employees' AND column_name = 'my_number' +""") +result = cur.fetchone() +if result: + print(f"✓ フィールド追加確認: {result}") +else: + print("✗ フィールドが見つかりません") + +conn.close() diff --git a/backend/app/main.py b/backend/app/main.py index 1616c85..d34291b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -81,6 +81,9 @@ app.include_router(year_locks.router) from app.routers import cash app.include_router(cash.router) +from app.routers import file_upload +app.include_router(file_upload.router) + # ───────────────────────── # 給与モジュール (Payroll Module) # ───────────────────────── @@ -90,6 +93,9 @@ app.include_router(payroll_employees_router) from app.payroll.settings.router import router as payroll_settings_router app.include_router(payroll_settings_router) +from app.payroll.settings.standard_remuneration_router import router as standard_remuneration_router +app.include_router(standard_remuneration_router) + from app.payroll.calculation.router import router as payroll_calculation_router app.include_router(payroll_calculation_router) @@ -99,6 +105,16 @@ app.include_router(payroll_bonus_router) from fastapi.staticfiles import StaticFiles import os +from pathlib import Path + +# アップロードファイル用の静的ファイル配信 +uploads_dir = Path("uploads") +uploads_dir.mkdir(exist_ok=True) +app.mount( + "/uploads", + StaticFiles(directory="uploads"), + name="uploads", +) # 静的ファイル配信(コメントアウト - ディレクトリが存在しない) # app.mount( diff --git a/backend/app/payroll/bonus/service.py b/backend/app/payroll/bonus/service.py index 198c59d..3ecc305 100644 --- a/backend/app/payroll/bonus/service.py +++ b/backend/app/payroll/bonus/service.py @@ -11,20 +11,22 @@ class BonusCalculationService: """賞与計算サービス""" @staticmethod - def get_insurance_rate(rate_type: str, target_date: date) -> Dict[str, Any]: - """保険料率を取得""" + def get_insurance_rate(rate_type: str, target_date: date, calculation_target: str = '賞与') -> Dict[str, Any]: + """保険料率を取得(賞与用)""" with get_connection() as conn: with conn.cursor() as cur: + # rate_yearで年度を判定 + target_year = target_date.year cur.execute( """ SELECT * FROM insurance_rates WHERE rate_type = %s - AND valid_from <= %s - AND (valid_to IS NULL OR valid_to >= %s) - ORDER BY valid_from DESC + AND rate_year = %s + AND calculation_target = %s + ORDER BY rate_id DESC LIMIT 1 """, - (rate_type, target_date, target_date) + (rate_type, target_year, calculation_target) ) return cur.fetchone() @@ -134,10 +136,15 @@ class BonusCalculationService: ) employee_info = cur.fetchone() - # 年齢を計算 + # 年齢を計算(介護保険は40歳以上65歳未満が対象) age = 0 if employee_info and employee_info["birth_date"]: - age = (payment_date - employee_info["birth_date"]).days // 365 + birth_date = employee_info["birth_date"] + # より正確な年齢計算 + age = payment_date.year - birth_date.year + # 誕生日がまだ来ていない場合は-1 + if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day): + age -= 1 # 健康保険(賞与額の1/1000で計算) health_insurance_rate = BonusCalculationService.get_insurance_rate("健康保険", payment_date) diff --git a/backend/app/payroll/calculation/schemas.py b/backend/app/payroll/calculation/schemas.py index 00e4ec9..163c29f 100644 --- a/backend/app/payroll/calculation/schemas.py +++ b/backend/app/payroll/calculation/schemas.py @@ -81,6 +81,7 @@ class MonthlyPayroll(MonthlyPayrollBase): # 控除項目 health_insurance: Decimal + care_insurance: Decimal pension_insurance: Decimal employment_insurance: Decimal income_tax: Decimal diff --git a/backend/app/payroll/calculation/service.py b/backend/app/payroll/calculation/service.py index d9128b0..3dc6e2f 100644 --- a/backend/app/payroll/calculation/service.py +++ b/backend/app/payroll/calculation/service.py @@ -12,17 +12,21 @@ class PayrollCalculationService: @staticmethod def get_active_dependents_count(employee_id: int, target_date: date) -> int: - """有効な扶養家族数を取得""" + """有効な扶養家族数を取得(源泉税控除対象のみ:19歳以上)""" with get_connection() as conn: with conn.cursor() as cur: + # その年の12月31日時点で19歳以上の扶養家族のみをカウント + # 16-18歳は住民税のみ対象なので源泉税控除対象外 cur.execute( """ SELECT COUNT(*) as count FROM dependents WHERE employee_id = %s AND valid_from <= %s AND (valid_to IS NULL OR valid_to >= %s) + AND birth_date IS NOT NULL + AND EXTRACT(YEAR FROM AGE(DATE(EXTRACT(YEAR FROM %s) || '-12-31'), birth_date)) >= 19 """, - (employee_id, target_date, target_date) + (employee_id, target_date, target_date, target_date) ) result = cur.fetchone() return result["count"] if result else 0 @@ -46,20 +50,22 @@ class PayrollCalculationService: return cur.fetchone() @staticmethod - def get_insurance_rate(rate_type: str, target_date: date) -> Dict[str, Any]: + def get_insurance_rate(rate_type: str, target_date: date, calculation_target: str = '給与') -> Dict[str, Any]: """保険料率を取得""" with get_connection() as conn: with conn.cursor() as cur: + # rate_yearで年度を判定 + target_year = target_date.year cur.execute( """ SELECT * FROM insurance_rates WHERE rate_type = %s - AND valid_from <= %s - AND (valid_to IS NULL OR valid_to >= %s) - ORDER BY valid_from DESC + AND rate_year = %s + AND calculation_target = %s + ORDER BY rate_id DESC LIMIT 1 """, - (rate_type, target_date, target_date) + (rate_type, target_year, calculation_target) ) return cur.fetchone() @@ -202,10 +208,15 @@ class PayrollCalculationService: ) employee_info = cur.fetchone() - # 年齢を計算(介護保険は40歳以上が対象) + # 年齢を計算(介護保険は40歳以上65歳未満が対象) age = 0 if employee_info and employee_info["birth_date"]: - age = (payment_date - employee_info["birth_date"]).days // 365 + birth_date = employee_info["birth_date"] + # より正確な年齢計算 + age = payment_date.year - birth_date.year + # 誕生日がまだ来ていない場合は-1 + if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day): + age -= 1 # 健康保険(上限適用後の標準報酬月額で計算) health_insurance_rate = PayrollCalculationService.get_insurance_rate("健康保険", payment_date) diff --git a/backend/app/payroll/employees/router.py b/backend/app/payroll/employees/router.py index f2a3fe1..0a51cd1 100644 --- a/backend/app/payroll/employees/router.py +++ b/backend/app/payroll/employees/router.py @@ -18,19 +18,24 @@ def create_employee(employee: schemas.EmployeeCreate): cur.execute( """ INSERT INTO employees ( - employee_code, name, name_kana, birth_date, gender, + employee_code, name, name_kana, birth_date, gender, my_number, email, phone, postal_code, address, hire_date, termination_date, is_active, - bank_name, bank_branch, bank_account_number, bank_account_type, notes - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + bank_name, bank_branch, bank_account_number, bank_account_type, notes, + photo_url, my_number_card_image_url, residence_card_image_url, + health_insurance_card_image_url, other_id_image_url + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """, ( employee.employee_code, employee.name, employee.name_kana, - employee.birth_date, employee.gender, + employee.birth_date, employee.gender, employee.my_number, employee.email, employee.phone, employee.postal_code, employee.address, employee.hire_date, employee.termination_date, employee.is_active, employee.bank_name, employee.bank_branch, - employee.bank_account_number, employee.bank_account_type, employee.notes + employee.bank_account_number, employee.bank_account_type, employee.notes, + employee.photo_url, employee.my_number_card_image_url, + employee.residence_card_image_url, employee.health_insurance_card_image_url, + employee.other_id_image_url ), ) result = cur.fetchone() diff --git a/backend/app/payroll/employees/schemas.py b/backend/app/payroll/employees/schemas.py index 5edda0e..a6f64a6 100644 --- a/backend/app/payroll/employees/schemas.py +++ b/backend/app/payroll/employees/schemas.py @@ -54,6 +54,7 @@ class EmployeeBase(BaseModel): name_kana: Optional[str] = None birth_date: Optional[date] = None gender: Optional[str] = None + my_number: Optional[str] = None email: Optional[EmailStr] = None phone: Optional[str] = None postal_code: Optional[str] = None @@ -66,6 +67,11 @@ class EmployeeBase(BaseModel): bank_account_number: Optional[str] = None bank_account_type: Optional[str] = None # 普通/当座 notes: Optional[str] = None + photo_url: Optional[str] = None + my_number_card_image_url: Optional[str] = None + residence_card_image_url: Optional[str] = None + health_insurance_card_image_url: Optional[str] = None + other_id_image_url: Optional[str] = None class EmployeeCreate(EmployeeBase): @@ -80,6 +86,7 @@ class EmployeeUpdate(BaseModel): name_kana: Optional[str] = None birth_date: Optional[date] = None gender: Optional[str] = None + my_number: Optional[str] = None email: Optional[EmailStr] = None phone: Optional[str] = None postal_code: Optional[str] = None @@ -92,6 +99,11 @@ class EmployeeUpdate(BaseModel): bank_account_number: Optional[str] = None bank_account_type: Optional[str] = None notes: Optional[str] = None + photo_url: Optional[str] = None + my_number_card_image_url: Optional[str] = None + residence_card_image_url: Optional[str] = None + health_insurance_card_image_url: Optional[str] = None + other_id_image_url: Optional[str] = None class Employee(EmployeeBase): diff --git a/backend/app/payroll/settings/router.py b/backend/app/payroll/settings/router.py index bbc8b82..ba417b7 100644 --- a/backend/app/payroll/settings/router.py +++ b/backend/app/payroll/settings/router.py @@ -427,7 +427,7 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None): # .xlsファイルの処理完了 conn.commit() print(f"インポート完了: {imported_count}件のデータをインポートしました") - return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました"} + return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました", "year": extracted_year} else: # .xlsx形式 - openpyxlを使用 @@ -568,7 +568,7 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None): # .xlsxファイルの処理完了 conn.commit() print(f"インポート完了: {imported_count}件のデータをインポートしました") - return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました"} + return {"message": f"{extracted_year}年度の所得税率表を{imported_count}件インポートしました", "year": extracted_year} # CSV file処理 else: @@ -611,35 +611,75 @@ def import_income_tax_table(file: UploadFile = File(...), tax_year: int = None): return {"message": f"{extracted_year}年度のデータを{imported_count}件インポートしました(既存データを置き換え)", "year": extracted_year} -@router.get("/income-tax", response_model=List[schemas.IncomeTaxTable]) -def get_income_tax_table(target_date: date = None, dependents_count: int = None): - """所得税率表を取得""" - if target_date is None: - target_date = date.today() - +@router.get("/income-tax/years") +def get_income_tax_years(): + """所得税率表の年度一覧を取得""" with get_connection() as conn: with conn.cursor() as cur: - if dependents_count is not None: - cur.execute( - """ - SELECT * FROM income_tax_table - WHERE valid_from <= %s - AND (valid_to IS NULL OR valid_to >= %s) - AND dependents_count = %s - ORDER BY monthly_income_from - """, - (target_date, target_date, dependents_count) - ) + cur.execute( + """ + SELECT DISTINCT tax_year + FROM income_tax_table + WHERE tax_year IS NOT NULL + ORDER BY tax_year DESC + """ + ) + years = [row['tax_year'] for row in cur.fetchall()] + return {"years": years} + + +@router.get("/income-tax", response_model=List[schemas.IncomeTaxTable]) +def get_income_tax_table(target_date: date = None, dependents_count: int = None, tax_year: int = None): + """所得税率表を取得""" + with get_connection() as conn: + with conn.cursor() as cur: + # tax_yearが指定されている場合はそれを優先 + if tax_year is not None: + if dependents_count is not None: + cur.execute( + """ + SELECT * FROM income_tax_table + WHERE tax_year = %s + AND dependents_count = %s + ORDER BY monthly_income_from + """, + (tax_year, dependents_count) + ) + else: + cur.execute( + """ + SELECT * FROM income_tax_table + WHERE tax_year = %s + ORDER BY dependents_count, monthly_income_from + """, + (tax_year,) + ) else: - cur.execute( - """ - SELECT * FROM income_tax_table - WHERE valid_from <= %s - AND (valid_to IS NULL OR valid_to >= %s) - ORDER BY dependents_count, monthly_income_from - """, - (target_date, target_date) - ) + # target_dateで検索 + if target_date is None: + target_date = date.today() + + if dependents_count is not None: + cur.execute( + """ + SELECT * FROM income_tax_table + WHERE valid_from <= %s + AND (valid_to IS NULL OR valid_to >= %s) + AND dependents_count = %s + ORDER BY monthly_income_from + """, + (target_date, target_date, dependents_count) + ) + else: + cur.execute( + """ + SELECT * FROM income_tax_table + WHERE valid_from <= %s + AND (valid_to IS NULL OR valid_to >= %s) + ORDER BY dependents_count, monthly_income_from + """, + (target_date, target_date) + ) return cur.fetchall() diff --git a/backend/app/payroll/settings/standard_remuneration_router.py b/backend/app/payroll/settings/standard_remuneration_router.py new file mode 100644 index 0000000..289bb98 --- /dev/null +++ b/backend/app/payroll/settings/standard_remuneration_router.py @@ -0,0 +1,358 @@ +from fastapi import APIRouter, UploadFile, File, HTTPException, Form +from fastapi.responses import JSONResponse +from typing import List, Optional +from pydantic import BaseModel +from decimal import Decimal +import openpyxl +from io import BytesIO +from app.core.database import get_connection + +router = APIRouter(prefix="/payroll/settings/standard-remuneration", tags=["standard_remuneration"]) + + +class StandardRemunerationRow(BaseModel): + grade: str + monthly_amount: Decimal + salary_from: Decimal + salary_to: Optional[Decimal] + health_insurance_no_care: Decimal + health_insurance_with_care: Decimal + pension_insurance: Decimal + + +@router.get("/years") +async def get_years(): + """登録済みの年度一覧を取得""" + try: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT DISTINCT rate_year FROM standard_remuneration ORDER BY rate_year DESC") + rows = cur.fetchall() + years = [row["rate_year"] for row in rows] + return years + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("") +async def get_standard_remuneration(year: int, prefecture: Optional[str] = None): + """指定された年度・都道府県の標準報酬月額表を取得""" + try: + with get_connection() as conn: + with conn.cursor() as cur: + if prefecture: + query = """ + SELECT rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, + health_insurance_no_care, health_insurance_with_care, pension_insurance + FROM standard_remuneration + WHERE rate_year = %s AND prefecture = %s + ORDER BY prefecture, + CASE WHEN grade ~ '^[0-9]+$' THEN CAST(grade AS INTEGER) ELSE 999 END, + grade + """ + cur.execute(query, (year, prefecture)) + else: + query = """ + SELECT rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, + health_insurance_no_care, health_insurance_with_care, pension_insurance + FROM standard_remuneration + WHERE rate_year = %s + ORDER BY prefecture, + CASE WHEN grade ~ '^[0-9]+$' THEN CAST(grade AS INTEGER) ELSE 999 END, + grade + """ + cur.execute(query, (year,)) + + rows = cur.fetchall() + + result = [] + for row in rows: + result.append({ + "rate_year": row["rate_year"], + "prefecture": row["prefecture"], + "grade": row["grade"], + "monthly_amount": row["monthly_amount"], + "salary_from": row["salary_from"], + "salary_to": row["salary_to"], + "health_insurance_no_care": row["health_insurance_no_care"], + "health_insurance_with_care": row["health_insurance_with_care"], + "pension_insurance": row["pension_insurance"] + }) + + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/import") +async def import_standard_remuneration( + file: UploadFile = File(...), + prefecture: str = Form(...) +): + """Excelファイルから標準報酬月額表をインポート(年度はA1セルから自動取得)""" + try: + # Excelファイルを読み込み + contents = await file.read() + wb = openpyxl.load_workbook(BytesIO(contents), data_only=True) + + # 指定された都道府県のシートを取得 + if prefecture not in wb.sheetnames: + raise HTTPException( + status_code=400, + detail=f"シート '{prefecture}' が見つかりません。利用可能なシート: {', '.join(wb.sheetnames)}" + ) + + ws = wb[prefecture] + + # A1セルから年度を取得 + # 例: "令和7年3月分(4月納付分)からの健康保険・厚生年金保険の保険料額表(東京都)" + a1_value = ws["A1"].value + if not a1_value: + raise HTTPException(status_code=400, detail="A1セルに年度情報がありません") + + # 令和年号から西暦年度を計算(例: 令和7年 = 2025年度) + import re + match = re.search(r'令和(\d+)年', str(a1_value)) + if match: + reiwa_year = int(match.group(1)) + rate_year = 2018 + reiwa_year # 令和元年 = 2019年 + else: + # 西暦が直接記載されている場合 + match = re.search(r'(\d{4})年', str(a1_value)) + if match: + rate_year = int(match.group(1)) + else: + raise HTTPException( + status_code=400, + detail=f"A1セルから年度を抽出できません: {a1_value}" + ) + + # データを抽出(12行目から開始) + data_rows = [] + errors = [] + + # min_row=12で12行目から読み取りを開始 + # enumerate()はイテレータの要素番号なので、start=0にして、実際の行番号は手動で計算 + for idx, row in enumerate(ws.iter_rows(min_row=12, values_only=True)): + row_idx = 12 + idx # 実際のExcel行番号 + # 等級が空の場合は終了 + if not row[0]: + break + + try: + grade = str(row[0]).strip() + + # 各値を取得し、Noneや空白を処理 + def clean_value(val, field_name=""): + if val is None or val == '': + return None + + # 数値型の場合はそのまま返す + if isinstance(val, (int, float)): + return val + + # 文字列の場合 + if isinstance(val, str): + # 空白を除去 + val = val.strip() + # 空、ハイフン、全角ダッシュなどをNoneに + if val in ('', '-', '-', '―', 'ー', '−'): + return None + # 「円」「未満」「以上」などの文字を除去 + val = val.replace('円', '').replace('未満', '').replace('以上', '') + # カンマを除去 + val = val.replace(',', '').replace(',', '') + # 全角数字を半角に変換 + val = val.translate(str.maketrans('0123456789', '0123456789')) + # 再度空チェック + if val == '': + return None + return val + + return val + + # デバッグ用に元のデータを保存(最初の7列を確認) + raw_data = { + 'col0_grade': row[0] if len(row) > 0 else None, + 'col1': row[1] if len(row) > 1 else None, + 'col2': row[2] if len(row) > 2 else None, + 'col3': row[3] if len(row) > 3 else None, + 'col4': row[4] if len(row) > 4 else None, + 'col5': row[5] if len(row) > 5 else None, + 'col6': row[6] if len(row) > 6 else None, + 'col7': row[7] if len(row) > 7 else None, + } + + # 報酬月額範囲の処理 + # 複数のパターンに対応: + # パターンA: 列2に範囲("58,000円~63,000円") + # パターンB: 列2=下限, 列3=「~」, 列4=上限 + # パターンC: 列2=下限, 列3=上限 + + monthly_amount = clean_value(row[1], 'monthly_amount') + + # 列3が「~」かチェック + col3_raw = str(row[3]) if len(row) > 3 and row[3] is not None else '' + col3_cleaned = col3_raw.strip() + + # 列2に範囲が含まれているかチェック + col2_raw = str(row[2]) if len(row) > 2 and row[2] is not None else '' + + if col3_cleaned in ('~', '〜', '-'): + # パターンB: 列2=下限, 列3=「~」, 列4=上限 + salary_from = clean_value(row[2], 'salary_from') + salary_to = clean_value(row[4] if len(row) > 4 else None, 'salary_to') + # 保険料は列5以降 + health_no_care = clean_value(row[5] if len(row) > 5 else None, 'health_no_care') + health_with_care = clean_value(row[6] if len(row) > 6 else None, 'health_with_care') + pension = clean_value(row[7] if len(row) > 7 else None, 'pension') + + elif '~' in col2_raw or '〜' in col2_raw: + # パターンA: 列2に範囲が1セルに入っている + range_text = col2_raw.replace('円', '').replace('未満', '').replace('以上', '') + parts = range_text.replace('〜', '~').split('~') + + if len(parts) >= 2: + salary_from = clean_value(parts[0].strip(), 'salary_from') + salary_to = clean_value(parts[1].strip(), 'salary_to') + else: + salary_from = clean_value(row[2], 'salary_from') + salary_to = None + + # 保険料は列3以降 + health_no_care = clean_value(row[3] if len(row) > 3 else None, 'health_no_care') + health_with_care = clean_value(row[4] if len(row) > 4 else None, 'health_with_care') + pension = clean_value(row[5] if len(row) > 5 else None, 'pension') + else: + # パターンC: 列2=下限、列3=上限の通常パターン + salary_from = clean_value(row[2], 'salary_from') + salary_to = clean_value(row[3] if len(row) > 3 else None, 'salary_to') + health_no_care = clean_value(row[4] if len(row) > 4 else None, 'health_no_care') + health_with_care = clean_value(row[5] if len(row) > 5 else None, 'health_with_care') + pension = clean_value(row[6] if len(row) > 6 else None, 'pension') + + # 必須フィールドのチェック(詳細なエラーメッセージ) + missing_fields = [] + if monthly_amount is None: + missing_fields.append(f"標準報酬月額(列2)={repr(raw_data['col1'])}") + if salary_from is None: + missing_fields.append(f"報酬月額下限={repr(raw_data['col2'])}") + if health_no_care is None: + missing_fields.append(f"健保(介護なし)={repr(raw_data.get('col4') or raw_data.get('col3'))}") + if health_with_care is None: + missing_fields.append(f"健保(介護あり)={repr(raw_data.get('col5') or raw_data.get('col4'))}") + if pension is None: + missing_fields.append(f"厚生年金={repr(raw_data.get('col6') or raw_data.get('col5'))}") + + if missing_fields: + errors.append(f"行{row_idx}: 必須フィールドが空: {', '.join(missing_fields)}") + continue + + # Decimal変換(より詳細なエラー処理) + try: + monthly_amount = Decimal(str(monthly_amount)) + except Exception as e: + errors.append(f"行{row_idx}: 標準報酬月額の変換エラー: {repr(raw_data['col1'])} -> {e}") + continue + + try: + salary_from = Decimal(str(salary_from)) + except Exception as e: + errors.append(f"行{row_idx}: 報酬月額下限の変換エラー: {repr(raw_data['col2'])} -> {e}") + continue + + try: + salary_to = Decimal(str(salary_to)) if salary_to is not None else None + except Exception as e: + errors.append(f"行{row_idx}: 報酬月額上限の変換エラー: 範囲から抽出または{repr(raw_data.get('col3'))} -> {e}") + continue + + try: + health_no_care = Decimal(str(health_no_care)) + except Exception as e: + errors.append(f"行{row_idx}: 健保(介護なし)の変換エラー: {repr(health_no_care)} (元データ列={raw_data}) -> {e}") + continue + + try: + health_with_care = Decimal(str(health_with_care)) + except Exception as e: + errors.append(f"行{row_idx}: 健保(介護あり)の変換エラー: {repr(health_with_care)} (元データ列={raw_data}) -> {e}") + continue + + try: + pension = Decimal(str(pension)) + except Exception as e: + errors.append(f"行{row_idx}: 厚生年金の変換エラー: {repr(pension)} (元データ列={raw_data}) -> {e}") + continue + + data_rows.append(( + rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, + health_no_care, health_with_care, pension + )) + + except Exception as e: + errors.append(f"行{row_idx}: 予期しないエラー: {type(e).__name__}: {str(e)}") + continue + + if not data_rows: + error_msg = "有効なデータが見つかりませんでした" + if errors: + error_msg += "\n\nエラー詳細:\n" + "\n".join(errors[:15]) + raise HTTPException(status_code=400, detail=error_msg) + + # データベースに保存 + with get_connection() as conn: + with conn.cursor() as cur: + # 既存データを削除 + cur.execute( + "DELETE FROM standard_remuneration WHERE rate_year = %s AND prefecture = %s", + (rate_year, prefecture) + ) + + # 新しいデータを挿入 + insert_query = """ + INSERT INTO standard_remuneration + (rate_year, prefecture, grade, monthly_amount, salary_from, salary_to, + health_insurance_no_care, health_insurance_with_care, pension_insurance) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """ + + cur.executemany(insert_query, data_rows) + conn.commit() + imported_count = len(data_rows) + + return { + "message": "インポートが完了しました", + "year": rate_year, + "prefecture": prefecture, + "imported_count": imported_count, + "source": a1_value + } + + except ValueError as e: + raise HTTPException(status_code=400, detail="年度は数値で入力してください") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.delete("") +async def delete_standard_remuneration(year: int, prefecture: str): + """指定された年度・都道府県の標準報酬月額表を削除""" + try: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "DELETE FROM standard_remuneration WHERE rate_year = %s AND prefecture = %s", + (year, prefecture) + ) + deleted_count = cur.rowcount + conn.commit() + + return { + "message": "削除しました", + "year": year, + "prefecture": prefecture, + "deleted_count": deleted_count + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/routers/file_upload.py b/backend/app/routers/file_upload.py new file mode 100644 index 0000000..5dfa804 --- /dev/null +++ b/backend/app/routers/file_upload.py @@ -0,0 +1,173 @@ +""" +ファイルアップロード管理 +""" +from fastapi import APIRouter, UploadFile, File, HTTPException +from pathlib import Path +import shutil +import uuid +from datetime import datetime + +router = APIRouter(prefix="/files", tags=["File Upload"]) + +# アップロード先のベースディレクトリ +UPLOAD_BASE_DIR = Path("uploads") +UPLOAD_BASE_DIR.mkdir(exist_ok=True) + +# 許可されるファイルタイプ +ALLOWED_EXTENSIONS = { + 'image': {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}, + 'document': {'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt'}, +} + +def get_file_extension(filename: str) -> str: + """ファイル拡張子を取得""" + return Path(filename).suffix.lower() + +def is_allowed_file(filename: str, file_type: str = None) -> bool: + """許可されたファイルタイプか確認""" + ext = get_file_extension(filename) + if file_type == 'image': + return ext in ALLOWED_EXTENSIONS['image'] + elif file_type == 'document': + return ext in ALLOWED_EXTENSIONS['document'] + else: + # すべての許可された拡張子 + all_extensions = set() + for exts in ALLOWED_EXTENSIONS.values(): + all_extensions.update(exts) + return ext in all_extensions + +def generate_unique_filename(original_filename: str, prefix: str = "") -> str: + """一意のファイル名を生成""" + ext = get_file_extension(original_filename) + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + unique_id = str(uuid.uuid4())[:8] + + if prefix: + return f"{prefix}_{timestamp}_{unique_id}{ext}" + return f"{timestamp}_{unique_id}{ext}" + +@router.post("/upload/employee/{employee_code}") +async def upload_employee_file( + employee_code: str, + file: UploadFile = File(...), + file_type: str = "image" +): + """ + 従業員関連ファイルをアップロード + + Args: + employee_code: 従業員コード + file: アップロードするファイル + file_type: ファイルタイプ (image, document) + + Returns: + アップロードされたファイルのパス + """ + # ファイルタイプチェック + if not is_allowed_file(file.filename, file_type): + raise HTTPException( + status_code=400, + detail=f"許可されていないファイル形式です。{file_type}タイプの許可される拡張子: {ALLOWED_EXTENSIONS.get(file_type, 'なし')}" + ) + + # 保存先ディレクトリ作成 + employee_dir = UPLOAD_BASE_DIR / "employees" / employee_code + employee_dir.mkdir(parents=True, exist_ok=True) + + # 一意のファイル名生成 + unique_filename = generate_unique_filename(file.filename, employee_code) + file_path = employee_dir / unique_filename + + # ファイル保存 + try: + with file_path.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイル保存に失敗しました: {str(e)}") + + # 相対パスを返す(データベースに保存する値) + relative_path = f"/uploads/employees/{employee_code}/{unique_filename}" + + return { + "success": True, + "file_path": relative_path, + "filename": unique_filename, + "original_filename": file.filename, + "size": file_path.stat().st_size + } + +@router.post("/upload/general") +async def upload_general_file( + file: UploadFile = File(...), + category: str = "general" +): + """ + 一般ファイルをアップロード + + Args: + file: アップロードするファイル + category: カテゴリー (general, reports, temp など) + + Returns: + アップロードされたファイルのパス + """ + # ファイルタイプチェック + if not is_allowed_file(file.filename): + raise HTTPException( + status_code=400, + detail=f"許可されていないファイル形式です" + ) + + # 保存先ディレクトリ作成 + category_dir = UPLOAD_BASE_DIR / category + category_dir.mkdir(parents=True, exist_ok=True) + + # 一意のファイル名生成 + unique_filename = generate_unique_filename(file.filename) + file_path = category_dir / unique_filename + + # ファイル保存 + try: + with file_path.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイル保存に失敗しました: {str(e)}") + + # 相対パスを返す + relative_path = f"/uploads/{category}/{unique_filename}" + + return { + "success": True, + "file_path": relative_path, + "filename": unique_filename, + "original_filename": file.filename, + "size": file_path.stat().st_size + } + +@router.delete("/delete") +async def delete_file(file_path: str): + """ + ファイルを削除 + + Args: + file_path: 削除するファイルのパス (/uploads/... 形式) + """ + # セキュリティ: パストラバーサル攻撃を防ぐ + if ".." in file_path or file_path.startswith("/"): + file_path = file_path.lstrip("/") + + full_path = Path(file_path) + + # uploadsディレクトリ内のファイルのみ削除可能 + if not str(full_path).startswith("uploads"): + raise HTTPException(status_code=400, detail="無効なファイルパスです") + + if not full_path.exists(): + raise HTTPException(status_code=404, detail="ファイルが見つかりません") + + try: + full_path.unlink() + return {"success": True, "message": "ファイルを削除しました"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"ファイル削除に失敗しました: {str(e)}") diff --git a/backend/check_insurance_rates.py b/backend/check_insurance_rates.py new file mode 100644 index 0000000..c806ea5 --- /dev/null +++ b/backend/check_insurance_rates.py @@ -0,0 +1,25 @@ +import psycopg + +conn = psycopg.connect( + host='192.168.0.61', + port=55432, + dbname='njts_acct', + user='njts_app', + password='njts_app2025' +) + +cur = conn.cursor() +cur.execute(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'insurance_rates' + ORDER BY ordinal_position +""") + +rows = cur.fetchall() +print('insurance_ratesテーブルのカラム:') +for r in rows: + print(f' {r[0]}: {r[1]}') + +cur.close() +conn.close() diff --git a/backend/check_photo_data.py b/backend/check_photo_data.py new file mode 100644 index 0000000..499eadc --- /dev/null +++ b/backend/check_photo_data.py @@ -0,0 +1,33 @@ +import psycopg + +conn = psycopg.connect( + host='192.168.0.61', + port=55432, + dbname='njts_acct', + user='njts_app', + password='njts_app2025' +) + +cur = conn.cursor() +cur.execute(""" + SELECT employee_id, employee_code, name, + photo_url IS NOT NULL as has_photo, + CASE WHEN photo_url IS NOT NULL THEN LENGTH(photo_url) ELSE 0 END as photo_length, + CASE WHEN photo_url IS NOT NULL THEN SUBSTRING(photo_url, 1, 50) ELSE NULL END as photo_preview + FROM employees + WHERE employee_code IN ('20190001', '20190002') + ORDER BY employee_code +""") + +rows = cur.fetchall() +print('従業員の写真データ状況:') +print('-' * 80) +for r in rows: + print(f'ID: {r[0]}, Code: {r[1]}, Name: {r[2]}') + print(f' Has Photo: {r[3]}, Photo Length: {r[4]}') + if r[5]: + print(f' Photo Preview: {r[5]}...') + print() + +cur.close() +conn.close() diff --git a/backend/check_std_table.py b/backend/check_std_table.py new file mode 100644 index 0000000..76f94d5 --- /dev/null +++ b/backend/check_std_table.py @@ -0,0 +1,51 @@ +import sys +sys.path.insert(0, r'c:\workspace\njts-accounting-core\backend') +from app.core.database import get_connection + +try: + with get_connection() as conn: + with conn.cursor() as cur: + # テーブルの存在確認 + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'standard_remuneration' + ) + """) + exists = cur.fetchone()["exists"] + print(f"テーブル存在: {exists}") + + if exists: + # データ件数確認 + cur.execute("SELECT COUNT(*) as count FROM standard_remuneration") + count = cur.fetchone()["count"] + print(f"データ件数: {count}") + + # 年度リスト取得 + cur.execute("SELECT DISTINCT rate_year FROM standard_remuneration ORDER BY rate_year DESC") + years = cur.fetchall() + print(f"年度リスト: {years}") + else: + print("テーブルが存在しません。作成します...") + cur.execute(""" + CREATE TABLE IF NOT EXISTS standard_remuneration ( + id SERIAL PRIMARY KEY, + rate_year INTEGER NOT NULL, + prefecture VARCHAR(50) NOT NULL, + grade VARCHAR(20) NOT NULL, + monthly_amount DECIMAL(12,2) NOT NULL, + salary_from DECIMAL(12,2) NOT NULL, + salary_to DECIMAL(12,2), + health_insurance_no_care DECIMAL(12,2) NOT NULL, + health_insurance_with_care DECIMAL(12,2) NOT NULL, + pension_insurance DECIMAL(12,2) NOT NULL, + UNIQUE(rate_year, prefecture, grade) + ) + """) + conn.commit() + print("テーブルを作成しました") + +except Exception as e: + print(f"エラー: {e}") + import traceback + traceback.print_exc() diff --git a/backend/create_std_remuneration_table.py b/backend/create_std_remuneration_table.py new file mode 100644 index 0000000..79e55d6 --- /dev/null +++ b/backend/create_std_remuneration_table.py @@ -0,0 +1,53 @@ +import psycopg2 +import os +from dotenv import load_dotenv + +load_dotenv() + +DB_CONFIG = { + "host": os.getenv("DB_HOST", "192.168.0.61"), + "port": os.getenv("DB_PORT", "55432"), + "database": os.getenv("DB_NAME", "njts_acct"), + "user": os.getenv("DB_USER", "njts_app"), + "password": os.getenv("DB_PASSWORD", "njts_app2025"), +} + +def create_standard_remuneration_table(): + """標準報酬月額表テーブルを作成""" + conn = psycopg2.connect(**DB_CONFIG) + cur = conn.cursor() + + # テーブル作成 + sql = """ + CREATE TABLE IF NOT EXISTS standard_remuneration ( + id SERIAL PRIMARY KEY, + rate_year INTEGER NOT NULL, + prefecture VARCHAR(50) NOT NULL, + grade VARCHAR(20) NOT NULL, + monthly_amount DECIMAL(12, 2) NOT NULL, + salary_from DECIMAL(12, 2) NOT NULL, + salary_to DECIMAL(12, 2), + health_insurance_no_care DECIMAL(12, 2) NOT NULL, + health_insurance_with_care DECIMAL(12, 2) NOT NULL, + pension_insurance DECIMAL(12, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(rate_year, prefecture, grade) + ); + + -- インデックス作成 + CREATE INDEX IF NOT EXISTS idx_standard_remuneration_year_pref + ON standard_remuneration(rate_year, prefecture); + CREATE INDEX IF NOT EXISTS idx_standard_remuneration_salary + ON standard_remuneration(rate_year, prefecture, salary_from, salary_to); + """ + + cur.execute(sql) + conn.commit() + + print("✅ standard_remuneration テーブルを作成しました") + + cur.close() + conn.close() + +if __name__ == "__main__": + create_standard_remuneration_table() diff --git a/backend/create_std_table.py b/backend/create_std_table.py new file mode 100644 index 0000000..c390ba2 --- /dev/null +++ b/backend/create_std_table.py @@ -0,0 +1,58 @@ +import os +import sys + +# 環境変数を設定 +os.environ["DB_HOST"] = "192.168.0.61" +os.environ["DB_PORT"] = "55432" +os.environ["DB_NAME"] = "njts_acct" +os.environ["DB_USER"] = "njts_app" +os.environ["DB_PASSWORD"] = "njts_app2025" + +sys.path.insert(0, r'c:\workspace\njts-accounting-core\backend') +from app.core.database import get_connection + +try: + with get_connection() as conn: + with conn.cursor() as cur: + print("standard_remunerationテーブルを作成中...") + cur.execute(""" + CREATE TABLE IF NOT EXISTS standard_remuneration ( + id SERIAL PRIMARY KEY, + rate_year INTEGER NOT NULL, + prefecture VARCHAR(50) NOT NULL, + grade VARCHAR(20) NOT NULL, + monthly_amount DECIMAL(12,2) NOT NULL, + salary_from DECIMAL(12,2) NOT NULL, + salary_to DECIMAL(12,2), + health_insurance_no_care DECIMAL(12,2) NOT NULL, + health_insurance_with_care DECIMAL(12,2) NOT NULL, + pension_insurance DECIMAL(12,2) NOT NULL, + UNIQUE(rate_year, prefecture, grade) + ) + """) + + # インデックスを作成 + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_std_rem_year_pref + ON standard_remuneration(rate_year, prefecture) + """) + + conn.commit() + print("✓ テーブルとインデックスを作成しました") + + # テーブルの確認 + cur.execute(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'standard_remuneration' + ORDER BY ordinal_position + """) + columns = cur.fetchall() + print("\n作成されたカラム:") + for col in columns: + print(f" - {col['column_name']}: {col['data_type']}") + +except Exception as e: + print(f"エラー: {e}") + import traceback + traceback.print_exc() diff --git a/backend/debug_care_insurance.py b/backend/debug_care_insurance.py new file mode 100644 index 0000000..7966b8f --- /dev/null +++ b/backend/debug_care_insurance.py @@ -0,0 +1,128 @@ +""" +介護保険計算のデバッグスクリプト +""" +from datetime import date +from app.core.database import get_connection + +# 従業員20190001の情報を確認 +employee_code = "20190001" +payment_date = date(2025, 12, 31) + +print("=" * 60) +print(f"従業員コード: {employee_code}") +print(f"支給日: {payment_date}") +print("=" * 60) + +with get_connection() as conn: + with conn.cursor() as cur: + # 従業員情報を取得 + cur.execute( + """ + SELECT employee_id, employee_code, name, birth_date + FROM employees + WHERE employee_code = %s + """, + (employee_code,) + ) + employee = cur.fetchone() + + if not employee: + print("従業員が見つかりません") + exit(1) + + print(f"\n【従業員情報】") + print(f" ID: {employee['employee_id']}") + print(f" コード: {employee['employee_code']}") + print(f" 氏名: {employee['name']}") + print(f" 生年月日: {employee['birth_date']}") + + # 年齢を計算 + if employee['birth_date']: + birth_date = employee['birth_date'] + age = payment_date.year - birth_date.year + if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day): + age -= 1 + print(f" 年齢({payment_date}時点): {age}歳") + print(f" 介護保険対象: {'はい (40-64歳)' if 40 <= age < 65 else 'いいえ'}") + else: + print(f" 生年月日が未設定です") + age = 0 + + # 介護保険料率を確認(2025年) + print(f"\n【介護保険料率 - 2025年】") + cur.execute( + """ + SELECT * FROM insurance_rates + WHERE rate_type = '介護保険' + AND rate_year = 2025 + ORDER BY rate_id + """ + ) + rates_2025 = cur.fetchall() + + if rates_2025: + for rate in rates_2025: + print(f" ID: {rate['rate_id']}") + print(f" 計算対象: {rate['calculation_target']}") + print(f" 都道府県: {rate['prefecture']}") + print(f" 従業員負担率: {float(rate['employee_rate']) * 100:.3f}%") + print(f" 会社負担率: {float(rate['employer_rate']) * 100:.3f}%") + print(f" 年齢下限: {rate['care_insurance_age_threshold']}歳以上") + print(f" 備考: {rate['notes']}") + print() + else: + print(" ⚠️ 2025年の介護保険料率が見つかりません!") + + # 介護保険料率を確認(2026年) + print(f"【介護保険料率 - 2026年】") + cur.execute( + """ + SELECT * FROM insurance_rates + WHERE rate_type = '介護保険' + AND rate_year = 2026 + ORDER BY rate_id + """ + ) + rates_2026 = cur.fetchall() + + if rates_2026: + for rate in rates_2026: + print(f" ID: {rate['rate_id']}") + print(f" 計算対象: {rate['calculation_target']}") + print(f" 都道府県: {rate['prefecture']}") + print(f" 従業員負担率: {float(rate['employee_rate']) * 100:.3f}%") + print(f" 会社負担率: {float(rate['employer_rate']) * 100:.3f}%") + print(f" 年齢下限: {rate['care_insurance_age_threshold']}歳以上") + print() + else: + print(" 2026年の介護保険料率が見つかりません") + + # 2025年12月の給与計算データを確認 + print(f"\n【2025年12月の給与計算】") + cur.execute( + """ + SELECT * FROM monthly_payroll + WHERE employee_id = %s + AND payroll_year = 2025 + AND payroll_month = 12 + ORDER BY payroll_id DESC + LIMIT 1 + """, + (employee['employee_id'],) + ) + payroll = cur.fetchone() + + if payroll: + print(f" Payroll ID: {payroll['payroll_id']}") + print(f" 基本給: ¥{float(payroll['base_salary']):,.0f}") + print(f" 健康保険: ¥{float(payroll['health_insurance']):,.0f}") + print(f" 介護保険: ¥{float(payroll['care_insurance']):,.0f}") + print(f" 厚生年金: ¥{float(payroll['pension_insurance']):,.0f}") + print(f" 雇用保険: ¥{float(payroll['employment_insurance']):,.0f}") + print(f" 計算日: {payroll['created_at']}") + else: + print(" 給与計算データが見つかりません") + +print("\n" + "=" * 60) +print("診断完了") +print("=" * 60) diff --git a/backend/fix_photo_columns.py b/backend/fix_photo_columns.py new file mode 100644 index 0000000..8a134de --- /dev/null +++ b/backend/fix_photo_columns.py @@ -0,0 +1,42 @@ +import psycopg + +conn = psycopg.connect( + host='192.168.0.61', + port=55432, + dbname='njts_acct', + user='njts_app', + password='njts_app2025' +) + +cur = conn.cursor() + +# ALTER TABLE実行 +cur.execute(""" + ALTER TABLE employees + ALTER COLUMN photo_url TYPE TEXT, + ALTER COLUMN my_number_card_image_url TYPE TEXT, + ALTER COLUMN residence_card_image_url TYPE TEXT, + ALTER COLUMN health_insurance_card_image_url TYPE TEXT, + ALTER COLUMN other_id_image_url TYPE TEXT +""") +conn.commit() + +print('写真カラムのデータ型をTEXTに変更しました') + +# 確認 +cur.execute(""" + SELECT column_name, data_type, character_maximum_length + FROM information_schema.columns + WHERE table_name = 'employees' + AND column_name LIKE '%_url' + ORDER BY column_name +""") + +result = cur.fetchall() +print('\nカラム情報:') +for r in result: + length = r[2] if r[2] else '制限なし' + print(f' {r[0]}: {r[1]} ({length})') + +cur.close() +conn.close() diff --git a/backend/sql/add_employee_photos.sql b/backend/sql/add_employee_photos.sql new file mode 100644 index 0000000..7b385e3 --- /dev/null +++ b/backend/sql/add_employee_photos.sql @@ -0,0 +1,15 @@ +-- 従業員の写真・身分証明書画像フィールドを追加 +-- Add employee photo and ID document image fields + +ALTER TABLE employees +ADD COLUMN IF NOT EXISTS photo_url VARCHAR(500), +ADD COLUMN IF NOT EXISTS my_number_card_image_url VARCHAR(500), +ADD COLUMN IF NOT EXISTS residence_card_image_url VARCHAR(500), +ADD COLUMN IF NOT EXISTS health_insurance_card_image_url VARCHAR(500), +ADD COLUMN IF NOT EXISTS other_id_image_url VARCHAR(500); + +COMMENT ON COLUMN employees.photo_url IS '本人写真のURL'; +COMMENT ON COLUMN employees.my_number_card_image_url IS 'マイナンバーカード画像のURL'; +COMMENT ON COLUMN employees.residence_card_image_url IS '在留カード画像のURL'; +COMMENT ON COLUMN employees.health_insurance_card_image_url IS '健康保険証画像のURL'; +COMMENT ON COLUMN employees.other_id_image_url IS 'その他の身分証明書画像のURL'; diff --git a/backend/sql/add_my_number_field.sql b/backend/sql/add_my_number_field.sql new file mode 100644 index 0000000..13b4229 --- /dev/null +++ b/backend/sql/add_my_number_field.sql @@ -0,0 +1,12 @@ +-- マイナンバーカード番号フィールドを追加 +-- Add my_number field to employees table + +ALTER TABLE employees +ADD COLUMN IF NOT EXISTS my_number VARCHAR(12); + +COMMENT ON COLUMN employees.my_number IS 'マイナンバー(個人番号)12桁'; + +-- マイナンバーは機密情報なので、ユニーク制約を追加 +CREATE UNIQUE INDEX IF NOT EXISTS idx_employees_my_number +ON employees(my_number) +WHERE my_number IS NOT NULL; diff --git a/backend/sql/create_standard_remuneration.sql b/backend/sql/create_standard_remuneration.sql new file mode 100644 index 0000000..e3214bc --- /dev/null +++ b/backend/sql/create_standard_remuneration.sql @@ -0,0 +1,32 @@ +-- 標準報酬月額表テーブル +CREATE TABLE IF NOT EXISTS standard_remuneration ( + id SERIAL PRIMARY KEY, + rate_year INTEGER NOT NULL, + prefecture VARCHAR(50) NOT NULL, + grade VARCHAR(20) NOT NULL, + monthly_amount DECIMAL(12, 2) NOT NULL, + salary_from DECIMAL(12, 2) NOT NULL, + salary_to DECIMAL(12, 2), + health_insurance_no_care DECIMAL(12, 2) NOT NULL, + health_insurance_with_care DECIMAL(12, 2) NOT NULL, + pension_insurance DECIMAL(12, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(rate_year, prefecture, grade) +); + +-- インデックス作成 +CREATE INDEX IF NOT EXISTS idx_standard_remuneration_year_pref + ON standard_remuneration(rate_year, prefecture); +CREATE INDEX IF NOT EXISTS idx_standard_remuneration_salary + ON standard_remuneration(rate_year, prefecture, salary_from, salary_to); + +COMMENT ON TABLE standard_remuneration IS '健康保険・厚生年金保険の標準報酬月額表'; +COMMENT ON COLUMN standard_remuneration.rate_year IS '適用年度'; +COMMENT ON COLUMN standard_remuneration.prefecture IS '都道府県名'; +COMMENT ON COLUMN standard_remuneration.grade IS '等級'; +COMMENT ON COLUMN standard_remuneration.monthly_amount IS '標準報酬月額'; +COMMENT ON COLUMN standard_remuneration.salary_from IS '報酬月額(下限)'; +COMMENT ON COLUMN standard_remuneration.salary_to IS '報酬月額(上限、最高等級の場合はNULL)'; +COMMENT ON COLUMN standard_remuneration.health_insurance_no_care IS '健康保険料(介護保険なし)'; +COMMENT ON COLUMN standard_remuneration.health_insurance_with_care IS '健康保険料(介護保険あり)'; +COMMENT ON COLUMN standard_remuneration.pension_insurance IS '厚生年金保険料'; diff --git a/backend/sql/fix_photo_column_size.sql b/backend/sql/fix_photo_column_size.sql new file mode 100644 index 0000000..ef1e135 --- /dev/null +++ b/backend/sql/fix_photo_column_size.sql @@ -0,0 +1,15 @@ +-- 写真フィールドのデータ型をVARCHAR(500)からTEXTに変更 +-- Base64エンコードされた画像データは数万文字になるため + +ALTER TABLE employees + ALTER COLUMN photo_url TYPE TEXT, + ALTER COLUMN my_number_card_image_url TYPE TEXT, + ALTER COLUMN residence_card_image_url TYPE TEXT, + ALTER COLUMN health_insurance_card_image_url TYPE TEXT, + ALTER COLUMN other_id_image_url TYPE TEXT; + +-- 確認 +SELECT column_name, data_type, character_maximum_length +FROM information_schema.columns +WHERE table_name = 'employees' + AND column_name LIKE '%_url'; diff --git a/backend/sql/insert_2025_insurance_rates.sql b/backend/sql/insert_2025_insurance_rates.sql new file mode 100644 index 0000000..772f9fe --- /dev/null +++ b/backend/sql/insert_2025_insurance_rates.sql @@ -0,0 +1,34 @@ +-- 2025年度の保険料率を追加 + +-- 健康保険(2025年度 東京都) +INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES +(2025, '健康保険', '給与', '東京都', 0.04985, 0.04985, NULL, '健康保険料率 9.97% (労使折半) - 給与'), +(2025, '健康保険', '賞与', '東京都', 0.04985, 0.04985, NULL, '健康保険料率 9.97% (労使折半) - 賞与') +ON CONFLICT DO NOTHING; + +-- 介護保険(2025年度) +INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES +(2025, '介護保険', '給与', '東京都', 0.0091, 0.0091, 40, '介護保険料率 1.82% (労使折半、40歳以上対象) - 給与'), +(2025, '介護保険', '賞与', '東京都', 0.0091, 0.0091, 40, '介護保険料率 1.82% (労使折半、40歳以上対象) - 賞与') +ON CONFLICT DO NOTHING; + +-- 厚生年金(2025年度) +INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES +(2025, '厚生年金', '給与', '東京都', 0.09150, 0.09150, NULL, '厚生年金保険料率 18.3% (労使折半) - 給与'), +(2025, '厚生年金', '賞与', '東京都', 0.09150, 0.09150, NULL, '厚生年金保険料率 18.3% (労使折半) - 賞与') +ON CONFLICT DO NOTHING; + +-- 雇用保険(2025年度) +INSERT INTO insurance_rates (rate_year, rate_type, calculation_target, prefecture, employee_rate, employer_rate, care_insurance_age_threshold, notes) VALUES +(2025, '雇用保険', '給与', '東京都', 0.006, 0.009, NULL, '雇用保険料率 従業員0.6% 会社0.9% - 給与'), +(2025, '雇用保険', '賞与', '東京都', 0.006, 0.009, NULL, '雇用保険料率 従業員0.6% 会社0.9% - 賞与') +ON CONFLICT DO NOTHING; + +-- 確認 +SELECT rate_year, rate_type, calculation_target, + ROUND(employee_rate * 100, 3) as employee_pct, + ROUND(employer_rate * 100, 3) as employer_pct, + care_insurance_age_threshold +FROM insurance_rates +WHERE rate_year = 2025 +ORDER BY rate_type, calculation_target; diff --git a/backend/test_care_insurance.py b/backend/test_care_insurance.py new file mode 100644 index 0000000..c7c8172 --- /dev/null +++ b/backend/test_care_insurance.py @@ -0,0 +1,94 @@ +"""介護保険デバッグスクリプト""" +import psycopg +from datetime import date +from decimal import Decimal + +# 接続 +conn = psycopg.connect('host=192.168.0.61 port=55432 dbname=njts_acct user=njts_app password=njts_app2025') +cur = conn.cursor() + +# 従業員情報取得 +cur.execute("SELECT employee_id, employee_code, name, birth_date FROM employees WHERE employee_code = '20190001'") +emp = cur.fetchone() +print(f'従業員: ID={emp[0]}, コード={emp[1]}, 名前={emp[2]}, 生年月日={emp[3]}') + +# 年齢計算 +birth_date = emp[3] +payment_date = date(2025, 12, 25) +age = payment_date.year - birth_date.year +if (payment_date.month, payment_date.day) < (birth_date.month, birth_date.day): + age -= 1 +print(f'\n支給日: {payment_date}') +print(f'年齢: {age}歳') +print(f'介護保険対象: {40 <= age < 65}') + +# 2025年介護保険料率取得 +print('\n=== 介護保険料率検索 ===') +cur.execute(""" + SELECT rate_id, rate_year, rate_type, calculation_target, employee_rate, employer_rate, prefecture + FROM insurance_rates + WHERE rate_type = '介護保険' + AND rate_year = 2025 + AND calculation_target = '給与' + ORDER BY rate_id DESC + LIMIT 1 +""") +rate = cur.fetchone() +if rate: + print(f'料率ID: {rate[0]}') + print(f'年度: {rate[1]}') + print(f'種類: {rate[2]}') + print(f'計算対象: {rate[3]}') + print(f'従業員負担率: {rate[4]} ({float(rate[4])*100:.3f}%)') + print(f'会社負担率: {rate[5]} ({float(rate[5])*100:.3f}%)') + print(f'都道府県: {rate[6]}') +else: + print('料率が見つかりません!') + +# 給与設定取得 +print('\n=== 給与設定 ===') +cur.execute(""" + SELECT base_salary FROM salary_settings + WHERE employee_id = %s + AND valid_from <= %s + AND (valid_to IS NULL OR valid_to >= %s) + ORDER BY valid_from DESC + LIMIT 1 +""", (emp[0], payment_date, payment_date)) +salary = cur.fetchone() +if salary: + print(f'基本給: ¥{salary[0]:,}') +else: + print('給与設定が見つかりません!') + +# 計算 +if rate and salary: + print('\n=== 介護保険計算 ===') + base = Decimal(str(salary[0])) + rate_val = Decimal(str(rate[4])) # employee_rate + care_ins = (base * rate_val).quantize(Decimal('1')) + print(f'計算式: ¥{base:,} × {rate_val} = ¥{care_ins:,}') + +# 実際の給与データ確認 +print('\n=== 実際の給与データ ===') +cur.execute(""" + SELECT payroll_id, payment_date, base_salary, health_insurance, care_insurance, status + FROM monthly_payroll + WHERE employee_id = %s + AND EXTRACT(YEAR FROM payment_date) = 2025 + AND EXTRACT(MONTH FROM payment_date) = 12 + ORDER BY payroll_id DESC + LIMIT 1 +""", (emp[0],)) +payroll = cur.fetchone() +if payroll: + print(f'給与ID: {payroll[0]}') + print(f'支給日: {payroll[1]}') + print(f'基本給: ¥{payroll[2]:,}') + print(f'健康保険: ¥{payroll[3]:,}') + print(f'介護保険: ¥{payroll[4]:,}') + print(f'ステータス: {payroll[5]}') +else: + print('2025年12月の給与データが見つかりません!') + +conn.close() diff --git a/backend/uploads/employees/20190002/20190002_20260120_165205_10b99e35.png b/backend/uploads/employees/20190002/20190002_20260120_165205_10b99e35.png new file mode 100644 index 0000000000000000000000000000000000000000..c06b90144fb1c04854e03945233ab511fb536a08 GIT binary patch literal 9291 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 literal 0 HcmV?d00001 diff --git a/frontend/index.html b/frontend/index.html index 5b78c0b..2d1c0c3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,9 +10,9 @@ diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index 721fdd0..ff240d5 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -44,7 +44,7 @@

修正仕訳入力

- + diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index de765ee..e6ce19a 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -122,7 +122,7 @@ type="date" id="entryDate" min="1900-01-01" - max="2999-12-31" + max="2099-12-31" onchange="onEntryDateChanged()" /> @@ -264,10 +264,10 @@ type="date" id="searchFromDate" min="1900-01-01" - max="2999-12-31" + max="2099-12-31" /> ~ - + diff --git a/frontend/journal-list.html b/frontend/journal-list.html index de13208..b5ca176 100644 --- a/frontend/journal-list.html +++ b/frontend/journal-list.html @@ -27,8 +27,8 @@

仕訳一覧

- ~ - + ~ + diff --git a/frontend/journal.html b/frontend/journal.html index 2300707..82f93de 100644 --- a/frontend/journal.html +++ b/frontend/journal.html @@ -10,7 +10,7 @@

diff --git a/frontend/pages/cash.html b/frontend/pages/cash.html index deba512..fdbfa0a 100644 --- a/frontend/pages/cash.html +++ b/frontend/pages/cash.html @@ -78,9 +78,9 @@ - + 〜 - + diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html index 5bc8043..d0951fc 100644 --- a/frontend/payroll-calculation.html +++ b/frontend/payroll-calculation.html @@ -108,10 +108,22 @@ font-size: 1.2em; color: #007bff; } + .back-link { + display: inline-block; + margin-bottom: 20px; + color: #007bff; + text-decoration: none; + font-size: 14px; + } + .back-link:hover { + text-decoration: underline; + }
+ ← 給与管理メニューに戻る +

給与管理 - 月次給与計算

@@ -188,7 +200,13 @@
- +
@@ -292,7 +310,7 @@ diff --git a/frontend/payroll-salary-settings.html b/frontend/payroll-salary-settings.html index 8305d7f..631f10a 100644 --- a/frontend/payroll-salary-settings.html +++ b/frontend/payroll-salary-settings.html @@ -56,311 +56,374 @@ table th { background: #f0f0f0; } + .back-link { + display: inline-block; + margin-bottom: 20px; + color: #007bff; + text-decoration: none; + font-size: 14px; + } + .back-link:hover { + text-decoration: underline; + }
+ ← 給与管理メニューに戻る +

給与管理 - 給与設定

-
- - -
+ -