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
Copyright (c) , 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 ..doctype.. import
class < DocTypeName
( Document ) : """
- 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
[
"
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_", {"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 ( "
" , name ) doc . check_permission ( "read" ) return doc . get_summary ( ) Step 5: Generate Service Layer Create / /services/ _service.py : """ Service Business logic for 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
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
. Args: data: Document data Returns: Created document summary Raises: frappe.ValidationError: If validation fails """ self . check_permission ( " " , "create" , throw = True ) self . validate_mandatory ( data , [ "title" , "date" ] ) doc = self . repo . create ( data ) self . log_activity ( " " , doc . name , "Created" ) return doc . get_summary ( ) def update ( self , name : str , data : dict ) - dict : """ Update existing
. 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 ( " " , "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
(
"
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 ( "
" , "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 ( "
" , "cancel" , doc = doc , throw = True ) if reason : frappe . db . set_value ( " " , name , "cancellation_reason" , reason ) doc . cancel ( ) self . log_activity ( " " , 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
/ /repositories/ _repository.py : """ Repository Data access layer for . """ 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
database operations. """ doctype = " " 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
/test_ .py :
Copyright (c) , 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
.""" @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"
:
"
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"
:
"
( UnitTestCase ) : """Unit tests for
(no database).""" def test_validation_logic ( self ) : """Test validation without database.""" pass Step 8: Show Preview and Confirm
DocType Creation Preview
DocType:
Files to Create:
📁
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? | ||
| Step 9: Execute and Verify | ||
| After approval, create all files and run: | ||
| bench | ||
| --site | ||
| < | ||
| site | ||
| > | ||
| migrate | ||
| bench | ||
| --site | ||
| < | ||
| site | ||
| > | ||
| run-tests | ||
| --doctype | ||
| " |
||
| Output Format | ||
| ## DocType Created | ||
| Name: |
||
| Path: |
||
| ### Files Created: | ||
| - ✅ |
||
| - ✅ |
||
| - ✅ |
||
| - ✅ test_ |
||
| - ✅ |
||
| - ✅ |
||
| ### 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>" |
||
| 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 |