chore: 年度リストを昇順に変更

This commit is contained in:
admin
2026-06-07 21:58:20 +09:00
parent 91632ea5b6
commit 4a500ff5e9
25 changed files with 1531 additions and 67 deletions

View File

@@ -146,13 +146,6 @@ def run_auto_migration():
except Exception as e: except Exception as e:
print(f"⚠️ ねんきんポータル テーブル作成エラー(無視): {e}") print(f"⚠️ ねんきんポータル テーブル作成エラー(無視): {e}")
# ── 銀行明細テーブル作成 ──
try:
from app.modules.bank_statements.migration import create_bank_statement_table
create_bank_statement_table()
except Exception as e:
print(f"⚠️ 銀行明細テーブル作成エラー(無視): {e}")
except Exception as e: except Exception as e:
print(f"\n❌ 無法連接到數據庫或執行遷移: {e}") print(f"\n❌ 無法連接到數據庫或執行遷移: {e}")
print("\n提示:") print("\n提示:")

View File

@@ -321,12 +321,22 @@ def list_statements(
year: Optional[int] = None, year: Optional[int] = None,
month: Optional[int] = None, month: Optional[int] = None,
bank_name: Optional[str] = None, bank_name: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
limit: int = 10000, limit: int = 10000,
offset: int = 0, offset: int = 0,
): ):
conditions: list = [] conditions: list = []
params: list = [] params: list = []
if date_from:
conditions.append("tx_date >= %s::date")
params.append(date_from)
if date_to:
conditions.append("tx_date <= %s::date")
params.append(date_to)
# date_from/date_to が両方未指定の場合のみ year/month フォールバック
if not date_from and not date_to:
if year: if year:
conditions.append("EXTRACT(YEAR FROM tx_date)::int = %s") conditions.append("EXTRACT(YEAR FROM tx_date)::int = %s")
params.append(year) params.append(year)

View File

@@ -8,6 +8,9 @@ import secrets
import os import os
import json import json
import time import time
import re
import io
import zipfile
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -233,3 +236,243 @@ async def mark_post_done(access_token: str, post_id: str) -> dict:
) )
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()
# ── 申請書作成 API ─────────────────────────────────────────────
async def get_procedure_skeleton(access_token: str, proc_id: str) -> dict:
"""
手続選択:申請書スケルトン(ひな形)取得
GET /procedure/{proc_id}
Returns: {results: {file_data (base64 zip), configuration_file_name, file_info}}
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/procedure/{proc_id}",
headers={"Authorization": f"Bearer {access_token}"},
timeout=60,
)
resp.raise_for_status()
return resp.json()
async def submit_apply(access_token: str, proc_id: str,
file_name: str, file_data_b64: str,
trial: bool = True) -> dict:
"""
申請データ送信
POST /apply {"proc_id": "...", "send_file": {"file_name": "...", "file_data": "base64..."}}
Returns: {results: {arrive_id, arrive_date, proc_name, ...}}
"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
if trial:
headers["X-eGovAPI-Trial"] = "true"
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{API_BASE}/apply",
headers=headers,
json={
"proc_id": proc_id,
"send_file": {
"file_name": file_name,
"file_data": file_data_b64,
},
},
timeout=120,
)
if not resp.is_success:
try:
err_body = resp.json()
except Exception:
err_body = resp.text
raise RuntimeError(f"HTTP {resp.status_code}: {err_body}")
return resp.json()
# ── スケルトン XML 修正 ──────────────────────────────────────────
def _fill_empty_tag(text: str, tag: str, value: str) -> str:
"""kousei.xml 内の空タグ <tag/> または <tag></tag> を <tag>value</tag> に置換"""
# self-closing: <tag/> または <tag />
text = re.sub(r'<' + re.escape(tag) + r'\s*/>', f'<{tag}>{value}</{tag}>', text)
# empty: <tag></tag> または <tag> </tag>
text = re.sub(
r'<' + re.escape(tag) + r'\s*>\s*</' + re.escape(tag) + r'>',
f'<{tag}>{value}</{tag}>', text
)
return text
def modify_skeleton_for_post_apply(
file_data_b64: str,
proc_id: str,
proc_name: str,
uketsuke_kikan_id: str,
shinsei_shubetsu: str = "申込",
shimei: str = "テスト タロウ",
shimei_furigana: str = "テスト タロウ",
yubin: str = "1300022",
jusho: str = "東京都墨田区江東橋4丁目31番10号",
jigyosho_seiri_mae: str = "41",
jigyosho_seiri_ato: str = "シスメ",
jigyosho_bangou: str = "23090",
) -> str:
"""
スケルトン ZIP の kousei.xml に必須フィールドを埋めて再パックする。
戻り値: 修正済み ZIP の base64 文字列
"""
raw = base64.b64decode(file_data_b64)
buf_out = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(raw), 'r') as zin:
with zipfile.ZipFile(buf_out, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
name_lower = item.filename.lower()
if name_lower.endswith('/kousei.xml') or name_lower == 'kousei.xml':
data = _modify_kousei_xml(
data, proc_id, proc_name, uketsuke_kikan_id,
shinsei_shubetsu, shimei, shimei_furigana, yubin, jusho
)
elif name_lower.endswith('_01.xml') and 'check' not in name_lower:
# 申請書XML: 事業所情報を埋める
data = _modify_apply_xml(data, jigyosho_seiri_mae, jigyosho_seiri_ato, jigyosho_bangou)
zout.writestr(item, data)
return base64.b64encode(buf_out.getvalue()).decode()
def _modify_apply_xml(xml_bytes: bytes, seiri_mae: str, seiri_ato: str, bangou: str) -> bytes:
"""申請書XML の事業所情報を埋める"""
try:
text = xml_bytes.decode('utf-8')
except Exception:
text = xml_bytes.decode('shift_jis', errors='replace')
text = _fill_empty_tag(text, '事業所整理記号_前', seiri_mae)
text = _fill_empty_tag(text, '事業所整理記号_後', seiri_ato)
text = _fill_empty_tag(text, '事業所番号', bangou)
return text.encode('utf-8')
def _modify_kousei_xml(
xml_bytes: bytes,
proc_id: str,
proc_name: str,
uketsuke_kikan_id: str,
shinsei_shubetsu: str,
shimei: str,
shimei_furigana: str,
yubin: str,
jusho: str,
teishutsusaki_id: str = "900API00000000001001001",
teishutsusaki_name: str = "総務省,行政管理局,API",
jusho_furigana: str = "トウキョウトスミダクコウトウバシ4チョウメ31バン10ゴウ",
tel: str = "03-1234-5678",
email: str = "test@example.com",
) -> bytes:
"""kousei.xml の必須フィールドをテキスト置換で埋める(名前空間非依存)"""
try:
text = xml_bytes.decode('utf-8')
except Exception:
text = xml_bytes.decode('shift_jis', errors='replace')
fills = [
('受付行政機関ID', uketsuke_kikan_id),
('手続ID', proc_id),
('手続名称', proc_name),
('申請種別', shinsei_shubetsu),
('氏名フリガナ', shimei_furigana), # フリガナを先に(氏名より長いので誤置換防止)
('氏名', shimei),
('郵便番号', yubin),
('住所', jusho),
('提出先識別子', teishutsusaki_id),
('提出先名称', teishutsusaki_name),
('住所フリガナ', jusho_furigana),
('電話番号', tel),
('電子メールアドレス', email),
]
for tag, value in fills:
text = _fill_empty_tag(text, tag, value)
# 申請書属性情報を </提出先情報> の直後に挿入(スケルトンに存在しないため)
shinsei_attr = (
'<申請書属性情報>'
'<申請書様式ID>900A13800001000001</申請書様式ID>'
'<申請書様式バージョン>0001</申請書様式バージョン>'
'<申請書様式名称>APIテスト用手続(電子送達関係手続)(通)0001_01</申請書様式名称>'
'<申請書ファイル名称>900A13800001000001_01.xml</申請書ファイル名称>'
'</申請書属性情報>'
)
if '<申請書属性情報>' not in text:
text = text.replace('</提出先情報>', '</提出先情報>' + shinsei_attr, 1)
return text.encode('utf-8')
async def submit_post_apply(access_token: str, proc_id: str,
file_name: str, file_data_b64: str) -> dict:
"""
電子送達利用申込み
POST /post-apply {"proc_id": "...", "send_file": {"file_name": "...", "file_data": "base64..."}}
Returns: {results: {arrive_id, arrive_date, proc_name, ...}}
※電子送達はトライアル非対応のため X-eGovAPI-Trial は送信しない
"""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{API_BASE}/post-apply",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"proc_id": proc_id,
"send_file": {
"file_name": file_name,
"file_data": file_data_b64,
},
},
timeout=120,
)
if not resp.is_success:
try:
err_body = resp.json()
except Exception:
err_body = resp.text
raise RuntimeError(f"HTTP {resp.status_code}: {err_body}")
return resp.json()
async def get_apply_list(access_token: str, limit: int = 100) -> dict:
"""
申請案件一覧取得
GET /apply/lists
Returns: {results: {apply_list: [...]}}
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/apply/lists",
headers={"Authorization": f"Bearer {access_token}"},
params={"limit": limit},
timeout=30,
)
resp.raise_for_status()
return resp.json()
async def get_apply_detail(access_token: str, arrive_id: str) -> dict:
"""
申請案件取得(詳細)
GET /apply/{arrive_id}
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{API_BASE}/apply/{arrive_id}",
headers={"Authorization": f"Bearer {access_token}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()

View File

@@ -29,6 +29,10 @@ class CallbackRequest(BaseModel):
state: str state: str
class ApplyRequest(BaseModel):
proc_id: str # 16文字の手続識別子
class ConfigRequest(BaseModel): class ConfigRequest(BaseModel):
software_id: str software_id: str
api_key: str api_key: str
@@ -245,6 +249,18 @@ async def save_config(req: ConfigRequest):
return {"message": "設定を保存しました"} return {"message": "設定を保存しました"}
@router.get("/config-info")
async def get_config_info():
"""現在の設定情報APIキーは非表示を返す"""
import os
cfg = egov_api.load_api_config()
return {
"software_id": cfg.get("client_id", ""),
"redirect_uri": cfg.get("redirect_uri", ""),
"environment": os.environ.get("EGOV_ENV", "production"),
}
@router.get("/auth-url") @router.get("/auth-url")
async def get_auth_url(): async def get_auth_url():
"""OAuth2 + PKCE 認可 URL を生成して返す""" """OAuth2 + PKCE 認可 URL を生成して返す"""
@@ -370,3 +386,154 @@ async def debug_page():
return {"browser_state": state.status, **diag} return {"browser_state": state.status, **diag}
except Exception as e: except Exception as e:
return {"error": str(e), "status": state.status} return {"error": str(e), "status": state.status}
# ── 申請書作成・一覧 エンドポイント ────────────────────────────
async def _ensure_valid_token() -> str:
"""有効なアクセストークンを返す(期限切れ時は自動更新)"""
cfg = egov_api.load_api_config()
if not cfg["client_id"] or not cfg["client_secret"]:
raise HTTPException(status_code=400, detail="e-Gov API 資格情報が未設定です")
# メモリが空の場合はファイルから expires_at を正しく復元
if not state.access_token:
saved = egov_api.load_tokens()
if saved.get("access_token"):
state.access_token = saved["access_token"]
state.refresh_token = saved.get("refresh_token")
state.token_expires_at = saved.get("expires_at") # float タイムスタンプをそのまま復元
# トークンが期限切れ → リフレッシュを試みる
if not state.is_token_valid() and state.refresh_token:
try:
token_resp = await egov_api.refresh_tokens_request(
state.refresh_token, cfg["client_id"], cfg["client_secret"]
)
state.set_tokens(token_resp)
egov_api.save_tokens({
"access_token": state.access_token,
"refresh_token": state.refresh_token,
"expires_at": state.token_expires_at,
})
except Exception as e:
state.access_token = None
raise HTTPException(
status_code=401,
detail=f"トークン更新失敗: {e}。再度 GビズIDログイン が必要です。"
)
if not state.access_token:
raise HTTPException(status_code=401, detail="認証が必要です。GビズID でログインしてください。")
if not state.is_token_valid():
raise HTTPException(
status_code=401,
detail="アクセストークンが期限切れです。再度 GビズIDログイン してください。"
)
return state.access_token
@router.post("/test-apply")
async def test_apply(req: ApplyRequest):
"""
テスト申請書作成:スケルトン(ひな形)を取得してそのまま申請送信する。
到達番号arrive_idを返す。
"""
import re
if not re.fullmatch(r"[A-Za-z0-9]{16}", req.proc_id):
raise HTTPException(status_code=400, detail="proc_id は半角英数字16文字です")
access_token = await _ensure_valid_token()
# 1. スケルトン取得
try:
skel_resp = await egov_api.get_procedure_skeleton(access_token, req.proc_id)
except Exception as e:
raise HTTPException(status_code=502, detail=f"スケルトン取得失敗: {e}")
results = skel_resp.get("results", {})
file_data_b64 = results.get("file_data", "")
if not file_data_b64:
raise HTTPException(status_code=502, detail="スケルトンデータが空です")
# 2. 申請送信(スケルトンをそのまま送信)
# 電子申請手続 → POST /applyトライアルモード
# 電子送達手続 → POST /post-applyトライアル非対応のため自動フォールバック
# 電子送達手続の場合は kousei.xml に必須フィールドを埋めてから送信する
file_name = f"{req.proc_id}.zip"
apply_resp = None
api_type = "電子申請"
try:
apply_resp = await egov_api.submit_apply(
access_token, req.proc_id, file_name, file_data_b64, trial=True
)
except RuntimeError as e:
err_str = str(e)
if "対象手続ではありません" in err_str or "post-apply" in err_str.lower():
# 電子送達利用申込み手続 → kousei.xml 修正 → /post-apply
api_type = "電子送達利用申込み"
# 公式手続一覧 egov_applapi_testproclist.xlsx より正しい手続名称
proc_name = "APIテスト用手続(電子送達関係手続)(通)0001/APIテスト用手続(電子送達関係手続)(通)0001"
# スケルトン kousei.xml に必須フィールドを埋める
try:
modified_file_data = egov_api.modify_skeleton_for_post_apply(
file_data_b64=file_data_b64,
proc_id=req.proc_id,
proc_name=proc_name,
uketsuke_kikan_id="100900", # 電子送達関係手続の受付行政機関ID
shinsei_shubetsu="新規申請",
shimei="テスト タロウ",
shimei_furigana="テスト タロウ",
yubin="1300022",
jusho="東京都墨田区江東橋4丁目31番10号",
)
except Exception as emx:
raise HTTPException(status_code=502, detail=f"スケルトン修正失敗: {emx}")
try:
apply_resp = await egov_api.submit_post_apply(
access_token, req.proc_id, file_name, modified_file_data
)
except Exception as e2:
raise HTTPException(status_code=502, detail=f"電子送達利用申込み失敗: {e2}")
else:
raise HTTPException(status_code=502, detail=f"申請送信失敗: {err_str}")
except Exception as e:
raise HTTPException(status_code=502, detail=f"申請送信失敗: {e}")
res = apply_resp.get("results", {})
return {
"arrive_id": res.get("arrive_id", ""),
"arrive_date": res.get("arrive_date", ""),
"proc_name": res.get("proc_name", ""),
"proc_id": req.proc_id,
"api_type": api_type,
"raw": res,
}
@router.get("/apply-list")
async def apply_list():
"""申請案件一覧取得"""
access_token = await _ensure_valid_token()
try:
resp = await egov_api.get_apply_list(access_token)
except Exception as e:
raise HTTPException(status_code=502, detail=f"申請案件一覧取得失敗: {e}")
results = resp.get("results", {})
apply_list_data = results.get("apply_list", [])
items = []
for item in apply_list_data:
items.append({
"arrive_id": item.get("arrive_id", ""),
"arrive_date": item.get("arrive_date", ""),
"proc_id": item.get("proc_id", ""),
"proc_name": item.get("proc_name", ""),
"status": item.get("status", ""),
"sub_status": item.get("sub_status", ""),
})
return {"apply_list": items, "total": len(items)}

View File

@@ -2,9 +2,54 @@ from fastapi import APIRouter, HTTPException, Query
from decimal import Decimal from decimal import Decimal
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from app.modules.trial_balance.service import fetch_trial_balance from app.modules.trial_balance.service import fetch_trial_balance
from app.core.database import get_connection
router = APIRouter(prefix="/trial-balance", tags=["試算表"]) router = APIRouter(prefix="/trial-balance", tags=["試算表"])
@router.get("/fiscal-years", summary="仕訳データから会計年度一覧取得5月期")
def get_fiscal_years():
REIWA_OFFSET = 2018 # 令和元年 = 2019年
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
SELECT
MIN(entry_date)::date AS min_date,
MAX(entry_date)::date AS max_date
FROM journal_entries
WHERE is_deleted = false
""")
row = cur.fetchone()
if not row or not row["min_date"]:
return []
min_date = row["min_date"]
max_date = row["max_date"]
def fiscal_end_year(d):
return d.year + 1 if d.month >= 6 else d.year
start_fy = fiscal_end_year(min_date)
end_fy = fiscal_end_year(max_date)
result = []
for fy in range(end_fy, start_fy - 1, -1):
reiwa = fy - REIWA_OFFSET
if reiwa > 0:
wareki = f"令和{reiwa}"
elif reiwa == 0:
wareki = "平成31/令和元年"
else:
wareki = f"平成{fy - 1988}"
label = f"{wareki}({fy}年)5月期"
result.append({
"label": label,
"date_from": f"{fy - 1}-06-01",
"date_to": f"{fy}-05-31",
})
return result
@router.get("", summary="試算表(全科目)") @router.get("", summary="試算表(全科目)")
def get_trial_balance( def get_trial_balance(
fiscal_year: Optional[int] = Query(None, description="会計年度(例: 2025"), fiscal_year: Optional[int] = Query(None, description="会計年度(例: 2025"),

View File

@@ -97,3 +97,53 @@ def get_trial_balance(
rows = cur.fetchall() rows = cur.fetchall()
return rows return rows
@router.get("/fiscal-years", summary="仕訳データから会計年度一覧取得5月期")
def get_fiscal_years():
"""
仕訳の最小・最大日付から5月期の年度リストを生成して返す。
例: 2025-06-01〜2026-05-31 → {"label": "令和8年(2026年)5月期", "date_from": "2025-06-01", "date_to": "2026-05-31"}
"""
REIWA_OFFSET = 2018 # 令和元年 = 2019年
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
SELECT
MIN(entry_date)::date AS min_date,
MAX(entry_date)::date AS max_date
FROM journal_entries
WHERE is_deleted = false
""")
row = cur.fetchone()
if not row or not row["min_date"]:
return []
min_date = row["min_date"]
max_date = row["max_date"]
# 5月期の年度を計算: 決算年end_yearは entry_date の月が6以上なら翌年、1〜5なら当年
def fiscal_end_year(d):
return d.year + 1 if d.month >= 6 else d.year
start_fy = fiscal_end_year(min_date)
end_fy = fiscal_end_year(max_date)
result = []
for fy in range(start_fy, end_fy + 1): # 小さい順(昇順)
reiwa = fy - REIWA_OFFSET
if reiwa > 0:
wareki = f"令和{reiwa}"
elif reiwa == 0:
wareki = "平成31/令和元年"
else:
wareki = f"平成{fy - 1988}"
label = f"{wareki}({fy}年)5月期"
result.append({
"label": label,
"date_from": f"{fy - 1}-06-01",
"date_to": f"{fy}-05-31",
})
return result

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
egov_guide.pdf Normal file

Binary file not shown.

BIN
faq.pdf Normal file

Binary file not shown.

BIN
final_test_spec_post.xlsx Normal file

Binary file not shown.

View File

@@ -405,6 +405,29 @@
cursor: default; cursor: default;
} }
/* ── 印刷ヘッダー ── */
.print-header {
display: none;
}
@media print {
.print-header {
display: block;
margin-bottom: 12px;
border-bottom: 2px solid #000;
padding-bottom: 8px;
}
.print-header .ph-company {
font-size: 16px;
font-weight: bold;
margin-bottom: 4px;
}
.print-header .ph-sub {
font-size: 12px;
display: flex;
gap: 24px;
}
}
/* ── 印刷スタイル ── */ /* ── 印刷スタイル ── */
@media print { @media print {
.no-print { .no-print {
@@ -434,32 +457,68 @@
</style> </style>
</head> </head>
<body> <body>
<!-- 印刷時のみ表示されるヘッダー -->
<div class="print-header">
<div class="ph-company" id="printCompany"></div>
<div class="ph-sub">
<span id="printBank"></span>
<span id="printDate"></span>
</div>
</div>
<div class="page-header no-print"> <div class="page-header no-print">
<h2>🏦 銀行明細照会</h2> <h2>🏦 銀行明細照会</h2>
<div class="header-btns"> <div class="header-btns">
<button class="btn btn-orange" onclick="openImportModal()"> <button class="btn btn-orange" onclick="openImportModal()">
CSV取込 CSV取込
</button> </button>
<button class="btn btn-blue" onclick="window.print()">印刷</button> <button class="btn btn-blue" onclick="doPrint()">印刷</button>
<a class="btn btn-gray" href="index.html">← メニューへ戻る</a> <a class="btn btn-gray" href="index.html">← メニューへ戻る</a>
</div> </div>
</div> </div>
<!-- 印刷設定(画面のみ) -->
<div
class="search-form no-print"
style="padding: 10px 18px; background: #fff8e1; border-color: #ffe082"
>
<label style="font-weight: bold; white-space: nowrap">印刷設定:</label>
<label style="white-space: nowrap">会社名:</label>
<input
type="text"
id="printCompanyInput"
style="
width: 220px;
padding: 4px 8px;
border: 1px solid #999;
border-radius: 3px;
font-size: 13px;
"
placeholder="例:株式会社〇〇"
/>
</div>
<div id="msgBar" class="msg-bar no-print"></div> <div id="msgBar" class="msg-bar no-print"></div>
<!-- 検索フォーム --> <!-- 検索フォーム -->
<div class="search-form no-print"> <div
<label>年:</label> class="search-form no-print"
style="flex-direction: column; align-items: flex-start; gap: 10px"
>
<div
style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px"
>
<label style="min-width: 3em">開始:</label>
<input <input
type="number" type="number"
id="filterYear" id="startYear"
min="2000" min="2000"
max="2099" max="2099"
style="width: 80px" style="width: 72px"
/> placeholder="年"
<label>月:</label> /><span></span>
<select id="filterMonth"> <select id="startMonth" style="width: 60px">
<option value="">全月</option> <option value=""></option>
<option value="1">1月</option> <option value="1">1月</option>
<option value="2">2月</option> <option value="2">2月</option>
<option value="3">3月</option> <option value="3">3月</option>
@@ -473,11 +532,58 @@
<option value="11">11月</option> <option value="11">11月</option>
<option value="12">12月</option> <option value="12">12月</option>
</select> </select>
<input
type="number"
id="startDay"
min="1"
max="31"
style="width: 48px"
placeholder="日"
/><span></span>
<span style="margin: 0 6px; color: #999"></span>
<label style="min-width: 3em">終了:</label>
<input
type="number"
id="endYear"
min="2000"
max="2099"
style="width: 72px"
placeholder="年"
/><span></span>
<select id="endMonth" style="width: 60px">
<option value=""></option>
<option value="1">1月</option>
<option value="2">2月</option>
<option value="3">3月</option>
<option value="4">4月</option>
<option value="5">5月</option>
<option value="6">6月</option>
<option value="7">7月</option>
<option value="8">8月</option>
<option value="9">9月</option>
<option value="10">10月</option>
<option value="11">11月</option>
<option value="12">12月</option>
</select>
<input
type="number"
id="endDay"
min="1"
max="31"
style="width: 48px"
placeholder="日"
/><span></span>
</div>
<div
style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px"
>
<label>銀行:</label> <label>銀行:</label>
<select id="filterBank"> <select id="filterBank">
<option value="">全銀行</option> <option value="">全銀行</option>
</select> </select>
<button class="btn btn-primary" onclick="loadData()">検索</button> <button class="btn btn-primary" onclick="loadData()">検索</button>
<button class="btn btn-gray" onclick="clearDateFilter()">クリア</button>
</div>
</div> </div>
<!-- 月別チップ --> <!-- 月別チップ -->
@@ -638,16 +744,114 @@
} }
// ── 明細データ読み込み ────────────────────────────────── // ── 明細データ読み込み ──────────────────────────────────
function _lastDayOfMonth(year, month) {
return new Date(parseInt(year), parseInt(month), 0).getDate();
}
// 会社名をlocalStorageに保存・復元
const COMPANY_KEY = "bankPrintCompanyName";
document.addEventListener("DOMContentLoaded", function () {
const saved = localStorage.getItem(COMPANY_KEY) || "";
const input = document.getElementById("printCompanyInput");
if (input && saved) input.value = saved;
input &&
input.addEventListener("input", function () {
localStorage.setItem(COMPANY_KEY, this.value);
});
});
function doPrint() {
const company = (
document.getElementById("printCompanyInput").value || ""
).trim();
const bankVal = document.getElementById("filterBank").value;
// 期間ラベル生成
const sy = document.getElementById("startYear").value;
const sm = document.getElementById("startMonth").value;
const sd = document.getElementById("startDay").value;
const ey = document.getElementById("endYear").value;
const em = document.getElementById("endMonth").value;
const ed = document.getElementById("endDay").value;
let periodLabel = "";
if (sy || ey) {
const from = sy
? sy +
(sm ? "/" + String(sm).padStart(2, "0") : "") +
(sd ? "/" + String(sd).padStart(2, "0") : "")
: "";
const to = ey
? ey +
(em ? "/" + String(em).padStart(2, "0") : "") +
(ed ? "/" + String(ed).padStart(2, "0") : "")
: "";
if (from && to) periodLabel = " (" + from + " 〜 " + to + ")";
else if (from) periodLabel = " (" + from + " 〜)";
else if (to) periodLabel = " (〜 " + to + ")";
}
// 印刷ヘッダーを設定
document.getElementById("printCompany").textContent = company;
document.getElementById("printBank").textContent = bankVal
? "銀行:" + bankVal + periodLabel
: "期間:全期間" + periodLabel || "全期間";
const today = new Date();
const dateStr =
today.getFullYear() +
"年" +
(today.getMonth() + 1) +
"月" +
today.getDate() +
"日";
document.getElementById("printDate").textContent = "印刷日:" + dateStr;
window.print();
}
function clearDateFilter() {
[
"startYear",
"startMonth",
"startDay",
"endYear",
"endMonth",
"endDay",
].forEach((id) => {
const el = document.getElementById(id);
if (el) el.value = "";
});
loadData();
}
async function loadData() { async function loadData() {
const year = document.getElementById("filterYear").value; const sy = document.getElementById("startYear").value.trim();
const month = document.getElementById("filterMonth").value; const sm = document.getElementById("startMonth").value;
const sd = document.getElementById("startDay").value.trim();
const ey = document.getElementById("endYear").value.trim();
const em = document.getElementById("endMonth").value;
const ed = document.getElementById("endDay").value.trim();
const bank = document.getElementById("filterBank").value; const bank = document.getElementById("filterBank").value;
hideMsg("msgBar"); hideMsg("msgBar");
const qp = []; const qp = [];
if (year) qp.push(`year=${year}`); if (sy) {
if (month) qp.push(`month=${month}`); const m = sm ? String(sm).padStart(2, "0") : "01";
const d = sd ? String(sd).padStart(2, "0") : "01";
qp.push(`date_from=${sy}-${m}-${d}`);
}
if (ey) {
const m = em ? String(em).padStart(2, "0") : "12";
let d;
if (ed) {
d = String(ed).padStart(2, "0");
} else if (em) {
d = String(_lastDayOfMonth(ey, em)).padStart(2, "0");
} else {
d = "31";
}
qp.push(`date_to=${ey}-${m}-${d}`);
}
if (bank) qp.push(`bank_name=${encodeURIComponent(bank)}`); if (bank) qp.push(`bank_name=${encodeURIComponent(bank)}`);
const url = "/bank-statements" + (qp.length ? "?" + qp.join("&") : ""); const url = "/bank-statements" + (qp.length ? "?" + qp.join("&") : "");
@@ -658,7 +862,7 @@
_allRows = data.rows; _allRows = data.rows;
_currentPage = 1; _currentPage = 1;
renderMonthlyChips(data.monthly, year, month); renderMonthlyChips(data.monthly, { sy, sm, sd, ey, em, ed });
renderStats(); renderStats();
renderTable(); renderTable();
updateBankFilter(data.banks); updateBankFilter(data.banks);
@@ -682,18 +886,23 @@
} }
// ── 月別チップ ────────────────────────────────────────── // ── 月別チップ ──────────────────────────────────────────
function renderMonthlyChips(monthly, selYear, selMonth) { function renderMonthlyChips(monthly, filter) {
const { sy, sm, sd, ey, em, ed } = filter || {};
const wrap = document.getElementById("monthlyChips"); const wrap = document.getElementById("monthlyChips");
wrap.innerHTML = ""; wrap.innerHTML = "";
if (!monthly.length) return; if (!monthly.length) return;
// 「全月」チップ // 「全月」チップ:開始・終了が同じ年で月未指定の場合にアクティブ
const noMonthSelected = !sm && !em && !sd && !ed;
const allChip = document.createElement("span"); const allChip = document.createElement("span");
allChip.className = allChip.className =
"month-chip no-print" + (!selMonth ? " active" : ""); "month-chip no-print" + (noMonthSelected ? " active" : "");
allChip.textContent = "全月"; allChip.textContent = "全月";
allChip.onclick = () => { allChip.onclick = () => {
document.getElementById("filterMonth").value = ""; document.getElementById("startMonth").value = "";
document.getElementById("startDay").value = "";
document.getElementById("endMonth").value = "";
document.getElementById("endDay").value = "";
loadData(); loadData();
}; };
wrap.appendChild(allChip); wrap.appendChild(allChip);
@@ -702,15 +911,22 @@
const [y, mo] = m.ym.split("-"); const [y, mo] = m.ym.split("-");
const chip = document.createElement("span"); const chip = document.createElement("span");
const isActive = const isActive =
String(selYear) === y && String(selMonth) === String(parseInt(mo)); sy === y &&
ey === y &&
String(parseInt(sm || "0")) === String(parseInt(mo)) &&
String(parseInt(em || "0")) === String(parseInt(mo)) &&
!sd &&
!ed;
chip.className = "month-chip no-print" + (isActive ? " active" : ""); chip.className = "month-chip no-print" + (isActive ? " active" : "");
chip.innerHTML = `${parseInt(mo)}月 <small style="color:inherit;opacity:.8">${ chip.innerHTML = `${parseInt(mo)}月 <small style="color:inherit;opacity:.8">${m.count}件</small>`;
m.count
}件</small>`;
chip.title = `出金 ${fmt(m.total_debit)} / 入金 ${fmt(m.total_credit)}`; chip.title = `出金 ${fmt(m.total_debit)} / 入金 ${fmt(m.total_credit)}`;
chip.onclick = () => { chip.onclick = () => {
document.getElementById("filterYear").value = y; document.getElementById("startYear").value = y;
document.getElementById("filterMonth").value = parseInt(mo); document.getElementById("startMonth").value = parseInt(mo);
document.getElementById("startDay").value = "";
document.getElementById("endYear").value = y;
document.getElementById("endMonth").value = parseInt(mo);
document.getElementById("endDay").value = "";
loadData(); loadData();
}; };
wrap.appendChild(chip); wrap.appendChild(chip);

View File

@@ -376,5 +376,4 @@ html {
padding: 20px 16px !important; padding: 20px 16px !important;
box-sizing: border-box !important; box-sizing: border-box !important;
} }
} }

View File

@@ -279,6 +279,72 @@
background: #f3eeff; background: #f3eeff;
color: #6f42c1; color: #6f42c1;
} }
/* テーブル */
.tbl-wrap {
overflow-x: auto;
margin-top: 12px;
}
table.apply-tbl {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
table.apply-tbl th,
table.apply-tbl td {
border: 1px solid #dee2e6;
padding: 5px 8px;
white-space: nowrap;
vertical-align: top;
}
table.apply-tbl th {
background: #f1f3f5;
font-weight: 700;
}
table.apply-tbl tr:hover td {
background: #f8f9fa;
}
.proc-select {
width: 100%;
padding: 8px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 13px;
margin-bottom: 10px;
}
.arrive-badge {
font-weight: 700;
color: #0a3622;
background: #d1e7dd;
padding: 2px 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
cursor: pointer;
}
.info-box {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
padding: 10px 14px;
font-size: 12px;
line-height: 1.7;
margin-bottom: 14px;
}
.env-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 700;
}
.env-sandbox {
background: #fff3cd;
color: #856404;
}
.env-production {
background: #d1e7dd;
color: #0a3622;
}
</style> </style>
</head> </head>
<body> <body>
@@ -422,6 +488,193 @@
</div> </div>
</div> </div>
<!-- ─── 申請書作成(最終確認試験用) ───────────────────── -->
<div class="card" id="applyCard">
<h2>📝 テスト申請書作成(最終確認試験用)</h2>
<p class="desc">
検証環境へテスト手続を送信して到達番号を取得します。<br />
取得した到達番号は試験データ表と Excel
に記入してデジタル庁へ送付してください。
</p>
<div class="info-box">
⚠️ ログイン後に操作可能です。<br />
環境:
<span id="envLabel" class="env-badge env-sandbox">sandbox検証</span>
&nbsp; ソフトウェアID:
<code id="swIdLabel" style="font-size: 11px"></code>
</div>
<div class="form-group">
<label>手続識別子(対象手続を選択)</label>
<select class="proc-select" id="procSelect">
<optgroup label="電子送達関係">
<option value="900A013800001000">
900A013800001000 APIテスト用手続電子送達0001電子送達1件
</option>
</optgroup>
<optgroup label="社会保険関係 標準形式">
<option value="950A010002010000">
950A010002010000 APIテスト社保通1007 公文書(署名あり)
</option>
<option value="950A010002011000">
950A010002011000 APIテスト社保通1008 公文書(署名なし)
</option>
<option value="950A010700001000">
950A010700001000 APIテスト社保通1001 手続終了
</option>
<option value="950A010700002000">
950A010700002000 APIテスト社保通1002 手続終了
</option>
<option value="950A010700005000">
950A010700005000 APIテスト社保通1003 取下げ承認
</option>
<option value="950A010700006000">
950A010700006000 APIテスト社保通1004 取下げ却下
</option>
</optgroup>
<optgroup label="労働保険関係 標準形式">
<option value="950A010200003000">
950A010200003000 APIテスト労保通1001 手続終了
</option>
<option value="950A010200004000">
950A010200004000 APIテスト労保通1002 手続終了
</option>
<option value="950A010002012000">
950A010002012000 APIテスト労保通1004 コメント(メッセージ)
</option>
<option value="950A010002016000">
950A010002016000 APIテスト労保通1008 コメント(公文書)
</option>
</optgroup>
<optgroup label="社会保険関係 個別署名形式">
<option value="950A101220029000">
950A101220029000 APIテスト社保個1001 手続終了
</option>
<option value="950A102200038000">
950A102200038000 APIテスト社保個1005 コメント(メッセージ)
</option>
<option value="950A102200039000">
950A102200039000 APIテスト社保個1006 公文書(署名あり)
</option>
</optgroup>
<optgroup label="その他">
<option value="900A020700013000">
900A020700013000 APIテスト労保適用0003 補正申請
</option>
<option value="900A102800072000">
900A102800072000 APIテスト労保適用個0014 プレ印字
</option>
</optgroup>
</select>
</div>
<div
style="
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 14px;
"
>
<button
class="btn btn-primary"
id="applyBtn"
onclick="doTestApply()"
style="background: #6f42c1"
>
📤 テスト申請を送信
</button>
<span
id="applySpinner"
style="display: none; font-size: 13px; color: #555"
>⏳ 送信中...</span
>
</div>
<!-- 結果テーブル -->
<div id="applyResult" style="display: none">
<div class="dl-result" style="margin-bottom: 10px">
<span>✅ 到達番号: </span>
<strong
id="arriveIdResult"
class="arrive-badge"
title="クリックでコピー"
onclick="copyArriveId()"
></strong>
<span
style="font-size: 12px; color: #6c757d; margin-left: 8px"
id="arriveDateResult"
></span>
</div>
<div style="font-size: 12px; color: #555" id="procNameResult"></div>
</div>
<div
id="applyError"
style="
display: none;
color: #842029;
font-size: 13px;
margin-top: 8px;
padding: 8px 12px;
background: #f8d7da;
border-radius: 4px;
"
></div>
</div>
<!-- ─── 申請案件一覧 ─────────────────────────────────────── -->
<div class="card" id="listCard">
<h2>📋 申請案件一覧(到達番号確認)</h2>
<p class="desc">
送信済みの申請案件一覧を表示します。到達番号を Excel
に転記してデジタル庁へ提出してください。
</p>
<div
style="display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap"
>
<button class="btn btn-secondary" onclick="loadApplyList()">
🔄 一覧を更新
</button>
<button
class="btn"
onclick="exportApplyListCsv()"
style="background: #0d6efd; color: #fff"
>
📥 CSV エクスポート
</button>
</div>
<div
id="listStatus"
style="font-size: 13px; color: #6c757d; margin-bottom: 8px"
></div>
<div class="tbl-wrap" id="applyListWrap">
<table class="apply-tbl">
<thead>
<tr>
<th>#</th>
<th>到達番号</th>
<th>到達日時</th>
<th>手続識別子</th>
<th>手続名</th>
<th>ステータス</th>
<th>サブステータス</th>
</tr>
</thead>
<tbody id="applyListBody">
<tr>
<td
colspan="7"
style="text-align: center; color: #aaa; padding: 20px"
>
「一覧を更新」ボタンで取得してください
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script> <script>
let timer = null; let timer = null;
let currentTab = "api"; let currentTab = "api";
@@ -677,10 +930,174 @@
const r = await fetch("/egov/status"); const r = await fetch("/egov/status");
const d = await r.json(); const d = await r.json();
apply(d); apply(d);
// 環境・ソフトウェアID を表示
document.getElementById("envLabel").textContent =
d.environment === "sandbox"
? "sandbox検証環境"
: "production本番環境";
document.getElementById("envLabel").className =
"env-badge " +
(d.environment === "sandbox" ? "env-sandbox" : "env-production");
if (!["idle", "logged_in", "error"].includes(d.status)) if (!["idle", "logged_in", "error"].includes(d.status))
startPolling(); startPolling();
} catch (_) {} } catch (_) {}
// ソフトウェアID 表示
try {
const r2 = await fetch("/egov/config-info");
if (r2.ok) {
const cfg = await r2.json();
document.getElementById("swIdLabel").textContent =
cfg.software_id || "—";
}
} catch (_) {}
})(); })();
// ─── テスト申請書作成 ────────────────────────────────────
let _applyResults = [];
async function doTestApply() {
const procId = document.getElementById("procSelect").value;
const applyBtn = document.getElementById("applyBtn");
const spinner = document.getElementById("applySpinner");
const resultDiv = document.getElementById("applyResult");
const errorDiv = document.getElementById("applyError");
applyBtn.disabled = true;
show("applySpinner");
resultDiv.style.display = "none";
errorDiv.style.display = "none";
try {
const res = await fetch("/egov/test-apply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proc_id: procId }),
});
const data = await res.json();
if (!res.ok) {
errorDiv.textContent =
"エラー: " + (data.detail || JSON.stringify(data));
errorDiv.style.display = "block";
return;
}
document.getElementById("arriveIdResult").textContent =
data.arrive_id || "(取得中)";
document.getElementById("arriveDateResult").textContent =
data.arrive_date ? ` 到達: ${data.arrive_date}` : "";
document.getElementById("procNameResult").textContent =
data.proc_name || "";
resultDiv.style.display = "block";
// 送信済みリストに追加
_applyResults.unshift({
arrive_id: data.arrive_id,
arrive_date: data.arrive_date,
proc_id: procId,
proc_name: data.proc_name,
status: "",
sub_status: "",
});
renderApplyList(_applyResults);
} catch (err) {
errorDiv.textContent = "通信エラー: " + err.message;
errorDiv.style.display = "block";
} finally {
applyBtn.disabled = false;
hide("applySpinner");
}
}
function copyArriveId() {
const val = document.getElementById("arriveIdResult").textContent;
navigator.clipboard
.writeText(val)
.then(() => alert("コピーしました: " + val))
.catch(() => {});
}
// ─── 申請案件一覧 ────────────────────────────────────────
async function loadApplyList() {
const statusDiv = document.getElementById("listStatus");
statusDiv.textContent = "⏳ 一覧を取得中...";
try {
const res = await fetch("/egov/apply-list");
if (!res.ok) {
const e = await res.json();
statusDiv.textContent = "エラー: " + (e.detail || "不明");
return;
}
const data = await res.json();
_applyResults = data.apply_list || [];
renderApplyList(_applyResults);
statusDiv.textContent = `${data.total}`;
} catch (err) {
statusDiv.textContent = "通信エラー: " + err.message;
}
}
function renderApplyList(items) {
const tbody = document.getElementById("applyListBody");
if (!items.length) {
tbody.innerHTML =
'<tr><td colspan="7" style="text-align:center;color:#aaa;padding:20px;">申請案件はありません</td></tr>';
return;
}
tbody.innerHTML = items
.map(
(it, i) => `
<tr>
<td>${i + 1}</td>
<td><span class="arrive-badge" onclick="navigator.clipboard.writeText('${esc(it.arrive_id)}')" title="クリックでコピー">${esc(it.arrive_id)}</span></td>
<td>${esc(it.arrive_date)}</td>
<td style="font-family:monospace;font-size:11px">${esc(it.proc_id)}</td>
<td>${esc(it.proc_name)}</td>
<td>${esc(it.status)}</td>
<td>${esc(it.sub_status)}</td>
</tr>
`,
)
.join("");
}
function esc(s) {
return String(s || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function exportApplyListCsv() {
if (!_applyResults.length) {
alert("データがありません");
return;
}
const header = [
"到達番号",
"到達日時",
"手続識別子",
"手続名",
"ステータス",
"サブステータス",
];
const rows = _applyResults.map((it) =>
[
it.arrive_id,
it.arrive_date,
it.proc_id,
it.proc_name,
it.status,
it.sub_status,
]
.map((v) => `"${String(v || "").replace(/"/g, '""')}"`)
.join(","),
);
const csv = "\uFEFF" + [header.join(","), ...rows].join("\r\n");
const link = document.createElement("a");
link.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
link.download = "egov_apply_list.csv";
link.click();
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -254,6 +254,41 @@ function initTrialBalance() {
} }
} }
// 年度ドロップダウンを読み込む
async function loadFiscalYears() {
try {
const res = await fetch("/trial-balance/fiscal-years");
if (!res.ok) return;
const years = await res.json();
const sel = document.getElementById("fiscalYear");
if (!sel) return;
years.forEach((fy) => {
const opt = document.createElement("option");
opt.value = JSON.stringify({
date_from: fy.date_from,
date_to: fy.date_to,
});
opt.textContent = fy.label;
sel.appendChild(opt);
});
} catch (e) {
console.warn("年度一覧取得失敗:", e);
}
}
loadFiscalYears();
// 年度選択 → 開始・終了日を自動セット(ユーザーは自由に変更可能)
document.getElementById("fiscalYear").addEventListener("change", function () {
if (!this.value) return;
try {
const { date_from, date_to } = JSON.parse(this.value);
document.getElementById("dateFrom").value = date_from;
document.getElementById("dateTo").value = date_to;
} catch (e) {
/* ignore */
}
});
// 檢索按鈕的事件 // 檢索按鈕的事件
document.getElementById("btnSearch").addEventListener("click", () => { document.getElementById("btnSearch").addEventListener("click", () => {
const dateFrom = document.getElementById("dateFrom").value; const dateFrom = document.getElementById("dateFrom").value;

View File

@@ -1331,7 +1331,7 @@
</div> </div>
<div class="calc-note" style="background:#eef6ee; border-left:3px solid #28a745; padding:6px 10px; margin:6px 0; font-size:0.88em; color:#1a5c1a; line-height:1.7"> <div class="calc-note" style="background:#eef6ee; border-left:3px solid #28a745; padding:6px 10px; margin:6px 0; font-size:0.88em; color:#1a5c1a; line-height:1.7">
健康保険 ¥${_health.toLocaleString()} 介護保険 ¥${_care.toLocaleString()} 子ども・子育て支援金 ¥${_child.toLocaleString()} <strong>¥${_healthCareChild.toLocaleString()}</strong><br> 健康保険 ¥${_health.toLocaleString()} 介護保険 ¥${_care.toLocaleString()} 子ども・子育て支援金 ¥${_child.toLocaleString()} <strong>¥${_healthCareChild.toLocaleString()}</strong><br>
¥${_healthCareChild.toLocaleString()}(上記合計) 厚生年金 ¥${_pension.toLocaleString()}${' '}${_emp > 0 ? ` 雇用保険 ¥${_emp.toLocaleString()}` : ''} <strong>社会保険小計 ¥${_socialSub.toLocaleString()}</strong> ¥${_healthCareChild.toLocaleString()}(上記合計) 厚生年金 ¥${_pension.toLocaleString()}${" "}${_emp > 0 ? ` 雇用保険 ¥${_emp.toLocaleString()}` : ""} <strong>社会保険小計 ¥${_socialSub.toLocaleString()}</strong>
</div> </div>
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_socialSub.toLocaleString()}</span></div> <div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_socialSub.toLocaleString()}</span></div>
<div class="detail-row"> <div class="detail-row">
@@ -1379,7 +1379,12 @@
document.getElementById("payrollDetail").style.display = "block"; document.getElementById("payrollDetail").style.display = "block";
// モバイルでは明細パネルへ自動スクロール // モバイルでは明細パネルへ自動スクロール
if (window.innerWidth <= 767 && splitRightSalary) { if (window.innerWidth <= 767 && splitRightSalary) {
setTimeout(function() { splitRightSalary.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50); setTimeout(function () {
splitRightSalary.scrollIntoView({
behavior: "smooth",
block: "start",
});
}, 50);
} }
} catch (error) { } catch (error) {
alert("給与明細の取得に失敗しました"); alert("給与明細の取得に失敗しました");
@@ -1918,7 +1923,7 @@
</div> </div>
<div class="calc-note" style="background:#eef6ee; border-left:3px solid #28a745; padding:6px 10px; margin:6px 0; font-size:0.88em; color:#1a5c1a; line-height:1.7"> <div class="calc-note" style="background:#eef6ee; border-left:3px solid #28a745; padding:6px 10px; margin:6px 0; font-size:0.88em; color:#1a5c1a; line-height:1.7">
健康保険 ¥${_bHealth.toLocaleString()} 介護保険 ¥${_bCare.toLocaleString()} 子ども・子育て支援金 ¥${_bChild.toLocaleString()} <strong>¥${_bHealthCareChild.toLocaleString()}</strong><br> 健康保険 ¥${_bHealth.toLocaleString()} 介護保険 ¥${_bCare.toLocaleString()} 子ども・子育て支援金 ¥${_bChild.toLocaleString()} <strong>¥${_bHealthCareChild.toLocaleString()}</strong><br>
¥${_bHealthCareChild.toLocaleString()}(上記合計) 厚生年金 ¥${_bPension.toLocaleString()}${' '}${_bEmp > 0 ? ` 雇用保険 ¥${_bEmp.toLocaleString()}` : ''} <strong>社会保険小計 ¥${_bSocialSub.toLocaleString()}</strong> ¥${_bHealthCareChild.toLocaleString()}(上記合計) 厚生年金 ¥${_bPension.toLocaleString()}${" "}${_bEmp > 0 ? ` 雇用保険 ¥${_bEmp.toLocaleString()}` : ""} <strong>社会保険小計 ¥${_bSocialSub.toLocaleString()}</strong>
</div> </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 style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_bSocialSub.toLocaleString()}</span></div>
<div class="detail-row"> <div class="detail-row">
@@ -1964,7 +1969,12 @@
document.getElementById("bonusDetail").style.display = "block"; document.getElementById("bonusDetail").style.display = "block";
// モバイルでは明細パネルへ自動スクロール // モバイルでは明細パネルへ自動スクロール
if (window.innerWidth <= 767 && splitRightBonus) { if (window.innerWidth <= 767 && splitRightBonus) {
setTimeout(function() { splitRightBonus.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50); setTimeout(function () {
splitRightBonus.scrollIntoView({
behavior: "smooth",
block: "start",
});
}, 50);
} }
} catch (error) { } catch (error) {
alert("賞与明細の取得に失敗しました"); alert("賞与明細の取得に失敗しました");
@@ -2638,19 +2648,33 @@
// CSVデータを生成 // CSVデータを生成
let csvContent = let csvContent =
"データ,従業員コード,従業員名,年月,支給額,控除額,差引支給額\n"; "データ,従業員コード,従業員名,年月,支給額,社会保険,所得税,控除額合計,差引支給額\n";
// 給与データ // 給与データ
if (result.data.salary) { if (result.data.salary) {
(result.data.salary || []).forEach((salary) => { (result.data.salary || []).forEach((salary) => {
csvContent += `給与,${salary.employee_code},${salary.employee_name},${salary.payroll_year}-${String(salary.payroll_month).padStart(2, "0")},${salary.total_payment},${salary.total_deduction},${salary.net_payment}\n`; const socialIns =
Number(salary.health_insurance || 0) +
Number(salary.care_insurance || 0) +
Number(salary.pension_insurance || 0) +
Number(salary.employment_insurance || 0) +
Number(salary.child_support || 0);
const incomeTax = Number(salary.income_tax || 0);
csvContent += `給与,${salary.employee_code},${salary.employee_name},${salary.payroll_year}-${String(salary.payroll_month).padStart(2, "0")},${salary.total_payment},${socialIns},${incomeTax},${salary.total_deduction},${salary.net_payment}\n`;
}); });
} }
// 賞与データ // 賞与データ
if (result.data.bonus) { if (result.data.bonus) {
(result.data.bonus || []).forEach((bonus) => { (result.data.bonus || []).forEach((bonus) => {
csvContent += `賞与,${bonus.employee_code},${bonus.employee_name},${bonus.bonus_year}-${String(bonus.bonus_month).padStart(2, "0")},${bonus.total_bonus},${bonus.total_deduction},${bonus.net_bonus}\n`; const socialIns =
Number(bonus.health_insurance || 0) +
Number(bonus.care_insurance || 0) +
Number(bonus.pension_insurance || 0) +
Number(bonus.employment_insurance || 0) +
Number(bonus.child_support || 0);
const incomeTax = Number(bonus.income_tax || 0);
csvContent += `賞与,${bonus.employee_code},${bonus.employee_name},${bonus.bonus_year}-${String(bonus.bonus_month).padStart(2, "0")},${bonus.total_bonus},${socialIns},${incomeTax},${bonus.total_deduction},${bonus.net_bonus}\n`;
}); });
} }

View File

@@ -292,6 +292,20 @@
</div> </div>
<div class="search-form"> <div class="search-form">
<label for="fiscalYear">年度:</label>
<select
id="fiscalYear"
style="
padding: 4px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 13px;
min-width: 200px;
"
>
<option value="">-- 選択してください --</option>
</select>
<label for="dateFrom">開始年月日:</label> <label for="dateFrom">開始年月日:</label>
<input type="date" id="dateFrom" min="1900-01-01" max="2099-12-31" /> <input type="date" id="dateFrom" min="1900-01-01" max="2099-12-31" />

View File

@@ -19,6 +19,12 @@ files = [
f"{NAS_BASE}/backend/app/db_auto_migration.py"), f"{NAS_BASE}/backend/app/db_auto_migration.py"),
(r"c:\workspace\njts-accounting-core\backend\app\main.py", (r"c:\workspace\njts-accounting-core\backend\app\main.py",
f"{NAS_BASE}/backend/app/main.py"), f"{NAS_BASE}/backend/app/main.py"),
# e-Gov モジュール
(r"c:\workspace\njts-accounting-core\backend\app\modules\egov\egov_api.py",
f"{NAS_BASE}/backend/app/modules/egov/egov_api.py"),
(r"c:\workspace\njts-accounting-core\backend\app\modules\egov\router.py",
f"{NAS_BASE}/backend/app/modules/egov/router.py"),
# フロントエンド
(r"c:\workspace\njts-accounting-core\frontend\bank-statement.html", (r"c:\workspace\njts-accounting-core\frontend\bank-statement.html",
f"{NAS_BASE}/backend/frontend/bank-statement.html"), f"{NAS_BASE}/backend/frontend/bank-statement.html"),
(r"c:\workspace\njts-accounting-core\frontend\index.html", (r"c:\workspace\njts-accounting-core\frontend\index.html",

62
read_openapi.py Normal file
View File

@@ -0,0 +1,62 @@
import json
with open(r"docs\openapi_検証.json", encoding="utf-8") as f:
api = json.load(f)
components = api.get("components", {}).get("schemas", {})
# results_apply の内容
ra = components.get("results_apply", {})
props = ra.get("properties", {})
print("=== results_apply ===")
for k, v in props.items():
desc = v.get("description", "")[:100]
print(f" {k}: {v.get('type','?')} - {desc}")
# apply/lists のパラメータ
print("\n=== /apply/lists parameters ===")
get = api["paths"]["/apply/lists"].get("get", {})
for p in get.get("parameters", []):
pname = p.get("name", "")
pdesc = p.get("description", "")[:60]
print(f" {pname} ({p.get('in','')}): {pdesc}")
# results_apply_lists
ral = components.get("results_apply_lists", {})
print("results_apply_lists props:", list(ral.get("properties", {}).keys())[:10])
TARGET_PATHS = ["/apply", "/apply/lists", "/apply/{arrive_id}", "/post/lists", "/post/{post_id}", "/procedure/{proc_id}"]
for path in TARGET_PATHS:
path_obj = api["paths"].get(path, {})
if not path_obj:
continue
print(f"\n=== {path} ===")
for method in ["get", "post", "put", "delete"]:
if method not in path_obj:
continue
details = path_obj[method]
if not isinstance(details, dict):
continue
summary = details.get("summary", "")
print(f" {method.upper()}: {summary}")
rb = details.get("requestBody", {})
if rb:
content = rb.get("content", {})
print(f" request content-types: {list(content.keys())}")
for ct, schema_wrap in content.items():
schema = schema_wrap.get("schema", {})
props = schema.get("properties", {})
required = schema.get("required", [])
print(f" required: {required}")
for pname, pdef in list(props.items())[:10]:
desc = str(pdef.get("description", ""))[:60]
print(f" {pname}: {pdef.get('type','?')} - {desc}")
resp200 = details.get("responses", {}).get("200", {})
resp_content = resp200.get("content", {})
if resp_content:
for ct, schema_wrap in resp_content.items():
schema = schema_wrap.get("schema", {})
results_schema = schema.get("properties", {}).get("results", {})
results_props = results_schema.get("properties", {})
print(f" response results props: {list(results_props.keys())[:10]}")

94
test_org_id.py Normal file
View File

@@ -0,0 +1,94 @@
import sys, os, base64, zipfile, io, re, json, asyncio
sys.path.insert(0, 'backend')
import httpx
import paramiko
def _fill_empty_tag(text, tag, value):
text = re.sub(r'<' + re.escape(tag) + r'\s*/>', f'<{tag}>{value}</{tag}>', text)
text = re.sub(r'<' + re.escape(tag) + r'\s*>\s*</' + re.escape(tag) + r'>', f'<{tag}>{value}</{tag}>', text)
return text
def modify_skeleton(file_data_b64, org_id, apply_type):
raw = base64.b64decode(file_data_b64)
buf_out = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(raw), 'r') as zin:
with zipfile.ZipFile(buf_out, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
name_lower = item.filename.lower()
if name_lower.endswith('/kousei.xml') or name_lower == 'kousei.xml':
text = data.decode('utf-8', errors='replace')
fills = [
('受付行政機関ID', org_id),
('手続ID', '900A013800001000'),
('手続名称', 'APIテスト用手続(電子送達関係手続)(通)0001'),
('申請種別', apply_type),
('氏名フリガナ', 'テスト タロウ'),
('氏名', 'テスト タロウ'),
('郵便番号', '1300022'),
('住所', '東京都墨田区江東橋4丁目31番10号'),
]
for tag, value in fills:
text = _fill_empty_tag(text, tag, value)
data = text.encode('utf-8')
zout.writestr(item, data)
return base64.b64encode(buf_out.getvalue()).decode()
async def test(org_id, access_token, file_data_b64):
modified = modify_skeleton(file_data_b64, org_id, '新規申請')
async with httpx.AsyncClient() as client:
r = await client.post(
'https://api2.sbx.e-gov.go.jp/shinsei/v2/post-apply',
headers={
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
},
json={
'proc_id': '900A013800001000',
'send_file': {
'file_name': '900A013800001000.zip',
'file_data': modified,
},
},
timeout=120,
)
print(f'org_id={org_id}: HTTP {r.status_code}')
try:
body = r.json()
if r.is_success:
print('SUCCESS:', json.dumps(body, ensure_ascii=False))
else:
report = body.get('report_list', [])
for rep in report:
print(' ERROR:', rep.get('content', ''))
if not report:
print(' BODY:', json.dumps(body, ensure_ascii=False)[:300])
except Exception:
print('BODY:', r.text[:500])
return r.status_code
async def main():
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('192.168.0.61', username='root', password='59911784')
stdin, stdout, stderr = ssh.exec_command('cat /volume1/docker/njts-accounting/backend/egov_data/tokens.json')
tokens = json.loads(stdout.read())
access_token = tokens['access_token']
ssh.close()
# スケルトン取得
resp = httpx.get(
'https://api2.sbx.e-gov.go.jp/shinsei/v2/procedure/900A013800001000',
headers={'Authorization': f'Bearer {access_token}'},
timeout=30
)
file_data_b64 = resp.json()['results']['file_data']
for org_id in ['100001', '100138', '100900', '100000', '100013']:
sc = await test(org_id, access_token, file_data_b64)
if sc == 200:
break
asyncio.run(main())

89
test_proc_name.py Normal file
View File

@@ -0,0 +1,89 @@
import re, json, httpx, paramiko, base64, zipfile, io, asyncio
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('192.168.0.61', username='root', password='59911784')
stdin, stdout, stderr = ssh.exec_command('cat /volume1/docker/njts-accounting/backend/egov_data/tokens.json')
tokens = json.loads(stdout.read())
access_token = tokens['access_token']
ssh.close()
resp = httpx.get(
'https://api2.sbx.e-gov.go.jp/shinsei/v2/procedure/900A013800001000',
headers={'Authorization': f'Bearer {access_token}'},
timeout=30
)
file_data_b64 = resp.json()['results']['file_data']
candidates = [
'APIテスト用手続(電子送達関係手続)(通)0001',
'APIテスト用手続(電子送達関係手続)(通)0001_01',
'電子送付開始手続き',
'【オンライン事業所年金情報サービス】電子送付開始手続き',
'APIテスト用手続(電子送達関係手続)',
'APIテスト用手続(電子送達関係手続)(通)',
'オンライン事業所年金情報サービス 電子送付開始手続き',
'APIテスト用手続(電子送達関係手続)(通)0001 電子送付開始手続き',
]
def fill_tag(text, tag, value):
text = re.sub(r'<' + re.escape(tag) + r'\s*/>', f'<{tag}>{value}</{tag}>', text)
text = re.sub(r'<' + re.escape(tag) + r'\s*>\s*</' + re.escape(tag) + r'>', f'<{tag}>{value}</{tag}>', text)
return text
def build_zip(file_data_b64, proc_name):
raw = base64.b64decode(file_data_b64)
buf = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(raw)) as zin:
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if 'kousei' in item.filename.lower():
text = data.decode('utf-8', 'replace')
for tag, val in [
('受付行政機関ID', '100001'),
('手続ID', '900A013800001000'),
('手続名称', proc_name),
('申請種別', '新規申請'),
('氏名フリガナ', 'テスト タロウ'),
('氏名', 'テスト タロウ'),
('郵便番号', '1300022'),
('住所', '東京都墨田区江東橋4丁目31番10号'),
]:
text = fill_tag(text, tag, val)
data = text.encode('utf-8')
zout.writestr(item, data)
return base64.b64encode(buf.getvalue()).decode()
async def try_name(proc_name):
modified = build_zip(file_data_b64, proc_name)
async with httpx.AsyncClient() as client:
r = await client.post(
'https://api2.sbx.e-gov.go.jp/shinsei/v2/post-apply',
headers={'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'},
json={'proc_id': '900A013800001000', 'send_file': {'file_name': '900A013800001000.zip', 'file_data': modified}},
timeout=60,
)
name_short = repr(proc_name[:35])
print(f'name={name_short}: HTTP {r.status_code}')
if r.is_success:
print(' SUCCESS:', json.dumps(r.json(), ensure_ascii=False)[:200])
else:
body = r.json()
errs = [rep.get('content', '') for rep in body.get('report_list', [])]
if errs:
for e in errs:
print(f' ERR: {e}')
else:
print(f' BODY: {json.dumps(body, ensure_ascii=False)[:200]}')
return r.status_code
async def main():
for name in candidates:
sc = await try_name(name)
if sc == 200:
break
asyncio.run(main())

BIN
testproclist.xlsx Normal file

Binary file not shown.