This commit is contained in:
admin
2026-05-14 22:02:06 +09:00
parent 78022b3b15
commit b9f994b1e1
22 changed files with 1225 additions and 126 deletions

185
analyze_accountant_diff.py Normal file
View File

@@ -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()

View File

@@ -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",

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

14
check_api_821.py Normal file
View File

@@ -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}')

20
check_bonus_table.py Normal file
View File

@@ -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()

View File

@@ -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()

82
check_jan2026_bank.py Normal file
View File

@@ -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()

70
check_jan2026_bank2.py Normal file
View File

@@ -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()

6
check_jl_cols.py Normal file
View File

@@ -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()

65
check_salary_bonus.py Normal file
View File

@@ -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()

140
compare_with_accountant.py Normal file
View File

@@ -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()

Binary file not shown.

Binary file not shown.

33
fix_jan2026.py Normal file
View File

@@ -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("完了")

View File

@@ -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;

View File

@@ -861,6 +861,7 @@
<div class="search-buttons">
<button onclick="searchJournals()">🔍 検索</button>
<button onclick="printSearchResults()">🖨️ 印刷</button>
<button onclick="clearSearchConditions()" style="background: #6c757d; color: white; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer;">🔄 条件クリア</button>
</div>
</div>
@@ -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");
}
}
// --------------------------------------------------

View File

@@ -2,7 +2,10 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<script src="js/auth.js"></script>
@@ -241,36 +244,54 @@
<div id="tab-salary" class="tab-content-calc active">
<div class="split-layout">
<div class="split-left">
<div class="filters" style="display:flex; flex-direction:column; gap:8px;">
<label style="display:flex; align-items:center; gap:8px;">
<span style="white-space:nowrap; min-width:60px;">対象年:</span>
<input
type="number"
id="filterYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<label style="display:flex; align-items:center; gap:8px;">
<span style="white-space:nowrap; min-width:60px;">従業員:</span>
<select id="filterEmployee" style="flex:1;">
<option value="">全員</option>
</select>
</label>
<div style="display:flex; gap:8px;">
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">新規計算</button>
</div>
</div>
<div
class="filters"
style="display: flex; flex-direction: column; gap: 8px"
>
<label style="display: flex; align-items: center; gap: 8px">
<span style="white-space: nowrap; min-width: 60px"
>対象年:</span
>
<input
type="number"
id="filterYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<label style="display: flex; align-items: center; gap: 8px">
<span style="white-space: nowrap; min-width: 60px"
>従業員:</span
>
<select id="filterEmployee" style="flex: 1">
<option value="">全員</option>
</select>
</label>
<div style="display: flex; gap: 8px">
<button class="btn btn-primary" onclick="loadPayrolls()">
検索
</button>
<button class="btn btn-success" onclick="showCalculateForm()">
新規計算
</button>
</div>
</div>
<div id="payrollList" class="payroll-list"></div>
</div><!-- /split-left -->
<div id="payrollList" class="payroll-list"></div>
</div>
<!-- /split-left -->
<div class="split-right" id="splitRightSalary">
<div id="payrollDetail" class="payroll-detail" style="display:none"></div>
</div><!-- /split-right -->
</div><!-- /split-layout -->
<div
id="payrollDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<!-- /split-right -->
</div>
<!-- /split-layout -->
<!-- 給与計算フォーム(モーダル風) -->
<div
@@ -472,37 +493,45 @@
</button>
</form>
</div>
</div>
<!-- 賞与計算タブ -->
<div id="tab-bonus" class="tab-content-calc">
<div class="split-layout">
<div class="split-left">
<div class="filters">
<label>
対象年:
<input
type="number"
id="filterBonusYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<button class="btn btn-primary" onclick="loadBonus()">検索</button>
<button class="btn btn-success" onclick="showBonusForm()">
新規計算
</button>
</div>
<div class="filters">
<label>
対象年:
<input
type="number"
id="filterBonusYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<button class="btn btn-primary" onclick="loadBonus()">
検索
</button>
<button class="btn btn-success" onclick="showBonusForm()">
新規計算
</button>
</div>
<div id="bonusList" class="payroll-list"></div>
</div><!-- /split-left -->
<div id="bonusList" class="payroll-list"></div>
</div>
<!-- /split-left -->
<div class="split-right" id="splitRightBonus">
<div id="bonusDetail" class="payroll-detail" style="display:none"></div>
</div><!-- /split-right -->
</div><!-- /split-layout -->
<div
id="bonusDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<!-- /split-right -->
</div>
<!-- /split-layout -->
<!-- 賞与計算フォーム -->
<div
@@ -637,7 +666,6 @@
</button>
</form>
</div>
</div>
<!-- 账票出力タブ -->
@@ -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 @@
</div>
<div class="detail-row"><span>雇用保険:</span><span>¥${_bEmp.toLocaleString()}</span></div>
<div class="calc-note">
${_bEmp > 0
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
: "賞与に対する雇用保険は別途計算方式によるまたは0円"
${
_bEmp > 0
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
: "賞与に対する雇用保険は別途計算方式によるまたは0円"
}
</div>
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_bChild.toLocaleString()}</span></div>
@@ -1829,12 +1902,13 @@
}
</div>
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_bSocialSub.toLocaleString()}</span></div>
<div class="detail-row"><span>所得税:</span><span>¥${Number(
bonus.income_tax || 0,
).toLocaleString()}</span></div>
<div class="detail-row">
<span>所得税 <small style="color:#666;">(甲欄${bDependentCount}人)</small>:</span>
<span>¥${Number(bonus.income_tax || 0).toLocaleString()}</span>
</div>
<div class="calc-note">
課税対象額 = 賞与総支給額 ¥${_bTotal.toLocaleString()} 社会保険料計 ¥${_bSocialSub.toLocaleString()} = ¥${_bTaxable.toLocaleString()}<br>
→ 賞与に対する源泉徴収税額の算出率の表を参照して計算
→ 賞与に対する源泉徴収税額の算出率の表(甲欄・扶養${bDependentCount}人)を参照して計算
</div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(
bonus.resident_tax || 0,
@@ -1856,6 +1930,9 @@
</div>
</div>
<button class="btn btn-primary" onclick="editBonus(${
bonus.bonus_id
})">編集</button>
<button class="btn btn-primary" onclick="recalculateBonus(${
bonus.bonus_id
})">再計算</button>
@@ -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 = `
<div style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: 600px; max-height: 90vh; overflow-y: auto; background: white; padding: 30px; border: 2px solid #007bff; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 1000;">
<h2>賞与データ編集</h2>
<form id="editBonusForm" onsubmit="saveBonusEdit(event, ${bonusId})">
<div class="detail-section">
<h3>基本情報</h3>
<div class="form-row">
<div class="form-group">
<label>支給日</label>
<input type="date" name="payment_date" value="${bonus.payment_date}" required />
</div>
<div class="form-group">
<label>賞与種別</label>
<select name="bonus_type">
<option value="夏季賞与" ${bonus.bonus_type === '夏季賞与' ? 'selected' : ''}>夏季賞与</option>
<option value="冬季賞与" ${bonus.bonus_type === '冬季賞与' ? 'selected' : ''}>冬季賞与</option>
<option value="決算賞与" ${bonus.bonus_type === '決算賞与' ? 'selected' : ''}>決算賞与</option>
<option value="その他" ${bonus.bonus_type === 'その他' ? 'selected' : ''}>その他</option>
</select>
</div>
</div>
</div>
<div class="detail-section">
<h3>支給項目</h3>
<div class="form-row">
<div class="form-group">
<label>基本賞与額</label>
<input type="number" name="base_bonus" step="1" value="${bonus.base_bonus}" required />
</div>
<div class="form-group">
<label>業績賞与</label>
<input type="number" name="performance_bonus" step="1" value="${bonus.performance_bonus}" />
</div>
<div class="form-group">
<label>その他賞与</label>
<input type="number" name="other_bonus" step="1" value="${bonus.other_bonus}" />
</div>
</div>
</div>
<div class="detail-section">
<h3>控除項目(手動入力)</h3>
<div class="form-row">
<div class="form-group">
<label>その他控除</label>
<input type="number" name="other_deduction" step="1" value="${bonus.other_deduction}" />
</div>
</div>
</div>
<p style="font-size:12px; color:#666;">※保存後に再計算を行うと社会保険料・所得税が自動更新されます。</p>
<button type="submit" class="btn btn-primary">保存して再計算</button>
<button type="button" class="btn btn-secondary" onclick="this.closest('div').parentElement.remove()">キャンセル</button>
</form>
</div>
`;
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 += "</div>";
}

View File

@@ -215,9 +215,13 @@
<li>給与計算と賞与計算で異なる料率を設定できます</li>
<li>事業所所在地(都道府県)ごとに設定できます</li>
<li>介護保険の適用年齢は設定可能です(デフォルト: 40歳以上</li>
<li><strong>年度適用期間:</strong> 「年度」に設定した値は <strong>その年の4月〜翌年3月</strong> に適用されます。<br>
2026年度設定 → 2026年4月〜2027年3月の計算に参照されます。<br>
2026年1月〜3月分は 2025年度rate_year=2025の料率が参照されます。</li>
<li>
<strong>年度適用期間:</strong> 「年度」に設定した値は
<strong>その年の4月〜翌年3月</strong> に適用されます。<br />
2026年度設定 → 2026年4月〜2027年3月の計算に参照されます。<br />
2026年1月〜3月分は
2025年度rate_year=2025の料率が参照されます。
</li>
</ul>
</div>
@@ -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;
}
}
}

112
split_bonus_from_salary.py Normal file
View File

@@ -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("完了しました。")