#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""滞留債権の年齢調べ表を作る

経理財務 × AI活用例（https://ai-keiri-site.vercel.app）の配布ファイルです。
入金期日からの経過日数で債権を区分し、取引先ごとの年齢調べ表を作ります。

使い方（ターミナルで実行します）
    python3 aging_table.py sample-receivables.csv
    python3 aging_table.py 自社データ.csv --base-date 2026-09-30 --out 年齢調べ.csv

入力CSVに必要な列（列名は完全一致させてください）
    取引先コード, 取引先名, 請求番号, 請求日, 入金期日, 請求金額, 入金額, 残高
    日付は YYYY-MM-DD 形式。残高が0の行は自動で除きます。

このスクリプトは Python の標準機能だけで動きます。追加のインストールは不要です。
出力はあくまで一次集計です。貸倒引当金の見積りや取引先の状況判断は行いません。
"""

import argparse
import csv
import sys
import unicodedata
from datetime import date, datetime

# 経過日数の区分。(区分名, この日数まで) の並び。最後の None は「それ以上すべて」。
BUCKETS = [
    ("期日内", 0),
    ("1〜30日", 30),
    ("31〜60日", 60),
    ("61〜90日", 90),
    ("91〜180日", 180),
    ("181日以上", None),
]

REQUIRED_COLUMNS = [
    "取引先コード", "取引先名", "請求番号", "請求日", "入金期日",
    "請求金額", "入金額", "残高",
]


def parse_date(value, row_number, column):
    try:
        return datetime.strptime(value.strip(), "%Y-%m-%d").date()
    except ValueError:
        raise SystemExit(
            "{0}行目の「{1}」が日付として読めません: {2}\n"
            "YYYY-MM-DD の形式（例 2026-09-30）にしてください。".format(row_number, column, value)
        )


def parse_amount(value, row_number, column):
    text = value.strip().replace(",", "").replace("￥", "").replace("¥", "")
    if text == "":
        return 0
    try:
        return int(round(float(text)))
    except ValueError:
        raise SystemExit(
            "{0}行目の「{1}」が金額として読めません: {2}".format(row_number, column, value)
        )


def bucket_of(days_overdue):
    """経過日数から区分名を返す。期日前・当日は「期日内」。"""
    if days_overdue <= 0:
        return BUCKETS[0][0]
    for name, limit in BUCKETS[1:]:
        if limit is None or days_overdue <= limit:
            return name
    return BUCKETS[-1][0]


def read_rows(path, base_date):
    with open(path, encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        missing = [c for c in REQUIRED_COLUMNS if c not in (reader.fieldnames or [])]
        if missing:
            raise SystemExit(
                "入力CSVに次の列が見つかりません: {0}\n"
                "1行目の列名を確認してください。".format("、".join(missing))
            )

        rows = []
        for i, raw in enumerate(reader, start=2):
            balance = parse_amount(raw["残高"], i, "残高")
            if balance == 0:
                continue
            due = parse_date(raw["入金期日"], i, "入金期日")
            days = (base_date - due).days
            rows.append({
                "取引先コード": raw["取引先コード"].strip(),
                "取引先名": raw["取引先名"].strip(),
                "請求番号": raw["請求番号"].strip(),
                "入金期日": due,
                "経過日数": days,
                "区分": bucket_of(days),
                "残高": balance,
            })
        return rows


def build_table(rows):
    """取引先ごと・区分ごとに残高を合計する。"""
    names = [b[0] for b in BUCKETS]
    table = {}
    order = []
    for r in rows:
        key = (r["取引先コード"], r["取引先名"])
        if key not in table:
            table[key] = dict((n, 0) for n in names)
            order.append(key)
        table[key][r["区分"]] += r["残高"]
    return names, order, table


def yen(n):
    return "{0:,}".format(n)


def disp_width(text):
    """全角文字を2文字分として数えた表示幅を返す（表の桁をそろえるため）。"""
    return sum(2 if unicodedata.east_asian_width(c) in "FWA" else 1 for c in text)


def pad_left(text, width):
    return " " * max(0, width - disp_width(text)) + text


def pad_right(text, width):
    return text + " " * max(0, width - disp_width(text))


def print_table(names, order, table, base_date, rows):
    name_w = max([disp_width(n) for _, n in order] + [disp_width("取引先")]) + 2
    col_w = max([disp_width(n) for n in names] + [13]) + 1
    total_w = name_w + col_w * (len(names) + 1)

    header = pad_right("取引先", name_w) + "".join(pad_left(n, col_w) for n in names) \
        + pad_left("合計", col_w)
    print("")
    print("年齢調べ表（基準日 {0}）".format(base_date.strftime("%Y年%m月%d日")))
    print("-" * total_w)
    print(header)
    print("-" * total_w)

    totals = dict((n, 0) for n in names)
    for key in order:
        code, name = key
        line = pad_right(name, name_w)
        row_total = 0
        for n in names:
            v = table[key][n]
            totals[n] += v
            row_total += v
            line += pad_left(yen(v) if v else "-", col_w)
        line += pad_left(yen(row_total), col_w)
        print(line)

    print("-" * total_w)
    line = pad_right("合計", name_w)
    grand = 0
    for n in names:
        line += pad_left(yen(totals[n]), col_w)
        grand += totals[n]
    line += pad_left(yen(grand), col_w)
    print(line)
    print("")

    overdue = [r for r in rows if r["経過日数"] > 90]
    if overdue:
        print("91日を超えて滞留している債権（{0}件）".format(len(overdue)))
        for r in sorted(overdue, key=lambda x: -x["経過日数"]):
            print("  {0}  {1}  {2}  経過{3}日  {4}円".format(
                r["取引先名"], r["請求番号"], r["入金期日"], r["経過日数"], yen(r["残高"])))
        print("")
        print("※ 区分と金額は入力データの一次集計です。回収可能性の判断、")
        print("   貸倒懸念債権への該当の有無は、取引先の状況を確認したうえで人が行ってください。")
        print("")


def write_csv(path, names, order, table):
    with open(path, "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["取引先コード", "取引先名"] + names + ["合計"])
        totals = dict((n, 0) for n in names)
        for code, name in order:
            values = [table[(code, name)][n] for n in names]
            for n, v in zip(names, values):
                totals[n] += v
            w.writerow([code, name] + values + [sum(values)])
        total_values = [totals[n] for n in names]
        w.writerow(["", "合計"] + total_values + [sum(total_values)])


def main():
    p = argparse.ArgumentParser(description="滞留債権の年齢調べ表を作ります。")
    p.add_argument("csv_path", help="入力CSVのファイル名")
    p.add_argument("--base-date", default=None,
                   help="基準日（YYYY-MM-DD）。省略すると今日の日付を使います。")
    p.add_argument("--out", default=None, help="集計結果を書き出すCSVのファイル名")
    args = p.parse_args()

    if args.base_date:
        base_date = parse_date(args.base_date, 0, "--base-date")
    else:
        base_date = date.today()

    rows = read_rows(args.csv_path, base_date)
    if not rows:
        print("残高のある債権がありませんでした。")
        return

    names, order, table = build_table(rows)
    print_table(names, order, table, base_date, rows)

    if args.out:
        write_csv(args.out, names, order, table)
        print("集計結果を {0} に書き出しました。".format(args.out))


if __name__ == "__main__":
    try:
        main()
    except FileNotFoundError as e:
        sys.exit("ファイルが見つかりません: {0}".format(e.filename))
