Claude-skill-registry doctype-patterns
Frappe DocType creation patterns, field types, controller hooks, and data modeling best practices. Use when creating DocTypes, designing data models, adding fields, or setting up document relationships in Frappe/ERPNext.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/doctype-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-doctype-patterns && rm -rf "$T"
manifest:
skills/data/doctype-patterns/SKILL.mdsource content
Frappe DocType Patterns
Comprehensive guide to creating and configuring DocTypes in Frappe Framework, the core building block for all Frappe applications.
When to Use This Skill
- Creating new DocTypes
- Adding or modifying fields on DocTypes
- Designing data models and relationships
- Setting up naming patterns and autoname
- Configuring permissions and workflows
- Creating child tables
- Working with Virtual or Single DocTypes
DocType Directory Structure
When you create a DocType named "My Custom DocType" in module "My Module":
my_app/ └── my_module/ └── doctype/ └── my_custom_doctype/ ├── my_custom_doctype.json # DocType definition ├── my_custom_doctype.py # Python controller ├── my_custom_doctype.js # Client script ├── test_my_custom_doctype.py # Test file └── __init__.py
DocType JSON Structure
{ "name": "My Custom DocType", "module": "My Module", "doctype": "DocType", "engine": "InnoDB", "field_order": ["field1", "field2"], "fields": [ { "fieldname": "field1", "fieldtype": "Data", "label": "Field 1", "reqd": 1 } ], "permissions": [ { "role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1 } ], "autoname": "naming_series:", "naming_rule": "By \"Naming Series\" field", "is_submittable": 0, "istable": 0, "issingle": 0, "track_changes": 1, "sort_field": "modified", "sort_order": "DESC" }
Field Types Reference
Text Fields
| Type | Description | Use Case |
|---|---|---|
| Single line text (140 chars) | Names, codes, short text |
| Multi-line text | Short descriptions |
| Multi-line text (unlimited) | Long descriptions |
| Rich text with formatting | Content, notes |
| Syntax-highlighted code | Python, JS, JSON |
| WYSIWYG HTML | Email templates |
| Markdown input | Documentation |
| Masked input | Secrets (stored encrypted) |
Numeric Fields
| Type | Description | Use Case |
|---|---|---|
| Integer | Counts, quantities |
| Decimal number | Measurements |
| Money with precision | Prices, amounts |
| 0-100 percentage | Discounts, rates |
| Star rating (0-1) | Reviews, scores |
Date/Time Fields
| Type | Description | Use Case |
|---|---|---|
| Date only | Birth dates, due dates |
| Date and time | Timestamps |
| Time only | Schedules |
| Time duration | Task duration |
Selection Fields
| Type | Description | Use Case |
|---|---|---|
| Dropdown options | Status, type |
| Boolean checkbox | Flags, toggles |
| Text with suggestions | Tags |
Link Fields
| Type | Description | Use Case |
|---|---|---|
| Reference to another DocType | Foreign key relationship |
| Reference based on another field | Polymorphic links |
| Child table (1-to-many) | Line items, details |
| Many-to-many via link | Multiple selections |
Special Fields
| Type | Description | Use Case |
|---|---|---|
| Single file attachment | Documents |
| Image with preview | Photos, logos |
| Display image from URL field | Gallery |
| Signature pad | Approvals |
| Map coordinates | Locations |
| Barcode/QR display | Inventory |
| JSON data | Configuration |
Layout Fields
| Type | Description | Use Case |
|---|---|---|
| Horizontal section divider | Form organization |
| Vertical column divider | Multi-column layout |
| Tab navigation | Large forms |
| Static HTML content | Instructions, headers |
| Section heading | Visual separation |
| Clickable button | Actions |
Field Options
Common Field Properties
{ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", "options": "Customer", "reqd": 1, "unique": 0, "in_list_view": 1, "in_standard_filter": 1, "in_global_search": 1, "bold": 1, "read_only": 0, "hidden": 0, "print_hide": 0, "no_copy": 0, "allow_in_quick_entry": 1, "translatable": 0, "default": "", "description": "Select the customer", "depends_on": "eval:doc.is_customer", "mandatory_depends_on": "eval:doc.status=='Active'", "read_only_depends_on": "eval:doc.docstatus==1" }
Link Field Options
{ "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "filters": { "disabled": 0, "customer_type": "Company" }, "ignore_user_permissions": 0 }
Select Field Options
{ "fieldname": "status", "fieldtype": "Select", "options": "\nDraft\nPending\nApproved\nRejected", "default": "Draft" }
Dynamic Link
{ "fieldname": "party_type", "fieldtype": "Link", "options": "DocType" }, { "fieldname": "party", "fieldtype": "Dynamic Link", "options": "party_type" }
Naming Patterns (autoname)
Naming Series
{ "autoname": "naming_series:", "naming_rule": "By \"Naming Series\" field" }
Add a naming_series field:
{ "fieldname": "naming_series", "fieldtype": "Select", "options": "INV-.YYYY.-\nINV-.MM.-.YYYY.-", "default": "INV-.YYYY.-" }
Field-Based Naming
{ "autoname": "field:customer_code", "naming_rule": "By fieldname" }
Expression-Based
{ "autoname": "format:{customer_type}-{###}", "naming_rule": "Expression" }
Hash/Random
{ "autoname": "hash", "naming_rule": "Random" }
Prompt (Manual)
{ "autoname": "Prompt", "naming_rule": "Set by user" }
Controller Lifecycle Hooks
# my_doctype.py import frappe from frappe.model.document import Document class MyDocType(Document): # ===== BEFORE DATABASE OPERATIONS ===== def autoname(self): """Set the document name before saving""" self.name = f"{self.prefix}-{frappe.generate_hash()[:8]}" def before_naming(self): """Called before autoname, can modify naming logic""" pass def validate(self): """Validate data before save (called on insert and update)""" self.validate_dates() self.calculate_totals() def before_validate(self): """Called before validate""" pass def before_save(self): """Called before document is saved to database""" self.modified_by_script = True def before_insert(self): """Called before new document is inserted""" self.set_defaults() # ===== AFTER DATABASE OPERATIONS ===== def after_insert(self): """Called after new document is inserted""" self.notify_users() def on_update(self): """Called after document is saved (insert or update)""" self.update_related_docs() def after_save(self): """Called after on_update, always runs""" pass def on_change(self): """Called when document changes in database""" pass # ===== SUBMISSION WORKFLOW ===== def before_submit(self): """Called before document is submitted""" self.validate_for_submit() def on_submit(self): """Called after document is submitted""" self.create_gl_entries() def before_cancel(self): """Called before document is cancelled""" self.validate_cancellation() def on_cancel(self): """Called after document is cancelled""" self.reverse_gl_entries() def on_update_after_submit(self): """Called when submitted doc is updated (limited fields)""" pass # ===== DELETION ===== def before_delete(self): """Called before document is deleted""" self.check_dependencies() def after_delete(self): """Called after document is deleted""" self.cleanup_attachments() def on_trash(self): """Called when document is trashed""" pass def after_restore(self): """Called after document is restored from trash""" pass # ===== CUSTOM METHODS ===== def validate_dates(self): if self.end_date and self.start_date > self.end_date: frappe.throw("End date cannot be before start date") def calculate_totals(self): self.total = sum(d.amount for d in self.items)
Child Table (Table Field)
Parent DocType
{ "fieldname": "items", "fieldtype": "Table", "label": "Items", "options": "My DocType Item", "reqd": 1 }
Child DocType JSON
{ "name": "My DocType Item", "module": "My Module", "doctype": "DocType", "istable": 1, "editable_grid": 1, "fields": [ { "fieldname": "item", "fieldtype": "Link", "options": "Item", "in_list_view": 1, "reqd": 1 }, { "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1 }, { "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1 }, { "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, "read_only": 1 } ] }
Single DocType (Settings)
For application settings that have only one record:
{ "name": "My App Settings", "module": "My Module", "doctype": "DocType", "issingle": 1, "fields": [ { "fieldname": "enable_feature", "fieldtype": "Check", "label": "Enable Feature" }, { "fieldname": "api_key", "fieldtype": "Password", "label": "API Key" } ] }
Access in code:
settings = frappe.get_single("My App Settings") if settings.enable_feature: do_something()
Virtual DocType
DocType without database table, computed on-the-fly:
{ "name": "My Virtual DocType", "module": "My Module", "doctype": "DocType", "is_virtual": 1 }
Controller:
class MyVirtualDocType(Document): @staticmethod def get_list(args): # Return list of virtual documents return [{"name": "doc1", "value": 100}] @staticmethod def get_count(args): return len(MyVirtualDocType.get_list(args)) @staticmethod def get_stats(args): return {}
Permissions
{ "permissions": [ { "role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1, "submit": 1, "cancel": 1, "amend": 1, "report": 1, "export": 1, "import": 1, "share": 1, "print": 1, "email": 1 }, { "role": "Sales User", "read": 1, "write": 1, "create": 1, "if_owner": 1 } ] }
Best Practices
Naming Conventions
- Use singular names: "Customer" not "Customers"
- Use Title Case with spaces: "Sales Invoice"
- Fieldnames use snake_case:
customer_name
Field Design
- Put most important fields first
- Use Section Breaks to organize
- Use Tab Breaks for complex forms
- Set
for key fieldsin_list_view - Set
for filterable fieldsin_standard_filter
Performance
- Index frequently queried fields with
search_index: 1 - Use
to prevent unnecessary validationread_only - Limit child table rows with
max_attachments
Data Integrity
- Use
for unique constraintsunique: 1 - Set appropriate
(required) flagsreqd - Use
for conditional visibilitydepends_on - Use
for conditional requirementsmandatory_depends_on
Common Patterns
Status Field Pattern
{ "fieldname": "status", "fieldtype": "Select", "options": "\nDraft\nPending Approval\nApproved\nRejected", "default": "Draft", "in_list_view": 1, "in_standard_filter": 1, "read_only": 1, "allow_on_submit": 1 }
Amount Calculation Pattern
[ {"fieldname": "qty", "fieldtype": "Float"}, {"fieldname": "rate", "fieldtype": "Currency"}, {"fieldname": "amount", "fieldtype": "Currency", "read_only": 1} ]
With controller:
def validate(self): for item in self.items: item.amount = flt(item.qty) * flt(item.rate) self.total = sum(item.amount for item in self.items)
Linked Document Pattern
{ "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "reqd": 1 }, { "fieldname": "customer_name", "fieldtype": "Data", "fetch_from": "customer.customer_name", "read_only": 1 }