frappe-doctype

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Frappe DocType Creation

Frappe DocType 创建

Create a production-ready Frappe v15 DocType with complete controller implementation, service layer integration, repository pattern, and test coverage.
创建具备完整控制器实现、服务层集成、Repository模式和测试覆盖的生产级Frappe v15 DocType。

When to Use

适用场景

  • Creating a new DocType for a Frappe application
  • Need proper controller with lifecycle hooks
  • Want service layer for business logic separation
  • Require repository for clean data access
  • Building submittable/amendable documents
  • 为Frappe应用创建新的DocType
  • 需要带有生命周期钩子的合规控制器
  • 希望通过服务层分离业务逻辑
  • 要求使用Repository实现清晰的数据访问
  • 构建可提交/可修订的文档

Arguments

命令参数

/frappe-doctype <doctype_name> [--module <module>] [--submittable] [--child]
Examples:
/frappe-doctype Sales Order
/frappe-doctype Invoice Item --child
/frappe-doctype Purchase Request --submittable
/frappe-doctype <doctype_name> [--module <module>] [--submittable] [--child]
示例:
/frappe-doctype Sales Order
/frappe-doctype Invoice Item --child
/frappe-doctype Purchase Request --submittable

Procedure

操作流程

Step 1: Gather DocType Requirements

步骤1:收集DocType需求

Ask the user for:
  1. DocType Name (Title Case, e.g., "Sales Order")
  2. Module (which module this belongs to)
  3. DocType Type:
    • Standard (regular CRUD document)
    • Submittable (has workflow: Draft → Submitted → Cancelled)
    • Child Table (embedded in parent documents)
    • Single (configuration/settings document)
  4. Key Fields (at least the primary fields needed)
  5. Naming Pattern:
    • Autoname (series like
      SO-.YYYY.-.#####
      )
    • Field-based (use a specific field value)
    • Prompt (user enters name)
向用户确认以下信息:
  1. DocType名称(首字母大写,例如:"Sales Order")
  2. 所属模块(该DocType归属于哪个模块)
  3. DocType类型:
    • 标准类型(常规CRUD文档)
    • 可提交类型(具备工作流:草稿 → 已提交 → 已取消)
    • 子表类型(嵌入在父文档中)
    • 单页类型(配置/设置类文档)
  4. 核心字段(至少需要的基础字段)
  5. 命名规则:
    • 自动命名(如
      SO-.YYYY.-.#####
      这样的序列)
    • 基于字段(使用特定字段的值作为名称)
    • 手动输入(用户自行输入名称)

Step 2: Analyze and Design

步骤2:分析与设计

Based on requirements, determine:
  • Field types and properties
  • Link relationships to other DocTypes
  • Required indexes for performance
  • Permission model (roles that can access)
  • Workflow requirements
根据需求确定:
  • 字段类型与属性
  • 与其他DocType的关联关系
  • 为性能优化所需的索引
  • 权限模型(可访问的角色)
  • 工作流需求

Step 3: Generate DocType JSON

步骤3:生成DocType JSON定义

Create the DocType definition
<doctype_folder>/<doctype_name>.json
:
json
{
  "name": "<DocType Name>",
  "module": "<Module>",
  "doctype": "DocType",
  "naming_rule": "By \"Naming Series\" field",
  "autoname": "naming_series:",
  "is_submittable": 0,
  "is_tree": 0,
  "istable": 0,
  "editable_grid": 1,
  "track_changes": 1,
  "track_seen": 1,
  "engine": "InnoDB",
  "fields": [
    {
      "fieldname": "naming_series",
      "fieldtype": "Select",
      "label": "Series",
      "options": "<PREFIX>-.YYYY.-.#####",
      "reqd": 1,
      "in_list_view": 0
    },
    {
      "fieldname": "title",
      "fieldtype": "Data",
      "label": "Title",
      "reqd": 1,
      "in_list_view": 1,
      "in_standard_filter": 1
    },
    {
      "fieldname": "status",
      "fieldtype": "Select",
      "label": "Status",
      "options": "\nDraft\nPending\nCompleted\nCancelled",
      "default": "Draft",
      "in_list_view": 1,
      "in_standard_filter": 1
    },
    {
      "fieldname": "column_break_1",
      "fieldtype": "Column Break"
    },
    {
      "fieldname": "date",
      "fieldtype": "Date",
      "label": "Date",
      "default": "Today",
      "reqd": 1,
      "in_list_view": 1
    },
    {
      "fieldname": "section_break_details",
      "fieldtype": "Section Break",
      "label": "Details"
    },
    {
      "fieldname": "description",
      "fieldtype": "Text Editor",
      "label": "Description"
    },
    {
      "fieldname": "amended_from",
      "fieldtype": "Link",
      "label": "Amended From",
      "no_copy": 1,
      "options": "<DocType Name>",
      "print_hide": 1,
      "read_only": 1
    }
  ],
  "permissions": [
    {
      "role": "System Manager",
      "read": 1,
      "write": 1,
      "create": 1,
      "delete": 1,
      "submit": 0,
      "cancel": 0,
      "amend": 0
    }
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
  "title_field": "title"
}
创建DocType定义文件
<doctype_folder>/<doctype_name>.json
json
{
  "name": "<DocType Name>",
  "module": "<Module>",
  "doctype": "DocType",
  "naming_rule": "By \"Naming Series\" field",
  "autoname": "naming_series:",
  "is_submittable": 0,
  "is_tree": 0,
  "istable": 0,
  "editable_grid": 1,
  "track_changes": 1,
  "track_seen": 1,
  "engine": "InnoDB",
  "fields": [
    {
      "fieldname": "naming_series",
      "fieldtype": "Select",
      "label": "Series",
      "options": "<PREFIX>-.YYYY.-.#####",
      "reqd": 1,
      "in_list_view": 0
    },
    {
      "fieldname": "title",
      "fieldtype": "Data",
      "label": "Title",
      "reqd": 1,
      "in_list_view": 1,
      "in_standard_filter": 1
    },
    {
      "fieldname": "status",
      "fieldtype": "Select",
      "label": "Status",
      "options": "\nDraft\nPending\nCompleted\nCancelled",
      "default": "Draft",
      "in_list_view": 1,
      "in_standard_filter": 1
    },
    {
      "fieldname": "column_break_1",
      "fieldtype": "Column Break"
    },
    {
      "fieldname": "date",
      "fieldtype": "Date",
      "label": "Date",
      "default": "Today",
      "reqd": 1,
      "in_list_view": 1
    },
    {
      "fieldname": "section_break_details",
      "fieldtype": "Section Break",
      "label": "Details"
    },
    {
      "fieldname": "description",
      "fieldtype": "Text Editor",
      "label": "Description"
    },
    {
      "fieldname": "amended_from",
      "fieldtype": "Link",
      "label": "Amended From",
      "no_copy": 1,
      "options": "<DocType Name>",
      "print_hide": 1,
      "read_only": 1
    }
  ],
  "permissions": [
    {
      "role": "System Manager",
      "read": 1,
      "write": 1,
      "create": 1,
      "delete": 1,
      "submit": 0,
      "cancel": 0,
      "amend": 0
    }
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
  "title_field": "title"
}

Step 4: Generate Controller with v15 Type Annotations

步骤4:生成带v15类型注解的控制器

Create
<doctype_folder>/<doctype_name>.py
:
python
undefined
创建文件
<doctype_folder>/<doctype_name>.py
python
undefined

Copyright (c) <year>, <author> and contributors

Copyright (c) <year>, <author> and contributors

For license information, please see license.txt

For license information, please see license.txt

import frappe from frappe import _ from frappe.model.document import Document from frappe.model.docstatus import DocStatus # v15: Helper for docstatus checks from typing import TYPE_CHECKING
if TYPE_CHECKING: from frappe.types import DF # Import child table types if needed # from <app>.<module>.doctype.<child_doctype>.<child_doctype> import <ChildDocType>
class <DocTypeName>(Document): """ <DocType Name> - <brief description>
Lifecycle:
    Draft → (validate) → Saved → (submit) → Submitted → (cancel) → Cancelled
"""

# begin: auto-generated types
# This section is auto-generated by Frappe. Do not modify manually.
if TYPE_CHECKING:
    amended_from: DF.Link | None
    date: DF.Date
    description: DF.TextEditor | None
    naming_series: DF.Literal["<PREFIX>-.YYYY.-.#####"]
    status: DF.Literal["", "Draft", "Pending", "Completed", "Cancelled"]
    title: DF.Data
# end: auto-generated types

def before_validate(self) -> None:
    """Auto-set default values before validation."""
    self._set_defaults()

def validate(self) -> None:
    """Validate document before save. Throw exception to prevent saving."""
    self._validate_business_rules()

def before_save(self) -> None:
    """Called before document is saved to database."""
    self._update_status()

def after_insert(self) -> None:
    """Called after new document is inserted."""
    self._notify_creation()

def on_update(self) -> None:
    """Called when existing document is updated."""
    pass

def before_submit(self) -> None:
    """Called before document submission. Validate submission requirements."""
    self._validate_submit_conditions()

def on_submit(self) -> None:
    """Called after document submission. Create dependent records."""
    self._process_submission()

def before_cancel(self) -> None:
    """Validate cancellation conditions."""
    self._validate_cancel_conditions()

def on_cancel(self) -> None:
    """Handle cancellation cleanup."""
    self._process_cancellation()

def on_trash(self) -> None:
    """Called when document is deleted. Cleanup related data."""
    pass

# ──────────────────────────────────────────────────────────────────────────
# Private Methods
# ──────────────────────────────────────────────────────────────────────────

def _set_defaults(self) -> None:
    """Set default values for fields."""
    if not self.date:
        self.date = frappe.utils.today()

def _validate_business_rules(self) -> None:
    """Validate business rules specific to this DocType."""
    if not self.title:
        frappe.throw(_("Title is required"))

def _update_status(self) -> None:
    """Update status based on document state using DocStatus helper."""
    # v15: Use DocStatus helper for readable status checks
    if self.docstatus.is_draft() and not self.status:
        self.status = "Draft"

def _notify_creation(self) -> None:
    """Send notifications after creation."""
    # frappe.publish_realtime("new_<doctype>", {"name": self.name})
    pass

def _validate_submit_conditions(self) -> None:
    """Check all conditions required for submission."""
    pass

def _process_submission(self) -> None:
    """Process document submission - create GL entries, update stocks, etc."""
    self.db_set("status", "Completed")

def _validate_cancel_conditions(self) -> None:
    """Check if document can be cancelled."""
    pass

def _process_cancellation(self) -> None:
    """Reverse submission effects."""
    self.db_set("status", "Cancelled")

# ──────────────────────────────────────────────────────────────────────────
# Public API Methods (call from services or whitelisted methods)
# ──────────────────────────────────────────────────────────────────────────

def get_summary(self) -> dict:
    """Return document summary for API responses."""
    return {
        "name": self.name,
        "title": self.title,
        "status": self.status,
        "date": str(self.date)
    }
import frappe from frappe import _ from frappe.model.document import Document from frappe.model.docstatus import DocStatus # v15: Helper for docstatus checks from typing import TYPE_CHECKING
if TYPE_CHECKING: from frappe.types import DF # Import child table types if needed # from <app>.<module>.doctype.<child_doctype>.<child_doctype> import <ChildDocType>
class <DocTypeName>(Document): """ <DocType Name> - <brief description>
Lifecycle:
    Draft → (validate) → Saved → (submit) → Submitted → (cancel) → Cancelled
"""

# begin: auto-generated types
# This section is auto-generated by Frappe. Do not modify manually.
if TYPE_CHECKING:
    amended_from: DF.Link | None
    date: DF.Date
    description: DF.TextEditor | None
    naming_series: DF.Literal["<PREFIX>-.YYYY.-.#####"]
    status: DF.Literal["", "Draft", "Pending", "Completed", "Cancelled"]
    title: DF.Data
# end: auto-generated types

def before_validate(self) -> None:
    """Auto-set default values before validation."""
    self._set_defaults()

def validate(self) -> None:
    """Validate document before save. Throw exception to prevent saving."""
    self._validate_business_rules()

def before_save(self) -> None:
    """Called before document is saved to database."""
    self._update_status()

def after_insert(self) -> None:
    """Called after new document is inserted."""
    self._notify_creation()

def on_update(self) -> None:
    """Called when existing document is updated."""
    pass

def before_submit(self) -> None:
    """Called before document submission. Validate submission requirements."""
    self._validate_submit_conditions()

def on_submit(self) -> None:
    """Called after document submission. Create dependent records."""
    self._process_submission()

def before_cancel(self) -> None:
    """Validate cancellation conditions."""
    self._validate_cancel_conditions()

def on_cancel(self) -> None:
    """Handle cancellation cleanup."""
    self._process_cancellation()

def on_trash(self) -> None:
    """Called when document is deleted. Cleanup related data."""
    pass

# ──────────────────────────────────────────────────────────────────────────
# Private Methods
# ──────────────────────────────────────────────────────────────────────────

def _set_defaults(self) -> None:
    """Set default values for fields."""
    if not self.date:
        self.date = frappe.utils.today()

def _validate_business_rules(self) -> None:
    """Validate business rules specific to this DocType."""
    if not self.title:
        frappe.throw(_("Title is required"))

def _update_status(self) -> None:
    """Update status based on document state using DocStatus helper."""
    # v15: Use DocStatus helper for readable status checks
    if self.docstatus.is_draft() and not self.status:
        self.status = "Draft"

def _notify_creation(self) -> None:
    """Send notifications after creation."""
    # frappe.publish_realtime("new_<doctype>", {"name": self.name})
    pass

def _validate_submit_conditions(self) -> None:
    """Check all conditions required for submission."""
    pass

def _process_submission(self) -> None:
    """Process document submission - create GL entries, update stocks, etc."""
    self.db_set("status", "Completed")

def _validate_cancel_conditions(self) -> None:
    """Check if document can be cancelled."""
    pass

def _process_cancellation(self) -> None:
    """Reverse submission effects."""
    self.db_set("status", "Cancelled")

# ──────────────────────────────────────────────────────────────────────────
# Public API Methods (call from services or whitelisted methods)
# ──────────────────────────────────────────────────────────────────────────

def get_summary(self) -> dict:
    """Return document summary for API responses."""
    return {
        "name": self.name,
        "title": self.title,
        "status": self.status,
        "date": str(self.date)
    }

──────────────────────────────────────────────────────────────────────────────

──────────────────────────────────────────────────────────────────────────────

Whitelisted Methods (accessible via REST API)

Whitelisted Methods (accessible via REST API)

──────────────────────────────────────────────────────────────────────────────

──────────────────────────────────────────────────────────────────────────────

@frappe.whitelist() def get_<doctype_snake>_summary(name: str) -> dict: """ Get document summary.
Args:
    name: Document name

Returns:
    Document summary dict
"""
doc = frappe.get_doc("<DocType Name>", name)
doc.check_permission("read")
return doc.get_summary()
undefined
@frappe.whitelist() def get_<doctype_snake>_summary(name: str) -> dict: """ Get document summary.
Args:
    name: Document name

Returns:
    Document summary dict
"""
doc = frappe.get_doc("<DocType Name>", name)
doc.check_permission("read")
return doc.get_summary()
undefined

Step 5: Generate Service Layer

步骤5:生成服务层

Create
<app>/<module>/services/<doctype_snake>_service.py
:
python
"""
<DocType Name> Service

Business logic for <DocType Name> operations.
"""

import frappe
from frappe import _
from typing import Optional
from <app>.<module>.services.base import BaseService
from <app>.<module>.repositories.<doctype_snake>_repository import <DocTypeName>Repository


class <DocTypeName>Service(BaseService):
    """
    Service class for <DocType Name> business logic.

    All business rules and complex operations should be implemented here,
    not in the DocType controller.
    """

    def __init__(self):
        super().__init__()
        self.repo = <DocTypeName>Repository()

    def create(self, data: dict) -> dict:
        """
        Create a new <DocType Name>.

        Args:
            data: Document data

        Returns:
            Created document summary

        Raises:
            frappe.ValidationError: If validation fails
        """
        self.check_permission("<DocType Name>", "create", throw=True)
        self.validate_mandatory(data, ["title", "date"])

        doc = self.repo.create(data)
        self.log_activity("<DocType Name>", doc.name, "Created")

        return doc.get_summary()

    def update(self, name: str, data: dict) -> dict:
        """
        Update existing <DocType Name>.

        Args:
            name: Document name
            data: Fields to update

        Returns:
            Updated document summary
        """
        doc = self.repo.get_or_throw(name, for_update=True)
        self.check_permission("<DocType Name>", "write", doc=doc, throw=True)

        # Business validation
        if doc.status == "Completed":
            frappe.throw(_("Cannot modify completed documents"))

        doc.update(data)
        doc.save()
        self.log_activity("<DocType Name>", name, "Updated", data)

        return doc.get_summary()

    def submit(self, name: str) -> dict:
        """
        Submit document for processing.

        Args:
            name: Document name

        Returns:
            Submitted document summary
        """
        doc = self.repo.get_or_throw(name, for_update=True)
        self.check_permission("<DocType Name>", "submit", doc=doc, throw=True)

        # Pre-submit validation
        self._validate_submission(doc)

        doc.submit()
        return doc.get_summary()

    def cancel(self, name: str, reason: Optional[str] = None) -> dict:
        """
        Cancel submitted document.

        Args:
            name: Document name
            reason: Cancellation reason

        Returns:
            Cancelled document summary
        """
        doc = self.repo.get_or_throw(name, for_update=True)
        self.check_permission("<DocType Name>", "cancel", doc=doc, throw=True)

        if reason:
            frappe.db.set_value("<DocType Name>", name, "cancellation_reason", reason)

        doc.cancel()
        self.log_activity("<DocType Name>", name, "Cancelled", {"reason": reason})

        return doc.get_summary()

    def get_dashboard_stats(self) -> dict:
        """Get statistics for dashboard."""
        return {
            "total": self.repo.get_count(),
            "draft": self.repo.get_count({"status": "Draft"}),
            "pending": self.repo.get_count({"status": "Pending"}),
            "completed": self.repo.get_count({"status": "Completed"})
        }

    def _validate_submission(self, doc) -> None:
        """Validate all requirements for submission."""
        if doc.docstatus != 0:
            frappe.throw(_("Document must be in draft state to submit"))
创建文件
<app>/<module>/services/<doctype_snake>_service.py
python
"""
<DocType Name> Service

Business logic for <DocType Name> operations.
"""

import frappe
from frappe import _
from typing import Optional
from <app>.<module>.services.base import BaseService
from <app>.<module>.repositories.<doctype_snake>_repository import <DocTypeName>Repository


class <DocTypeName>Service(BaseService):
    """
    Service class for <DocType Name> business logic.

    All business rules and complex operations should be implemented here,
    not in the DocType controller.
    """

    def __init__(self):
        super().__init__()
        self.repo = <DocTypeName>Repository()

    def create(self, data: dict) -> dict:
        """
        Create a new <DocType Name>.

        Args:
            data: Document data

        Returns:
            Created document summary

        Raises:
            frappe.ValidationError: If validation fails
        """
        self.check_permission("<DocType Name>", "create", throw=True)
        self.validate_mandatory(data, ["title", "date"])

        doc = self.repo.create(data)
        self.log_activity("<DocType Name>", doc.name, "Created")

        return doc.get_summary()

    def update(self, name: str, data: dict) -> dict:
        """
        Update existing <DocType Name>.

        Args:
            name: Document name
            data: Fields to update

        Returns:
            Updated document summary
        """
        doc = self.repo.get_or_throw(name, for_update=True)
        self.check_permission("<DocType Name>", "write", doc=doc, throw=True)

        # Business validation
        if doc.status == "Completed":
            frappe.throw(_("Cannot modify completed documents"))

        doc.update(data)
        doc.save()
        self.log_activity("<DocType Name>", name, "Updated", data)

        return doc.get_summary()

    def submit(self, name: str) -> dict:
        """
        Submit document for processing.

        Args:
            name: Document name

        Returns:
            Submitted document summary
        """
        doc = self.repo.get_or_throw(name, for_update=True)
        self.check_permission("<DocType Name>", "submit", doc=doc, throw=True)

        # Pre-submit validation
        self._validate_submission(doc)

        doc.submit()
        return doc.get_summary()

    def cancel(self, name: str, reason: Optional[str] = None) -> dict:
        """
        Cancel submitted document.

        Args:
            name: Document name
            reason: Cancellation reason

        Returns:
            Cancelled document summary
        """
        doc = self.repo.get_or_throw(name, for_update=True)
        self.check_permission("<DocType Name>", "cancel", doc=doc, throw=True)

        if reason:
            frappe.db.set_value("<DocType Name>", name, "cancellation_reason", reason)

        doc.cancel()
        self.log_activity("<DocType Name>", name, "Cancelled", {"reason": reason})

        return doc.get_summary()

    def get_dashboard_stats(self) -> dict:
        """Get statistics for dashboard."""
        return {
            "total": self.repo.get_count(),
            "draft": self.repo.get_count({"status": "Draft"}),
            "pending": self.repo.get_count({"status": "Pending"}),
            "completed": self.repo.get_count({"status": "Completed"})
        }

    def _validate_submission(self, doc) -> None:
        """Validate all requirements for submission."""
        if doc.docstatus != 0:
            frappe.throw(_("Document must be in draft state to submit"))

Step 6: Generate Repository

步骤6:生成Repository层

Create
<app>/<module>/repositories/<doctype_snake>_repository.py
:
python
"""
<DocType Name> Repository

Data access layer for <DocType Name>.
"""

import frappe
from frappe.query_builder import DocType
from typing import Optional
from <app>.<module>.repositories.base import BaseRepository
from <app>.<module>.doctype.<doctype_folder>.<doctype_snake> import <DocTypeName>


class <DocTypeName>Repository(BaseRepository[<DocTypeName>]):
    """
    Repository for <DocType Name> database operations.
    """

    doctype = "<DocType Name>"

    def get_by_status(
        self,
        status: str,
        limit: int = 20,
        offset: int = 0
    ) -> list[dict]:
        """Get documents by status."""
        return self.get_list(
            filters={"status": status},
            fields=["name", "title", "date", "status", "owner"],
            order_by="date desc",
            limit=limit,
            offset=offset
        )

    def get_recent(self, days: int = 7) -> list[dict]:
        """Get documents created in the last N days."""
        from_date = frappe.utils.add_days(frappe.utils.today(), -days)
        return self.get_list(
            filters={"creation": [">=", from_date]},
            fields=["name", "title", "date", "status", "creation"],
            order_by="creation desc"
        )

    def search(
        self,
        query: str,
        filters: Optional[dict] = None,
        limit: int = 20
    ) -> list[dict]:
        """Full-text search on title and description."""
        base_filters = filters or {}
        base_filters["title"] = ["like", f"%{query}%"]

        return self.get_list(
            filters=base_filters,
            fields=["name", "title", "date", "status"],
            limit=limit
        )

    def get_with_related(self, name: str) -> dict:
        """Get document with related data."""
        doc = self.get_or_throw(name)
        return {
            **doc.as_dict(),
            # Add related data here
            # "items": self._get_items(name),
            # "comments": self._get_comments(name)
        }

    def bulk_update_status(self, names: list[str], status: str) -> int:
        """Bulk update status for multiple documents."""
        dt = DocType(self.doctype)
        return (
            frappe.qb.update(dt)
            .set(dt.status, status)
            .set(dt.modified, frappe.utils.now())
            .set(dt.modified_by, frappe.session.user)
            .where(dt.name.isin(names))
            .run()
        )
创建文件
<app>/<module>/repositories/<doctype_snake>_repository.py
python
"""
<DocType Name> Repository

Data access layer for <DocType Name>.
"""

import frappe
from frappe.query_builder import DocType
from typing import Optional
from <app>.<module>.repositories.base import BaseRepository
from <app>.<module>.doctype.<doctype_folder>.<doctype_snake> import <DocTypeName>


class <DocTypeName>Repository(BaseRepository[<DocTypeName>]):
    """
    Repository for <DocType Name> database operations.
    """

    doctype = "<DocType Name>"

    def get_by_status(
        self,
        status: str,
        limit: int = 20,
        offset: int = 0
    ) -> list[dict]:
        """Get documents by status."""
        return self.get_list(
            filters={"status": status},
            fields=["name", "title", "date", "status", "owner"],
            order_by="date desc",
            limit=limit,
            offset=offset
        )

    def get_recent(self, days: int = 7) -> list[dict]:
        """Get documents created in the last N days."""
        from_date = frappe.utils.add_days(frappe.utils.today(), -days)
        return self.get_list(
            filters={"creation": [">=", from_date]},
            fields=["name", "title", "date", "status", "creation"],
            order_by="creation desc"
        )

    def search(
        self,
        query: str,
        filters: Optional[dict] = None,
        limit: int = 20
    ) -> list[dict]:
        """Full-text search on title and description."""
        base_filters = filters or {}
        base_filters["title"] = ["like", f"%{query}%"]

        return self.get_list(
            filters=base_filters,
            fields=["name", "title", "date", "status"],
            limit=limit
        )

    def get_with_related(self, name: str) -> dict:
        """Get document with related data."""
        doc = self.get_or_throw(name)
        return {
            **doc.as_dict(),
            # Add related data here
            # "items": self._get_items(name),
            # "comments": self._get_comments(name)
        }

    def bulk_update_status(self, names: list[str], status: str) -> int:
        """Bulk update status for multiple documents."""
        dt = DocType(self.doctype)
        return (
            frappe.qb.update(dt)
            .set(dt.status, status)
            .set(dt.modified, frappe.utils.now())
            .set(dt.modified_by, frappe.session.user)
            .where(dt.name.isin(names))
            .run()
        )

Step 7: Generate Test File

步骤7:生成测试文件

Create
<doctype_folder>/test_<doctype_snake>.py
:
python
undefined
创建文件
<doctype_folder>/test_<doctype_snake>.py
python
undefined

Copyright (c) <year>, <author> and contributors

Copyright (c) <year>, <author> and contributors

For license information, please see license.txt

For license information, please see license.txt

import frappe from frappe.tests import IntegrationTestCase, UnitTestCase from <app>.<module>.services.<doctype_snake>_service import <DocTypeName>Service
class Test<DocTypeName>(IntegrationTestCase): """Integration tests for <DocType Name>."""
@classmethod
def setUpClass(cls):
    super().setUpClass()
    cls.service = <DocTypeName>Service()

def test_create_document(self):
    """Test document creation via service."""
    data = {
        "title": "Test Document",
        "date": frappe.utils.today()
    }
    result = self.service.create(data)

    self.assertIsNotNone(result.get("name"))
    self.assertEqual(result.get("title"), "Test Document")

def test_create_requires_mandatory_fields(self):
    """Test that mandatory fields are validated."""
    with self.assertRaises(frappe.ValidationError):
        self.service.create({})

def test_submit_document(self):
    """Test document submission."""
    # Create draft
    doc = frappe.get_doc({
        "doctype": "<DocType Name>",
        "title": "Submit Test",
        "date": frappe.utils.today()
    }).insert()

    # Submit via service
    result = self.service.submit(doc.name)
    self.assertEqual(result.get("status"), "Completed")

def test_cannot_modify_completed(self):
    """Test that completed documents cannot be modified."""
    doc = frappe.get_doc({
        "doctype": "<DocType Name>",
        "title": "Completed Test",
        "date": frappe.utils.today(),
        "status": "Completed"
    }).insert()

    with self.assertRaises(frappe.ValidationError):
        self.service.update(doc.name, {"title": "New Title"})

def test_get_dashboard_stats(self):
    """Test dashboard statistics."""
    stats = self.service.get_dashboard_stats()

    self.assertIn("total", stats)
    self.assertIn("draft", stats)
    self.assertIn("completed", stats)
class Unit<DocTypeName>(UnitTestCase): """Unit tests for <DocType Name> (no database)."""
def test_validation_logic(self):
    """Test validation without database."""
    pass
undefined
import frappe from frappe.tests import IntegrationTestCase, UnitTestCase from <app>.<module>.services.<doctype_snake>_service import <DocTypeName>Service
class Test<DocTypeName>(IntegrationTestCase): """Integration tests for <DocType Name>."""
@classmethod
def setUpClass(cls):
    super().setUpClass()
    cls.service = <DocTypeName>Service()

def test_create_document(self):
    """Test document creation via service."""
    data = {
        "title": "Test Document",
        "date": frappe.utils.today()
    }
    result = self.service.create(data)

    self.assertIsNotNone(result.get("name"))
    self.assertEqual(result.get("title"), "Test Document")

def test_create_requires_mandatory_fields(self):
    """Test that mandatory fields are validated."""
    with self.assertRaises(frappe.ValidationError):
        self.service.create({})

def test_submit_document(self):
    """Test document submission."""
    # Create draft
    doc = frappe.get_doc({
        "doctype": "<DocType Name>",
        "title": "Submit Test",
        "date": frappe.utils.today()
    }).insert()

    # Submit via service
    result = self.service.submit(doc.name)
    self.assertEqual(result.get("status"), "Completed")

def test_cannot_modify_completed(self):
    """Test that completed documents cannot be modified."""
    doc = frappe.get_doc({
        "doctype": "<DocType Name>",
        "title": "Completed Test",
        "date": frappe.utils.today(),
        "status": "Completed"
    }).insert()

    with self.assertRaises(frappe.ValidationError):
        self.service.update(doc.name, {"title": "New Title"})

def test_get_dashboard_stats(self):
    """Test dashboard statistics."""
    stats = self.service.get_dashboard_stats()

    self.assertIn("total", stats)
    self.assertIn("draft", stats)
    self.assertIn("completed", stats)
class Unit<DocTypeName>(UnitTestCase): """Unit tests for <DocType Name> (no database)."""
def test_validation_logic(self):
    """Test validation without database."""
    pass
undefined

Step 8: Show Preview and Confirm

步骤8:展示预览并确认

undefined
undefined

DocType Creation Preview

DocType 创建预览

DocType: <DocType Name> Module: <Module> Type: Standard | Submittable | Child Table
DocType: <DocType Name> 模块: <Module> 类型: 标准 | 可提交 | 子表

Files to Create:

待创建文件:

📁 <module>/doctype/<doctype_folder>/ ├── 📄 <doctype_snake>.json # DocType definition ├── 📄 <doctype_snake>.py # Controller with hooks ├── 📄 <doctype_snake>.js # Client-side script └── 📄 test_<doctype_snake>.py # Test cases
📁 <module>/services/ └── 📄 <doctype_snake>_service.py # Business logic
📁 <module>/repositories/ └── 📄 <doctype_snake>_repository.py # Data access
📁 <module>/doctype/<doctype_folder>/ ├── 📄 <doctype_snake>.json # DocType 定义文件 ├── 📄 <doctype_snake>.py # 带钩子的控制器 ├── 📄 <doctype_snake>.js # 客户端脚本 └── 📄 test_<doctype_snake>.py # 测试用例
📁 <module>/services/ └── 📄 <doctype_snake>_service.py # 业务逻辑层
📁 <module>/repositories/ └── 📄 <doctype_snake>_repository.py # 数据访问层

Fields:

字段列表:

FieldTypeRequired
naming_seriesSelectYes
titleDataYes
statusSelectNo
dateDateYes
descriptionText EditorNo

Create this DocType with all layers?
undefined
字段类型是否必填
naming_seriesSelect
titleData
statusSelect
dateDate
descriptionText Editor

是否创建包含所有层级的该DocType?
undefined

Step 9: Execute and Verify

步骤9:执行创建与验证

After approval, create all files and run:
bash
bench --site <site> migrate
bench --site <site> run-tests --doctype "<DocType Name>"
获得用户确认后,创建所有文件并运行以下命令:
bash
bench --site <site> migrate
bench --site <site> run-tests --doctype "<DocType Name>"

Output Format

输出格式

undefined
undefined

DocType Created

DocType 创建完成

Name: <DocType Name> Path: <app>/<module>/doctype/<doctype_folder>/
名称: <DocType Name> 路径: <app>/<module>/doctype/<doctype_folder>/

Files Created:

已创建文件:

  • ✅ <doctype_snake>.json
  • ✅ <doctype_snake>.py (controller)
  • ✅ <doctype_snake>.js (client)
  • ✅ test_<doctype_snake>.py
  • ✅ <doctype_snake>_service.py
  • ✅ <doctype_snake>_repository.py
  • ✅ <doctype_snake>.json
  • ✅ <doctype_snake>.py (控制器)
  • ✅ <doctype_snake>.js (客户端)
  • ✅ test_<doctype_snake>.py
  • ✅ <doctype_snake>_service.py
  • ✅ <doctype_snake>_repository.py

Next Steps:

后续操作:

  1. Run
    bench --site <site> migrate
    to create database table
  2. Add permissions in DocType settings
  3. Create any child tables needed
  4. Run tests:
    bench --site <site> run-tests --doctype "<DocType Name>"
undefined
  1. 运行
    bench --site <site> migrate
    创建数据库表
  2. 在DocType设置中配置权限
  3. 创建所需的子表(如有)
  4. 运行测试:
    bench --site <site> run-tests --doctype "<DocType Name>"
undefined

Rules

规则要求

  1. v15 Type Annotations — Always include
    TYPE_CHECKING
    block with type hints
  2. Multi-Layer Pattern — Create service and repository for every DocType
  3. No Business Logic in Controller — Controllers call services, services implement logic
  4. Comprehensive Tests — Every DocType must have test coverage
  5. Proper Naming — DocType folder/file names must be snake_case
  6. ALWAYS Confirm — Never create files without explicit user approval
  7. Index Planning — Add indexes for frequently filtered fields
  1. v15类型注解 — 必须包含
    TYPE_CHECKING
    代码块及类型提示
  2. 多层架构模式 — 为每个DocType创建服务层和Repository层
  3. 控制器无业务逻辑 — 控制器仅调用服务层,服务层实现业务逻辑
  4. 全面测试覆盖 — 每个DocType必须具备测试用例
  5. 规范命名 — DocType的文件夹/文件名必须使用蛇形命名法(snake_case)
  6. 必须确认 — 未经用户明确确认不得创建文件
  7. 索引规划 — 为频繁过滤的字段添加索引