Create a Frappe Framework v15 DocType with proper controller, service layer, repository, and test files. Triggers: "create doctype", "new doctype", "frappe doctype", "add doctype", "/frappe-doctype". Generates production-ready DocType with type annotations, lifecycle hooks, and multi-layer architecture integration.
/plugin marketplace add sergio-bershadsky/ai/plugin install frappe-dev@bershadsky-claude-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Create a production-ready Frappe v15 DocType with complete controller implementation, service layer integration, repository pattern, and test coverage.
/frappe-doctype <doctype_name> [--module <module>] [--submittable] [--child]
Examples:
/frappe-doctype Sales Order
/frappe-doctype Invoice Item --child
/frappe-doctype Purchase Request --submittable
Ask the user for:
SO-.YYYY.-.#####)Based on requirements, determine:
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"
}
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["<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)
# ──────────────────────────────────────────────────────────────────────────────
@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()
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 = <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"))
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 = "<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()
)
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 = <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
## 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:
| Field | Type | Required |
|-------|------|----------|
| naming_series | Select | Yes |
| title | Data | Yes |
| status | Select | No |
| date | Date | Yes |
| description | Text Editor | No |
---
Create this DocType with all layers?
After approval, create all files and run:
bench --site <site> migrate
bench --site <site> run-tests --doctype "<DocType Name>"
## 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 <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>"`
TYPE_CHECKING block with type hintsThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.