B2B の話をしていると必ず出てくるのが「1社で何人も発注する」です。購買部に3人いて、営業所からも発注が上がる。EC-CUBE の会員はメールアドレス1つに1アカウントなので、そのままだと3人でログイン情報を使い回すことになります。
卸売サイトを立ち上げる順番でも見積依頼でも触れずに来た部分です。作ってみます。
作るもの
取引先の管理者が、マイページから担当者のアカウントを発行できるようにします。

画面は EC-CUBE 4.4 の開発環境で動かしたものです。
やることは4つです。
| やること | 中身 |
|---|---|
| 会員に親子関係を持たせる | 会員エンティティの拡張と列の追加 |
| 担当者を追加・削除する画面 | マイページのコントローラとフォーム |
| 注文履歴を会社単位にする | 一覧はクエリの差し込み口、詳細はイベント |
| ナビと履歴の表示 | マイページのテンプレート |
プラグインにはせず、app/Customize に置く前提で書いています。
取引先を作るか、会員に親を持たせるか
最初に設計の話をします。素直に考えると「取引先」というエンティティを作って、そこに会員をぶら下げたくなります。作りませんでした。
取引先にあたるものは、会員グループ管理プラグインの会員グループがすでに持っているからです。卸価格も、見せる商品も、使える支払い方法も、紐づく先はグループです。ここに会社をもう1つ足すと、価格の根拠が二重になります。
必要なのは「誰が誰の代わりに発注しているか」だけです。会員に親を1つ持たせれば足ります。
| 会員 | 親 | 役割 |
|---|---|---|
| 山田 太郎 | なし | 取引先の管理者 |
| 鈴木 花子 | 山田 太郎 | 担当者 |
| 佐藤 健 | 山田 太郎 | 担当者 |
親を持たない会員が管理者、親を持つ会員が担当者です。同じ親を持つ会員の集まりが、1つの会社になります。
会員に親を持たせる
会員エンティティに、自分自身への関連を1つ足します。EC-CUBE には本体のエンティティを拡張する仕組みがあり、app/Customize/Entity にトレイトを置いて対象のエンティティを指定すると、そこに書いた項目が本体の会員に生えます。プラグインで会員を拡張するときと同じやり方です。
足すのは3つです。
| 足すもの | 中身 |
|---|---|
| 親アカウントへの関連 | 管理者自身は空。担当者には管理者が入る |
| 担当者の一覧 | 上の関連の逆向き |
| 退会していない担当者を返すメソッド | 一覧や集計はこちらを使う |
dtb_customer に parent_customer_id という列が増えるので、マイグレーションも書きます。ここで1つ注意があります。dtb_customer の主キーは符号なしなので、外部キーになる列にも符号なしを指定します。 付け忘れると型が合わず、外部キーを張れません。
もう1つ。プラグインと違って、app/Customize のエンティティ拡張は置いただけでは反映されません。プロキシの再生成、マイグレーションの実行、キャッシュの削除を自分で流します。EC-CUBE のコンソールコマンドでいうと eccube:generate:proxies、doctrine:migrations:migrate、cache:clear の3つです。
誰を管理者として扱うか
親を持たない会員が管理者、と書きました。ただそのままだと、一般消費者にも「担当者アカウント」のメニューが出てしまいます。
卸売サイトなら、会員グループに所属しているかどうかで分けられます。管理者かどうかを返すメソッドを会員に足して、こう判定しました。
- 親を持っていたら管理者ではない(担当者だから)
- 会員グループ管理プラグインが入っていれば、グループに所属している会員だけを管理者とする
- 入っていなければ、親を持たない会員すべてを管理者とする
②と③の切り替えは、会員グループを取得するメソッドが存在するかどうかで見ています。プラグインが会員に足すメソッドなので、入っていない環境では存在しません。この書き方なら、プラグインの有無で壊れません。
グループ以外の条件で分けたいなら、会員に判定用の列を足して管理画面から立てるほうが確実です。その場合は管理画面の会員編集フォームにも項目が要ります。
担当者を登録する
登録フォームは、会員登録フォームから住所と生年月日と性別を落としたものです。残すのは氏名、フリガナ、電話番号、メールアドレス、パスワードの5つ。
住所を入力させないのは、担当者の住所は会社の住所だからです。 管理者のものをそのままコピーします。
フォームの対象を本体の会員エンティティにしておくと、メールアドレスの重複チェックが本体の検証で効きます。担当者に既存会員のメールアドレスを入れられても弾かれます。

保存するときにやることが3つあります。
本会員として登録する。 会員を新規に作るときは本体のリポジトリのメソッドを使います。これは仮会員のステータスで会員を返してくるので、本会員に上書きします。誰が登録したのか分からない相手ではなく、取引先の管理者が身元を確認して登録するアカウントなので、確認メールを送って本人にクリックさせる意味がありません。
それでも本体のメソッドを経由する。 自分で会員を組み立てると、パスワード再発行に使うシークレットキーの採番が漏れます。あとで再発行しようとしてキーがなくて困ります。
管理者から引き継ぐ。 会社名、郵便番号、都道府県、住所、そして会員グループをコピーします。
引き継ぎのなかで会員グループが一番の勘所です。 卸価格も、見せる商品も、支払い方法も、判定はすべて会員グループを見ています。ここをコピーしておけば、担当者は管理者と同じ条件で買い物ができます。忘れると、担当者だけ定価で買うことになります。
手元の環境では、卸売Aグループ(7掛け)に所属する管理者が担当者を追加すると、担当者にも同じグループが付いて、税込 ¥3,080 の商品が ¥2,156 で表示されました。
担当者を削除する
一覧の × ボタンです。
レコードは消しません。 消すと過去の受注から会員が消えます。本体の退会処理と同じく、ステータスを退会にして、メールアドレスをランダムなダミーに置き換えます。こうしておくと同じメールアドレスで再登録できます。
削除の前に、その担当者の親がログイン中の管理者かどうかを必ず確認します。 ここを省くと、URLのIDを書き換えるだけで他社の会員を退会させられます。
ボタンは本体のお届け先一覧と同じ書き方にしました。EC-CUBE には、リンクをクリックしたときに指定した種別のリクエストへ組み立て直す JavaScript が入っています。確認ダイアログの文言も属性で渡せます。
注文履歴を会社単位にする
担当者が発注できるようになると、次に言われるのが「誰が何を頼んだか管理者が見たい」です。
マイページの注文履歴は、受注リポジトリが「この会員の受注」という条件で組み立てています。そしてその最後に、EC-CUBE は割り込み口を用意しています。 条件を組み立て終えたクエリを、あらかじめ決められたキーとともに外へ渡す仕組みです。
このキーに反応するクラスを1つ置くだけで、コントローラもリポジトリも触らずに条件を足せます。ログイン中の会員が管理者なら、自分の担当者の受注も含める、という条件を追加しました。
書くときの注意が1つあります。元の「この会員の受注」という条件は消さず、担当者の分を足す形にします。 条件を丸ごと差し替えると、元の条件が使っていたパラメータが宙に浮き、Doctrine が例外を投げます。差し替えたいときは、パラメータのほうも一緒に整理する必要があります。
サービスの登録は要りません。app/Customize を名前空間で一括登録してあれば、この割り込み口の約束を満たしたクラスは EC-CUBE が自動で拾います。
これで管理者のマイページに自社の受注が並びます。

担当者のほうは今までどおり、自分の分しか見えません。

ナビのタブが管理者と違います。担当者に担当者管理は要らず、退会は管理者の仕事なので、どちらも出していません。
履歴の詳細を開けるようにする
一覧に並んでも、「詳細を見る」を押すと 404 になります。マイページの詳細画面は、注文番号とログイン中の会員という条件で受注を1件引いているためです。ここはクエリビルダを使っていないので、さきほどの割り込み口は効きません。
ただ、この直後にイベントが飛んでいます。本体は、そのイベントで受注を取り直してから 404 かどうかを判定します。 つまりイベントで差し替えられます。
イベントを受け取るクラスを1つ置いて、こう書きました。
- 本体が受注を取れていたら何もしない
- ログイン中の会員が管理者でなければ何もしない
- そうでなければ、自分の担当者の受注として注文番号で引き直し、見つかったら差し替える
注文番号はイベントの引数に入っていないので、リクエストの属性から取ります。
一覧と詳細で手段が変わるのは気持ち悪いところですが、コントローラを丸ごと上書きするより副作用が小さくて済みます。
テンプレート
本体からコピーして書き換えるのが2枚、新しく作るのが2枚です。
| テンプレート | すること |
|---|---|
| マイページのナビ | 管理者にだけ担当者アカウントのタブを出す。担当者には退会手続きを出さない |
| 注文履歴の一覧 | 自分以外の発注に発注者名を出す |
| 担当者の一覧(新規) | 追加ボタンと担当者の行 |
| 担当者の追加フォーム(新規) | 入力欄と引き継ぎの説明 |
ナビの出し分けは、さきほど会員に足した「管理者かどうか」を Twig から呼ぶだけです。Twig はメソッド名を補ってくれるので、判定用のメソッドをそのまま属性のように書けます。
注文履歴で発注者名を出すのは、自分以外の発注だけにしました。全部の行に自分の名前が並んでも読みにくいだけです。
新しく作る2枚は、本体のお届け先一覧と会員情報編集の作りに寄せています。EC-CUBE のクラス名をそのまま使えば、追加ボタンや × ボタン、入力欄の見た目が本体と揃うので、CSS は書かずに済みます。エラーのある項目に印を付ける関数も本体が用意しています。
入力欄で1つ気をつける点があります。メールアドレスとパスワードは確認用と2つで1組の項目です。 まとめて出そうとすると確認用の欄だけ枠から外れるので、1つ目と2つ目に分けて並べます。
テンプレートを本体からコピーすると、EC-CUBE のバージョンアップで本体側が変わったとき、差分を当て直すのは自分の仕事になります。プラグインとして配布するならテンプレート用のイベントで差し込みますが、自社サイトのカスタマイズなら、コピーして Git で管理するほうが読みやすいと思います。
割り切ったところ
担当者は自分で会員情報を編集できます。 マイページの会員情報編集をそのまま使えるので、住所や会社名を勝手に書き換えられます。会社の情報を固定したいなら、編集フォームから該当項目を落とすか、保存時に管理者の値へ戻す処理が要ります。
(追記:穴だと書いたものが、穴ではなかったで塞ぎました。フォームと保存後の二重で固定しています。)
発注に上限を設けていません。 担当者が1回で1000万円の発注をしても通ります。1回あたりの上限や月間の枠を持たせるなら、購入フローの検証に条件を足してください。
担当者の数に制限がありません。 会員登録の入り口ではないので大量に作られる心配は薄いのですが、気になるなら追加画面で人数を見て上限を掛けてください。
管理者が退会すると担当者が宙に浮きます。 外部キーを、親が消えたら空になる設定にしているためです。親を失った担当者は「親を持たない会員」になり、条件次第では管理者として扱われます。管理者の退会時に担当者もまとめて退会させる処理を足すのが安全です。
(訂正:この説明は誤りでした。 外部キーに削除時の指定はしておらず、担当者がぶら下がっている会員は削除できません。宙に浮くことはありません。実際の問題は、EC-CUBEの退会が状態を変えるだけなので、管理者が退会しても担当者はそのまま発注できてしまうことでした。詳しくは穴だと書いたものが、穴ではなかったに書いています。)
承認フローは入れていません。 担当者が発注したものは、そのまま受注になります。この続きは発注の承認フローの記事に書きました。
EC-CUBE 4.2 / 4.3 での違い
4.4 で動かしたものです。4.2 / 4.3 では書き方が変わります。
| 4.2 / 4.3 | 4.4 |
|---|---|
| ルーティングとテンプレート指定はアノテーション | 属性記法 |
Sensio\Bundle\FrameworkExtraBundle\Configuration\Template | Symfony\Bridge\Twig\Attribute\Template |
Eccube\Annotation\EntityExtension | Eccube\Attribute\EntityExtension |
| エンティティのマッピングもアノテーション | 属性記法 |
エンティティの書き方が変わるのが一番の違いです。4.3 までは本体のエンティティも Doctrine のアノテーションなので、拡張するトレイトも合わせます。
Security の名前空間も注意が要ります。4.2 は Symfony 5.4 なので Symfony\Component\Security\Core\Security、4.3 は Symfony 6.4 なので 4.4 と同じ Symfony\Bundle\SecurityBundle\Security です。
クエリの割り込み口も、履歴の詳細で使ったイベントも 4.2 にあります。本体が受注を取り直してから 404 を判定する作りも同じでした。この記事の仕組みはそのまま動きます。変更点の全体はEC-CUBE 4.4 でプラグインが動かなくなる変更点にまとめています。
会員グループ管理を入れている場合
卸売サイトなら会員グループ管理プラグインと組み合わせることになると思います。
このカスタマイズは、会員グループを担当者へコピーするところだけがプラグインとの接点です。あとは EC-CUBE の会員として普通に振る舞うので、会員グループ価格管理アドオンの卸価格も、支払い方法管理アドオンの請求書払いも、担当者にそのまま効きます。
グループを取引先ごとに作っているなら、担当者アカウントは会社の中の話なので何も変わりません。掛け率ごとのグループにしているなら、同じ掛け率の別会社と1つのグループを共有しますが、担当者の親は自社の管理者なので、注文履歴が混ざる心配は要りません。
価格の条件はグループ、社内の関係は親子。分けておくと、取引先が増えても担当者が増えても、触る場所が変わりません。
B2B サイト全体でどこまでが標準・プラグインで、どこからが作るものになるのかは、B2Bサイト全体の線引きにまとめました。