mcpField
F
frappe-impl-controllers

frappe-impl-controllers

github.com/impertio-studio開発198123 回インストール30 週間前に更新
リポジトリ

frappe-impl-controllers について

カスタムアプリで DocType のサーバー側ロジックを実装する Frappe 開発者向けのワークフローガイドです。フックの実行順序や注意点を示し、検証、ERPNext の拡張、テストまで扱います。

主な機能

  1. 01DocType コントローラーの作成
  2. 02ライフサイクルフックの選択
  3. 03データ検証と変更検知
  4. 04命名とドキュメント送信のカスタマイズ
  5. 05ERPNext コントローラーの拡張
  6. 06テストの作成と実行

活用シーン

  1. 01DocType にサーバー側の検証や計算を追加
  2. 02保存後の通知や関連ドキュメントを処理
  3. 03標準 ERPNext DocType の動作をカスタマイズ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-impl-controllers

インストール後、Claude Code で/frappe-impl-controllersを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
anti-patterns.md
decision-tree.md
examples.md
workflows.md

詳細

ドキュメントコントローラー — 実装ワークフロー

Python の機能をフルに活用して、サーバー側の DocType ロジックを構築するための手順を説明します。正確な構文については、frappe-syntax-controllers を参照してください。

バージョン:v14/v15/v16 | v15+:自動生成される型注釈をサポート

クイック判断:コントローラーと Server Script のどちらを使うべきですか?

NEED full Python (imports, classes, generators)?     → Controller
NEED external libraries (requests, pandas)?          → Controller
NEED try/except with rollback?                       → Controller
NEED frappe.enqueue() for background jobs?           → Controller
NEED to extend standard ERPNext DocType?             → Controller
Quick validation without custom app?                 → Server Script
Simple auto-fill or notification?                    → Server Script

ルール:カスタムアプリが必要な場合は、必ずコントローラーを使用してください。ノーコードでプロトタイプを作成する場合は、必ず Server Script を使用してください。

ワークフロー 1:新しいコントローラーを作成する

ステップ 1:Frappe UI または bench new-doctype で DocType を作成します

ステップ 2:ファイルは次の場所に自動生成されます:

apps/myapp/myapp/{module}/doctype/{doctype_name}/{doctype_name}.py

ステップ 3:コントローラークラスを実装します:

import frappe
from frappe import _
from frappe.model.document import Document

class MyDocType(Document):
    def validate(self):
        self.validate_dates()
        self.calculate_totals()

    def validate_dates(self):
        if self.from_date and self.to_date and self.from_date > self.to_date:
            frappe.throw(_("From Date cannot be after To Date"))

    def calculate_totals(self):
        self.total = sum(item.amount for item in self.items)

ステップ 4:bench restart を実行します(または開発時のホットリロードには bench watch を使用します)

命名規則:DocType「Sales Order」→ クラス SalesOrder、ファイル sales_order.py

ワークフロー 2:適切なフックを選択する

WHAT DO YOU WANT?
├── Validate data / calculate fields before save?
│   └── validate — changes to self ARE saved
│
├── Action AFTER save (emails, linked docs, logs)?
│   └── on_update — changes to self NOT saved (use db_set)
│
├── Only for NEW documents?
│   └── after_insert
│
├── Before/after SUBMIT?
│   ├── Check before submit → before_submit
│   └── Ledger entries after → on_submit
│
├── Before/after CANCEL?
│   ├── Prevent cancel → before_cancel
│   └── Reverse entries → on_cancel
│
├── Before DELETE?
│   └── on_trash (throw to prevent)
│
├── Custom document naming?
│   └── autoname
│
└── Detect ANY change (including db_set)?
    └── on_change

実行順序を含むすべてのフックについては、references/decision-tree.md を参照してください。

重要:validate と on_update の違い

項目validateon_update
タイミングDB 書き込み前DB 書き込み後
self.x = y は保存されるかはいいいえ — db_set を使用します
throw で中断できるかはいすでに保存済みです
get_doc_before_save()利用可能利用可能
用途検証、計算通知、リンクされたドキュメント
# WRONG — changes in on_update are NOT saved
def on_update(self):
    self.status = "Completed"  # LOST!

# CORRECT — use db_set
def on_update(self):
    frappe.db.set_value(self.doctype, self.name, "status", "Completed")

ワークフロー 3:エラーを収集する検証

def validate(self):
    errors = []
    if not self.items:
        errors.append(_("At least one item is required"))
    for item in self.items:
        if item.qty <= 0:
            errors.append(_("Row {0}: Qty must be positive").format(item.idx))
    if self.from_date > self.to_date:
        errors.append(_("From Date cannot be after To Date"))
    if errors:
        frappe.throw("<br>".join(errors))

ワークフロー 4:フィールドの変更を検出する

def validate(self):
    old = self.get_doc_before_save()
    if old and old.status != self.status:
        self.flags.status_changed = True
        self.status_changed_on = frappe.utils.now()

def on_update(self):
    if self.flags.get('status_changed'):
        self.notify_status_change()

ルール:フック間でデータを渡す場合は、必ず self.flags を使用してください。外部状態には絶対に依存しないでください。

ワークフロー 5:カスタム命名(autoname)

from frappe.model.naming import getseries

def autoname(self):
    # Format: PRJ-CUST-2025-001
    code = (self.customer or "GEN")[:4].upper()
    year = frappe.utils.getdate(self.start_date or frappe.utils.today()).year
    prefix = f"PRJ-{code}-{year}-"
    self.name = getseries(prefix, 3)

別の方法 — before_naming:

def before_naming(self):
    if self.is_priority:
        self.naming_series = "PRIORITY-.#####"
    else:
        self.naming_series = "STD-.#####"

ワークフロー 6:提出可能なドキュメント

DRAFT (docstatus=0) → submit() → SUBMITTED (docstatus=1) → cancel() → CANCELLED (docstatus=2)

submit():  validate → before_submit → [DB: docstatus=1] → on_update → on_submit
cancel():  before_cancel → [DB: docstatus=2] → on_cancel
class PurchaseOrder(Document):
    def validate(self):
        self.validate_items()
        self.calculate_totals()

    def before_submit(self):
        # ONLY submit-specific checks here
        if self.total > 100000 and not self.manager_approval:
            frappe.throw(_("Manager approval required for POs over 100,000"))

    def on_submit(self):
        self.update_ordered_qty()
        self.create_purchase_receipt_draft()

    def before_cancel(self):
        if frappe.db.exists("Purchase Invoice",
                {"purchase_order": self.name, "docstatus": 1}):
            frappe.throw(_("Cancel linked invoices first"))

    def on_cancel(self):
        self.reverse_ordered_qty()

ルール:validate と before_submit の間で検証を重複させないでください。validate は必ず before_submit より先に実行されます。

ワークフロー 7:標準の ERPNext コントローラーをオーバーライドする

方法 A:全面的なオーバーライド(hooks.py)
# hooks.py
override_doctype_class = {
    "Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}

# myapp/overrides.py
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice

class CustomSalesInvoice(SalesInvoice):
    def validate(self):
        super().validate()  # ALWAYS call parent first
        self.custom_validation()
方法 B:イベントハンドラー(より安全で、クラスをオーバーライドしない)
# hooks.py
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.validate_sales_invoice",
    }
}

# myapp/events.py
def validate_sales_invoice(doc, method=None):
    if doc.grand_total < 0:
        frappe.throw(_("Invalid total"))
方法 C:extend_doctype_class(v16+)
# hooks.py
extend_doctype_class = {
    "Sales Invoice": "myapp.extends.SalesInvoiceExtend"
}

# myapp/extends.py — Only methods to add/override
class SalesInvoiceExtend:
    def custom_method(self):
        pass

ルール:オーバーライドでは必ず super().validate() を呼び出してください。親メソッドを省略しないでください。標準の ERPNext ロジックは親メソッドに依存しています。

ワークフロー 8:ホワイトリスト登録されたメソッド(クライアントから呼び出し可能)

class Quotation(Document):
    @frappe.whitelist()
    def apply_discount(self, discount_percent):
        if discount_percent < 0 or discount_percent > 100:
            frappe.throw(_("Discount must be 0-100"))
        self.discount_amount = self.total * (discount_percent / 100)
        self.grand_total = self.total - self.discount_amount
        self.save()
        return {"grand_total": self.grand_total}

クライアント側の呼び出し:

frm.call('apply_discount', { discount_percent: 10 }).then(r => {
    frm.reload_doc();
});

ワークフロー 9:フラグシステム

# Document-level flags (built-in)
doc.flags.ignore_permissions = True    # Bypass permission checks
doc.flags.ignore_validate = True       # Skip validate() hook
doc.flags.ignore_mandatory = True      # Skip required field check

# Custom flags for inter-hook communication
def validate(self):
    if self.is_urgent:
        self.flags.needs_notification = True

def on_update(self):
    if self.flags.get('needs_notification'):
        self.notify_team()

ワークフロー 10:コントローラーをテストする

# tests/test_my_doctype.py
import frappe
from frappe.tests.utils import FrappeTestCase

class TestMyDocType(FrappeTestCase):
    def test_validate_dates(self):
        doc = frappe.get_doc({
            "doctype": "My DocType",
            "from_date": "2025-01-10",
            "to_date": "2025-01-01"  # Before from_date
        })
        self.assertRaises(frappe.ValidationError, doc.insert)

    def test_calculate_totals(self):
        doc = frappe.get_doc({
            "doctype": "My DocType",
            "items": [
                {"item": "A", "qty": 2, "rate": 100},
                {"item": "B", "qty": 3, "rate": 50}
            ]
        })
        doc.insert()
        self.assertEqual(doc.total, 350)

実行:bench run-tests --module myapp.module.doctype.my_doctype.test_my_doctype

実行順序のリファレンス

INSERT
before_insert → before_naming → autoname → before_validate →
validate → before_save → [DB INSERT] → after_insert →
on_update → on_change
SAVE(既存)
before_validate → validate → before_save → [DB UPDATE] →
on_update → on_change
SUBMIT
validate → before_submit → [DB: docstatus=1] →
on_update → on_submit → on_change

アンチパターンのクイックチェック

してはいけないこと代わりに行うこと
on_update 内の self.x = yfrappe.db.set_value(...)
on_update 内の self.save()無限ループが発生します
フック内の frappe.db.commit()フレームワークに任せます
validate 内の重い処理on_update 内で frappe.enqueue() を使用します
super().validate() を省略する必ず最初に親メソッドを呼び出します
ループ内の frappe.get_doc()frappe.get_cached_doc() を使用します
しきい値をハードコードするSettings DocType を使用します

完全な一覧については、references/anti-patterns.md を参照してください。

関連スキル

  • frappe-syntax-controllers — フックの正確なシグネチャと API
  • frappe-errors-controllers — エラー処理のパターン
  • frappe-impl-serverscripts — Server Script で十分な場合
  • frappe-syntax-hooks — hooks.py の設定
  • frappe-core-database — frappe.db.* の操作

すべてのフックについては、references/decision-tree.md を参照してください。 拡張パターンについては、references/workflows.md を参照してください。 完全な動作例については、references/examples.md を参照してください。

コメント