修正
This commit is contained in:
@@ -138,7 +138,14 @@ def run_auto_migration():
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── ねんきんポータル テーブル作成 ──
|
||||
try:
|
||||
from app.modules.nenkin_portal.migration import create_nenkin_tables
|
||||
create_nenkin_tables()
|
||||
except Exception as e:
|
||||
print(f"⚠️ ねんきんポータル テーブル作成エラー(無視): {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 無法連接到數據庫或執行遷移: {e}")
|
||||
print("\n提示:")
|
||||
|
||||
@@ -124,6 +124,8 @@ app.include_router(payroll_vouchers_router)
|
||||
from app.payroll.withholding_slip.router import router as withholding_slip_router
|
||||
app.include_router(withholding_slip_router)
|
||||
|
||||
from app.modules.nenkin_portal.router import router as nenkin_portal_router
|
||||
app.include_router(nenkin_portal_router)
|
||||
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import os
|
||||
|
||||
0
backend/app/modules/nenkin_portal/__init__.py
Normal file
0
backend/app/modules/nenkin_portal/__init__.py
Normal file
119
backend/app/modules/nenkin_portal/migration.py
Normal file
119
backend/app/modules/nenkin_portal/migration.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
ねんきんポータル DB テーブル作成マイグレーション
|
||||
"""
|
||||
import os
|
||||
from psycopg import connect
|
||||
|
||||
|
||||
def create_nenkin_tables():
|
||||
conn = connect(
|
||||
host=os.getenv("DB_HOST"),
|
||||
port=int(os.getenv("DB_PORT", 5432)),
|
||||
dbname=os.getenv("DB_NAME"),
|
||||
user=os.getenv("DB_USER"),
|
||||
password=os.getenv("DB_PASSWORD"),
|
||||
)
|
||||
|
||||
with conn.cursor() as cur:
|
||||
# ──────────────────────────────────
|
||||
# 文書ヘッダーマスター(全種別共通)
|
||||
# ──────────────────────────────────
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nenkin_documents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_no VARCHAR(50) UNIQUE NOT NULL,
|
||||
document_type VARCHAR(10) NOT NULL,
|
||||
document_type_name VARCHAR(100),
|
||||
zip_filename VARCHAR(255),
|
||||
office_name VARCHAR(200),
|
||||
office_number VARCHAR(20),
|
||||
office_kigo VARCHAR(50),
|
||||
nenkin_jimusho VARCHAR(100),
|
||||
purpose_year INTEGER,
|
||||
purpose_month INTEGER,
|
||||
issue_date DATE,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# ──────────────────────────────────
|
||||
# 社会保険料額情報 (SHAKAI)
|
||||
# ──────────────────────────────────
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nenkin_shakai_hokenryo (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER REFERENCES nenkin_documents(id) ON DELETE CASCADE,
|
||||
kenkou_hokenryo NUMERIC,
|
||||
kousei_nenkin_hokenryo NUMERIC,
|
||||
kodomo_kyoshutsukin NUMERIC,
|
||||
total NUMERIC,
|
||||
nofu_kigen_date DATE,
|
||||
office_postcode VARCHAR(10),
|
||||
office_address VARCHAR(300)
|
||||
)
|
||||
""")
|
||||
|
||||
# ──────────────────────────────────
|
||||
# 保険料納入告知額・領収済額通知書 (NOUNYU)
|
||||
# ──────────────────────────────────
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nenkin_nounyu_tsuchi (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER REFERENCES nenkin_documents(id) ON DELETE CASCADE,
|
||||
kokuchi_kenkou NUMERIC,
|
||||
kokuchi_kounen NUMERIC,
|
||||
kokuchi_kodomo NUMERIC,
|
||||
kokuchi_total NUMERIC,
|
||||
kokuchi_nofu_kigen DATE,
|
||||
ryoshu_kenkou NUMERIC,
|
||||
ryoshu_kounen NUMERIC,
|
||||
ryoshu_kodomo NUMERIC,
|
||||
ryoshu_total NUMERIC,
|
||||
ryoshu_date DATE,
|
||||
ryoshu_nofu_year INTEGER,
|
||||
ryoshu_nofu_month INTEGER,
|
||||
office_postcode VARCHAR(10),
|
||||
office_address VARCHAR(300),
|
||||
office_name_line1 VARCHAR(200),
|
||||
office_name_line2 VARCHAR(200)
|
||||
)
|
||||
""")
|
||||
|
||||
# ──────────────────────────────────
|
||||
# 賞与保険料算出内訳書 ヘッダー (SHOYO)
|
||||
# ──────────────────────────────────
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nenkin_shoyo_header (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER REFERENCES nenkin_documents(id) ON DELETE CASCADE,
|
||||
jin_in_num INTEGER,
|
||||
menjyo_hokenryo_ritsu VARCHAR(50),
|
||||
page_num INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# ──────────────────────────────────
|
||||
# 賞与保険料算出内訳書 明細 (SHOYO)
|
||||
# ──────────────────────────────────
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS nenkin_shoyo_detail (
|
||||
id SERIAL PRIMARY KEY,
|
||||
document_id INTEGER REFERENCES nenkin_documents(id) ON DELETE CASCADE,
|
||||
header_id INTEGER REFERENCES nenkin_shoyo_header(id) ON DELETE CASCADE,
|
||||
seiri_num VARCHAR(20),
|
||||
shimei VARCHAR(100),
|
||||
shori_ymd VARCHAR(20),
|
||||
hassei_ymd VARCHAR(20),
|
||||
hyojun_shoyo_kenpo NUMERIC,
|
||||
hyojun_shoyo_kounen NUMERIC,
|
||||
kenpo_hongetsu NUMERIC,
|
||||
kenpo_zengetsu NUMERIC,
|
||||
kounen_hongetsu NUMERIC,
|
||||
kounen_zengetsu NUMERIC,
|
||||
page_num INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✓ ねんきんポータル テーブル作成完了")
|
||||
281
backend/app/modules/nenkin_portal/router.py
Normal file
281
backend/app/modules/nenkin_portal/router.py
Normal file
@@ -0,0 +1,281 @@
|
||||
"""
|
||||
ねんきんポータル API ルーター
|
||||
"""
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||
from app.core.database import get_connection
|
||||
from app.modules.nenkin_portal.service import parse_zip
|
||||
|
||||
router = APIRouter(prefix="/nenkin", tags=["ねんきんポータル"])
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# POST /nenkin/upload
|
||||
# ──────────────────────────────────────
|
||||
|
||||
@router.post("/upload", summary="社会保険電子文書 ZIPアップロード")
|
||||
async def upload_nenkin_zip(file: UploadFile = File(...)):
|
||||
if not file.filename.lower().endswith('.zip'):
|
||||
raise HTTPException(status_code=400, detail="ZIPファイルを選択してください")
|
||||
|
||||
data = await file.read()
|
||||
|
||||
try:
|
||||
parsed = parse_zip(data, file.filename)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"ZIPの解析に失敗しました: {e}")
|
||||
|
||||
doc_no = parsed['document_no']
|
||||
if not doc_no:
|
||||
raise HTTPException(status_code=400, detail="文書番号が取得できませんでした")
|
||||
|
||||
with get_connection() as conn, conn.cursor() as cur:
|
||||
# 重複チェック
|
||||
cur.execute(
|
||||
"SELECT id FROM nenkin_documents WHERE document_no = %s",
|
||||
(doc_no,)
|
||||
)
|
||||
if cur.fetchone():
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"文書番号 {doc_no} は既に登録されています"
|
||||
)
|
||||
|
||||
# nenkin_documents に挿入
|
||||
cur.execute("""
|
||||
INSERT INTO nenkin_documents
|
||||
(document_no, document_type, document_type_name, zip_filename,
|
||||
office_name, office_number, office_kigo, nenkin_jimusho,
|
||||
purpose_year, purpose_month, issue_date)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
RETURNING id
|
||||
""", (
|
||||
parsed['document_no'],
|
||||
parsed['document_type'],
|
||||
parsed['document_type_name'],
|
||||
parsed['zip_filename'],
|
||||
parsed['office_name'],
|
||||
parsed['office_number'],
|
||||
parsed['office_kigo'],
|
||||
parsed['nenkin_jimusho'],
|
||||
parsed['purpose_year'],
|
||||
parsed['purpose_month'],
|
||||
parsed['issue_date'],
|
||||
))
|
||||
doc_id = cur.fetchone()['id']
|
||||
|
||||
detail = parsed['detail']
|
||||
|
||||
if parsed['document_type'] == 'SHAKAI':
|
||||
cur.execute("""
|
||||
INSERT INTO nenkin_shakai_hokenryo
|
||||
(document_id, kenkou_hokenryo, kousei_nenkin_hokenryo,
|
||||
kodomo_kyoshutsukin, total, nofu_kigen_date,
|
||||
office_postcode, office_address)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
""", (
|
||||
doc_id,
|
||||
detail['kenkou_hokenryo'],
|
||||
detail['kousei_nenkin_hokenryo'],
|
||||
detail['kodomo_kyoshutsukin'],
|
||||
detail['total'],
|
||||
detail['nofu_kigen_date'],
|
||||
detail['office_postcode'],
|
||||
detail['office_address'],
|
||||
))
|
||||
|
||||
elif parsed['document_type'] == 'NOUNYU':
|
||||
cur.execute("""
|
||||
INSERT INTO nenkin_nounyu_tsuchi
|
||||
(document_id,
|
||||
kokuchi_kenkou, kokuchi_kounen, kokuchi_kodomo, kokuchi_total, kokuchi_nofu_kigen,
|
||||
ryoshu_kenkou, ryoshu_kounen, ryoshu_kodomo, ryoshu_total, ryoshu_date,
|
||||
ryoshu_nofu_year, ryoshu_nofu_month,
|
||||
office_postcode, office_address, office_name_line1, office_name_line2)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
""", (
|
||||
doc_id,
|
||||
detail['kokuchi_kenkou'], detail['kokuchi_kounen'],
|
||||
detail['kokuchi_kodomo'], detail['kokuchi_total'],
|
||||
detail['kokuchi_nofu_kigen'],
|
||||
detail['ryoshu_kenkou'], detail['ryoshu_kounen'],
|
||||
detail['ryoshu_kodomo'], detail['ryoshu_total'],
|
||||
detail['ryoshu_date'],
|
||||
detail['ryoshu_nofu_year'], detail['ryoshu_nofu_month'],
|
||||
detail['office_postcode'], detail['office_address'],
|
||||
detail['office_name_line1'], detail['office_name_line2'],
|
||||
))
|
||||
|
||||
elif parsed['document_type'] == 'SHOYO':
|
||||
cur.execute("""
|
||||
INSERT INTO nenkin_shoyo_header
|
||||
(document_id, jin_in_num, menjyo_hokenryo_ritsu, page_num)
|
||||
VALUES (%s,%s,%s,%s)
|
||||
RETURNING id
|
||||
""", (
|
||||
doc_id,
|
||||
detail['jin_in_num'],
|
||||
detail['menjyo_hokenryo_ritsu'],
|
||||
detail['page_num'],
|
||||
))
|
||||
header_id = cur.fetchone()['id']
|
||||
|
||||
for item in detail['items']:
|
||||
cur.execute("""
|
||||
INSERT INTO nenkin_shoyo_detail
|
||||
(document_id, header_id, seiri_num, shimei, shori_ymd, hassei_ymd,
|
||||
hyojun_shoyo_kenpo, hyojun_shoyo_kounen,
|
||||
kenpo_hongetsu, kenpo_zengetsu,
|
||||
kounen_hongetsu, kounen_zengetsu, page_num)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
""", (
|
||||
doc_id, header_id,
|
||||
item['seiri_num'], item['shimei'],
|
||||
item['shori_ymd'], item['hassei_ymd'],
|
||||
item['hyojun_shoyo_kenpo'], item['hyojun_shoyo_kounen'],
|
||||
item['kenpo_hongetsu'], item['kenpo_zengetsu'],
|
||||
item['kounen_hongetsu'], item['kounen_zengetsu'],
|
||||
item['page_num'],
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
'id': doc_id,
|
||||
'document_no': parsed['document_no'],
|
||||
'document_type': parsed['document_type'],
|
||||
'document_type_name': parsed['document_type_name'],
|
||||
'message': f"{parsed['document_type_name']} を登録しました",
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# GET /nenkin/documents
|
||||
# ──────────────────────────────────────
|
||||
|
||||
@router.get("/documents", summary="文書一覧取得")
|
||||
def list_documents(doc_type: str = None, year: int = None, month: int = None):
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if doc_type:
|
||||
conditions.append("document_type = %s")
|
||||
params.append(doc_type)
|
||||
if year:
|
||||
conditions.append("purpose_year = %s")
|
||||
params.append(year)
|
||||
if month:
|
||||
conditions.append("purpose_month = %s")
|
||||
params.append(month)
|
||||
|
||||
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
||||
|
||||
with get_connection() as conn, conn.cursor() as cur:
|
||||
cur.execute(f"""
|
||||
SELECT id, document_no, document_type, document_type_name,
|
||||
zip_filename, office_name, office_number, office_kigo,
|
||||
nenkin_jimusho, purpose_year, purpose_month,
|
||||
issue_date, uploaded_at
|
||||
FROM nenkin_documents
|
||||
{where}
|
||||
ORDER BY purpose_year DESC, purpose_month DESC, document_type, uploaded_at DESC
|
||||
""", params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
result = []
|
||||
for r in rows:
|
||||
item = dict(r)
|
||||
if item.get('issue_date'):
|
||||
item['issue_date'] = item['issue_date'].isoformat()
|
||||
if item.get('uploaded_at'):
|
||||
item['uploaded_at'] = item['uploaded_at'].isoformat()
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# GET /nenkin/documents/{doc_id}
|
||||
# ──────────────────────────────────────
|
||||
|
||||
@router.get("/documents/{doc_id}", summary="文書詳細取得")
|
||||
def get_document(doc_id: int):
|
||||
with get_connection() as conn, conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM nenkin_documents WHERE id = %s", (doc_id,))
|
||||
doc = cur.fetchone()
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="文書が見つかりません")
|
||||
|
||||
doc = dict(doc)
|
||||
if doc.get('issue_date'):
|
||||
doc['issue_date'] = doc['issue_date'].isoformat()
|
||||
if doc.get('uploaded_at'):
|
||||
doc['uploaded_at'] = doc['uploaded_at'].isoformat()
|
||||
|
||||
detail = {}
|
||||
dtype = doc['document_type']
|
||||
|
||||
if dtype == 'SHAKAI':
|
||||
cur.execute(
|
||||
"SELECT * FROM nenkin_shakai_hokenryo WHERE document_id = %s",
|
||||
(doc_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
detail = dict(row)
|
||||
if detail.get('nofu_kigen_date'):
|
||||
detail['nofu_kigen_date'] = detail['nofu_kigen_date'].isoformat()
|
||||
|
||||
elif dtype == 'NOUNYU':
|
||||
cur.execute(
|
||||
"SELECT * FROM nenkin_nounyu_tsuchi WHERE document_id = %s",
|
||||
(doc_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
detail = dict(row)
|
||||
for df in ('kokuchi_nofu_kigen', 'ryoshu_date'):
|
||||
if detail.get(df):
|
||||
detail[df] = detail[df].isoformat()
|
||||
|
||||
elif dtype == 'SHOYO':
|
||||
cur.execute(
|
||||
"SELECT * FROM nenkin_shoyo_header WHERE document_id = %s",
|
||||
(doc_id,)
|
||||
)
|
||||
hdr = cur.fetchone()
|
||||
if hdr:
|
||||
detail = dict(hdr)
|
||||
cur.execute("""
|
||||
SELECT * FROM nenkin_shoyo_detail
|
||||
WHERE document_id = %s
|
||||
ORDER BY CAST(NULLIF(TRIM(seiri_num), '') AS INTEGER) NULLS LAST
|
||||
""", (doc_id,))
|
||||
detail['items'] = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
return {'document': doc, 'detail': detail}
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# GET /nenkin/filter-options
|
||||
# ──────────────────────────────────────
|
||||
|
||||
@router.get("/filter-options", summary="フィルター選択肢取得")
|
||||
def get_filter_options():
|
||||
with get_connection() as conn, conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT purpose_year, purpose_month
|
||||
FROM nenkin_documents
|
||||
WHERE purpose_year IS NOT NULL
|
||||
ORDER BY purpose_year DESC, purpose_month DESC
|
||||
""")
|
||||
periods = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
cur.execute("""
|
||||
SELECT DISTINCT document_type, document_type_name
|
||||
FROM nenkin_documents
|
||||
ORDER BY document_type
|
||||
""")
|
||||
types = [dict(r) for r in cur.fetchall()]
|
||||
|
||||
return {'periods': periods, 'types': types}
|
||||
351
backend/app/modules/nenkin_portal/service.py
Normal file
351
backend/app/modules/nenkin_portal/service.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
ねんきんポータル ZIPパーサー
|
||||
対応文書種別:
|
||||
SHAKAI - 社会保険料額情報
|
||||
NOUNYU - 保険料納入告知額・領収済額通知書
|
||||
SHOYO - 賞与保険料算出内訳書
|
||||
"""
|
||||
import zipfile
|
||||
import io
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import date
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# 元号オフセット
|
||||
# ──────────────────────────────────────
|
||||
GENGOU_OFFSETS = {
|
||||
'令和': 2018, 'R': 2018,
|
||||
'平成': 1988, 'H': 1988,
|
||||
'昭和': 1925, 'S': 1925,
|
||||
'大正': 1911, 'T': 1911,
|
||||
'明治': 1867, 'M': 1867,
|
||||
}
|
||||
|
||||
# XSLファイル名 → 文書種別
|
||||
XSL_TYPE_MAP = {
|
||||
'yoshiki_04_shakai_003.xsl': 'SHAKAI',
|
||||
'yoshiki_29_zumitsu_001.xsl': 'NOUNYU',
|
||||
'yoshiki_03_shoyo_002.xsl': 'SHOYO',
|
||||
}
|
||||
|
||||
TYPE_NAMES = {
|
||||
'SHAKAI': '社会保険料額情報',
|
||||
'NOUNYU': '保険料納入告知額・領収済額通知書',
|
||||
'SHOYO': '賞与保険料算出内訳書',
|
||||
'HIHOKEN': '被保険者データ',
|
||||
'ZOGENS': '保険料増減内訳書',
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# ユーティリティ
|
||||
# ──────────────────────────────────────
|
||||
|
||||
def gengou_to_western(gengou: str, year_str) -> Optional[int]:
|
||||
try:
|
||||
offset = GENGOU_OFFSETS.get(str(gengou), 0)
|
||||
return int(str(year_str)) + offset if offset else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def make_date(gengou: str, year_str, month_str, day_str='1') -> Optional[date]:
|
||||
try:
|
||||
western = gengou_to_western(gengou, year_str)
|
||||
if not western:
|
||||
return None
|
||||
return date(western, int(month_str), int(day_str or 1))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def parse_reki_date(s: str) -> Optional[date]:
|
||||
"""R080430 → date(2026,4,30)"""
|
||||
if not s or len(s) < 7:
|
||||
return None
|
||||
prefix = s[0].upper()
|
||||
try:
|
||||
yy = int(s[1:3])
|
||||
mm = int(s[3:5])
|
||||
dd = int(s[5:7])
|
||||
offset = GENGOU_OFFSETS.get(prefix, 0)
|
||||
if not offset:
|
||||
return None
|
||||
return date(yy + offset, mm, dd)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def parse_amount(s) -> Optional[float]:
|
||||
"""'94,054' / '\\247,066' → float"""
|
||||
if s is None:
|
||||
return None
|
||||
cleaned = str(s).replace(',', '').replace('\\', '').replace('¥', '').replace('¥', '').strip()
|
||||
if not cleaned:
|
||||
return None
|
||||
try:
|
||||
return float(cleaned)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def get_text(elem, tag: str) -> str:
|
||||
if elem is None:
|
||||
return ''
|
||||
child = elem.find(tag)
|
||||
return child.text.strip() if child is not None and child.text else ''
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# 文書種別の自動判定
|
||||
# ──────────────────────────────────────
|
||||
|
||||
def detect_doc_type(names: list) -> str:
|
||||
for name in names:
|
||||
basename = name.split('/')[-1]
|
||||
if basename in XSL_TYPE_MAP:
|
||||
return XSL_TYPE_MAP[basename]
|
||||
return 'UNKNOWN'
|
||||
|
||||
|
||||
def get_document_no(zf, names: list) -> str:
|
||||
"""封筒XML から文書番号を取得"""
|
||||
envelope_name = next(
|
||||
(n for n in names if re.match(r'.*/\d+\.xml$', n)),
|
||||
None
|
||||
)
|
||||
if not envelope_name:
|
||||
return ''
|
||||
with zf.open(envelope_name) as f:
|
||||
etree = ET.parse(f)
|
||||
elem = etree.getroot().find('.//DOCNO')
|
||||
return elem.text.strip() if elem is not None and elem.text else ''
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# メインエントリー
|
||||
# ──────────────────────────────────────
|
||||
|
||||
def parse_zip(zip_bytes: bytes, zip_filename: str) -> Dict[str, Any]:
|
||||
zf = zipfile.ZipFile(io.BytesIO(zip_bytes))
|
||||
names = zf.namelist()
|
||||
doc_type = detect_doc_type(names)
|
||||
|
||||
if doc_type == 'SHAKAI':
|
||||
return _parse_shakai(zf, names, zip_filename)
|
||||
elif doc_type == 'NOUNYU':
|
||||
return _parse_nounyu(zf, names, zip_filename)
|
||||
elif doc_type == 'SHOYO':
|
||||
return _parse_shoyo(zf, names, zip_filename)
|
||||
else:
|
||||
raise ValueError(f"未対応の文書種別です(XSLが不明): {[n.split('/')[-1] for n in names]}")
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# 社会保険料額情報 (SHAKAI)
|
||||
# ──────────────────────────────────────
|
||||
|
||||
def _parse_shakai(zf, names: list, zip_filename: str) -> Dict[str, Any]:
|
||||
xml_name = next(
|
||||
n for n in names
|
||||
if '社会保険料額情報' in n.split('/')[-1] and n.endswith('.xml')
|
||||
)
|
||||
with zf.open(xml_name) as f:
|
||||
root = ET.parse(f).getroot()
|
||||
|
||||
lst = root.find('shakaiHokenRyougakuList')
|
||||
header = lst.find('header')
|
||||
|
||||
jigyosho_name = get_text(header, 'jigyoshoName').replace('<br/>', ' ')
|
||||
jigyosho_num = get_text(header, 'jigyoshoNum')
|
||||
jigyosho_kigo = get_text(header, 'jigyoshoSeiriKigo')
|
||||
nenkin_jimusho = get_text(header, 'nenkinJimusho1')
|
||||
nofu_gengou = get_text(header, 'nouhuMokutekiYearGengou')
|
||||
nofu_year = get_text(header, 'nouhuMokutekiYear')
|
||||
nofu_month = get_text(header, 'nouhuMokutekiMonth')
|
||||
|
||||
hakkou_gengou = get_text(lst, 'hakkouYmdGengou')
|
||||
hakkou_year = get_text(lst, 'hakkouYmdYear')
|
||||
hakkou_month = get_text(lst, 'hakkouYmdMonth')
|
||||
hakkou_day = get_text(lst, 'hakkouYmdDate')
|
||||
|
||||
nofu_kigen_gengou = get_text(lst, 'nouhuKigenGengou')
|
||||
nofu_kigen_year = get_text(lst, 'nouhuKigenYear')
|
||||
nofu_kigen_month = get_text(lst, 'nouhuKigenMonth')
|
||||
nofu_kigen_day = get_text(lst, 'nouhuKigenDate')
|
||||
|
||||
return {
|
||||
'document_no': get_document_no(zf, names),
|
||||
'document_type': 'SHAKAI',
|
||||
'document_type_name': TYPE_NAMES['SHAKAI'],
|
||||
'zip_filename': zip_filename,
|
||||
'office_name': jigyosho_name,
|
||||
'office_number': jigyosho_num,
|
||||
'office_kigo': jigyosho_kigo,
|
||||
'nenkin_jimusho': nenkin_jimusho,
|
||||
'purpose_year': gengou_to_western(nofu_gengou, nofu_year),
|
||||
'purpose_month': int(nofu_month) if nofu_month else None,
|
||||
'issue_date': make_date(hakkou_gengou, hakkou_year, hakkou_month, hakkou_day),
|
||||
'detail': {
|
||||
'kenkou_hokenryo': parse_amount(get_text(lst, 'kenkouHokenRyou')),
|
||||
'kousei_nenkin_hokenryo': parse_amount(get_text(lst, 'kouseiNenkinHokenRyou')),
|
||||
'kodomo_kyoshutsukin': parse_amount(get_text(lst, 'kodomoKosodateKyoshutsuKin')),
|
||||
'total': parse_amount(get_text(lst, 'total')),
|
||||
'nofu_kigen_date': make_date(nofu_kigen_gengou, nofu_kigen_year, nofu_kigen_month, nofu_kigen_day),
|
||||
'office_postcode': get_text(lst, 'jigyoushoPostCode'),
|
||||
'office_address': get_text(lst, 'jigyoushoShozaichi').replace('<br/>', ' '),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# 保険料納入告知額・領収済額通知書 (NOUNYU)
|
||||
# ──────────────────────────────────────
|
||||
|
||||
def _parse_nounyu(zf, names: list, zip_filename: str) -> Dict[str, Any]:
|
||||
xml_name = next(
|
||||
n for n in names
|
||||
if '保険料納入告知額' in n.split('/')[-1] and n.endswith('.xml')
|
||||
)
|
||||
with zf.open(xml_name) as f:
|
||||
root = ET.parse(f).getroot()
|
||||
|
||||
lst = root.find('hokenRyouRyoshuzumigakutsuchiShoList')
|
||||
header = lst.find('header')
|
||||
|
||||
jigyosho_num = get_text(header, 'jigyoshoNum')
|
||||
jigyosho_kigo = get_text(header, 'jigyoshoSeiriKigo')
|
||||
nenkin_jimusho = get_text(header, 'nenkinJimusho1')
|
||||
nofu_gengou = get_text(header, 'nouhuMokutekiYearGengou')
|
||||
nofu_year = get_text(header, 'nouhuMokutekiYear')
|
||||
nofu_month = get_text(header, 'nouhuMokutekiMonth')
|
||||
|
||||
hakkou_gengou = get_text(lst, 'hakkouYmdGengou')
|
||||
hakkou_year = get_text(lst, 'hakkouYmdYear')
|
||||
hakkou_month = get_text(lst, 'hakkouYmdMonth')
|
||||
hakkou_day = get_text(lst, 'hakkouYmdDate')
|
||||
|
||||
nofu_kigen_gengou = get_text(lst, 'nouhuKigenGengou')
|
||||
nofu_kigen_year = get_text(lst, 'nouhuKigenYear')
|
||||
nofu_kigen_month = get_text(lst, 'nouhuKigenMonth')
|
||||
nofu_kigen_day = get_text(lst, 'nouhuKigenDate')
|
||||
|
||||
ryoshu_gengou = get_text(lst, 'ryoshuDtGengo')
|
||||
ryoshu_year = get_text(lst, 'ryoshuDtYear')
|
||||
ryoshu_month = get_text(lst, 'ryoshuDtMonth')
|
||||
ryoshu_day = get_text(lst, 'ryoshuDtDate')
|
||||
|
||||
ryoshu_nofu_gengou = get_text(lst, 'ryoshuNouhuMokutekiYmGengou')
|
||||
ryoshu_nofu_year = get_text(lst, 'ryoshuNouhuMokutekiYmYear')
|
||||
ryoshu_nofu_month = get_text(lst, 'ryoshuNouhuMokutekiYmMonth')
|
||||
|
||||
name1 = get_text(lst, 'jigyoshoNameLine1')
|
||||
name2 = get_text(lst, 'jigyoshoNameLine2')
|
||||
office_name = (name1 + ' ' + name2).strip()
|
||||
|
||||
return {
|
||||
'document_no': get_document_no(zf, names),
|
||||
'document_type': 'NOUNYU',
|
||||
'document_type_name': TYPE_NAMES['NOUNYU'],
|
||||
'zip_filename': zip_filename,
|
||||
'office_name': office_name,
|
||||
'office_number': jigyosho_num,
|
||||
'office_kigo': jigyosho_kigo,
|
||||
'nenkin_jimusho': nenkin_jimusho,
|
||||
'purpose_year': gengou_to_western(nofu_gengou, nofu_year),
|
||||
'purpose_month': int(nofu_month) if nofu_month else None,
|
||||
'issue_date': make_date(hakkou_gengou, hakkou_year, hakkou_month, hakkou_day),
|
||||
'detail': {
|
||||
'kokuchi_kenkou': parse_amount(get_text(lst, 'kenkouHokenRyou')),
|
||||
'kokuchi_kounen': parse_amount(get_text(lst, 'kouseiNenkinHokenRyou')),
|
||||
'kokuchi_kodomo': parse_amount(get_text(lst, 'kodomoKosodateKyoshutsuKin')),
|
||||
'kokuchi_total': parse_amount(get_text(lst, 'total')),
|
||||
'kokuchi_nofu_kigen': make_date(nofu_kigen_gengou, nofu_kigen_year, nofu_kigen_month, nofu_kigen_day),
|
||||
'ryoshu_kenkou': parse_amount(get_text(lst, 'ryoshuKenkouHokenRyou')),
|
||||
'ryoshu_kounen': parse_amount(get_text(lst, 'ryoshuKouseiNenkinHokenRyou')),
|
||||
'ryoshu_kodomo': parse_amount(get_text(lst, 'ryoshuKodomoKosodateKyoshutsuKin')),
|
||||
'ryoshu_total': parse_amount(get_text(lst, 'ryoshuTotal')),
|
||||
'ryoshu_date': make_date(ryoshu_gengou, ryoshu_year, ryoshu_month, ryoshu_day),
|
||||
'ryoshu_nofu_year': gengou_to_western(ryoshu_nofu_gengou, ryoshu_nofu_year),
|
||||
'ryoshu_nofu_month': int(ryoshu_nofu_month) if ryoshu_nofu_month else None,
|
||||
'office_postcode': get_text(lst, 'jigyoushoPostCode'),
|
||||
'office_address': (get_text(lst, 'jigyoushoShozaichiLine1') + ' ' + get_text(lst, 'jigyoushoShozaichiLine2')).strip(),
|
||||
'office_name_line1': name1,
|
||||
'office_name_line2': name2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────
|
||||
# 賞与保険料算出内訳書 (SHOYO)
|
||||
# ──────────────────────────────────────
|
||||
|
||||
def _parse_shoyo(zf, names: list, zip_filename: str) -> Dict[str, Any]:
|
||||
# メインXML(.xmlかつ「賞与保険料算出内訳書」を含むもの)
|
||||
xml_name = next(
|
||||
n for n in names
|
||||
if '賞与保険料算出内訳書' in n.split('/')[-1]
|
||||
and n.endswith('.xml')
|
||||
)
|
||||
with zf.open(xml_name) as f:
|
||||
root = ET.parse(f).getroot()
|
||||
|
||||
shoyo_list = root.find('shouyoList')
|
||||
header = shoyo_list.find('header')
|
||||
|
||||
jigyosho_name = get_text(header, 'jigyoshoName')
|
||||
jigyosho_num = get_text(header, 'jigyoshoNum')
|
||||
jigyosho_kigo = get_text(header, 'jigyoshoSeiriKigo')
|
||||
nenkin_jimusho = get_text(header, 'nenkinJimusho2').replace('\u3000', ' ').strip()
|
||||
nofu_gengou = get_text(header, 'nouhuMokutekiYearGengou')
|
||||
nofu_year = get_text(header, 'nouhuMokutekiYear')
|
||||
nofu_month = get_text(header, 'nouhuMokutekiMonth')
|
||||
jin_in = get_text(header, 'jinInNum')
|
||||
menjyo = get_text(header, 'menjyoHokenRyouRitu')
|
||||
|
||||
# 各被保険者の明細
|
||||
items = []
|
||||
for uchiwake in shoyo_list.findall('uchiwake'):
|
||||
hyojun = uchiwake.find('hyoujyunShouyoGaku')
|
||||
kenpo_ryou = uchiwake.find('kenKouHokenRyou')
|
||||
kounen_ryou = uchiwake.find('kouseiNenkinHokenRyou')
|
||||
|
||||
hassei_ymd = get_text(hyojun, 'hasseiYMD') if hyojun is not None else ''
|
||||
parsed_hassei = parse_reki_date(hassei_ymd)
|
||||
|
||||
items.append({
|
||||
'seiri_num': get_text(uchiwake, 'seiriNum').strip(),
|
||||
'shimei': get_text(uchiwake, 'shimei'),
|
||||
'shori_ymd': get_text(uchiwake, 'shoriYMD'),
|
||||
'hassei_ymd': hassei_ymd,
|
||||
'hassei_date_iso': parsed_hassei.isoformat() if parsed_hassei else None,
|
||||
'hyojun_shoyo_kenpo': parse_amount(get_text(hyojun, 'getsuGakuKenpo') if hyojun is not None else ''),
|
||||
'hyojun_shoyo_kounen': parse_amount(get_text(hyojun, 'getsuGakuKounen') if hyojun is not None else ''),
|
||||
'kenpo_hongetsu': parse_amount(get_text(kenpo_ryou, 'hongetsuGaku') if kenpo_ryou is not None else ''),
|
||||
'kenpo_zengetsu': parse_amount(get_text(kenpo_ryou, 'zengetsuIzenMonth') if kenpo_ryou is not None else ''),
|
||||
'kounen_hongetsu': parse_amount(get_text(kounen_ryou, 'hongetsuGaku') if kounen_ryou is not None else ''),
|
||||
'kounen_zengetsu': parse_amount(get_text(kounen_ryou, 'zengetsuIzenMonth') if kounen_ryou is not None else ''),
|
||||
'page_num': int(get_text(uchiwake, 'pageNum') or 0),
|
||||
})
|
||||
|
||||
return {
|
||||
'document_no': get_document_no(zf, names),
|
||||
'document_type': 'SHOYO',
|
||||
'document_type_name': TYPE_NAMES['SHOYO'],
|
||||
'zip_filename': zip_filename,
|
||||
'office_name': jigyosho_name,
|
||||
'office_number': jigyosho_num,
|
||||
'office_kigo': jigyosho_kigo,
|
||||
'nenkin_jimusho': nenkin_jimusho,
|
||||
'purpose_year': gengou_to_western(nofu_gengou, nofu_year),
|
||||
'purpose_month': int(nofu_month) if nofu_month else None,
|
||||
'issue_date': None,
|
||||
'detail': {
|
||||
'jin_in_num': int(jin_in) if jin_in else 0,
|
||||
'menjyo_hokenryo_ritsu': menjyo,
|
||||
'page_num': int(get_text(shoyo_list, 'pageNum') or 1),
|
||||
'items': items,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user