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}{tag}>', text)
+ # empty: または
+ 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()
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
+ に転記してデジタル庁へ提出してください。
+
+
+
+
+
+
+
+
+
+
+ | # |
+ 到達番号 |
+ 到達日時 |
+ 手続識別子 |
+ 手続名 |
+ ステータス |
+ サブステータス |
+
+
+
+
+ |
+ 「一覧を更新」ボタンで取得してください
+ |
+
+
+
+
+
+