## 依頼文

# 依頼文（実装担当のAIに渡す）

## 1. 目的
harukaze に `order-suggest` というサブコマンドを足します。`python -m harukaze order-suggest --date 2026-10-26` と打つと、基準日の時点で有効在庫が発注点を下回っている「販売中」の商品と、その発注数が、仕入先ごとに並んだ発注書の形の CSV で標準出力に出るようにします。仕入先ごとの合計金額が最低発注金額に届かないときは、その仕入先の行に印をつけます。

## 2. 背景
- 総務・購買の購買担当は、毎週月曜に発注点を切った商品を目で探して発注書を作っています。時間がかかるうえ、見落としもあります。
- この CSV を見て、仕入先ごとの発注書を作ります。そのため列は、既存の `po-export` が出す発注書 CSV（`harukaze/orders.py` の `PO_COLUMNS`）にそろえます。
- コマンドの形（`order-suggest --date YYYY-MM-DD`）は、情シスと話して決めたものです。

## 3. 触ってよい範囲
- 新しく作ってよいファイル: `harukaze/` 直下の `.py` を1つ（例: `harukaze/suggest.py`）と、`tests/` 直下の `test_*.py` を1つ（例: `tests/test_suggest.py`）。
- 変えてよい既存ファイル: `harukaze/cli.py`（`order-suggest` を `command` デコレーターで登録するだけ）と、`README.md`（「コマンド一覧」の表に1行足すだけ）。
- このほかのファイルやフォルダは、作ったり変えたり消したりしないでください（README.md の「開発の決まり」と同じです）。

## 4. やらないこと
- `harukaze/orders.py`・`stock.py`・`store.py`・`csvout.py`・`__init__.py`・`__main__.py`、`data/` の CSV、既存のテスト（`tests/helpers.py` も含む）は変えないでください。`PO_COLUMNS` そのものを書き換えるのもだめです。
- 既存のサブコマンド（`products`・`suppliers`・`stock`・`moves`・`po-export`）の動きは変えないでください。
- 発注データを保存する、ファイルに書き出す、発注番号をふる、`open_orders.csv` を更新する、といったことはしません。出力先は標準出力だけです。
- 最低発注金額に届かせるために、発注数を増やしたり商品を足したりはしません。印をつけるだけです。
- 外部ライブラリは足しません（Python 3.11 以上の標準ライブラリだけで書きます）。
- バージョン番号は上げず、コミットもしません。

## 5. 決まり（要件）
1. サブコマンドの名前は `order-suggest` です。`--date YYYY-MM-DD` は必須の引数で、型は既存の `_date_arg`（`parse_date`）と同じにします。`--date` を付けずに実行したら、終了コードは 0 以外になります。
2. `--data-dir` は、既存のほかのコマンドと同じように親パーサーから自動で付きます。新しく定義し直さないでください。
3. 候補にするかどうかを調べるのは、商品マスタ（`products.csv`）で状態が「販売中」の商品だけです。「終売予定」の商品と、「販売中」以外の状態の商品は、どんな在庫でも候補にしません。
4. 有効在庫は「基準日時点の現在庫 ＋ 発注済みで未入荷の数 − 引当済みの数」です。既存の関数をそのまま使って求めます。
   - 現在庫は `stock_as_of(store, 基準日)` の値です。基準日の当日の入出庫も数えます。
   - 未入荷の数は、`open_orders(store)` のうち、その商品コードの行の数量の合計です。
   - 引当済みの数は `allocated_qty(store, 商品コード)` です。
   - 3つを `available_stock` に渡して、有効在庫にします。
5. 候補になるのは、有効在庫が発注点より小さい商品だけです。有効在庫が発注点と同じ商品は候補にしません。
6. まず「発注点 × 2 − 有効在庫」を出し、それを入数の倍数に切り上げた数を発注数にします。切り上げには既存の `round_up_to_case` を使います。
7. 「金額」は、発注数 × 仕入単価（税抜）です。
8. 出力は CSV です。列は `PO_COLUMNS` と同じ8列を同じ順に並べ、そのあとに「保留」列を1つ足して、全部で9列にします。列の中身は、`po-export` の同じ名前の列と同じ意味にします。
9. 仕入先ごとに、その仕入先の候補行の「金額」を合計します。合計が仕入先マスタの最低発注金額より小さいときは、その仕入先の行すべての「保留」列に「最低金額未満」と書きます。合計が最低発注金額以上のときは、「保留」列を空にします。
10. 行は仕入先コードの昇順に並べ、仕入先コードが同じ行は商品コードの昇順に並べます（`po-export` と同じ並びです）。
11. 書き出しには既存の `write_csv` を使います（UTF-8、改行は LF、見出し行あり）。候補が1件もないときは、見出し行だけを出して、終了コードは 0 にします。
12. README.md の「コマンド一覧」の表に、`order-suggest --date YYYY-MM-DD` とその説明（「基準日時点の発注候補を仕入先ごとに出す」）を1行足します。
13. 新しいテストファイルで、少なくとも次のことを確かめます。データは `tests/helpers.py` の `make_data_dir` で一時フォルダに作ります。
    - 有効在庫が発注点と同じ商品は候補に入らない
    - 発注点を1下回る商品は候補に入る
    - 「終売予定」の商品は候補に入らない
    - 未入荷の数が足されている（現在庫だけなら下回るが、未入荷を足すと下回らない商品は出ない）
    - 引当済みの数が引かれている
    - 発注数が入数の倍数に切り上げられている
    - 「保留」は仕入先の全行で同じ値になり、合計が最低発注金額と同じときは空になる
    - 行の並び順
    - 列が9列で、上の決まり8の順になっている
    - 基準日より後の入出庫は数えない

## 6. 注意点
- 未入荷（`open_orders.csv`）と引当（`allocations.csv`）は、日付で絞らずに全部の行を使ってください（担当者から「既存の関数をそのまま使う」と指示されています）。入荷予定日が基準日より前の行も、そのまま未入荷として数えます（例: PO-2610-01）。
- 発注点が 0 の商品でも、有効在庫がマイナスなら候補になります。今のデータでは、1014 が有効在庫 −3 で発注数 10 になります。これで正しい動きなので、外さないでください。
- 基準日の当日の入出庫も数えます。今のデータでは、1004 の 2026-10-26 の出庫 10 個があるので、`--date 2026-10-26` なら候補に入り、`--date 2026-10-25` なら有効在庫 60 が発注点 60 と同じになって候補に入りません。
- `store.products()` はファイルの並び順で返ってきて、商品コード順ではありません。並べ替えは決まり10のとおりに自分でしてください。
- 計算の部分は新しいモジュールに関数として置き、`cli.py` には登録と呼び出しだけを書いてください（`po-export` と `orders.py` の分け方と同じです）。商品コードと仕入先から行を作るところは、既存の `build_po_rows` を使ってもかまいません。その場合も「保留」列は自分で足してください。
- 出力は必ず `write_csv` を通してください。print で書くと、Windows では改行が CRLF になったり、文字コードがずれたりします。CLI のテストは、既存の `tests/test_cli.py` の `run_cli` と同じように `PYTHONIOENCODING=utf-8` を付けてサブプロセスで動かしてください。
- テストで作る一時ファイルは、`make_data_dir` か `tempfile` で作り、`addCleanup` で消してください。作業フォルダにファイルを残さないでください（`__pycache__` は `.gitignore` に入っているので、残ってもかまいません）。

## 7. 完了の条件と確かめ方
1. `python -m unittest discover -s tests` を実行して、既存のテストと新しいテストが全部通ること。
2. `python -m harukaze order-suggest --date 2026-10-26` の出力が、見出し行（仕入先コード, 仕入先名, 商品コード, 商品名, 入数, 発注数, 仕入単価（税抜）, 金額, 保留）と、次の10行だけであること。この順に並び、改行は LF です。

| 仕入先コード | 商品コード | 発注数 | 金額 | 保留 |
|---|---|---|---|---|
| 201 | 1001 | 50 | 19000 | （空） |
| 201 | 1022 | 50 | 13000 | （空） |
| 201 | 1023 | 20 | 10800 | （空） |
| 202 | 1007 | 40 | 18000 | （空） |
| 202 | 1009 | 18 | 17640 | （空） |
| 202 | 1014 | 10 | 2400 | （空） |
| 203 | 1004 | 80 | 7600 | 最低金額未満 |
| 203 | 1012 | 60 | 7200 | 最低金額未満 |
| 204 | 1015 | 20 | 17800 | 最低金額未満 |
| 204 | 1018 | 15 | 19500 | 最低金額未満 |

   仕入先ごとの合計は、201 が 42,800円（最低 30,000円）、202 が 38,040円（最低 20,000円）、203 が 14,800円（最低 15,000円）、204 が 37,300円（最低 50,000円）です。1003・1006（未入荷を足すと発注点以上）、1019・1024（発注点と同じ）、1011・1020（終売予定）は出てはいけません。
3. `python -m harukaze order-suggest --date 2026-10-25` では 1004 が出ず、203 の行は 1012 だけになること（「保留」は「最低金額未満」のまま）。
4. `python -m harukaze order-suggest` を `--date` なしで実行すると、終了コードが 0 以外になること。
5. `git status` で、変わっているのが `harukaze/cli.py` と `README.md`、新しいファイルが `harukaze/` 直下の `.py` 1つと `tests/` 直下の `test_*.py` 1つだけであること。出力ファイルなど、ほかのファイルが残っていないことも確かめてください。
