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:
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:
print(f"\n❌ 無法連接到數據庫或執行遷移: {e}")
print("\n提示:")

View File

@@ -321,18 +321,28 @@ def list_statements(
year: Optional[int] = None,
month: Optional[int] = None,
bank_name: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
limit: int = 10000,
offset: int = 0,
):
conditions: list = []
params: list = []
if year:
conditions.append("EXTRACT(YEAR FROM tx_date)::int = %s")
params.append(year)
if month:
conditions.append("EXTRACT(MONTH FROM tx_date)::int = %s")
params.append(month)
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:
conditions.append("EXTRACT(YEAR FROM tx_date)::int = %s")
params.append(year)
if month:
conditions.append("EXTRACT(MONTH FROM tx_date)::int = %s")
params.append(month)
if bank_name:
conditions.append("bank_name = %s")
params.append(bank_name)

View File

@@ -8,6 +8,9 @@ import secrets
import os
import json
import time
import re
import io
import zipfile
from pathlib import Path
from typing import Optional
@@ -233,3 +236,243 @@ async def mark_post_done(access_token: str, post_id: str) -> dict:
)
resp.raise_for_status()
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
class ApplyRequest(BaseModel):
proc_id: str # 16文字の手続識別子
class ConfigRequest(BaseModel):
software_id: str
api_key: str
@@ -245,6 +249,18 @@ async def save_config(req: ConfigRequest):
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")
async def get_auth_url():
"""OAuth2 + PKCE 認可 URL を生成して返す"""
@@ -370,3 +386,154 @@ async def debug_page():
return {"browser_state": state.status, **diag}
except Exception as e:
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 typing import Dict, Any, List, Optional
from app.modules.trial_balance.service import fetch_trial_balance
from app.core.database import get_connection
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="試算表(全科目)")
def get_trial_balance(
fiscal_year: Optional[int] = Query(None, description="会計年度(例: 2025"),

View File

@@ -97,3 +97,53 @@ def get_trial_balance(
rows = cur.fetchall()
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;
}
/* ── 印刷ヘッダー ── */
.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 {
.no-print {
@@ -434,50 +457,133 @@
</style>
</head>
<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">
<h2>🏦 銀行明細照会</h2>
<div class="header-btns">
<button class="btn btn-orange" onclick="openImportModal()">
CSV取込
</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>
</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 class="search-form no-print">
<label>年:</label>
<input
type="number"
id="filterYear"
min="2000"
max="2099"
style="width: 80px"
/>
<label>月:</label>
<select id="filterMonth">
<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>
<label>銀行:</label>
<select id="filterBank">
<option value="">全銀行</option>
</select>
<button class="btn btn-primary" onclick="loadData()">検索</button>
<div
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
type="number"
id="startYear"
min="2000"
max="2099"
style="width: 72px"
placeholder="年"
/><span></span>
<select id="startMonth" 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="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>
<select id="filterBank">
<option value="">全銀行</option>
</select>
<button class="btn btn-primary" onclick="loadData()">検索</button>
<button class="btn btn-gray" onclick="clearDateFilter()">クリア</button>
</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() {
const year = document.getElementById("filterYear").value;
const month = document.getElementById("filterMonth").value;
const sy = document.getElementById("startYear").value.trim();
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;
hideMsg("msgBar");
const qp = [];
if (year) qp.push(`year=${year}`);
if (month) qp.push(`month=${month}`);
if (sy) {
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)}`);
const url = "/bank-statements" + (qp.length ? "?" + qp.join("&") : "");
@@ -658,7 +862,7 @@
_allRows = data.rows;
_currentPage = 1;
renderMonthlyChips(data.monthly, year, month);
renderMonthlyChips(data.monthly, { sy, sm, sd, ey, em, ed });
renderStats();
renderTable();
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");
wrap.innerHTML = "";
if (!monthly.length) return;
// 「全月」チップ
// 「全月」チップ:開始・終了が同じ年で月未指定の場合にアクティブ
const noMonthSelected = !sm && !em && !sd && !ed;
const allChip = document.createElement("span");
allChip.className =
"month-chip no-print" + (!selMonth ? " active" : "");
"month-chip no-print" + (noMonthSelected ? " active" : "");
allChip.textContent = "全月";
allChip.onclick = () => {
document.getElementById("filterMonth").value = "";
document.getElementById("startMonth").value = "";
document.getElementById("startDay").value = "";
document.getElementById("endMonth").value = "";
document.getElementById("endDay").value = "";
loadData();
};
wrap.appendChild(allChip);
@@ -702,15 +911,22 @@
const [y, mo] = m.ym.split("-");
const chip = document.createElement("span");
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.innerHTML = `${parseInt(mo)}月 <small style="color:inherit;opacity:.8">${
m.count
}件</small>`;
chip.innerHTML = `${parseInt(mo)}月 <small style="color:inherit;opacity:.8">${m.count}件</small>`;
chip.title = `出金 ${fmt(m.total_debit)} / 入金 ${fmt(m.total_credit)}`;
chip.onclick = () => {
document.getElementById("filterYear").value = y;
document.getElementById("filterMonth").value = parseInt(mo);
document.getElementById("startYear").value = y;
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();
};
wrap.appendChild(chip);

View File

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

View File

@@ -279,6 +279,72 @@
background: #f3eeff;
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>
</head>
<body>
@@ -422,6 +488,193 @@
</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>
let timer = null;
let currentTab = "api";
@@ -677,10 +930,174 @@
const r = await fetch("/egov/status");
const d = await r.json();
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))
startPolling();
} 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>
</body>
</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", () => {
const dateFrom = document.getElementById("dateFrom").value;

View File

@@ -1331,7 +1331,7 @@
</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">
健康保険 ¥${_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 class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_socialSub.toLocaleString()}</span></div>
<div class="detail-row">
@@ -1379,7 +1379,12 @@
document.getElementById("payrollDetail").style.display = "block";
// モバイルでは明細パネルへ自動スクロール
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) {
alert("給与明細の取得に失敗しました");
@@ -1918,7 +1923,7 @@
</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">
健康保険 ¥${_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 class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_bSocialSub.toLocaleString()}</span></div>
<div class="detail-row">
@@ -1964,7 +1969,12 @@
document.getElementById("bonusDetail").style.display = "block";
// モバイルでは明細パネルへ自動スクロール
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) {
alert("賞与明細の取得に失敗しました");
@@ -2638,19 +2648,33 @@
// CSVデータを生成
let csvContent =
"データ,従業員コード,従業員名,年月,支給額,控除額,差引支給額\n";
"データ,従業員コード,従業員名,年月,支給額,社会保険,所得税,控除額合計,差引支給額\n";
// 給与データ
if (result.data.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) {
(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 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>
<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"),
(r"c:\workspace\njts-accounting-core\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",
f"{NAS_BASE}/backend/frontend/bank-statement.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.