diff --git a/backend/app/db_auto_migration.py b/backend/app/db_auto_migration.py index 1130d78..92741d8 100644 --- a/backend/app/db_auto_migration.py +++ b/backend/app/db_auto_migration.py @@ -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提示:") diff --git a/backend/app/modules/bank_statements/router.py b/backend/app/modules/bank_statements/router.py index 6e56d35..546e49e 100644 --- a/backend/app/modules/bank_statements/router.py +++ b/backend/app/modules/bank_statements/router.py @@ -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) diff --git a/backend/app/modules/egov/egov_api.py b/backend/app/modules/egov/egov_api.py index 4344875..50884d8 100644 --- a/backend/app/modules/egov/egov_api.py +++ b/backend/app/modules/egov/egov_api.py @@ -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 内の空タグ または value に置換""" + # self-closing: または + text = re.sub(r'<' + re.escape(tag) + r'\s*/>', f'<{tag}>{value}', text) + # empty: または + text = re.sub( + r'<' + re.escape(tag) + r'\s*>\s*', + f'<{tag}>{value}', 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' + '<申請書様式バージョン>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() diff --git a/backend/app/modules/egov/router.py b/backend/app/modules/egov/router.py index ae467b7..ba8ce09 100644 --- a/backend/app/modules/egov/router.py +++ b/backend/app/modules/egov/router.py @@ -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)} diff --git a/backend/app/modules/trial_balance/router.py b/backend/app/modules/trial_balance/router.py index 9f8798a..3d51810 100644 --- a/backend/app/modules/trial_balance/router.py +++ b/backend/app/modules/trial_balance/router.py @@ -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)"), diff --git a/backend/app/routers/trial_balance.py b/backend/app/routers/trial_balance.py index ba6d496..2488d79 100644 --- a/backend/app/routers/trial_balance.py +++ b/backend/app/routers/trial_balance.py @@ -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 diff --git a/docs/denshishinsei-guide_0813.pdf b/docs/denshishinsei-guide_0813.pdf new file mode 100644 index 0000000..4649cba Binary files /dev/null and b/docs/denshishinsei-guide_0813.pdf differ diff --git a/docs/e-Gov検証環境ログイン方法.xlsx b/docs/e-Gov検証環境ログイン方法.xlsx new file mode 100644 index 0000000..0769805 Binary files /dev/null and b/docs/e-Gov検証環境ログイン方法.xlsx differ diff --git a/docs/振込-三菱UFJ銀行.pdf b/docs/振込-三菱UFJ銀行.pdf new file mode 100644 index 0000000..f1490f1 Binary files /dev/null and b/docs/振込-三菱UFJ銀行.pdf differ diff --git a/docs/最終確認試験用データ情報(TID_202606010049)_1版.xlsx b/docs/最終確認試験用データ情報(TID_202606010049)_1版.xlsx new file mode 100644 index 0000000..5a60f93 Binary files /dev/null and b/docs/最終確認試験用データ情報(TID_202606010049)_1版.xlsx differ diff --git a/docs/最終確認試験用データ情報(TID_202606010049)_1版.zip b/docs/最終確認試験用データ情報(TID_202606010049)_1版.zip new file mode 100644 index 0000000..71d71e7 Binary files /dev/null and b/docs/最終確認試験用データ情報(TID_202606010049)_1版.zip differ diff --git a/egov_guide.pdf b/egov_guide.pdf new file mode 100644 index 0000000..4649cba Binary files /dev/null and b/egov_guide.pdf differ diff --git a/faq.pdf b/faq.pdf new file mode 100644 index 0000000..776bc20 Binary files /dev/null and b/faq.pdf differ diff --git a/final_test_spec_post.xlsx b/final_test_spec_post.xlsx new file mode 100644 index 0000000..a84f95c Binary files /dev/null and b/final_test_spec_post.xlsx differ diff --git a/frontend/bank-statement.html b/frontend/bank-statement.html index 7b56e4b..0fa0c21 100644 --- a/frontend/bank-statement.html +++ b/frontend/bank-statement.html @@ -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 @@ + + + + +
+ + + +
+
-
- - - - - - - +
+
+ + + + + + + + + +
+
+ + + + +
@@ -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)}月 ${ - m.count - }件`; + chip.innerHTML = `${parseInt(mo)}月 ${m.count}件`; 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); diff --git a/frontend/css/mobile.css b/frontend/css/mobile.css index 3db920f..b2826a5 100644 --- a/frontend/css/mobile.css +++ b/frontend/css/mobile.css @@ -376,5 +376,4 @@ html { padding: 20px 16px !important; box-sizing: border-box !important; } - } diff --git a/frontend/egov.html b/frontend/egov.html index 7bf36cf..c38624d 100644 --- a/frontend/egov.html +++ b/frontend/egov.html @@ -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; + } @@ -422,6 +488,193 @@
+ +
+

📝 テスト申請書作成(最終確認試験用)

+

+ 検証環境へテスト手続を送信して到達番号を取得します。
+ 取得した到達番号は試験データ表と Excel + に記入してデジタル庁へ送付してください。 +

+ +
+ ⚠️ ログイン後に操作可能です。
+ 環境: + sandbox(検証) +   ソフトウェアID: + +
+ +
+ + +
+
+ + +
+ + + +
+
+ + +
+

📋 申請案件一覧(到達番号確認)

+

+ 送信済みの申請案件一覧を表示します。到達番号を Excel + に転記してデジタル庁へ提出してください。 +

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + +
#到達番号到達日時手続識別子手続名ステータスサブステータス
+ 「一覧を更新」ボタンで取得してください +
+
+
+ diff --git a/frontend/js/trial-balance.js b/frontend/js/trial-balance.js index e90fe62..4f24497 100644 --- a/frontend/js/trial-balance.js +++ b/frontend/js/trial-balance.js @@ -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; diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html index 6bee2b4..f9df440 100644 --- a/frontend/payroll-calculation.html +++ b/frontend/payroll-calculation.html @@ -1331,7 +1331,7 @@
健康保険 ¥${_health.toLocaleString()} + 介護保険 ¥${_care.toLocaleString()} + 子ども・子育て支援金 ¥${_child.toLocaleString()} = ¥${_healthCareChild.toLocaleString()}
- ¥${_healthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_pension.toLocaleString()}${' '}${_emp > 0 ? `+ 雇用保険 ¥${_emp.toLocaleString()}` : ''}= 社会保険小計 ¥${_socialSub.toLocaleString()} + ¥${_healthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_pension.toLocaleString()}${" "}${_emp > 0 ? `+ 雇用保険 ¥${_emp.toLocaleString()}` : ""}= 社会保険小計 ¥${_socialSub.toLocaleString()}
社会保険小計:¥${_socialSub.toLocaleString()}
@@ -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 @@
健康保険 ¥${_bHealth.toLocaleString()} + 介護保険 ¥${_bCare.toLocaleString()} + 子ども・子育て支援金 ¥${_bChild.toLocaleString()} = ¥${_bHealthCareChild.toLocaleString()}
- ¥${_bHealthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_bPension.toLocaleString()}${' '}${_bEmp > 0 ? `+ 雇用保険 ¥${_bEmp.toLocaleString()}` : ''}= 社会保険小計 ¥${_bSocialSub.toLocaleString()} + ¥${_bHealthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_bPension.toLocaleString()}${" "}${_bEmp > 0 ? `+ 雇用保険 ¥${_bEmp.toLocaleString()}` : ""}= 社会保険小計 ¥${_bSocialSub.toLocaleString()}
社会保険小計:¥${_bSocialSub.toLocaleString()}
@@ -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`; }); } diff --git a/frontend/trial-balance.html b/frontend/trial-balance.html index e814ba2..12b75d6 100644 --- a/frontend/trial-balance.html +++ b/frontend/trial-balance.html @@ -292,6 +292,20 @@
+ + + diff --git a/nas_deploy.py b/nas_deploy.py index 327c955..ecf25e6 100644 --- a/nas_deploy.py +++ b/nas_deploy.py @@ -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", diff --git a/read_openapi.py b/read_openapi.py new file mode 100644 index 0000000..392bbd9 --- /dev/null +++ b/read_openapi.py @@ -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]}") diff --git a/test_org_id.py b/test_org_id.py new file mode 100644 index 0000000..a8c458f --- /dev/null +++ b/test_org_id.py @@ -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}', text) + text = re.sub(r'<' + re.escape(tag) + r'\s*>\s*', f'<{tag}>{value}', 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()) diff --git a/test_proc_name.py b/test_proc_name.py new file mode 100644 index 0000000..43969aa --- /dev/null +++ b/test_proc_name.py @@ -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}', text) + text = re.sub(r'<' + re.escape(tag) + r'\s*>\s*', f'<{tag}>{value}', 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()) diff --git a/testproclist.xlsx b/testproclist.xlsx new file mode 100644 index 0000000..3abe906 Binary files /dev/null and b/testproclist.xlsx differ