"""記事 returns-credit-invoice-calc(返品・値引きの返還インボイス、月次で税額はいくら変わる?)の計算と図。すべて架空のデータ。
架空の1か月:14回の納品(8%と10%が混在)と、返品・値引き・リベートの返還伝票4枚(元の請求・債権は3つ)。
売上と返還の差し引きの税額(納める側から見た「売上の税額 − 返還の税額」)を、4通りの単位で計算する。
  A 明細ごとに丸めて合計(国税庁 Q&A 問57 の注の考え方で認められない。比べるためだけに計算)
  B 書類ごと:納品書ごとに税率ごとに1回(売上)と、返還伝票ごとに税率ごとに1回(返還)を別々に出して引く
  C 月まとめの請求書1枚に別建てで書く:売上を税率ごとに1回、返還を税率ごとに1回(問62の1つ目の書き方)
  D 売上から返還を控除した金額で税率ごとに1回(問62の2つ目の書き方、基通1-8-20。注に「税率ごとに1回の端数処理」)
あわせて、返還伝票の「1万円未満」の判定を、伝票ごと(誤り)と元の請求・債権ごと(基通1-8-17)で比べる。
返還の消費税額等は消費税法57条の4第3項5号が「第1項第5号に準じて」計算するとしているので、売上と同じ税率ごと1回の考え方で計算した(筆者の読み)。
使い方: python scripts/figs/returns_credit_calc.py   (fields/sales-management-app/ で実行。標準ライブラリだけ)
出力: 標準出力に JSON、public/img/articles/returns-credit-invoice-calc-4ways.svg(図1。frontmatter の image とアイキャッチ)、
      scripts/figs/.out/rcc_fig2_flow.svg(図2。記事にインラインで貼る処理の流れ図)
"""
import json, math, pathlib, random, sys
from decimal import Decimal, ROUND_HALF_UP
sys.stdout.reconfigure(encoding='utf-8')

ROOT = pathlib.Path(__file__).resolve().parents[2]
SEED = 20261016
FONT = "'Hiragino Sans','Yu Gothic','Meiryo',sans-serif"

# ---------- 売上:14回の納品 ----------
rng = random.Random(SEED)
deliveries = []
for _ in range(14):
    lines = []
    for _ in range(rng.randint(2, 6)):
        rate = 8 if rng.random() < 0.7 else 10
        price = rng.choice([128, 198, 248, 356, 480, 615, 742, 980, 1260, 1580, 2380, 3150])
        lines.append((rate, price * rng.randint(1, 24)))
    deliveries.append(lines)

# ---------- 返還:伝票4枚(元の請求・債権は X・Y・Z の3つ)。明細は (税率, 税抜の金額) ----------
slips = [
    {'id': 'R1', 'kind': '返品', 'claim': 'X', 'lines': [(8, 2380), (8, 1980), (10, 1580)]},
    {'id': 'R2', 'kind': '返品', 'claim': 'X', 'lines': [(8, 3150), (10, 1260), (10, 742)]},
    {'id': 'R3', 'kind': '値引き', 'claim': 'Y', 'lines': [(10, 1800), (10, 1260)]},
    {'id': 'R4', 'kind': 'リベート', 'claim': 'Z', 'lines': [(8, 2480), (8, 2480), (8, 2480), (8, 2480), (8, 2480), (10, 1580), (10, 1580), (10, 1580), (10, 1580)]},
]


def rnd(x100, mode='floor'):
    q = Decimal(x100) / 100
    if mode == 'floor': return math.floor(q)
    if mode == 'ceil': return math.ceil(q)
    return int(q.quantize(Decimal('1'), rounding=ROUND_HALF_UP))


def by_rate(lines):
    s = {8: 0, 10: 0}
    for r, a in lines: s[r] += a
    return s


def compute(mode):
    S_lines = [x for d in deliveries for x in d]
    R_lines = [x for s in slips for x in s['lines']]
    Sb, Rb = by_rate(S_lines), by_rate(R_lines)
    res = {}
    # A 明細ごと
    sA = sum(rnd(a * r, mode) for r, a in S_lines); rA = sum(rnd(a * r, mode) for r, a in R_lines)
    res['A'] = dict(sales=sA, ret=rA, net=sA - rA, ops=len(S_lines) + len(R_lines))
    # B 書類ごと
    sB = rB = opsB = 0
    for d in deliveries:
        b = by_rate(d)
        for r in (8, 10):
            if b[r]: sB += rnd(b[r] * r, mode); opsB += 1
    for s in slips:
        b = by_rate(s['lines'])
        for r in (8, 10):
            if b[r]: rB += rnd(b[r] * r, mode); opsB += 1
    res['B'] = dict(sales=sB, ret=rB, net=sB - rB, ops=opsB)
    # C 月まとめ別建て
    sC = sum(rnd(Sb[r] * r, mode) for r in (8, 10)); rC = sum(rnd(Rb[r] * r, mode) for r in (8, 10))
    res['C'] = dict(sales=sC, ret=rC, net=sC - rC, ops=4)
    # D 控除後で1回
    nD = sum(rnd((Sb[r] - Rb[r]) * r, mode) for r in (8, 10))
    res['D'] = dict(sales=None, ret=None, net=nD, ops=2)
    res['base'] = {'sales': Sb, 'ret': Rb, 'net': {r: Sb[r] - Rb[r] for r in (8, 10)}}
    res['exact_net'] = str(sum(Decimal((Sb[r] - Rb[r]) * r) / 100 for r in (8, 10)))
    return res


out = {'seed': SEED, 'deliveries': len(deliveries), 'salesLines': sum(len(d) for d in deliveries),
       'slips': len(slips), 'returnLines': sum(len(s['lines']) for s in slips)}
out['floor'] = compute('floor'); out['half_up'] = compute('half_up'); out['ceil'] = compute('ceil')
# 返還の税込(伝票ごと・元の請求ごと)と1万円未満の判定
def incl(lines): return sum(rnd(a * (100 + r), 'floor') if False else a for r, a in lines)  # 置き場所のための関数(下で使わない)
def tax_incl(lines):
    b = by_rate(lines)
    return sum(b[r] + rnd(b[r] * r, 'floor') for r in (8, 10))
judge = {'slip': {s['id']: tax_incl(s['lines']) for s in slips}}
claims = {}
for s in slips: claims.setdefault(s['claim'], []).append(s)
judge['claim'] = {c: tax_incl([x for s in v for x in s['lines']]) for c, v in claims.items()}
judge['slipExempt'] = [k for k, v in judge['slip'].items() if v < 10000]
judge['claimExempt'] = [k for k, v in judge['claim'].items() if v < 10000]
out['judge'] = judge
# 申告で控除する返還の消費税額(国税分):原則(消費税法38条1項、返還した税込価額×7.8/110・軽減は6.24/108)と、
# 交付した適格返還請求書の写しを保存している場合の特例(消費税法施行令58条1項、記載した消費税額等×78/100)。月まとめ別建て(C)の数字で比べる
Rb = f_rb = by_rate([x for s in slips for x in s['lines']])
inc = {r: Rb[r] + rnd(Rb[r] * r, 'floor') for r in (8, 10)}
out['controlTax'] = {'taxIncl': inc, 'principle': round(inc[8] * 6.24 / 108 + inc[10] * 7.8 / 110, 2),
                     'tsumiage': round(out['floor']['C']['ret'] * 78 / 100, 2)}
print(json.dumps(out, ensure_ascii=False, indent=1))

# ---------- 図1:4通りの差し引き税額(切り捨て) ----------
f = out['floor']
vals = [('明細ごと(不可)', f['A']['net'], '#a8a8a8', '#444444'),
        ('書類ごと', f['B']['net'], '#b5cc8e', '#111111'),
        ('月まとめに別建て', f['C']['net'], '#678b2c', '#ffffff'),
        ('控除後で1回', f['D']['net'], '#557323', '#ffffff')]
W, H = 720, 340; x0, x1 = 210, 650
lo = min(v[1] for v in vals) - 80; hi = max(v[1] for v in vals) + 10
X = lambda v: x0 + (v - lo) / (hi - lo) * (x1 - x0)
s = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' role='img' aria-labelledby='rcc-4w-t' font-family=\"{FONT}\">",
     f"<title id='rcc-4w-t'>架空の1か月(納品14回、返還伝票4枚、8%と10%が混在、切り捨て)で、売上の税額から返還の税額を引いた額を4通りで計算した横棒グラフ。明細ごと{f['A']['net']:,}円、書類ごと{f['B']['net']:,}円、月まとめに別建て{f['C']['net']:,}円、控除後で1回{f['D']['net']:,}円</title>",
     f"<rect width='{W}' height='{H}' fill='#fff'/>",
     "<text x='30' y='34' font-size='15' font-weight='700' fill='#111111'>売上の税額から返還の税額を引いた額(切り捨て、架空の1か月)</text>"]
for i, (lab, v, col, tc) in enumerate(vals):
    y = 58 + i * 58
    s.append(f"<text x='{x0 - 12}' y='{y + 26}' font-size='13' fill='#111111' text-anchor='end'>{lab}</text>")
    s.append(f"<rect x='{x0}' y='{y}' width='{X(v) - x0:.1f}' height='38' fill='{col}'/>")
    s.append(f"<text x='{X(v) - 10:.1f}' y='{y + 25}' font-size='14' font-weight='700' fill='{tc}' text-anchor='end'>{v:,}円</text>")
s.append(f"<line x1='{x0}' x2='{x0}' y1='50' y2='290' stroke='#444444' stroke-width='1'/>")
s.append(f"<text x='{x1}' y='308' font-size='11' fill='#6b6b6b' text-anchor='end'>軸は{lo:,}円から(差を見やすくするため)</text>")
s.append("<text x='30' y='328' font-size='11' fill='#6b6b6b'>商いの帳場の計算(架空のデータ、乱数の種 20261016、税抜。scripts/figs/returns_credit_calc.py)</text></svg>")
(ROOT / 'public/img/articles').mkdir(parents=True, exist_ok=True)
(ROOT / 'public/img/articles/returns-credit-invoice-calc-4ways.svg').write_text(''.join(s), encoding='utf-8')

# ---------- 図2:処理の流れ図(金額は上の計算値) ----------
j = out['judge']
def box(x, y, w, h, head, lines_, fill='#fff', stroke='#678b2c', hfill='#b5cc8e', htc='#111111'):
    t = [f"<rect x='{x}' y='{y}' width='{w}' height='{h}' rx='6' fill='{fill}' stroke='{stroke}' stroke-width='2'/>",
         f"<rect x='{x}' y='{y}' width='{w}' height='30' rx='6' fill='{hfill}'/>",
         f"<text x='{x + w / 2}' y='{y + 20}' font-size='13' font-weight='700' fill='{htc}' text-anchor='middle'>{head}</text>"]
    for k, l in enumerate(lines_):
        t.append(f"<text x='{x + 12}' y='{y + 52 + k * 20}' font-size='12' fill='#444444'>{l}</text>")
    return ''.join(t)
def arrow(x, y1, y2):
    return f"<path d='M{x} {y1} V{y2}' stroke='#444444' stroke-width='1.6'/><path d='M{x - 5} {y2 - 8} L{x} {y2} L{x + 5} {y2 - 8}' fill='none' stroke='#444444' stroke-width='1.6'/>"
fl = ["<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 700 450' width='700' height='450' role='img' aria-labelledby='rcc-flow-t' "
      f"font-family=\"{FONT}\" style='max-width:100%;height:auto'>",
      f"<title id='rcc-flow-t'>返還伝票を元の請求ごとに合算して税込1万円未満かを判定し、請求書は別建て{f['C']['net']:,}円か控除後{f['D']['net']:,}円で書く流れ図</title>",
      "<rect width='700' height='450' fill='#fff'/>",
      box(20, 16, 200, 104, '1 返還伝票を起こす', ['返品・値引き・リベートごと', '元の請求番号を持たせる', f"架空の月:伝票{out['slips']}枚"]),
      arrow(120, 120, 148),
      box(20, 148, 200, 124, '2 元の請求ごとに合算', ['税込の合計が1万円未満か', f"X={j['claim']['X']:,}円 Y={j['claim']['Y']:,}円", f"Z={j['claim']['Z']:,}円", '(伝票ごとに見ると誤る)']),
      f"<path d='M220 210 H250' stroke='#444444' stroke-width='1.6'/>",
      box(250, 148, 200, 124, '3 1万円以上は交付', ['X と Z は適格返還請求書', 'Y は交付義務なし', '(消法57の4第3項ただし書、', '消令70の9第3項2号)']),
      arrow(290, 272, 300) + arrow(410, 272, 300),
      box(120, 300, 200, 124, '4a 別建てで書く', ['売上の税額を税率ごとに1回', '返還の税額を税率ごとに1回', f"差し引き {f['C']['net']:,}円", '(問62の1つ目)'], stroke='#678b2c'),
      box(340, 300, 220, 124, '4b 控除後の金額で書く', ['売上−返還の金額から', '税率ごとに1回丸める', f"差し引き {f['D']['net']:,}円", '(問62の2つ目、基通1-8-20)'], stroke='#557323', hfill='#557323', htc='#ffffff'),
      "<rect x='480' y='16' width='200' height='112' rx='6' fill='#fbeeea' stroke='#c23b22'/>",
      "<text x='580' y='42' font-size='13' font-weight='700' fill='#a3301b' text-anchor='middle'>どちらでも不可</text>",
      "<text x='494' y='66' font-size='12' fill='#444444'>明細行ごとに税額を丸めて</text>",
      "<text x='494' y='86' font-size='12' fill='#444444'>足し引きする方法</text>",
      f"<text x='494' y='110' font-size='12' font-weight='700' fill='#a3301b'>差し引き {f['A']['net']:,}円(丸め{f['A']['ops']}回)</text>",
      "<text x='20' y='444' font-size='11' fill='#6b6b6b'>国税庁Q&amp;A(令和8年5月改訂)問27・28・62をもとに商いの帳場が作図。金額は架空のデータの計算値(切り捨て)</text></svg>"]
od = ROOT / 'scripts/figs/.out'; od.mkdir(parents=True, exist_ok=True)
(od / 'rcc_fig2_flow.svg').write_text(''.join(fl), encoding='utf-8')

# ---------- 図3:丸め方(切り捨て・四捨五入・切り上げ)ごとの差し引き税額 ----------
rows = [('明細ごと(不可)', 'A'), ('書類ごと', 'B'), ('月まとめに別建て', 'C'), ('控除後で1回', 'D')]
modes = [('floor', '切り捨て', '#678b2c'), ('half_up', '四捨五入', '#c6a47e'), ('ceil', '切り上げ', '#111111')]
allv = [out[m][k]['net'] for m, _, _ in modes for _, k in rows]
lo, hi = min(allv) - 6, max(allv) + 6
W, H = 720, 320; x0, x1 = 190, 680
X = lambda v: x0 + (v - lo) / (hi - lo) * (x1 - x0)
ex = float(out['floor']['exact_net'])
g = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' role='img' aria-labelledby='rcc-md-t' font-family=\"{FONT}\" style='max-width:100%;height:auto'>",
     f"<title id='rcc-md-t'>丸め方3つと計算の単位4つで並べた差し引き税額の点の図。月まとめに別建ては{out['floor']['C']['net']:,}円で変わらず、明細ごとは{out['floor']['A']['net']:,}円から{out['ceil']['A']['net']:,}円</title>",
     f"<rect width='{W}' height='{H}' fill='#fff'/>",
     "<text x='30' y='32' font-size='15' font-weight='700' fill='#111111'>丸め方を変えると、計算の単位ごとの差し引き税額はどう動くか</text>"]
for i, (lab, k) in enumerate(rows):
    y = 80 + i * 52
    g.append(f"<line x1='{x0}' x2='{x1}' y1='{y}' y2='{y}' stroke='#dcdcdc'/>")
    g.append(f"<text x='{x0 - 12}' y='{y + 4}' font-size='13' fill='#111111' text-anchor='end'>{lab}</text>")
    for m, ml, col in modes:
        v = out[m][k]['net']
        g.append(f"<circle cx='{X(v):.1f}' cy='{y}' r='7' fill='{col}'/>")
xe = X(ex)
g.append(f"<line x1='{xe:.1f}' x2='{xe:.1f}' y1='56' y2='268' stroke='#a3301b' stroke-width='1.4' stroke-dasharray='4 3'/>")
g.append(f"<text x='{xe:.1f}' y='284' font-size='11.5' fill='#a3301b' text-anchor='middle'>丸めない税額 {ex:,.2f}円</text>")
for n, (m, ml, col) in enumerate(modes):
    g.append(f"<circle cx='{200 + n * 100}' cy='305' r='6' fill='{col}'/><text x='{210 + n * 100}' y='309' font-size='12' fill='#444444'>{ml}</text>")
g.append(f"<text x='{x1}' y='309' font-size='11' fill='#6b6b6b' text-anchor='end'>横軸は{lo:,}円から{hi:,}円</text></svg>")
(od / 'rcc_fig3_modes.svg').write_text(''.join(g), encoding='utf-8')
