見積・価格表
位置づけ: 後続フェーズ
見積・価格表モジュールは、仕入先または催事から受領する価格表を基に部材の仕入原価・希望小売を管理し、そこから販売価格を時間軸付きで導出し、案件に紐づく御見積書を作成・印刷する一連の機能である。価格データは外部システム参照ではなく CRM 側で保持する。
設計の出自は、クライアント自作 ERP ValueOS のテーブル設計(products / suppliers / sales_prices / purchase_prices)と、実物の価格表 PDF・御見積書ひな形である。crm_app には lib/pricing.ts とストア(store/usePricing.ts)、および価格表・見積・帳票印刷・販売価格マスタの画面一式が動作する形で実装済みである。現場では現状、仕入(原価)内訳をすべて手入力しており、価格表からの自動引き当てが最優先要望である(定例会ログ 参照)。
スコープ/対象
Section titled “スコープ/対象”第1弾リリース(催事+アイテム+商社)の範囲外であり、後続フェーズで実装する。第1弾との接続点は次の3つである。
| 接続先 | 内容 |
|---|---|
| アイテム(商品マスタ) | productNo(型番)で部材・販売価格・商品マスタを横断して同定する |
| 卸金額 | 仕入価格・販売店向け卸価格の時間軸設計を本モジュールと統一する |
| 催事 | 催事専用価格表を独立の source として管理し、催事・会場とひも付ける |
Supabase への永続化・移行は未実装である。現時点はフロント内ストア(zustand persist、キー crm:pricing、スキーマバージョン3)で完結する。永続化はブラウザの localStorage 相当であり、端末・ブラウザをまたいだ共有や複数ユーザー間の同時編集の整合は担保されない。
データモデル
Section titled “データモデル”構造は「価格表(PriceBook)が版のヘッダ」「部材(Part)がその明細行」「パッケージ(PackageSet)が部材の組合せ」「販売価格(SalesPrice)が売値の時間軸」「見積(Quote)が出口」の5層で組み立てる。
正規化方針は、仕入価格を価格表側、販売価格を SalesPrice 側に分離することである。同じ商品でも仕入軸と販売軸を独立に改定できる。商品の同定キーは productNo(型番)であり、Part・SalesPrice・アイテム(商品マスタ)を横断で結ぶ共通キーとする。
版管理は「行を書き換えず、反映日付きの新しい行を積む」イミュータブル方式を採る。過去見積の再現性を担保する設計である。行の更新系関数(updatePriceBook / updatePart)はストアに存在するが、価格改定用の画面導線は無く、既存行の書き換えではなく新規版の追加が正規の運用である。
| エンティティ | 軸(キー) | 主な属性 | ValueOS対応 |
|---|---|---|---|
| PriceBook | source × manufacturerId × title × effectiveDate | category, note | 対応する版管理テーブルなし(ギャップ参照) |
| Part | bookId × no | productNo, retailPrice, netPrice, tier | products |
| PackageSet | bookId × id | members(no・qty・tier), setPrice | packages, package_items(tier は package_items.requirement_type に相当。案件スナップショットの case_packages / case_package_items は別層) |
| SalesPrice | productNo × channel × startDate | price, endDate, memo | sales_prices |
| Quote | id(dealId で案件と相互参照) | systemItems, workItems, discounts, taxRate | 対応するテーブルなし。確定後は orders 相当に接続する |
全フィールド仕様
Section titled “全フィールド仕様”lib/pricing.ts の型定義に基づく。「必須」は TypeScript の型上の必須(? の有無)であり、画面側の入力バリデーションとは一致しない場合がある(該当箇所は制約欄に注記)。
Manufacturer
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| id | string | 必須 | — | 一意。シード7件(MK01〜MK07) |
| name | string | 必須 | — | メーカー名 |
PriceBook
| フィールド | 型/enum | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| id | string | 必須 | — | 新規登録UIは "PB" + 連番(既存件数+1から未使用値まで加算) |
| source | string | 必須 | — | 仕入先/催事名。フリーテキスト、名寄せルールなし |
| category | "商社"|"施工店"|"アライアンス"|"催事" | 必須 | フォーム既定は"商社" | 4値enum |
| manufacturerId | string(FK→Manufacturer.id) | 必須 | 先頭メーカー | 未登録メーカーは選択不可(Select由来) |
| title | string | 必須 | — | 件名 |
| effectiveDate | string(YYYY-MM-DD) | 必須 | 当日日付 | 空文字チェックのみで日付形式の検証はコード上ない |
| note | string | 任意 | — | 消費税区分・保証条件などの自由記述 |
Part(価格表の1行)
| フィールド | 型/enum | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| id | string | 必須 | — | 新規登録UIは "PT" + 連番 |
| bookId | string(FK→PriceBook.id) | 必須 | — | — |
| no | number | 必須 | 既存最大No+1 | 価格表内の表示No。同一bookId内の重複防止はコード上ない |
| name | string | 必須 | — | 商品名 |
| productNo | string | 任意 | — | 型番。アイテムマスタとの横断キー |
| retailPrice | number | 任意 | — | 希望小売価格。未入力可 |
| netPrice | number | 必須 | — | 貴社仕切(仕入原価)。値引き行は負値で表現 |
| tier | "必須"|"選択必須"|"オプション" | 必須 | フォーム既定は"必須" | 見積作成画面での自動投入・択一制御には未接続(後述) |
PackageMember(パッケージの構成部材1件)
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| no | number(同一bookId内のPart.noを参照) | 必須 | — | 対応するPartが見つからない場合は表示・原価計算で無言で0/「—」扱い |
| qty | number | 必須 | 1 | Math.max(1, 入力値) で1未満を1に補正 |
| tier | "必須"|"選択必須"|"オプション" | 必須 | 対象Partのtierを継承 | Part.tierとは独立の値。パッケージごとに上書き可能 |
PackageSet
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| id | string | 必須 | — | 新規登録UIは "PK" + 連番 |
| bookId | string(FK) | 必須 | — | — |
| name | string | 必須 | — | — |
| members | PackageMember[] | 必須 | [] | 登録時は1件以上必須(0件はUIエラー) |
| setPrice | number | 必須 | — | 部材積上げと独立の値。負数も型上は許容(画面の下限チェックなし) |
SalesPrice
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| id | string | 必須 | — | 新規登録UIは "SP" + (件数+1) |
| productNo | string | 必須 | — | 型番 |
| productName | string | 任意 | — | — |
| channel | string | 任意(空=標準) | — | 型はフリーテキストだが、登録画面のSelectは「訪問販売/催事/カインズ」の3値に固定。シードには他の値も混在し、型と画面入力候補が一致していない |
| price | number | 必須 | — | 税抜 |
| startDate | string(YYYY-MM-DD) | 必須 | — | 反映日 |
| endDate | string | 任意 | — | 無指定は無期限 |
| memo | string | 任意 | — | — |
QuoteItem(システム明細・工事明細 共通)
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| name | string | 必須(型上) | "" | 空文字での保存を止めるバリデーションはない |
| model | string | 任意 | — | システム明細のみ入力欄あり。工事明細では常に非表示(帳票でも型番列は空表示) |
| qty | number | 必須 | 1 | `Number(入力) |
| unit | string | 必須 | ”式” | — |
| amount | number | 必須 | 0 | 負値許容(値引き相当の行を作れる)。同じく非数値入力は0 |
| cost | number | 任意 | undefined | 未設定行は原価集計上0として扱われる(後述の業務ルール参照) |
QuoteDiscount
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| label | string | 必須(型上) | "" | 空ラベルの保存を止めるバリデーションはない |
| amount | number | 必須 | 0 | 負値運用が前提(UIは負値のみ赤字表示。符号を強制するチェックはない) |
Quote
| フィールド | 型 | 必須 | 既定値 | 制約 |
|---|---|---|---|---|
| id | string | 必須 | 自動採番 Q-${Date.now().toString(36)}-${連番} | 一覧・詳細双方のキー |
| date | string(YYYY-MM-DD) | 必須 | 当日 | — |
| to | string | 必須(型上、空可) | "" | 「様」は付けずに保存する運用(帳票側で付与) |
| subject | string | 必須(型上、空可) | "" | DealDetailからの起票時は先頭商材の maker + model から自動生成、なければ「〈顧客名〉様」 |
| paymentTerms | string | 必須(型上、空可) | “お打ち合わせ” | — |
| systemItems | QuoteItem[] | 必須 | [] | — |
| workItems | QuoteItem[] | 必須 | [] | — |
| discounts | QuoteDiscount[] | 必須 | [] | — |
| taxRate | number | 必須 | 0.1 | 生成元(QuotesPage / DealDetail)は常に0.1固定で作成。QuoteEditorに編集UIがなく、画面操作での税率変更手段はない |
| dealId | string(FK→Deal.id) | 任意 | — | DealDetailからの起票時のみ設定。QuotesPageからの新規作成では未設定のまま |
| owner | string | 任意 | — | 担当者名(帳票に印字) |
| ownerMobile | string | 任意 | — | 担当者携帯(帳票に印字) |
価格表(PriceBook)
Section titled “価格表(PriceBook)”PriceBook は { source(仕入先/催事名), category, manufacturerId, title(件名), effectiveDate(反映日)} で構成する。同一の仕入先×メーカーで反映日違いの版が並存する。
category は「商社 / 施工店 / アライアンス / 催事」の4区分とし、受領した価格表 PDF 群のフォルダ構成に対応する。現在版の導出は currentBooks() が「反映日 <= 基準日」の版のうち、source×メーカー×件名ごとに最新の1件を採用する(詳細な判定ロジックは後述の業務ルール参照)。
催事専用価格表は独立の source として扱い、催事 モジュールの会場・催事とひも付ける。
部材(Part)
Section titled “部材(Part)”Part は価格表の1行であり、{ no(表内No), name, productNo, retailPrice(希望小売価格), netPrice(貴社仕入=原価), tier } で構成する。実装コード(lib/pricing.ts)上の型コメントは netPrice を「貴社仕切(仕入)」と表記するが、卸金額モジュールでの「仕切」(販売店向け卸価格)とは別概念であり、表記の整理は確認事項とする。
netPrice は値引き行を負値で表現する。これは値引きも部材の1行として価格表に載る実 PDF の構造をそのまま採用したものである。retailPrice は欠損を許容する任意項目である。
見積明細(QuoteEditor の「価格表から引く」カード)で価格表の部材から引き当てる際、売値(amount)は「retailPrice が存在し、かつ 0 より大きい値であればそれを採用し、それ以外(未設定または0円)は netPrice」の2段で決定し、cost には常に netPrice(仕入原価)を保持する。retailPrice = 0 を明示的な「無料」として登録した部材も自動的に netPrice にフォールバックする点に注意する(0円を意図した希望小売価格と、未入力の欠損とを画面上区別できない)。下記の SalesPrice を参照した自動引当は未実装であり、currentSalesPrice() は見積作成フローから呼び出されていない。
tier(区分)は「必須 / 選択必須 / オプション」の3値である。パッケージ詳細画面(PriceBookDetail)では構成部材を区分ごとに見出し分けして表示するが、見積作成画面(QuoteEditor の「価格表から引く」カード)では tier による自動投入・択一制御・追加可否の出し分けは実装されていない。表示される全ての部材・パッケージに対して「システムへ」「工事へ」ボタンが等しく提示され、どの区分の行を何件追加するかは操作者の手動判断に委ねられる。
パッケージ(PackageSet)
Section titled “パッケージ(PackageSet)”PackageSet は { name, members: PackageMember[], setPrice } で構成する。members は価格表内の部材No+数量の組合せである。
PackageMember は tier を持つ。部材そのものの区分(Part.tier)とは別に、そのパッケージでの必要度を表現する二重構造である。setPrice は部材積上げと独立に設定でき、セット割引を表現する。パッケージは価格表(bookId)に従属し、版が変わればパッケージも新版に載せ直す。
パッケージを見積のシステム明細に追加すると、明細行の amount には setPrice がそのまま入り、cost には構成部材(同一 bookId 内で no が一致する Part)の netPrice × qty の合計(packageCost())が入る。対応する Part が見つからない構成No は原価計算から無言で除外される(0円扱い)。
卸金額マスタ(wholesalePricing.ts)側でも itemKind として「商品 / パッケージ」を区別しており、本モジュールのパッケージ単位の卸価格設定と整合させる(卸金額 参照)。
販売価格(SalesPrice)と時間軸
Section titled “販売価格(SalesPrice)と時間軸”SalesPrice は { productNo, channel(商流/代理店。空=標準), price(税抜), startDate(反映日), endDate } で構成する。ValueOS の sales_prices に対応する。
現在価格の導出関数 currentSalesPrice() は、productNo × channel で「startDate <= 基準日 <= endDate(endDate 無しは無期限)」に該当するもののうち、反映日が最新の1件を採用する。なお channel 引数を渡さない場合は「channel 空=標準」のレコードに絞られるのではなく、全 channel を横断した最新1件が返る実装である(channel ? s.channel === channel : true)。ただし SalesPrice.channel は自由文字列であり(シードには「訪問販売」「催事」のほか「カインズ」のような個別代理店名も混在する)、CRM の Deal.channel(訪問販売/催事/反響/パートナー の4値enum)とは語彙が一致しない。案件から自動で該当商流の価格を引く連携も未実装であり、currentSalesPrice() は見積作成フロー(QuoteEditor)からは呼び出されていない。見積の売値は前述のとおり価格表 Part 側(retailPrice→netPrice)または原価マスタ(costbook)側で引き当てており、SalesPriceは現状、参照系マスタ(/sales-prices 画面での一覧・登録)として独立している。
同じ「反映日パターン」を卸金額(仕入価格+販売店向け卸価格 × 仕入先 × 反映日)にも適用し、価格系テーブルの時間軸設計を統一する。過去日を基準日にすれば当時の価格を再現できるため、見積・帳票の事後検証、および価格改定の予約登録(未来日の反映日)が可能である。
| 価格種別 | 軸 | 金額項目 | 導出関数 |
|---|---|---|---|
| 価格表(PriceBook × Part、仕入) | source × manufacturerId × title × effectiveDate | netPrice / retailPrice | currentBooks() |
| 販売価格(SalesPrice) | productNo × channel × startDate/endDate | price | currentSalesPrice() |
| 卸金額(WholesalePrice、商社モジュール) | 仕入先 × effectiveDate | purchasePrice / 卸価格 | 本モジュールと様式を統一する |
状態遷移・ワークフロー
Section titled “状態遷移・ワークフロー”部材投入ワークフロー(tier区分の実際の扱い)
Section titled “部材投入ワークフロー(tier区分の実際の扱い)”前述のとおり、tier区分は見積作成画面での自動化・強制には結びついていない。実際の操作フローは次のとおりである。
flowchart TD
A[価格表を選択<br/>PriceLookupCard] --> B{パッケージから追加するか<br/>部材単位で追加するか}
B -- パッケージ --> C["「システムへ」をクリック<br/>amount=setPrice, cost=packageCost()"]
B -- 部材単位 --> D[部材一覧から任意の行を選択<br/>tier区分はバッジ表示のみ・投入可否には無関係]
D --> E["「システムへ」または「工事へ」をクリック<br/>amount=retailPrice(>0)優先/netPrice, cost=netPrice"]
C --> F[明細行として追加される]
E --> F
G[原価マスタから引く<br/>CostLookupCard] --> H["「システムへ」をクリック<br/>amount=shikiri(仕切), cost=shiire(仕入)"]
H --> F
F --> I[数量・金額・型番を明細行で直接編集]
I --> J[割引行を追加(負値・赤字表示)]
J --> K[集計サマリで税込合計・原価合計・粗利を確認]
tier が実際に業務ルールとして機能するのは、パッケージ詳細画面(PriceBookDetail)での表示グルーピング(必須/選択必須/オプションの見出し分け)と、パッケージ登録時に構成部材ごとの必要度を記録する用途に限られる。
現在有効版の判定(反映日ロジック)
Section titled “現在有効版の判定(反映日ロジック)”currentBooks()(PriceBook)と currentSalesPrice()(SalesPrice)はいずれも「反映日 <= 基準日」で絞り込み、グループ内最新1件を採る同型のロジックだが、同日タイの解決方法が異なる。
flowchart TD
A[対象レコード一覧] --> B{反映日 <= 基準日か<br/>PriceBook: effectiveDate<br/>SalesPrice: startDate}
B -- いいえ --> Z[対象外]
B -- はい --> C{SalesPriceのみ:<br/>endDate未設定 or 基準日以前終了でないか}
C -- 期限切れ --> Z
C -- 有効/対象外(PriceBook) --> D["グルーピングキーで集約<br/>PriceBook: source×manufacturerId×title<br/>SalesPrice: productNo×channel"]
D --> E{グループ内で反映日が最大か}
E -- いいえ --> Z
E -- はい --> F[現在有効な1件として採用]
- PriceBook:
Mapへの書き込みがeffectiveDate > 既存.effectiveDateの厳密不等号のため、同日の版が複数存在する場合は配列内で先に処理された(登録が古い)版が採用され続ける。 - SalesPrice:
sortの比較関数が同日ペアに対して非対称な値(両方向とも-1)を返す実装になっており、同日タイの並び順は仕様として確定していない(ブラウザ・エンジンのソート実装依存)。
見積のライフサイクル
Section titled “見積のライフサイクル”Quote 型に status 相当のフィールドは存在しない。以下は画面遷移から推定した論理上の状態であり、業務ステータス(確定・送付済み等)や編集ロックは実装されていない。
stateDiagram-v2
[*] --> 下書き: 新規作成(QuotesPage「新規見積」/DealDetail「見積を作成」)
下書き --> 編集中: /quotes/$quoteId へ遷移、明細0件の状態
編集中 --> 編集中: 価格表・原価マスタからの引当/手入力での加除
編集中 --> 印刷表示: 「印刷 / DL」→ /quote-print/$quoteId
印刷表示 --> 編集中: 「戻る」で再度編集可能(無制限に繰り返し可)
note right of 編集中
確定・送付済み等のステータス、
印刷後の編集ロックは未実装。
removeQuote はストアに定義済みだが
呼び出すUIが存在せず、削除操作は事実上ない。
end note
主要フローのシーケンス
Section titled “主要フローのシーケンス”案件(Deal)起点で見積を作成し、価格表・原価マスタから明細を引き当て、御見積書として印刷するまでの一連の流れを示す。
sequenceDiagram
actor User as 営業/事務(BO)
participant Deal as DealDetail
participant Store as usePricing(zustand persist)
participant Editor as QuoteEditor
participant Lookup as PriceLookupCard / CostLookupCard
participant Print as QuotePrint
User->>Deal: 「見積を作成」をクリック
Deal->>Store: createQuote({dealId, to, subject, taxRate:0.1, ...})
Store-->>Deal: 新規Quote ID(Q-採番)
Deal->>Editor: /quotes/$quoteId へ遷移
Editor->>Store: quotes.find(id) を購読
User->>Lookup: 価格表の部材/パッケージ、または原価マスタの品目を選択
Lookup->>Editor: addSystemItem / addWorkItem(amount, cost を伴う)
Editor->>Store: updateQuote(id, {systemItems または workItems})
User->>Editor: 割引行・数量・金額を直接編集
Editor->>Store: updateQuote(id, {...})
Editor->>Editor: quoteNet / quoteTax / quoteTotal / quoteGrossProfit を都度再計算<br/>(派生値であり別途保存はしない)
User->>Print: 「印刷 / DL」→ /quote-print/$quoteId
Print->>Store: quotes.find(id) を購読
Print->>User: A4御見積書レイアウトを表示、window.print() で印刷
見積は宛名・件名・支払条件+システム(商品)明細①・工事明細②・割引(負値・赤字表示)・消費税で構成し、実物の御見積書ひな形(①小計+②小計−割引+税)を踏襲する。
| 導出関数 | 算出式 | 端数処理 |
|---|---|---|
quoteSystemSubtotal | Σ systemItems.amount | — |
quoteWorkSubtotal | Σ workItems.amount | — |
quoteDiscountTotal | Σ discounts.amount(通常は負値) | — |
quoteNet(改め合計・税抜) | quoteSystemSubtotal + quoteWorkSubtotal + quoteDiscountTotal | — |
quoteTax(消費税) | Math.round(quoteNet × taxRate) | 四捨五入相当(JSのMath.roundは正値で0.5を切り上げ)。taxRateは生成時0.1固定・画面編集不可 |
quoteTotal(税込合計) | quoteNet + quoteTax | — |
quoteCostTotal(原価合計) | Σ(systemItems ∪ workItems).(cost ?? 0) | cost未設定行は0として計算に混入する |
quoteGrossProfit(粗利) | quoteNet − quoteCostTotal | 税抜ベース(税込合計ではない) |
| 粗利率(画面表示のみ、関数化されていない) | net > 0 ? grossProfit / net : null | net が0以下の場合は画面上「—」表示 |
原価割れ(grossProfit < 0)の場合、集計サマリに「原価割れ」バッジと赤字表示(text-neg)を出す。御見積書(QuotePrint)自体には原価・粗利の項目はなく、顧客向け帳票には出力されない。
価格引当のフォールバック(3系統)
Section titled “価格引当のフォールバック(3系統)”見積の明細行は、次の3系統いずれかからの引当、または直接入力で作られる。SalesPrice を参照する自動引当経路は存在しない。
- 価格表の部材(
PriceLookupCard):amount = (part.retailPrice && part.retailPrice > 0) ? part.retailPrice : part.netPrice、cost = part.netPriceを常に保持。 - 価格表のパッケージ(
PriceLookupCard):amount = pkg.setPrice、cost = packageCost()(構成部材のnetPrice × qty合計。未解決の構成Noは0扱い)。 - 原価マスタ(
CostLookupCard、lib/costbook.ts経由・実データ約221品目src/data/jusetsu.json):amount = item.shikiri(仕切=卸価格)、cost = item.shiire(仕入原価)。システム明細への追加ボタンのみで、工事明細への追加導線はない。
いずれの経路も見積作成画面に到達した時点でのスナップショット値をコピーするだけであり、価格表・原価マスタ側の後続改定は既存見積に遡及しない(イミュータブル版管理の帰結)。
その他の業務ルール
Section titled “その他の業務ルール”- 数量・金額の入力は
Number(入力値) || 0でフォールバックするため、空文字や非数値を入力してもエラーにならず自動的に0になる。 - 部材の
noは同一価格表内で重複防止のバリデーションがない。パッケージのmembers[].noは同一bookId内の部材のみを参照でき、価格表をまたいだ参照はできない。 - 割引額(
QuoteDiscount.amount)は負値運用が前提だが、正値の入力を止めるチェックはない(正値を入れると割引ではなく加算になる)。 - 見積の
taxRateは生成時に常に0.1固定で作られ、QuoteEditorに編集UIがないため、税率が変わった場合の反映手段(複数税率対応・軽減税率など)は現状ない。
試作画面は次の7画面であり、サイドバー「価格・見積」に集約する(実装 app/src/app/routes.tsx の NAV 構成に準拠)。メーカーマスタ(/manufacturers)は Manufacturer の一覧・登録画面として動作するが、サイドバー「マスタ」側に集約されており本グループには含まれない。帳票(/quote-print/$quoteId)は見積詳細画面から遷移するルートとして動作するが、NAVには掲載されない。
| 画面 | パス | 内容 |
|---|---|---|
| 価格表 | /pricing | 価格表一覧(区分チップで絞込)、新規登録ドロワー、/pricing/$bookId で部材+パッケージの詳細・追加 |
| 価格照合 | /price-compare | 仕入先横断の価格照合・最安抽出 |
| 仕切(原価) | /costbook | 品目別の仕入原価・粗利の参照ビュー(画面名は実装NAVラベルに準拠。用語整理は確認事項参照) |
| 販売価格 | /sales-prices | SalesPrice の一覧・登録(MasterScreen 定型構成) |
| 卸金額 | /master-wholesale-price | 仕入価格・販売店向け卸価格の反映日管理(卸金額 と共通画面) |
| 手数料条件 | /fees | 商流別の手数料条件の参照ビュー |
| 見積 | /quotes | 見積の一覧・新規作成・/quotes/$quoteId で編集 |
画面ごとの挙動
Section titled “画面ごとの挙動”- 価格表一覧(
/pricing): 区分(商社/施工店/アライアンス/催事)チップで絞込。反映日が新しい順にソート。「新規価格表」ドロワーで仕入先名・区分・メーカー・件名・反映日(必須)と備考(任意)を登録する。 - 価格表詳細(
/pricing/$bookId): 部材テーブル(No・商品名・商品No・希望小売価格・貴社仕切・区分)とパッケージテーブル(名称・構成・セット価格)を表示。「部材を追加」ドロワーはNo(既存最大+1を初期値)・区分・商品名・商品No・希望小売価格・貴社仕切(必須)を入力する。「パッケージを追加」ドロワーは名称・セット価格に加え、価格表内の各部材ごとに「含める/数量/このパッケージでの区分」をチェックボックス+入力で指定し、1件以上の選択を必須とする。 - 価格照合(
/price-compare):productNo(無ければname)で名寄せしたグループごとに、仕入先別のnetPriceを安い順に並べ、最安値に「最安」バッジを付ける。netPrice <= 0(値引き行・0円行)は比較対象から除外する。「現在有効版」(既定、currentBooks()基準)と「全期間」(価格表の全版を横断)をトグルで切替できる。検索は部材名・型番の部分一致。並び順は「複数仕入先で比較可能なもの優先→仕入先数の多い順→名称の五十音順」。 - 見積一覧(
/quotes): 見積ID・日付・宛名・件名・税込合計・案件(dealIdがあればリンク)の一覧。「新規見積」は空の下書きを即座に作成し、編集画面へ遷移する(下書き保存の確認ステップはない)。 - 見積編集(
/quotes/$quoteId): 基本情報(日付・宛名・件名・支払条件・担当者・担当者携帯)、システム明細、工事明細、割引、右側スティッキーの集計サマリ(小計①②・①②合計・割引計・改め合計・消費税・税込合計・原価合計・粗利・粗利率)、価格表からの引当カード、原価マスタからの引当カード(カテゴリ絞込・品名/コード検索・上位30件表示、超過時は残数を表示)で構成する。全ての入力はストアへの直書きであり、下書き保存や取り消し(アンドゥ)はない。 - 帳票(
/quote-print/$quoteId): A4サイズを想定した御見積書レイアウトを画面上に再現し、window.print()で印刷する。宛名・自社情報(住所・電話・担当者)・件名・支払条件・明細表(システム/工事を縦書きラベルで区分、最低3行のフィラー行で表を安定させる)・小計①②・①〜②合計・割引・改め合計・消費税・税込合計を表示する。原価・粗利の項目はない。負値は「¥-金額」表記。 - 販売価格(
/sales-prices):MasterScreen定型(テーブル+ドロワー+履歴)で構成。列は商品名・型番・商流・販売価格・反映日・終了日。登録フォームの商流はSelect(訪問販売/催事/カインズの3択、空欄可)。 - 仕切(原価)(
/costbook)・手数料条件(/fees): いずれも参照ビューのみで、登録・編集UIはない。原価マスタはカテゴリ絞込対応の一覧表示、手数料条件はタブ(商流別)+行データの汎用ダンプ表示(行の型が多様なため防御的にキー:値で整形するのみ)。
登録UI(新規価格表・部材/パッケージ追加、販売価格の登録)は動作する。PDF/CSVからの一括取込は未実装である。
アイテムマスタ・卸金額との正規化
Section titled “アイテムマスタ・卸金額との正規化”第1弾のアイテム(商品マスタ)を商品の正本とし、Part.productNo / SalesPrice.productNo はアイテムの型番へ外部キー参照する形に寄せる(アイテム(商品マスタ) 参照)。
仕入先の名称は商社モジュールの仕入先マスタ(SUPPLIERS)を単一参照とし、価格表 source との名寄せルールを定める。仕切(原価)マスタ(costbook:画面名は実装NAVラベルに準拠、内容は品目別の仕入原価・粗利。データ源は src/data/jusetsu.json)は価格表 Part へ統合する暫定データである。この costbook 内では shikiri(仕切)が販売店向け卸価格を、shiire(仕入)が仕入原価を指す一方、本モジュールの Part.netPrice は「貴社仕切(仕入)」という表記で仕入原価そのものを指しており、同じモジュール内でも「仕切」の指す対象が食い違っている。手数料条件(fees: 商流別)は商流軸のデータを持つが、粗利計算等への接続は未実装である。加えて SalesPrice.channel(自由文字列)とは軸のキー表記が揃っておらず、商流軸の統一は確認事項とする。
ValueOS では価格の版管理を独立テーブルで持たず、case_products に価格スナップショット(purchase_price / sales_price / gross_profit)を案件登録時点の値として記録する方式を採る。本モジュールの PriceBook / SalesPrice による時間軸管理とは設計思想が異なるため、正規化・接続方針は ValueOS連携ギャップ と データモデル を参照する。
将来、価格表PDF・商社明細のスキャンをAIによって自動取込(OCR/構造化)する構想があり、価格表の登録工数削減を目的とする(AIエージェント 参照)。代理店へのSaaS提供時は、価格表・卸金額を商社機能側のマスタとして切り出し、CRMは参照する構成へ再編する可能性がある。
根拠は lib/permissions.ts の NAV_ALLOW 定義である。
| ロール | 本モジュールへのアクセス |
|---|---|
| 管理者 | 全画面("all")で許可。価格表・見積・原価・販売価格・手数料条件すべてに到達可能 |
| 営業 | /pricing /price-compare /costbook /fees /sales-prices /quotes を許可 |
| 事務(BO) | 同上6パスに加え、他モジュール(発注・申請・卸案件・各種マスタ等)も許可。ただし卸金額(/master-wholesale-price)は NAV_ALLOW に含まれない(到達可能なのは管理者のみ) |
| 発注・申請担当 | 本モジュールのいずれのパスも NAV_ALLOW に含まれない |
| 代理店/代理店(メンバー) | 同上。含まれない |
| 施工店 | 同上。含まれない |
canSeeNav() は Sidebar.tsx / MobileTabBar.tsx からのみ参照されており、サイドバー・モバイルタブへの表示可否を決めるに留まる。/quotes/$quoteId 等のルート自体に対するアクセスガードはコード上実装されておらず、URLを直接指定した場合の到達可否についての制御は確認できない。
原価・粗利の可視性については、lib/permissions.ts に canSeeCost(role) = !isExternal(role)(代理店・施工店には非表示とする想定)が定義されているが、features/pricing 配下(QuoteEditor / QuotePrint / PriceBookDetail 等)のいずれもこの関数を参照していない。原価合計・粗利・粗利率は、画面に到達できた利用者であれば無条件に表示される。現状は NAV_ALLOW によって代理店・施工店が本モジュールの画面自体に到達できないため実害は生じていないが、コスト情報を明示的に隠す実装はモジュール内に存在しない。
見積の削除・承認、価格改定の承認といった操作単位の権限定義は、本モジュール・lib/permissions.ts のいずれにも存在しない。
エッジケース・異常系
Section titled “エッジケース・異常系”- 価格表に部材・パッケージが0件の場合、
PriceLookupCardは「この価格表にはパッケージ・部材がありません」と表示し、追加操作自体ができない(クラッシュしない)。 - 見積の明細が0件の場合、各セクションは「明細なし」「割引なし」を表示し、集計は全て0円になる。
- 存在しない
quoteIdを指定した場合、QuoteEditorとQuotePrintはいずれも専用の「見積が見つかりません」画面と一覧への戻りリンクを表示する。 retailPrice = 0の部材は、意図した「0円商品」であってもnetPriceへフォールバックする(欠損と0円入力を画面上区別できない)。- 値引き行(
netPriceが負値の Part)をそのまま明細に追加するとamount/costともに負値になる。明細行の赤字表示はamount < 0のみで判定しており、costが負値でも色分けはされない。 - パッケージの
members[].noが価格表側で削除・変更され対応する Part が見つからなくなった場合、packageCost()は該当分を無言で0円扱いする(粗利が実態より高く算出される)。 - 価格表・原価マスタを経由しない手入力の明細行は
costが未設定のままになりやすく、原価合計に算入されない(粗利が実態より高く表示される)。 - 同一反映日で複数の
PriceBook版が並存する場合、currentBooks()は登録順(配列内の先勝ち)で1件に決定する。SalesPriceの同日タイは比較関数の実装上、確定した順序仕様がない。 SalesPrice.channel(自由文字列)とDeal.channel(4値enum)の語彙不一致により、将来の自動引当実装時は変換ルールが必要になる。Quoteに確定・送付済み等のステータスがなく、印刷後も無制限に再編集できる。帳票を差し替えても版の履歴は残らない。removeQuoteはストア(usePricing.ts)に定義済みだが、呼び出すUIコンポーネントが存在しない(grep上ゼロ件)。見積の削除操作は事実上未実装である。
productNoの同定キーをアイテムマスタの型番採番規約とどう一致させるか。現試作は文字列一致による仮実装である。- 価格表の
netPrice(仕入原価)と卸金額のpurchasePrice(仕入原価)は別テーブルで併存しており、重複整理の方針を商社モジュール側と合意する必要がある。 - 「仕切」の呼称が実装内で不統一である。原価マスタ(
costbook.ts)では仕切=販売店向け卸価格(shikiri。仕入原価のshiireとは別軸)を指すが、本モジュールのPart.netPriceはコード上「貴社仕切(仕入)」と表記され、実質は仕入原価を指す。画面名(NAVの「仕切(原価)」)も含め、用語統一を商社モジュール側と合意する必要がある。 SalesPrice.channel(自由文字列)とDeal.channel(訪問販売/催事/反響/パートナーの4値enum)の語彙不一致をどちらに寄せるか、また案件からの自動引当(現状未実装)を実装する場合の変換ルールを確定する必要がある。- 見積の値引きに対する承認フロー(上長承認・値引き権限)は未実装であり、要件ヒアリングが必要である。
- Supabase 永続化のテーブル設計、価格改定の監査ログ、税率変更(複数税率・軽減税率対応を含む)時の扱いを確定する必要がある。
- 催事価格表と催事モジュールの参照方向・キー設計を確定する必要がある。
- tier(必須/選択必須/オプション)区分を見積作成フローの自動化(必須の自動投入、選択必須の択一制御)として実装するかどうか、実装する場合のUI仕様を確定する必要がある。