"""記事「卸の直送・預かり・返品は販売管理でどう持つ?」(cms/articles/wholesale-drop-ship-consignment-returns.md)の計算と図。すべて架空のデータ。

1. 状態ごとに動く行(在庫・仕入・売上・請求)を数える
   小さな帳簿(4つの表)を作り、受注明細1行(商品1つ・数量10)を状態ごとの手順で流して、各表に足された行を数える。
   行の持ち方は商いの帳場が組むときの設計(直送は発注と仕入を作る、預かりは自社在庫の出庫と預かり在庫の入庫の2行、
   返品・値引きは売上のマイナス行と請求の返還の行)。分納は2回に分けた場合。
2. 架空の返品・値引き等50件に、適格返還請求書の1万円未満の判定を3通りで当てる
   50件はどれも別々の請求・債権に対するもの。種類は 返品20・値引き12・振込手数料相当額8・リベート(販売奨励金)10。
   明細は税抜の金額と税率(8% か 10%)を持ち、税込は税率ごとに合計して1回だけ切り捨てて出す。
   判定 (a) 明細行ごと(誤り) (b) 税率ごと(誤り。Q&A 問28の注) (c) 請求・債権の単位ごと(基通1-8-17、正しい)。
   (c) で1万円以上なのに (a)・(b) で「1万円未満」とされる件数を数える。
使い方: python scripts/figs/drop_ship_returns.py   (fields/sales-management-app/ で実行。標準ライブラリだけ)
出力: public/img/articles/wholesale-drop-ship-consignment-returns-states.svg(図1。frontmatter の image とアイキャッチ)、
      scripts/figs/.out/ に記事へ貼るインラインSVG 2枚(dsr_fig2_rows.svg・dsr_fig3_judge.svg)、数値は標準出力。
"""
import math, pathlib, random

ROOT = pathlib.Path(__file__).resolve().parents[2]
OUTDIR = ROOT / 'scripts/figs/.out'
SEED = 20261016
INK, GRAY, LINE, ACC, ACC_TXT, SOFT, MID = '#111111', '#6b6b6b', '#dcdcdc', '#678b2c', '#557323', '#eef3e6', '#b5cc8e'
FONT = "'Hiragino Sans','Yu Gothic','Meiryo',sans-serif"


# ---------- 1. 状態ごとに動く行 ----------
class Book:
    def __init__(self):
        self.t = {'在庫': [], '仕入': [], '売上': [], '請求': []}
        self.backlog = 0  # 受注残の更新の回数

    def add(self, table, **row):
        self.t[table].append(row)


def normal(b, q=10):
    b.add('在庫', kind='自社在庫の出庫', qty=-q); b.add('売上', qty=q); b.add('請求', kind='請求明細'); b.backlog += 1

def drop_ship(b, q=10):
    b.add('仕入', kind='発注', qty=q); b.add('仕入', kind='仕入(仕入先から得意先へ直送)', qty=q)
    b.add('売上', qty=q); b.add('請求', kind='請求明細'); b.backlog += 1

def keep(b, q=10):
    b.add('在庫', kind='自社在庫の出庫', qty=-q); b.add('在庫', kind='預かり在庫の入庫(得意先のもの)', qty=q)
    b.add('売上', qty=q); b.add('請求', kind='請求明細'); b.backlog += 1

def keep_out(b, q=10):
    b.add('在庫', kind='預かり在庫の出庫', qty=-q)

def partial(b, q=10, parts=(6, 4)):
    for p in parts:
        b.add('在庫', kind='自社在庫の出庫', qty=-p); b.add('売上', qty=p); b.add('請求', kind='請求明細'); b.backlog += 1

def ret(b, q=3):
    b.add('在庫', kind='戻りの入庫', qty=q); b.add('売上', qty=-q); b.add('請求', kind='返還の行')

def discount(b):
    b.add('売上', qty=0, amount='マイナス'); b.add('請求', kind='返還の行')


STATES = [('通常の出荷', normal), ('直送', drop_ship), ('預かり(売上を立てて預かる)', keep), ('預かりからの出荷', keep_out),
          ('分納(2回)', partial), ('返品(良品の戻り)', ret), ('値引き', discount)]


def count_rows():
    res = []
    for name, f in STATES:
        b = Book(); f(b)
        n = {k: len(v) for k, v in b.t.items()}
        res.append((name, n, sum(n.values()), b.backlog))
    return res


# ---------- 2. 返品・値引き等50件の判定 ----------
def tax_incl(lines):
    """税率ごとに税抜を合計し、税額を1回だけ切り捨てて足した税込(問57と同じく税率ごとに1回の端数処理)"""
    by = {}
    for amt, rate in lines:
        by[rate] = by.get(rate, 0) + amt
    return {r: a + math.floor(a * r / 100) for r, a in by.items()}


def make_events(rng):
    ev = []
    for _ in range(20):  # 返品:食品(8%)・日用品と資材(10%)の明細1〜4行
        lines = []
        for _ in range(rng.randint(1, 4)):
            rate = rng.choice([8, 8, 10])
            lines.append((rng.choice([180, 240, 320, 450, 680, 980, 1500, 2400]) * rng.randint(1, 12), rate))
        ev.append(('返品', lines))
    for _ in range(12):  # 値引き:品質・納期遅れの1行
        ev.append(('値引き', [(rng.choice([1000, 2000, 3000, 5000, 8000, 12000, 15000, 20000]), rng.choice([8, 10]))]))
    for _ in range(8):  # 振込手数料相当額
        ev.append(('振込手数料相当額', [(rng.choice([400, 600, 700]), 10)]))
    for _ in range(10):  # リベート:商品ごとの小さな額が5〜30行(問28の例②の形)
        lines = [(rng.choice([100, 150, 200, 300, 500, 800, 1200]) * rng.randint(1, 6), rng.choice([8, 10]))
                 for _ in range(rng.randint(5, 30))]
        ev.append(('リベート', lines))
    return ev


def judge(ev):
    rows = []
    for kind, lines in ev:
        by = tax_incl(lines)
        total = sum(by.values())
        line_max = max(a + math.floor(a * r / 100) for a, r in lines)
        rows.append(dict(kind=kind, n=len(lines), total=total,
                         need_line=line_max >= 10000, need_rate=max(by.values()) >= 10000, need_unit=total >= 10000))
    return rows


# ---------- SVG ----------
def T(x, y, s, size=12, fill=INK, anchor='start', weight=400):
    return f"<text x='{x}' y='{y}' font-size='{size}' fill='{fill}' text-anchor='{anchor}' font-weight='{weight}'>{s}</text>"


def box(x, y, w, h, lines, fill='#fff', stroke=ACC_TXT, bold=False):
    s = f"<rect x='{x}' y='{y}' width='{w}' height='{h}' rx='6' fill='{fill}' stroke='{stroke}' stroke-width='1.5'/>"
    for i, l in enumerate(lines):
        s += T(x + w / 2, y + 20 + i * 16, l, 12 if i == 0 else 10.5, INK if i == 0 else GRAY, 'middle', 700 if i == 0 else 400)
    return s


def arrow(x1, y1, x2, y2, label='', dash=False):
    d = " stroke-dasharray='4 3'" if dash else ''
    s = f"<line x1='{x1}' y1='{y1}' x2='{x2}' y2='{y2}' stroke='{GRAY}' stroke-width='1.5' marker-end='url(#ah)'{d}/>"
    if label:
        s += T((x1 + x2) / 2 + 4, (y1 + y2) / 2 - 4, label, 10, GRAY)
    return s


def fig1():
    W, H = 720, 420
    s = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' font-family=\"{FONT}\">",
         "<defs><marker id='ah' viewBox='0 0 10 10' refX='9' refY='5' markerWidth='7' markerHeight='7' orient='auto-start-reverse'>"
         f"<path d='M0,0 L10,5 L0,10 z' fill='{GRAY}'/></marker></defs>", f"<rect width='{W}' height='{H}' fill='#fff'/>"]
    s.append(box(20, 170, 110, 56, ['受注', '受注残が立つ'], SOFT))
    # 出荷の3つの道
    s.append(box(200, 30, 170, 62, ['通常の出荷', '自社在庫から出す'], '#fff'))
    s.append(box(200, 112, 170, 62, ['直送', '仕入先から得意先へ'], '#fff'))
    s.append(box(200, 194, 170, 62, ['預かり', '売上を立て品物は倉庫に'], '#fff'))
    s.append(box(200, 276, 170, 62, ['分納', '受注残を残して一部出荷'], '#fff'))
    for y in (61, 143, 225, 307):
        s.append(arrow(130, 198, 198, y))
    s.append(arrow(285, 338, 285, 372, '残りを出荷', dash=True))
    s.append(T(285, 392, '受注残が0になるまで', 10.5, GRAY, 'middle'))
    s.append(box(430, 112, 120, 62, ['売上', '請求明細になる'], SOFT))
    for y in (61, 143, 225, 307):
        s.append(arrow(370, y, 428, 143))
    s.append(box(430, 220, 120, 62, ['預かりから出荷', '在庫の行だけ'], '#fff', GRAY))
    s.append(arrow(370, 232, 428, 250, '', dash=True))
    # 売上のあと
    s.append(box(600, 40, 105, 62, ['返品', '売上マイナス'], '#fff', ACC))
    s.append(box(600, 122, 105, 62, ['値引き', '売上マイナス'], '#fff', ACC))
    s.append(box(600, 204, 105, 62, ['リベート', '売上マイナス'], '#fff', ACC))
    for y in (71, 153, 235):
        s.append(arrow(550, 143, 598, y))
    s.append(T(600, 292, '請求・債権の単位で', 10.5, ACC_TXT, 'start', 700))
    s.append(T(600, 308, '税込1万円以上なら', 10.5, ACC_TXT, 'start', 700))
    s.append(T(600, 324, '適格返還請求書', 10.5, ACC_TXT, 'start', 700))
    s.append(T(20, 410, '架空の卸の伝票の流れ。四角は状態、矢印は次に起きること。出典:商いの帳場の設計(scripts/figs/drop_ship_returns.py)', 10, GRAY))
    s.append('</svg>')
    return ''.join(s)


def fig2(rows):
    W, H = 700, 330
    cols = [('在庫', ACC), ('仕入', MID), ('売上', '#8a8a8a'), ('請求', '#c9c9c9')]
    x0, unit, top, bh, gap = 200, 52, 40, 22, 12
    s = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' role='img' aria-labelledby='dsr2-title' font-family=\"{FONT}\" style='max-width:100%;height:auto'>"]
    desc = '、'.join(f"{name}{tot}行" for name, n, tot, _ in rows)
    s.append(f"<title id='dsr2-title'>受注明細1行を状態ごとに流したとき、在庫・仕入・売上・請求の表に足される行の数の積み上げ棒。{desc}</title>")
    s.append(f"<rect width='{W}' height='{H}' fill='#fff'/>")
    for i, (c, col) in enumerate(cols):
        s.append(f"<rect x='{x0 + i * 80}' y='12' width='12' height='12' fill='{col}'/>" + T(x0 + i * 80 + 17, 22, c + 'の行', 11, INK))
    for j, (name, n, tot, _) in enumerate(rows):
        y = top + j * (bh + gap)
        s.append(T(x0 - 10, y + 15, name, 11.5, INK, 'end', 700))
        x = x0
        for c, col in cols:
            if n[c]:
                s.append(f"<rect x='{x}' y='{y}' width='{n[c] * unit - 2}' height='{bh}' fill='{col}'/>")
                s.append(T(x + n[c] * unit / 2 - 1, y + 15, n[c], 11, '#fff' if c in ('在庫', '売上') else INK, 'middle', 700))
            x += n[c] * unit
        s.append(T(x + 8, y + 15, f'計{tot}行', 11.5, INK, 'start', 700))
    s.append(T(20, H - 30, '受注明細1行(数量10)を流したときに足される行。分納は2回、返品は3個の戻り。受注残の更新は数えていない', 10.5, GRAY))
    s.append(T(20, H - 13, '架空の小さな帳簿で数えた計算値。出典:商いの帳場の計算(scripts/figs/drop_ship_returns.py)', 10.5, GRAY))
    s.append('</svg>')
    return ''.join(s)


def fig3(res):
    W, H = 700, 290
    labels = [('明細行ごとに判定', res['line']), ('税率ごとに判定', res['rate']), ('請求・債権の単位ごと(正しい)', res['unit'])]
    s = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' role='img' aria-labelledby='dsr3-title' font-family=\"{FONT}\" style='max-width:100%;height:auto'>"]
    s.append(f"<title id='dsr3-title'>架空の返品・値引き等50件で、適格返還請求書の交付が要ると判定された件数を判定の単位3通りで比べた横棒。"
             f"明細行ごと{res['line']}件、税率ごと{res['rate']}件、請求・債権の単位ごと{res['unit']}件</title>")
    s.append(f"<rect width='{W}' height='{H}' fill='#fff'/>")
    x0 = 230
    scale = 380 / 50
    for k in range(0, 51, 10):
        x = x0 + k * scale
        s.append(f"<line x1='{x}' x2='{x}' y1='30' y2='190' stroke='{LINE}'/>" + T(x, 206, f'{k}件', 10.5, GRAY, 'middle'))
    for i, (lab, v) in enumerate(labels):
        y = 44 + i * 48
        col = ACC if i == 2 else MID
        s.append(T(x0 - 10, y + 18, lab, 11.5, INK, 'end', 700))
        s.append(f"<rect x='{x0}' y='{y}' width='{v * scale}' height='28' fill='{col}'/>")
        s.append(T(x0 + v * scale + 8, y + 19, f'{v}件', 13, INK, 'start', 700))
    s.append(T(20, 236, f"交付が要る{res['unit']}件のうち、明細行ごとに判定すると{res['unit'] - res['line']}件を「1万円未満で不要」と取り違える(税率ごとは{res['unit'] - res['rate']}件)", 11, INK, 'start', 700))
    s.append(T(20, 258, '50件:返品20・値引き12・振込手数料相当額8・リベート10(どれも別々の請求に対するもの)。税込は税率ごとに1回切り捨て', 10.5, GRAY))
    s.append(T(20, 276, f'架空のデータ(乱数の種 {SEED})。出典:商いの帳場の計算(scripts/figs/drop_ship_returns.py)', 10.5, GRAY))
    s.append('</svg>')
    return ''.join(s)


if __name__ == '__main__':
    rows = count_rows()
    print('# 状態ごとの行')
    for name, n, tot, bl in rows:
        print(name, n, '計', tot, '受注残の更新', bl)
    ev = make_events(random.Random(SEED))
    jr = judge(ev)
    res = {'line': sum(r['need_line'] for r in jr), 'rate': sum(r['need_rate'] for r in jr), 'unit': sum(r['need_unit'] for r in jr)}
    print('# 50件の判定', res)
    for kind in ['返品', '値引き', '振込手数料相当額', 'リベート']:
        sub = [r for r in jr if r['kind'] == kind]
        print(kind, len(sub), '要(単位)', sum(r['need_unit'] for r in sub), '要(行)', sum(r['need_line'] for r in sub),
              '要(税率)', sum(r['need_rate'] for r in sub), '明細行', sum(r['n'] for r in sub),
              '税込 最小', min(r['total'] for r in sub), '最大', max(r['total'] for r in sub))
    print('明細行の合計', sum(r['n'] for r in jr), '1万円未満の件数(単位)', 50 - res['unit'])
    out = ROOT / 'public/img/articles'; out.mkdir(parents=True, exist_ok=True)
    (out / 'wholesale-drop-ship-consignment-returns-states.svg').write_text(fig1(), encoding='utf-8')
    OUTDIR.mkdir(parents=True, exist_ok=True)
    (OUTDIR / 'dsr_fig2_rows.svg').write_text(fig2(rows), encoding='utf-8')
    (OUTDIR / 'dsr_fig3_judge.svg').write_text(fig3(res), encoding='utf-8')
