chore: 年度リストを昇順に変更
This commit is contained in:
@@ -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提示:")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
docs/denshishinsei-guide_0813.pdf
Normal file
BIN
docs/denshishinsei-guide_0813.pdf
Normal file
Binary file not shown.
BIN
docs/e-Gov検証環境ログイン方法.xlsx
Normal file
BIN
docs/e-Gov検証環境ログイン方法.xlsx
Normal file
Binary file not shown.
BIN
docs/振込-三菱UFJ銀行.pdf
Normal file
BIN
docs/振込-三菱UFJ銀行.pdf
Normal file
Binary file not shown.
BIN
docs/最終確認試験用データ情報(TID_202606010049)_1版.xlsx
Normal file
BIN
docs/最終確認試験用データ情報(TID_202606010049)_1版.xlsx
Normal file
Binary file not shown.
BIN
docs/最終確認試験用データ情報(TID_202606010049)_1版.zip
Normal file
BIN
docs/最終確認試験用データ情報(TID_202606010049)_1版.zip
Normal file
Binary file not shown.
BIN
egov_guide.pdf
Normal file
BIN
egov_guide.pdf
Normal file
Binary file not shown.
BIN
final_test_spec_post.xlsx
Normal file
BIN
final_test_spec_post.xlsx
Normal file
Binary file not shown.
@@ -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);
|
||||
|
||||
@@ -376,5 +376,4 @@ html {
|
||||
padding: 20px 16px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
ソフトウェア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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
62
read_openapi.py
Normal 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
94
test_org_id.py
Normal 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
89
test_proc_name.py
Normal 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
BIN
testproclist.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user