frappe-doctype

安装量: 41
排名: #17644

安装

npx skills add https://github.com/sergio-bershadsky/ai --skill 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 [--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 /.json : { "name" : "" , "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" : "-.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" : "" , "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 /.py :

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 [ "-.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_", {"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 ( "" , 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 ( "" , "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" : "" , "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" : "" , "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 (no database).""" def test_validation_logic ( self ) : """Test validation without database.""" pass Step 8: Show Preview and Confirm

DocType Creation Preview

DocType: Module: Type: Standard | Submittable | Child Table

Files to Create:

📁 /doctype// ├── 📄 .json # DocType definition ├── 📄 .py # Controller with hooks ├── 📄 .js # Client-side script └── 📄 test_.py # Test cases 📁 /services/ └── 📄 _service.py # Business logic 📁 /repositories/ └── 📄 _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?
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: //doctype//
### Files Created:
- ✅ .json
- ✅ .py (controller)
- ✅ .js (client)
- ✅ test_.py
- ✅ _service.py
- ✅ _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>"
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
返回排行榜