diff --git a/analyze_accountant_diff.py b/analyze_accountant_diff.py new file mode 100644 index 0000000..d5f568a --- /dev/null +++ b/analyze_accountant_diff.py @@ -0,0 +1,185 @@ +""" +12月2025年の仕訳データを詳しく確認し、売上高の差異原因を特定する +""" +import psycopg2 + +conn = psycopg2.connect( + host='192.168.0.61', + port=55432, + dbname='njts_acct', + user='njts_app', + password='njts_app2025' +) +cur = conn.cursor() + +print("=" * 80) +print("【分析1】月別売上高の集計 (2025年6月〜12月)") +print("=" * 80) + +cur.execute(""" + SELECT + DATE_TRUNC('month', je.entry_date)::date AS month, + COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) AS revenue + FROM journal_entries je + JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE je.is_latest = true + AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' + AND je.entry_date <= '2025-12-31' + AND a.account_code = '701' + GROUP BY DATE_TRUNC('month', je.entry_date) + ORDER BY month +""") +rows = cur.fetchall() +total_sys = 0 +for row in rows: + print(f" {row[0].strftime('%Y年%m月')}: {row[1]:>12,.0f} 円") + total_sys += row[1] +print(f" {'合計':>10}: {total_sys:>12,.0f} 円") +print(f"\n 税理士の売上高合計: 28,896,368 円") +print(f" 差額: {total_sys - 28896368:>12,.0f} 円") + +print() +print("=" * 80) +print("【分析2】2025年12月の全仕訳明細") +print("=" * 80) + +cur.execute(""" + SELECT + je.entry_date, + je.journal_entry_id, + je.description, + a.account_code, + a.account_name, + jl.debit, + jl.credit + FROM journal_entries je + JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE je.is_latest = true + AND je.is_deleted = false + AND je.entry_date >= '2025-12-01' + AND je.entry_date <= '2025-12-31' + ORDER BY je.entry_date, je.journal_entry_id, jl.journal_line_id +""") +rows = cur.fetchall() + +if rows: + print(f"{'日付':<12} {'ID':<6} {'摘要':<30} {'科目CD':<8} {'科目名':<15} {'借方':>12} {'貸方':>12}") + print("-" * 100) + for row in rows: + print(f"{str(row[0]):<12} {row[1]:<6} {str(row[2])[:28]:<30} {str(row[3]):<8} {str(row[4]):<15} {row[5] if row[5] else 0:>12,.0f} {row[6] if row[6] else 0:>12,.0f}") +else: + print("2025年12月の仕訳データが見つかりません!") + +print() +print("=" * 80) +print("【分析3】売上高差異の内訳 (3,432,724円の差異原因)") +print("=" * 80) + +# Monthly revenue breakdown +print("\n月別売上高(システム vs 税理士推定):") +print(" ※税理士の月別内訳は不明なため、12月分の存在確認のみ実施") + +print() +print("=" * 80) +print("【分析4】2025年12月の売上関連仕訳のみ") +print("=" * 80) + +cur.execute(""" + SELECT + je.entry_date, + je.journal_entry_id, + je.description, + a.account_code, + a.account_name, + jl.debit, + jl.credit + FROM journal_entries je + JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE je.is_latest = true + AND je.is_deleted = false + AND je.entry_date >= '2025-12-01' + AND je.entry_date <= '2025-12-31' + AND a.account_code IN ('701', '702', '201') -- 売上高、受取利息、売掛金 + ORDER BY je.entry_date, je.journal_entry_id +""") +rows = cur.fetchall() +dec_revenue = 0 +if rows: + print(f"{'日付':<12} {'ID':<6} {'摘要':<30} {'科目名':<15} {'借方':>12} {'貸方':>12}") + print("-" * 90) + for row in rows: + credit = row[6] if row[6] else 0 + debit = row[5] if row[5] else 0 + if row[4] == '売上高': + dec_revenue += credit - debit + print(f"{str(row[0]):<12} {row[1]:<6} {str(row[2])[:28]:<30} {str(row[4]):<15} {debit:>12,.0f} {credit:>12,.0f}") + print(f"\n 2025年12月の売上高: {dec_revenue:>12,.0f} 円") +else: + print("2025年12月の売上関連仕訳なし") + +print() +print("=" * 80) +print("【分析5】費用項目の差異詳細") +print("=" * 80) + +system_data = { + "役員報酬": 2520000, + "給料手当": 7520000, + "法定福利費": 1439958, + "福利厚生費": 243760, + "外注費": 7490000, + "交際費": 806883, + "会議費": 440642, + "旅費交通費": 131763, + "通信費": 32063, + "消耗品費": 1061190, + "地代家賃": 1128184, + "支払手数料": 19470, + "支払報酬料": 165000, + "保険料": 33950, + "租税公課": -161590, +} + +accountant_data = { + "役員報酬": 2520000, + "給料手当": 2700000, + "賞与": 4300000, + "法定福利費": 1588604, + "福利厚生費": 242978, + "外注費": 7490000, + "交際費": 915007, + "会議費": 219070, + "旅費交通費": 522149, + "通信費": 31459, + "消耗品費": 1027007, + "修繕費": 986, + "新聞図書費": 9900, + "支払手数料": 168800, + "地代家賃": 1081822, + "保険料": 33950, + "租税公課": 33100, + "法人税住民税事業税": 747, +} + +all_keys = sorted(set(list(system_data.keys()) + list(accountant_data.keys()))) +print(f"{'費用科目':<15} {'税理士':>12} {'システム':>12} {'差異(+は自社多)':>15} 備考") +print("-" * 65) +total_diff = 0 +for k in all_keys: + acc = accountant_data.get(k, 0) + sys = system_data.get(k, 0) + diff = sys - acc + total_diff += diff + note = "" + if diff != 0: + note = "← 要確認" if abs(diff) > 100000 else "" + print(f"{k:<15} {acc:>12,.0f} {sys:>12,.0f} {diff:>+15,.0f} {note}") + +print(f"{'合計':<15} {sum(accountant_data.values()):>12,.0f} {sum(system_data.values()):>12,.0f} {total_diff:>+15,.0f}") +print(f"\n※費用差異: システムの費用が {total_diff:+,.0f} 円 (負=税理士の費用が多い)") + +conn.close() diff --git a/backend/app/payroll/bonus/router.py b/backend/app/payroll/bonus/router.py index 5a7237d..3790797 100644 --- a/backend/app/payroll/bonus/router.py +++ b/backend/app/payroll/bonus/router.py @@ -131,6 +131,40 @@ def approve_bonus(bonus_id: int, approved_by: str): return result +@router.put("/{bonus_id}", response_model=schemas.BonusPayment) +def update_bonus(bonus_id: int, request: schemas.BonusUpdateRequest): + """賞与の支給項目を更新(再計算は別途実行)""" + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT status FROM bonus_payments WHERE bonus_id = %s", (bonus_id,)) + row = cur.fetchone() + if not row: + raise HTTPException(status_code=404, detail="賞与が見つかりません") + if row["status"] == "approved": + raise HTTPException(status_code=400, detail="承認済みの賞与は編集できません") + + cur.execute( + """ + UPDATE bonus_payments SET + base_bonus = %s, performance_bonus = %s, other_bonus = %s, + other_deduction = %s, + payment_date = COALESCE(%s, payment_date), + bonus_type = COALESCE(%s, bonus_type) + WHERE bonus_id = %s + RETURNING * + """, + ( + request.base_bonus, request.performance_bonus, request.other_bonus, + request.other_deduction, + request.payment_date, request.bonus_type, + bonus_id + ) + ) + result = cur.fetchone() + conn.commit() + return result + + @router.post("/{bonus_id}/recalculate", response_model=schemas.BonusPayment) def recalculate_bonus(bonus_id: int): """賞与を再計算""" @@ -170,7 +204,8 @@ def recalculate_bonus(bonus_id: int): base_bonus = %s, performance_bonus = %s, other_bonus = %s, total_bonus = %s, health_insurance = %s, care_insurance = %s, pension_insurance = %s, - employment_insurance = %s, income_tax = %s, other_deduction = %s, + employment_insurance = %s, child_support = %s, + income_tax = %s, other_deduction = %s, total_deduction = %s, net_bonus = %s, calculated_at = %s, calculated_by = %s WHERE bonus_id = %s @@ -181,6 +216,7 @@ def recalculate_bonus(bonus_id: int): calc_result["other_bonus"], calc_result["total_bonus"], calc_result["health_insurance"], calc_result["care_insurance"], calc_result["pension_insurance"], calc_result["employment_insurance"], + calc_result["child_support"], calc_result["income_tax"], calc_result["other_deduction"], calc_result["total_deduction"], calc_result["net_bonus"], datetime.now(), "recalculated", diff --git a/backend/app/payroll/bonus/schemas.py b/backend/app/payroll/bonus/schemas.py index ff7e4b8..14d2c92 100644 --- a/backend/app/payroll/bonus/schemas.py +++ b/backend/app/payroll/bonus/schemas.py @@ -38,6 +38,16 @@ class BonusCalculationRequest(BaseModel): calculated_by: str = "system" +class BonusUpdateRequest(BaseModel): + """賞与支給項目の更新リクエスト""" + base_bonus: Decimal + performance_bonus: Decimal = Decimal("0") + other_bonus: Decimal = Decimal("0") + other_deduction: Decimal = Decimal("0") + payment_date: Optional[date] = None + bonus_type: Optional[str] = None + + class BonusPayment(BonusPaymentBase): """賞与計算結果""" bonus_id: int @@ -47,6 +57,7 @@ class BonusPayment(BonusPaymentBase): care_insurance: Decimal pension_insurance: Decimal employment_insurance: Decimal + child_support: Decimal = Decimal("0") income_tax: Decimal other_deduction: Decimal total_deduction: Decimal diff --git a/backend/app/payroll/bonus/service.py b/backend/app/payroll/bonus/service.py index 93eaa0a..5228368 100644 --- a/backend/app/payroll/bonus/service.py +++ b/backend/app/payroll/bonus/service.py @@ -89,27 +89,27 @@ class BonusCalculationService: @staticmethod def get_previous_salary_avg(employee_id: int, payment_date: date) -> Decimal: - """前月までの3ヶ月平均給与を取得(賞与所得税計算用)""" + """前月の社会保険料等控除後の給与を取得(賞与所得税計算用) + + 税法上は「前月分の社会保険料等控除後の給与」= 直前1ヶ月分のみを使用する。 + ステータス問わず最新の月次給与レコードを参照する。 + """ with get_connection() as conn: with conn.cursor() as cur: - # 前月までの3ヶ月の社会保険料等控除後の給与を取得 + # 賞与支払日より前の直近1ヶ月分の給与を取得(ステータス問わず) cur.execute( """ - SELECT AVG(total_payment - commute_allowance - health_insurance - care_insurance - pension_insurance - employment_insurance - COALESCE(child_support, 0)) as avg_salary - FROM ( - SELECT total_payment, commute_allowance, health_insurance, care_insurance, pension_insurance, employment_insurance, child_support - FROM monthly_payroll - WHERE employee_id = %s - AND payment_date < %s - AND status IN ('approved', 'paid') - ORDER BY payment_date DESC - LIMIT 3 - ) as recent_payroll + SELECT total_payment - commute_allowance - health_insurance - care_insurance - pension_insurance - employment_insurance - COALESCE(child_support, 0) as net_salary + FROM monthly_payroll + WHERE employee_id = %s + AND payment_date < %s + ORDER BY payment_date DESC + LIMIT 1 """, (employee_id, payment_date) ) result = cur.fetchone() - return Decimal(str(result["avg_salary"])) if result and result["avg_salary"] else Decimal("0") + return Decimal(str(result["net_salary"])) if result and result["net_salary"] else Decimal("0") @staticmethod def calculate_bonus_income_tax( @@ -230,11 +230,11 @@ class BonusCalculationService: total_bonus * Decimal(str(employment_insurance_rate["employee_rate"])) ).quantize(Decimal("1"), rounding=ROUND_HALF_UP) - # 子ども・子育て支援金(令和8年4月分以降のみ適用) - # 賞与の場合: 社会保険料率設定(insurance_rates)の料率 × 賞与総額で計算 + # 子ども・子育て支援金(令和8年5月支給分以降のみ適用) + # 賞与は bonus_month でなく実際の支給日(payment_date)で判定する child_support = Decimal("0") is_child_support_applicable = ( - (bonus_year == 2026 and bonus_month >= 5) or bonus_year >= 2027 + (payment_date.year == 2026 and payment_date.month >= 5) or payment_date.year >= 2027 ) if is_child_support_applicable: child_support_rate = BonusCalculationService.get_insurance_rate("子ども・子育て支援金", payment_date) diff --git a/backend/app/payroll/vouchers/schemas.py b/backend/app/payroll/vouchers/schemas.py index 1567af4..185693f 100644 --- a/backend/app/payroll/vouchers/schemas.py +++ b/backend/app/payroll/vouchers/schemas.py @@ -93,6 +93,7 @@ class BonusVoucherData(BaseModel): bonus_year: int bonus_month: int bonus_type: str = "" # 夏季賞与/冬季賞与/決算賞与/その他 + payment_date: Optional[str] = None # 実際の支給日 # 支給項目 base_bonus: Decimal = Decimal(0) # 基本賞与額 performance_bonus: Decimal = Decimal(0) # 業績賞与 @@ -103,6 +104,7 @@ class BonusVoucherData(BaseModel): care_insurance: Decimal = Decimal(0) pension_insurance: Decimal = Decimal(0) employment_insurance: Decimal = Decimal(0) + child_support: Decimal = Decimal(0) # 子ども・子育て支援金 income_tax: Decimal = Decimal(0) # 賞与所得税 other_deduction: Decimal = Decimal(0) total_deduction: Decimal = Decimal(0) diff --git a/backend/app/payroll/vouchers/service.py b/backend/app/payroll/vouchers/service.py index cb04d24..1312f36 100644 --- a/backend/app/payroll/vouchers/service.py +++ b/backend/app/payroll/vouchers/service.py @@ -200,9 +200,10 @@ def get_bonus_data_for_period( cur.execute(""" SELECT e.employee_id, e.employee_code, e.name, - bp.bonus_year, bp.bonus_month, bp.bonus_type, + bp.bonus_year, bp.bonus_month, bp.bonus_type, bp.payment_date, bp.base_bonus, bp.performance_bonus, bp.other_bonus, bp.total_bonus, bp.health_insurance, bp.care_insurance, bp.pension_insurance, bp.employment_insurance, + COALESCE(bp.child_support, 0) as child_support, bp.income_tax, bp.other_deduction, bp.total_deduction, bp.net_bonus FROM employees e @@ -226,6 +227,7 @@ def get_bonus_data_for_period( bonus_year=row.get("bonus_year"), bonus_month=row.get("bonus_month"), bonus_type=row.get("bonus_type", ""), + payment_date=str(row.get("payment_date")) if row.get("payment_date") else None, # 支給項目 base_bonus=Decimal(str(row.get("base_bonus", 0))) if row.get("base_bonus") else Decimal(0), performance_bonus=Decimal(str(row.get("performance_bonus", 0))) if row.get("performance_bonus") else Decimal(0), @@ -236,6 +238,7 @@ def get_bonus_data_for_period( care_insurance=Decimal(str(row.get("care_insurance", 0))) if row.get("care_insurance") else Decimal(0), pension_insurance=Decimal(str(row.get("pension_insurance", 0))) if row.get("pension_insurance") else Decimal(0), employment_insurance=Decimal(str(row.get("employment_insurance", 0))) if row.get("employment_insurance") else Decimal(0), + child_support=Decimal(str(row.get("child_support", 0))) if row.get("child_support") else Decimal(0), income_tax=Decimal(str(row.get("income_tax", 0))) if row.get("income_tax") else Decimal(0), other_deduction=Decimal(str(row.get("other_deduction", 0))) if row.get("other_deduction") else Decimal(0), total_deduction=Decimal(str(row.get("total_deduction", 0))) if row.get("total_deduction") else Decimal(0), @@ -254,23 +257,25 @@ def get_bonus_data_for_period( bonus_year=row[3], bonus_month=row[4], bonus_type=row[5] if row[5] else "", + payment_date=str(row[6]) if row[6] else None, # 支給項目 - base_bonus=Decimal(str(row[6])) if row[6] else Decimal(0), - performance_bonus=Decimal(str(row[7])) if row[7] else Decimal(0), - other_bonus=Decimal(str(row[8])) if row[8] else Decimal(0), - total_bonus=Decimal(str(row[9])) if row[9] else Decimal(0), + base_bonus=Decimal(str(row[7])) if row[7] else Decimal(0), + performance_bonus=Decimal(str(row[8])) if row[8] else Decimal(0), + other_bonus=Decimal(str(row[9])) if row[9] else Decimal(0), + total_bonus=Decimal(str(row[10])) if row[10] else Decimal(0), # 控除項目 - health_insurance=Decimal(str(row[10])) if row[10] else Decimal(0), - care_insurance=Decimal(str(row[11])) if row[11] else Decimal(0), - pension_insurance=Decimal(str(row[12])) if row[12] else Decimal(0), - employment_insurance=Decimal(str(row[13])) if row[13] else Decimal(0), - income_tax=Decimal(str(row[14])) if row[14] else Decimal(0), - other_deduction=Decimal(str(row[15])) if row[15] else Decimal(0), - total_deduction=Decimal(str(row[16])) if row[16] else Decimal(0), - net_bonus=Decimal(str(row[17])) if row[17] else Decimal(0), + health_insurance=Decimal(str(row[11])) if row[11] else Decimal(0), + care_insurance=Decimal(str(row[12])) if row[12] else Decimal(0), + pension_insurance=Decimal(str(row[13])) if row[13] else Decimal(0), + employment_insurance=Decimal(str(row[14])) if row[14] else Decimal(0), + child_support=Decimal(str(row[15])) if row[15] else Decimal(0), + income_tax=Decimal(str(row[16])) if row[16] else Decimal(0), + other_deduction=Decimal(str(row[17])) if row[17] else Decimal(0), + total_deduction=Decimal(str(row[18])) if row[18] else Decimal(0), + net_bonus=Decimal(str(row[19])) if row[19] else Decimal(0), # レガシー互換性用 - bonus_amount=Decimal(str(row[9])) if row[9] else Decimal(0), - tax_amount=Decimal(str(row[16])) if row[16] else Decimal(0) + bonus_amount=Decimal(str(row[10])) if row[10] else Decimal(0), + tax_amount=Decimal(str(row[18])) if row[18] else Decimal(0) ) data.append(bonus_obj) return data diff --git a/check_api_821.py b/check_api_821.py new file mode 100644 index 0000000..666a878 --- /dev/null +++ b/check_api_821.py @@ -0,0 +1,14 @@ +import urllib.request, json +url = 'http://192.168.0.61:18000/trial-balance?date_from=2025-06-01&date_to=2025-12-31' +with urllib.request.urlopen(url, timeout=5) as r: + data = json.loads(r.read()) +accounts = data['accounts'] +print('全科目コード:', [a.get('account_code') for a in accounts]) +print() +for a in accounts: + code = a.get('account_code', '') + if code and code >= '800': + name = a.get('account_name', '') + debit = a.get('debit', 0) or 0 + credit = a.get('credit', 0) or 0 + print(f' {code} {name:<15} debit:{debit:>10,.0f} credit:{credit:>10,.0f}') diff --git a/check_bonus_table.py b/check_bonus_table.py new file mode 100644 index 0000000..ed62ad6 --- /dev/null +++ b/check_bonus_table.py @@ -0,0 +1,20 @@ +import psycopg2 +conn = psycopg2.connect(host='192.168.0.61', port=55432, dbname='njts_acct', user='njts_app', password='njts_app2025') +cur = conn.cursor() + +# bonusテーブル名確認 +cur.execute("SELECT table_name FROM information_schema.tables WHERE table_name LIKE '%bonus%' OR table_name LIKE '%employee%'") +print("tables:", cur.fetchall()) + +# 王珏の賞与データを確認 +cur.execute(""" + SELECT b.bonus_id, b.bonus_year, b.bonus_month, b.bonus_type, b.payment_date, b.child_support, b.total_bonus + FROM bonus_payments b + JOIN employees e ON b.employee_id = e.employee_id + WHERE e.employee_code = '20190001' + ORDER BY b.bonus_year DESC, b.bonus_month DESC LIMIT 5 +""") +print("bonus data:", cur.fetchall()) + +cur.close() +conn.close() diff --git a/check_child_support_rate.py b/check_child_support_rate.py new file mode 100644 index 0000000..49bda4d --- /dev/null +++ b/check_child_support_rate.py @@ -0,0 +1,31 @@ +import psycopg2 +conn = psycopg2.connect(host='192.168.0.61', port=55432, dbname='njts_acct', user='njts_app', password='njts_app2025') +cur = conn.cursor() + +# 子ども・子育て支援金の保険料率を確認 +cur.execute(""" + SELECT * FROM insurance_rates + WHERE rate_type = '子ども・子育て支援金' + ORDER BY rate_year, calculation_target +""") +rows = cur.fetchall() +print("子ども・子育て支援金 rates:") +for r in rows: + print(r) + +print() + +# 王珏の賞与再確認(再計算後) +cur.execute(""" + SELECT b.bonus_id, b.bonus_year, b.bonus_month, b.bonus_type, b.payment_date, b.child_support, b.total_bonus + FROM bonus_payments b + JOIN employees e ON b.employee_id = e.employee_id + WHERE e.employee_code = '20190001' + ORDER BY b.bonus_year DESC, b.bonus_month DESC LIMIT 5 +""") +print("王珏 bonus:") +for r in cur.fetchall(): + print(r) + +cur.close() +conn.close() diff --git a/check_jan2026_bank.py b/check_jan2026_bank.py new file mode 100644 index 0000000..3d4a750 --- /dev/null +++ b/check_jan2026_bank.py @@ -0,0 +1,82 @@ +import psycopg2 +from decimal import Decimal + +conn = psycopg2.connect(host='192.168.0.61', port=55432, dbname='njts_acct', user='njts_app', password='njts_app2025') +cur = conn.cursor() + +# 普通預金のaccount_idを取得 +cur.execute("SELECT account_id, account_code, account_name FROM accounts WHERE account_code = '102'") +acc = cur.fetchone() +print(f"科目: {acc}") +account_id = acc[0] + +# 2026年1月の仕訳明細を全件取得 +cur.execute(""" + SELECT + jl.line_id, + je.journal_entry_id, + je.entry_date, + je.description, + jl.debit, + jl.credit, + je.is_latest, + je.is_deleted + FROM journal_lines jl + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = %s + AND je.entry_date >= '2026-01-01' + AND je.entry_date <= '2026-01-31' + AND je.is_deleted = false + ORDER BY je.entry_date, je.journal_entry_id +""", (account_id,)) + +rows = cur.fetchall() +print(f"\n全件(is_latest無視): {len(rows)}件") +print("-" * 90) +debit_all = Decimal(0) +credit_all = Decimal(0) +for r in rows: + jline_id, entry_id, date, desc, debit, credit, is_latest, is_deleted = r + d = Decimal(str(debit or 0)) + c = Decimal(str(credit or 0)) + debit_all += d + credit_all += c + print(f"entry_id={entry_id} date={date} is_latest={is_latest} debit={d:>12} credit={c:>12} | {desc[:30]}") + +print(f"\n合計 (全件): debit={debit_all:>12} credit={credit_all:>12}") + +# is_latest=trueのみ +cur.execute(""" + SELECT + jl.journal_line_id, + je.journal_entry_id, + je.entry_date, + je.description, + jl.debit, + jl.credit + FROM journal_lines jl + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = %s + AND je.entry_date >= '2026-01-01' + AND je.entry_date <= '2026-01-31' + AND je.is_deleted = false + AND je.is_latest = true + ORDER BY je.entry_date, je.journal_entry_id +""", (account_id,)) + +rows2 = cur.fetchall() +print(f"\nis_latest=true のみ: {len(rows2)}件") +print("-" * 90) +debit_latest = Decimal(0) +credit_latest = Decimal(0) +for r in rows2: + line_id, entry_id, date, desc, debit, credit = r + d = Decimal(str(debit or 0)) + c = Decimal(str(credit or 0)) + debit_latest += d + credit_latest += c + print(f"entry_id={entry_id} date={date} debit={d:>12} credit={c:>12} | {desc[:40]}") + +print(f"\n合計 (is_latest=true): debit={debit_latest:>12} credit={credit_latest:>12}") + +conn.close() diff --git a/check_jan2026_bank2.py b/check_jan2026_bank2.py new file mode 100644 index 0000000..75ba67f --- /dev/null +++ b/check_jan2026_bank2.py @@ -0,0 +1,70 @@ +import psycopg2 +from decimal import Decimal + +conn = psycopg2.connect(host='192.168.0.61', port=55432, dbname='njts_acct', user='njts_app', password='njts_app2025') +cur = conn.cursor() + +# 普通預金のaccount_id +cur.execute("SELECT account_id, account_code, account_name FROM accounts WHERE account_code = '102'") +acc = cur.fetchone() +print(f"科目: {acc}") +account_id = acc[0] + +# ===== 全件(is_latest無視)===== +cur.execute(""" + SELECT je.journal_entry_id, je.entry_date, je.description, + jl.debit, jl.credit, je.is_latest, je.is_deleted + FROM journal_lines jl + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = %s + AND je.entry_date >= '2026-01-01' + AND je.entry_date <= '2026-01-31' + AND je.is_deleted = false + ORDER BY je.entry_date, je.journal_entry_id +""", (account_id,)) + +rows = cur.fetchall() +print(f"\n全件(is_deleted=false): {len(rows)}件") +print("-" * 100) +debit_all = Decimal(0) +credit_all = Decimal(0) +for r in rows: + entry_id, date, desc, debit, credit, is_latest, is_deleted = r + d = Decimal(str(debit or 0)) + c = Decimal(str(credit or 0)) + debit_all += d + credit_all += c + print(f"entry_id={entry_id:5} date={date} is_latest={str(is_latest):5} debit={d:>12} credit={c:>12} | {(desc or '')[:40]}") + +print(f"\n合計 (全件): debit={debit_all:>12} credit={credit_all:>12}") + +# ===== is_latest=true のみ ===== +cur.execute(""" + SELECT je.journal_entry_id, je.entry_date, je.description, + jl.debit, jl.credit + FROM journal_lines jl + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = %s + AND je.entry_date >= '2026-01-01' + AND je.entry_date <= '2026-01-31' + AND je.is_deleted = false + AND je.is_latest = true + ORDER BY je.entry_date, je.journal_entry_id +""", (account_id,)) + +rows2 = cur.fetchall() +print(f"\nis_latest=true のみ: {len(rows2)}件") +print("-" * 100) +debit_latest = Decimal(0) +credit_latest = Decimal(0) +for r in rows2: + entry_id, date, desc, debit, credit = r + d = Decimal(str(debit or 0)) + c = Decimal(str(credit or 0)) + debit_latest += d + credit_latest += c + print(f"entry_id={entry_id:5} date={date} debit={d:>12} credit={c:>12} | {(desc or '')[:40]}") + +print(f"\n合計 (is_latest=true): debit={debit_latest:>12} credit={credit_latest:>12}") + +conn.close() diff --git a/check_jl_cols.py b/check_jl_cols.py new file mode 100644 index 0000000..0402330 --- /dev/null +++ b/check_jl_cols.py @@ -0,0 +1,6 @@ +import psycopg2 +conn = psycopg2.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 FROM information_schema.columns WHERE table_name='journal_lines' ORDER BY ordinal_position") +for r in cur.fetchall(): print(r[0]) +conn.close() diff --git a/check_salary_bonus.py b/check_salary_bonus.py new file mode 100644 index 0000000..60aa4ca --- /dev/null +++ b/check_salary_bonus.py @@ -0,0 +1,65 @@ +import psycopg2 +conn = psycopg2.connect(host='192.168.0.61', port=55432, dbname='njts_acct', user='njts_app', password='njts_app2025') +cur = conn.cursor() + +cur.execute(""" + SELECT a.account_code, a.account_name, + COALESCE(SUM(jl.debit),0) AS debit, + COALESCE(SUM(jl.credit),0) AS credit, + COALESCE(SUM(jl.debit),0) - COALESCE(SUM(jl.credit),0) AS balance + FROM accounts a + JOIN journal_lines jl ON a.account_id = jl.account_id + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE je.is_latest = true AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' AND je.entry_date <= '2025-12-31' + AND a.account_type = 'expense' + GROUP BY a.account_id, a.account_code, a.account_name + HAVING COALESCE(SUM(jl.debit),0) + COALESCE(SUM(jl.credit),0) > 0 + ORDER BY a.account_code +""") +rows = cur.fetchall() +print("=== 費用科目 残高試算表(2025年6月〜12月)===") +print(f"{'科目CD':<6} {'科目名':<15} {'期間借方':>12} {'期間貸方':>12} {'残高':>12}") +print("-" * 62) +total_bal = 0 +for r in rows: + print(f"{r[0]:<6} {r[1]:<15} {r[2]:>12,.0f} {r[3]:>12,.0f} {r[4]:>12,.0f}") + total_bal += r[4] +print("-" * 62) +print(f"{'費用合計':<22} {total_bal:>12,.0f}") + +cur.execute(""" + SELECT COALESCE(SUM(jl.credit),0) - COALESCE(SUM(jl.debit),0) + FROM journal_lines jl + JOIN accounts a ON jl.account_id = a.account_id + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE je.is_latest = true AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' AND je.entry_date <= '2025-12-31' + AND a.account_type = 'revenue' +""") +total_rev = cur.fetchone()[0] +print() +print(f"収益合計 : {total_rev:>12,.0f}") +print(f"当期純損益 : {total_rev - total_bal:>12,.0f}") +conn.close() + + + +cur.execute(""" + SELECT je.entry_date, je.description, jl.debit, jl.credit, jl.line_description + FROM journal_entries je + JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE je.is_latest = true AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' AND je.entry_date <= '2025-12-31' + AND a.account_code = '813' + ORDER BY je.entry_date +""") +rows = cur.fetchall() +print() +print('=== 813給料手当 の仕訳明細 ===') +for r in rows: + desc = str(r[4]) if r[4] else '' + print(f' {r[0]} {str(r[1])[:45]:<45} 借:{r[2] or 0:>10,.0f} 貸:{r[3] or 0:>10,.0f} {desc}') + +conn.close() diff --git a/compare_with_accountant.py b/compare_with_accountant.py new file mode 100644 index 0000000..3dfccb9 --- /dev/null +++ b/compare_with_accountant.py @@ -0,0 +1,140 @@ +""" +税理士の残高試算表とシステムのデータを比較するスクリプト +期間: 2025年6月1日〜2025年12月31日 +""" +import psycopg2 + +conn = psycopg2.connect( + host='192.168.0.61', + port=55432, + dbname='njts_acct', + user='njts_app', + password='njts_app2025' +) +cur = conn.cursor() + +print("=" * 80) +print("【システム】損益計算書 (2025年6月1日〜2025年12月31日)") +print("=" * 80) + +# Get P&L accounts breakdown (account_type: 収益・費用 etc.) +query = """ +SELECT + a.account_code, + a.account_name, + a.account_type, + COALESCE(SUM(jl.debit), 0) AS total_debit, + COALESCE(SUM(jl.credit), 0) AS total_credit, + CASE + WHEN a.account_type IN ('revenue', '収益', 'income') + THEN COALESCE(SUM(jl.credit), 0) - COALESCE(SUM(jl.debit), 0) + ELSE + COALESCE(SUM(jl.debit), 0) - COALESCE(SUM(jl.credit), 0) + END AS balance +FROM accounts a +LEFT JOIN journal_lines jl ON a.account_id = jl.account_id +LEFT JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + AND je.is_latest = true + AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' + AND je.entry_date <= '2025-12-31' +WHERE a.account_type NOT IN ('asset', 'liability', 'equity', '資産', '負債', '純資産', '資本') +GROUP BY a.account_id, a.account_code, a.account_name, a.account_type +HAVING COALESCE(SUM(jl.debit), 0) + COALESCE(SUM(jl.credit), 0) != 0 +ORDER BY a.account_code +""" + +cur.execute(query) +rows = cur.fetchall() + +if rows: + print(f"{'科目コード':<10} {'科目名':<20} {'種別':<15} {'借方合計':>12} {'貸方合計':>12} {'残高':>12}") + print("-" * 85) + for row in rows: + print(f"{str(row[0]):<10} {str(row[1]):<20} {str(row[2]):<15} {row[3]:>12,.0f} {row[4]:>12,.0f} {row[5]:>12,.0f}") +else: + print("データが見つかりません。account_typeを確認します...") + cur.execute("SELECT DISTINCT account_type FROM accounts ORDER BY account_type") + types = cur.fetchall() + print("account_typeの一覧:", [t[0] for t in types]) + +print() +print("=" * 80) +print("【システム】勘定科目別集計(全科目)") +print("=" * 80) + +query2 = """ +SELECT + a.account_code, + a.account_name, + a.account_type, + COALESCE(SUM(jl.debit), 0) AS total_debit, + COALESCE(SUM(jl.credit), 0) AS total_credit +FROM accounts a +JOIN journal_lines jl ON a.account_id = jl.account_id +JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id +WHERE je.is_latest = true + AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' + AND je.entry_date <= '2025-12-31' +GROUP BY a.account_id, a.account_code, a.account_name, a.account_type +ORDER BY a.account_code +""" + +cur.execute(query2) +rows2 = cur.fetchall() + +if rows2: + print(f"{'科目コード':<10} {'科目名':<25} {'種別':<15} {'期間借方':>12} {'期間貸方':>12}") + print("-" * 80) + for row in rows2: + print(f"{str(row[0]):<10} {str(row[1]):<25} {str(row[2]):<15} {row[3]:>12,.0f} {row[4]:>12,.0f}") + +print() +print("=" * 80) +print("【比較】税理士試算表との差異分析") +print("=" * 80) + +# Tax accountant figures from PDF +accountant_figures = { + "売上高": 28896368, + "役員報酬": 2520000, + "給料手当": 2700000, + "賞与": 4300000, + "法定福利費": 1588604, + "福利厚生費": 242978, + "外注費": 7490000, + "交際費": 915007, + "会議費": 219070, + "旅費交通費": 522149, + "通信費": 31459, + "消耗品費": 1027007, + "修繕費": 986, + "新聞図書費": 9900, + "支払手数料": 168800, + "地代家賃": 1081822, + "保険料": 33950, + "租税公課": 33100, + "受取利息": 4878, + "法人税住民税事業税": 747, +} + +print("\n税理士試算表の数値:") +total_revenue = 0 +total_expense = 0 +for k, v in accountant_figures.items(): + if k in ["売上高", "受取利息"]: + total_revenue += v + print(f" {k:<20}: {v:>12,.0f} (収益)") + else: + total_expense += v + print(f" {k:<20}: {v:>12,.0f} (費用)") + +print(f"\n 収益合計 : {total_revenue:>12,.0f}") +print(f" 費用合計 : {total_expense:>12,.0f}") +print(f" 当期純損益 : {total_revenue - total_expense:>12,.0f}") +print(f"\n 税理士の当期純損益: 6,015,667") +print(f" 自社の当期純損益 : 9,461,950") +print(f" 差 額 : {9461950 - 6015667:>12,.0f}") + +conn.close() diff --git a/docs/~$r8ippan3.xlsx b/docs/~$r8ippan3.xlsx deleted file mode 100644 index fd8a89d..0000000 Binary files a/docs/~$r8ippan3.xlsx and /dev/null differ diff --git a/docs/残高試算表20250601-20251231.PDF b/docs/残高試算表20250601-20251231.PDF new file mode 100644 index 0000000..d9e3ea6 Binary files /dev/null and b/docs/残高試算表20250601-20251231.PDF differ diff --git a/fix_jan2026.py b/fix_jan2026.py new file mode 100644 index 0000000..ffb97ef --- /dev/null +++ b/fix_jan2026.py @@ -0,0 +1,33 @@ +import psycopg2 +import requests + +conn = psycopg2.connect(host='192.168.0.61', port=55432, dbname='njts_acct', user='njts_app', password='njts_app2025') +cur = conn.cursor() + +# 一時的に承認解除 +cur.execute("UPDATE monthly_payroll SET status='calculated' WHERE payroll_id IN (10,11)") +conn.commit() +conn.close() +print("承認解除完了") + +# 再計算 +for pid in [10, 11]: + r = requests.post('http://192.168.0.61:18000/payroll/calculation/' + str(pid) + '/recalculate', json={}) + d = r.json() + if 'detail' in d: + print('id=' + str(pid) + ' ERROR: ' + str(d['detail'])) + else: + keys = ['health_insurance', 'care_insurance', 'pension_insurance', + 'employment_insurance', 'child_support', 'income_tax', 'resident_tax', 'other_deduction'] + comp = sum(float(d.get(k) or 0) for k in keys) + td = float(d.get('total_deduction') or 0) + print('id=' + str(pid) + + ' health=' + str(d['health_insurance']) + + ' care=' + str(d['care_insurance']) + + ' income_tax=' + str(d['income_tax']) + + ' total_ded=' + str(td) + + ' SUM=' + str(comp) + + ' DIFF=' + str(td - comp) + + ' net=' + str(d['net_payment'])) + +print("完了") diff --git a/frontend/index.html b/frontend/index.html index 4aa7573..6333747 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -29,10 +29,17 @@ padding: 20px; background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); + display: flex; + flex-direction: column; } .card h2 { margin-top: 0; } + .card p:last-child { + margin-top: auto; + margin-bottom: 0; + padding-top: 12px; + } .btn { display: inline-block; padding: 10px 14px; @@ -40,6 +47,7 @@ background: #007bff; color: #fff; text-decoration: none; + white-space: nowrap; } header { max-width: 980px; diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index 391bc80..221d135 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -861,6 +861,7 @@
+
@@ -2394,6 +2395,8 @@ }); const printArea = document.getElementById("printArea"); + // 印刷後に元の検索結果を復元できるよう保存 + const savedSearchHtml = printArea.innerHTML; printArea.innerHTML = ""; // 月ごとにデータをグループ化 @@ -2525,6 +2528,13 @@ printArea.appendChild(section); }); + // 印刷後に元の検索結果を復元する + const restoreAfterPrint = () => { + printArea.innerHTML = savedSearchHtml; + window.removeEventListener("afterprint", restoreAfterPrint); + }; + window.addEventListener("afterprint", restoreAfterPrint); + // 少し待ってから印刷(データの表示を待つ) setTimeout(() => { window.print(); @@ -2552,6 +2562,25 @@ searchJournals(); } + // 検索条件クリア(期間以外) + function clearSearchConditions() { + // 科目 + const accountInput = document.getElementById("searchAccountInput"); + if (accountInput) { + accountInput.value = ""; + accountInput.dataset.accountId = ""; + } + // 摘要 + const keyword = document.getElementById("searchKeyword"); + if (keyword) keyword.value = ""; + // 方向 + const side = document.getElementById("searchAccountSide"); + if (side) side.value = ""; + // 修正履歴を含む + const includeHistory = document.getElementById("searchIncludeHistory"); + if (includeHistory) includeHistory.checked = false; + } + // 检索结果を表に表示 async function searchJournals() { try { @@ -2784,14 +2813,70 @@ const data = await res.json(); - // 日付をコピー - document.getElementById("entryDate").value = data.entry_date; + // 行数で振り分け: 3行以上 → 複数行詳細フォーム、2行以下 → 簡易フォーム + if (data.lines && data.lines.length > 2) { + copyJournalToDetailForm(data); + } else { + copyJournalToSimpleForm(data); + } + } catch (error) { + console.error("Error:", error); + showAlert(`エラーが発生しました: ${error.message}`, "error"); + } + } - // 摘要をコピー - document.getElementById("description").value = data.description || ""; + // 複数行フォームへのコピー + function copyJournalToDetailForm(data) { + document.getElementById("detailEntryDate").value = data.entry_date; + document.getElementById("detailDescription").value = data.description || ""; - // 新UIでは簡易的にフォームに反映(最初の借方行の科目 + 最初の貸方行を取引手段に設定) - if (data.lines && data.lines.length > 0) { + // 既存行をクリア + document.getElementById("detailLinesTbody").innerHTML = ""; + detailRowSeq = 0; + + // 各明細行をコピー + data.lines.forEach((line) => { + const tr = detailAddLine(); + const accountInput = tr.querySelector(".detail-account-input"); + const acc = accounts.find((a) => a.account_id === line.account_id); + if (acc) { + accountInput.value = `${acc.account_code} ${acc.account_name}`; + accountInput.dataset.accountId = acc.account_id; + } + if (Number(line.debit) > 0) { + tr.querySelector(".detail-debit").value = Number(line.debit).toLocaleString(); + } + if (Number(line.credit) > 0) { + tr.querySelector(".detail-credit").value = Number(line.credit).toLocaleString(); + } + if (line.line_description) { + tr.querySelector(".detail-line-memo").value = line.line_description; + } + }); + + detailUpdateTotals(); + + // 詳細セクションを開いてスクロール + const detailSection = document.getElementById("detailSection"); + detailSection.open = true; + detailSection.scrollIntoView({ behavior: "smooth", block: "start" }); + + showAlert( + `仕訳を複製しました(複数行モード)。\n\n※ このコピーは新規仕訳として独立した記録になります。\n※ 日付と摘要を必要に応じて修正してから登録してください。`, + "success", + ); + } + + // 簡易フォームへのコピー(従来ロジック) + function copyJournalToSimpleForm(data) { + // 日付をコピー + document.getElementById("entryDate").value = data.entry_date; + + // 摘要をコピー + document.getElementById("description").value = data.description || ""; + + // 新UIでは簡易的にフォームに反映(最初の借方行の科目 + 最初の貸方行を取引手段に設定) + if (data.lines && data.lines.length > 0) { // 借方と貸方を分類 const debitLines = data.lines.filter((l) => Number(l.debit) > 0); const creditLines = data.lines.filter((l) => Number(l.credit) > 0); @@ -2961,10 +3046,6 @@ `仕訳を複製しました。\n\n※ このコピーは新規仕訳として独立した記録になります。\n※ 日付と摘要を必要に応じて修正してから登録してください。`, "success", ); - } catch (error) { - console.error("Error:", error); - showAlert(`エラーが発生しました: ${error.message}`, "error"); - } } // -------------------------------------------------- diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html index 7681ab7..69975b0 100644 --- a/frontend/payroll-calculation.html +++ b/frontend/payroll-calculation.html @@ -2,7 +2,10 @@ - + @@ -241,36 +244,54 @@
-
- - -
- - -
-
+
+ + +
+ + +
+
-
-
+
+
+
- -
-
+ + + + +
-
-
- - - -
+
+ + + +
-
-
+
+
+
- -
-
+ + + + +
- @@ -1750,20 +1778,64 @@ // 計算明細表示用の変数(賞与は総支給額に料率を乗じる) const _bHealth = Number(bonus.health_insurance || 0); + + // 源泉税控除対象人数を計算(給与と同じロジック) + const bDependentCount = employee.dependents + ? employee.dependents.filter((d) => { + if (d.valid_to && new Date(d.valid_to) < new Date()) return false; + if (!d.birth_date) return false; + const birthDate = new Date(d.birth_date); + const targetYear = bonus.bonus_year; + const yearEnd = new Date(targetYear, 11, 31); + let age = yearEnd.getFullYear() - birthDate.getFullYear(); + const monthDiff = yearEnd.getMonth() - birthDate.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())) { + age--; + } + return age >= 16; + }).length + : 0; const _bCare = Number(bonus.care_insurance || 0); const _bPension = Number(bonus.pension_insurance || 0); const _bEmp = Number(bonus.employment_insurance || 0); const _bChild = Number(bonus.child_support || 0); const _bTotal = Number(bonus.total_bonus || 0); - const _bHRateFull = _bTotal > 0 && _bHealth > 0 ? (((_bHealth * 2) / _bTotal) * 100).toFixed(3) : "-"; - const _bHRateHalf = _bTotal > 0 && _bHealth > 0 ? ((_bHealth / _bTotal) * 100).toFixed(3) : "-"; - const _bCRateFull = _bTotal > 0 && _bCare > 0 ? (((_bCare * 2) / _bTotal) * 100).toFixed(3) : null; - const _bCRateHalf = _bTotal > 0 && _bCare > 0 ? ((_bCare / _bTotal) * 100).toFixed(3) : null; - const _bPRateFull = _bTotal > 0 && _bPension > 0 ? (((_bPension * 2) / _bTotal) * 100).toFixed(3) : "-"; - const _bPRateHalf = _bTotal > 0 && _bPension > 0 ? ((_bPension / _bTotal) * 100).toFixed(3) : "-"; - const _bEmpRate = _bTotal > 0 && _bEmp > 0 ? ((_bEmp / _bTotal) * 100).toFixed(3) : "-"; - const _bCSRateFull = _bTotal > 0 && _bChild > 0 ? (((_bChild * 2) / _bTotal) * 100).toFixed(4) : null; - const _bCSRateHalf = _bTotal > 0 && _bChild > 0 ? ((_bChild / _bTotal) * 100).toFixed(4) : null; + const _bHRateFull = + _bTotal > 0 && _bHealth > 0 + ? (((_bHealth * 2) / _bTotal) * 100).toFixed(3) + : "-"; + const _bHRateHalf = + _bTotal > 0 && _bHealth > 0 + ? ((_bHealth / _bTotal) * 100).toFixed(3) + : "-"; + const _bCRateFull = + _bTotal > 0 && _bCare > 0 + ? (((_bCare * 2) / _bTotal) * 100).toFixed(3) + : null; + const _bCRateHalf = + _bTotal > 0 && _bCare > 0 + ? ((_bCare / _bTotal) * 100).toFixed(3) + : null; + const _bPRateFull = + _bTotal > 0 && _bPension > 0 + ? (((_bPension * 2) / _bTotal) * 100).toFixed(3) + : "-"; + const _bPRateHalf = + _bTotal > 0 && _bPension > 0 + ? ((_bPension / _bTotal) * 100).toFixed(3) + : "-"; + const _bEmpRate = + _bTotal > 0 && _bEmp > 0 + ? ((_bEmp / _bTotal) * 100).toFixed(3) + : "-"; + const _bCSRateFull = + _bTotal > 0 && _bChild > 0 + ? (((_bChild * 2) / _bTotal) * 100).toFixed(4) + : null; + const _bCSRateHalf = + _bTotal > 0 && _bChild > 0 + ? ((_bChild / _bTotal) * 100).toFixed(4) + : null; const _bSocialSub = _bHealth + _bCare + _bPension + _bEmp + _bChild; const _bTaxable = _bTotal - _bSocialSub; @@ -1815,9 +1887,10 @@
雇用保険:¥${_bEmp.toLocaleString()}
- ${_bEmp > 0 - ? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)` - : "賞与に対する雇用保険は別途計算方式による(または0円)" + ${ + _bEmp > 0 + ? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)` + : "賞与に対する雇用保険は別途計算方式による(または0円)" }
子ども・子育て支援金:¥${_bChild.toLocaleString()}
@@ -1829,12 +1902,13 @@ }
社会保険小計:¥${_bSocialSub.toLocaleString()}
-
所得税:¥${Number( - bonus.income_tax || 0, - ).toLocaleString()}
+
+ 所得税 (甲欄${bDependentCount}人): + ¥${Number(bonus.income_tax || 0).toLocaleString()} +
課税対象額 = 賞与総支給額 ¥${_bTotal.toLocaleString()} − 社会保険料計 ¥${_bSocialSub.toLocaleString()} = ¥${_bTaxable.toLocaleString()}
- → 賞与に対する源泉徴収税額の算出率の表を参照して計算 + → 賞与に対する源泉徴収税額の算出率の表(甲欄・扶養${bDependentCount}人)を参照して計算
住民税:¥${Number( bonus.resident_tax || 0, @@ -1856,6 +1930,9 @@
+ @@ -1872,6 +1949,105 @@ } } + async function editBonus(bonusId) { + try { + const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`); + const bonus = await response.json(); + + const html = ` +
+

賞与データ編集

+
+
+

基本情報

+
+
+ + +
+
+ + +
+
+
+
+

支給項目

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

控除項目(手動入力)

+
+
+ + +
+
+
+

※保存後に再計算を行うと社会保険料・所得税が自動更新されます。

+ + +
+
+ `; + + const editDiv = document.createElement("div"); + editDiv.innerHTML = html; + document.body.appendChild(editDiv); + } catch (error) { + alert("賞与データの取得に失敗しました"); + console.error(error); + } + } + + async function saveBonusEdit(event, bonusId) { + event.preventDefault(); + const form = event.target; + const formData = new FormData(form); + const data = {}; + formData.forEach((value, key) => { + data[key] = isNaN(value) || value === "" ? value : Number(value); + }); + + try { + const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + + if (response.ok) { + alert("賞与データを更新しました"); + form.closest("div").parentElement.remove(); + await recalculateBonus(bonusId, true); + } else { + const error = await response.json(); + alert("更新に失敗しました: " + error.detail); + } + } catch (error) { + alert("更新に失敗しました"); + console.error(error); + } + } + async function deleteBonus(bonusId) { if ( !confirm("この賞与データを削除しますか?\nこの操作は取り消せません。") @@ -1899,8 +2075,8 @@ } } - async function recalculateBonus(bonusId) { - if (!confirm("賞与を再計算しますか?")) return; + async function recalculateBonus(bonusId, skipConfirm = false) { + if (!skipConfirm && !confirm("賞与を再計算しますか?")) return; try { const response = await fetch( @@ -2270,10 +2446,14 @@ bonuses.forEach((bonus) => { const bonusYearMonth = `${bonus.bonus_year}-${bonus.bonus_month}`; const currentKey_new = `${emp.employee_id}-${bonusYearMonth}-bonus`; - const bonusPaymentDate = `${bonus.bonus_year}年${String(bonus.bonus_month).padStart(2, "0")}月${getLastDayOfMonth(bonus.bonus_year, bonus.bonus_month)}日`; - - // 新しいキーが異なる場合は新しいページを開く - if (currentKey !== currentKey_new) { + // DBに保存された実際の支給日を使用。なければ月末日を計算 + const bonusPaymentDate = bonus.payment_date + ? (() => { + const d = new Date(bonus.payment_date); + return `${d.getFullYear()}年${String(d.getMonth()+1).padStart(2,'0')}月${String(d.getDate()).padStart(2,'0')}日`; + })() + : `${bonus.bonus_year}年${String(bonus.bonus_month).padStart(2, "0")}月${getLastDayOfMonth(bonus.bonus_year, bonus.bonus_month)}日`; + if (currentKey && currentKey !== currentKey_new) { if (pageOpen) { printHtml += ""; } diff --git a/frontend/payroll-settings.html b/frontend/payroll-settings.html index bd14ee9..b09df8a 100644 --- a/frontend/payroll-settings.html +++ b/frontend/payroll-settings.html @@ -215,9 +215,13 @@
  • 給与計算と賞与計算で異なる料率を設定できます
  • 事業所所在地(都道府県)ごとに設定できます
  • 介護保険の適用年齢は設定可能です(デフォルト: 40歳以上)
  • -
  • 年度適用期間: 「年度」に設定した値は その年の4月〜翌年3月 に適用されます。
    - 例)2026年度設定 → 2026年4月〜2027年3月の計算に参照されます。
    - 2026年1月〜3月分は 2025年度(rate_year=2025)の料率が参照されます。
  • +
  • + 年度適用期間: 「年度」に設定した値は + その年の4月〜翌年3月 に適用されます。
    + 例)2026年度設定 → 2026年4月〜2027年3月の計算に参照されます。
    + 2026年1月〜3月分は + 2025年度(rate_year=2025)の料率が参照されます。 +
  • @@ -807,20 +811,34 @@ // メタデータに料率がない場合、データから逆算する if (rates.healthNoCareRate == null) { - const refRow = data.find(r => Number(r.monthly_amount) > 0 && Number(r.health_insurance_no_care) > 0); + const refRow = data.find( + (r) => + Number(r.monthly_amount) > 0 && + Number(r.health_insurance_no_care) > 0, + ); if (refRow) { const ma = Number(refRow.monthly_amount); - rates.healthNoCareRate = Number(refRow.health_insurance_no_care) / ma * 100; - rates.healthWithCareRate = Number(refRow.health_insurance_with_care) / ma * 100; + rates.healthNoCareRate = + (Number(refRow.health_insurance_no_care) / ma) * 100; + rates.healthWithCareRate = + (Number(refRow.health_insurance_with_care) / ma) * 100; if (Number(refRow.child_support ?? 0) > 0) { - rates.childSupportRate = Number(refRow.child_support) / ma * 100; + rates.childSupportRate = + (Number(refRow.child_support) / ma) * 100; } } // 厚生年金は低等級(1-3)で0になるため、pension_insurance > 0 の行を別途検索 if (rates.pensionRate == null) { - const pensionRefRow = data.find(r => Number(r.monthly_amount) > 0 && Number(r.pension_insurance ?? 0) > 0); + const pensionRefRow = data.find( + (r) => + Number(r.monthly_amount) > 0 && + Number(r.pension_insurance ?? 0) > 0, + ); if (pensionRefRow) { - rates.pensionRate = Number(pensionRefRow.pension_insurance) / Number(pensionRefRow.monthly_amount) * 100; + rates.pensionRate = + (Number(pensionRefRow.pension_insurance) / + Number(pensionRefRow.monthly_amount)) * + 100; } } } diff --git a/split_bonus_from_salary.py b/split_bonus_from_salary.py new file mode 100644 index 0000000..ce94c47 --- /dev/null +++ b/split_bonus_from_salary.py @@ -0,0 +1,112 @@ +""" +賞与を給料手当から分離する処理: +1. 賞与の勘定科目 (821) を追加 +2. JE:833 の給料手当行 (4,720,000) を + 給料手当 420,000 + 賞与 4,300,000 に分離 +""" +import psycopg2 + +conn = psycopg2.connect( + host='192.168.0.61', port=55432, + dbname='njts_acct', user='njts_app', password='njts_app2025' +) +cur = conn.cursor() + +print("=== 変更前の確認 ===") +cur.execute(""" + SELECT jl.journal_line_id, a.account_code, a.account_name, jl.debit, jl.credit, jl.line_description + FROM journal_lines jl + JOIN accounts a ON jl.account_id = a.account_id + WHERE jl.journal_entry_id = 833 + ORDER BY jl.journal_line_id +""") +rows = cur.fetchall() +for r in rows: + print(f" line_id:{r[0]} {r[1]} {r[2]:<15} 借:{r[3] or 0:>12,.0f} 貸:{r[4] or 0:>12,.0f} {r[5] or ''}") + +print() + +# Step 1: 賞与 (821) の科目を追加(存在しない場合のみ) +cur.execute("SELECT account_id FROM accounts WHERE account_code = '821'") +existing = cur.fetchone() +if existing: + print(f"科目 821 は既に存在します (ID:{existing[0]})") + bonus_account_id = existing[0] +else: + cur.execute(""" + INSERT INTO accounts (account_code, account_name, account_type) + VALUES ('821', '賞与', 'expense') + RETURNING account_id + """) + bonus_account_id = cur.fetchone()[0] + print(f"科目 821 賞与 を追加しました (ID:{bonus_account_id})") + +# Step 2: 給料手当行 (line_id:2162) の金額を 4,720,000→420,000 に変更 +cur.execute(""" + UPDATE journal_lines + SET debit = 420000, + line_description = NULL + WHERE journal_line_id = 2162 +""") +print(f"給料手当行 (line_id:2162) を 4,720,000→420,000 に更新") + +# Step 3: 賞与の新しい明細行を追加 +cur.execute(""" + INSERT INTO journal_lines (journal_entry_id, account_id, debit, credit, line_description) + VALUES (833, %s, 4300000, 0, '賞与') + RETURNING journal_line_id +""", (bonus_account_id,)) +new_line_id = cur.fetchone()[0] +print(f"賞与行 (line_id:{new_line_id}) を追加: 4,300,000 円") + +# Step 4: 仕訳の摘要も更新 +cur.execute(""" + UPDATE journal_entries + SET description = '張翔鶴 10月給料・賞与' + WHERE journal_entry_id = 833 +""") +print("仕訳摘要を更新: '張翔鶴 10月給料 + ボーナス' → '張翔鶴 10月給料・賞与'") + +conn.commit() + +print() +print("=== 変更後の確認 ===") +cur.execute(""" + SELECT jl.journal_line_id, a.account_code, a.account_name, jl.debit, jl.credit, jl.line_description + FROM journal_lines jl + JOIN accounts a ON jl.account_id = a.account_id + WHERE jl.journal_entry_id = 833 + ORDER BY jl.journal_line_id +""") +rows = cur.fetchall() +debit_total = 0 +credit_total = 0 +for r in rows: + d = r[3] or 0 + c = r[4] or 0 + debit_total += d + credit_total += c + print(f" line_id:{r[0]} {r[1]} {r[2]:<15} 借:{d:>12,.0f} 貸:{c:>12,.0f} {r[5] or ''}") +print(f" {'合計':<30} 借:{debit_total:>12,.0f} 貸:{credit_total:>12,.0f}") +print(f" 貸借バランス: {'✓ OK' if debit_total == credit_total else '✗ NG!'}") + +print() +print("=== 期間合計(給料手当・賞与)===") +cur.execute(""" + SELECT a.account_code, a.account_name, SUM(jl.debit) AS total + FROM journal_lines jl + JOIN accounts a ON jl.account_id = a.account_id + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE je.is_latest = true AND je.is_deleted = false + AND je.entry_date >= '2025-06-01' AND je.entry_date <= '2025-12-31' + AND a.account_code IN ('812', '813', '821') + GROUP BY a.account_code, a.account_name + ORDER BY a.account_code +""") +rows = cur.fetchall() +for r in rows: + print(f" {r[0]} {r[1]:<12}: {r[2] or 0:>12,.0f} 円") + +conn.close() +print() +print("完了しました。")