frappe-doctype

Frappe DocType Creation

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "frappe-doctype" with this command: npx skills add sergio-bershadsky/ai/sergio-bershadsky-ai-frappe-doctype

Frappe DocType Creation

Create a production-ready Frappe v15 DocType with complete controller implementation, service layer integration, repository pattern, and test coverage.

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

Arguments

/frappe-doctype <doctype_name> [--module <module>] [--submittable] [--child]

Examples:

/frappe-doctype Sales Order /frappe-doctype Invoice Item --child /frappe-doctype Purchase Request --submittable

Procedure

Step 1: Gather DocType Requirements

Ask the user for:

  • DocType Name (Title Case, e.g., "Sales Order")

  • Module (which module this belongs to)

  • DocType Type:

  • Standard (regular CRUD document)

  • Submittable (has workflow: Draft → Submitted → Cancelled)

  • Child Table (embedded in parent documents)

  • Single (configuration/settings document)

  • Key Fields (at least the primary fields needed)

  • Naming Pattern:

  • Autoname (series like SO-.YYYY.-.##### )

  • Field-based (use a specific field value)

  • Prompt (user enters name)

Step 2: Analyze and Design

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

Step 3: Generate DocType JSON

Create the DocType definition <doctype_folder>/<doctype_name>.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

Create <doctype_folder>/<doctype_name>.py :

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

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["&#x3C;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_&#x3C;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)

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

@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("&#x3C;DocType Name>", name)
doc.check_permission("read")
return doc.get_summary()

Step 5: Generate Service Layer

Create <app>/<module>/services/<doctype_snake>_service.py :

""" <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 = &#x3C;DocTypeName>Repository()

def create(self, data: dict) -> dict:
    """
    Create a new &#x3C;DocType Name>.

    Args:
        data: Document data

    Returns:
        Created document summary

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

    doc = self.repo.create(data)
    self.log_activity("&#x3C;DocType Name>", doc.name, "Created")

    return doc.get_summary()

def update(self, name: str, data: dict) -> dict:
    """
    Update existing &#x3C;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("&#x3C;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("&#x3C;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("&#x3C;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("&#x3C;DocType Name>", "cancel", doc=doc, throw=True)

    if reason:
        frappe.db.set_value("&#x3C;DocType Name>", name, "cancellation_reason", reason)

    doc.cancel()
    self.log_activity("&#x3C;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

Create <app>/<module>/repositories/<doctype_snake>_repository.py :

""" <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 = "&#x3C;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

Create <doctype_folder>/test_<doctype_snake>.py :

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

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 = &#x3C;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": "&#x3C;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": "&#x3C;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

Step 8: Show Preview and Confirm

DocType Creation Preview

DocType: <DocType Name> Module: <Module> Type: Standard | Submittable | Child Table

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

Fields:

FieldTypeRequired
naming_seriesSelectYes
titleDataYes
statusSelectNo
dateDateYes
descriptionText EditorNo

Create this DocType with all layers?

Step 9: Execute and Verify

After approval, create all files and run:

bench --site <site> migrate bench --site <site> run-tests --doctype "<DocType Name>"

Output Format

DocType Created

Name: <DocType Name> Path: <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

Next Steps:

  1. Run bench --site &#x3C;site> migrate to create database table
  2. Add permissions in DocType settings
  3. Create any child tables needed
  4. Run tests: bench --site &#x3C;site> run-tests --doctype "&#x3C;DocType Name>"

Rules

  • v15 Type Annotations — Always include TYPE_CHECKING block with type hints

  • Multi-Layer Pattern — Create service and repository for every DocType

  • No Business Logic in Controller — Controllers call services, services implement logic

  • Comprehensive Tests — Every DocType must have test coverage

  • Proper Naming — DocType folder/file names must be snake_case

  • ALWAYS Confirm — Never create files without explicit user approval

  • Index Planning — Add indexes for frequently filtered fields

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frappe-service

No summary provided by upstream source.

Repository SourceNeeds Review
General

frappe-app

No summary provided by upstream source.

Repository SourceNeeds Review
General

frappe-api

No summary provided by upstream source.

Repository SourceNeeds Review
General

frappe-test

No summary provided by upstream source.

Repository SourceNeeds Review