$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-customappUse when building a custom Frappe app from scratch. Covers bench new-app walkthrough, app structure decisions, adding DocTypes, hooks, patches, fixtures management, development workflow (bench migrate, build, clear-cache), testing, packaging, installing on another site, version m
| 1 | # Frappe Custom App - Implementation |
| 2 | |
| 3 | Workflow for building a custom Frappe app from scratch. For exact syntax, see `frappe-syntax-customapp`. |
| 4 | |
| 5 | **Version**: v14/v15/v16 compatible |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Main Decision: Do You Need a Custom App? |
| 10 | |
| 11 | ``` |
| 12 | WHAT CHANGES DO YOU NEED? |
| 13 | | |
| 14 | +-- Add fields to existing DocType? |
| 15 | | +-- NO APP NEEDED: Custom Field + Property Setter |
| 16 | | |
| 17 | +-- Simple automation/validation (<50 lines)? |
| 18 | | +-- NO APP NEEDED: Server Script or Client Script |
| 19 | | |
| 20 | +-- Complex business logic, new DocTypes, or Python code? |
| 21 | | +-- YES: Create custom app |
| 22 | | |
| 23 | +-- Integration with external system (needs imports)? |
| 24 | | +-- YES: Custom app REQUIRED (Server Scripts block imports) |
| 25 | | |
| 26 | +-- Custom reports with complex queries? |
| 27 | | +-- Script Report (no app) vs Query Report (app optional) |
| 28 | ``` |
| 29 | |
| 30 | **Rule**: ALWAYS start with the simplest solution. Server Scripts + Custom Fields solve 70% of needs without a custom app. |
| 31 | |
| 32 | --- |
| 33 | |
| 34 | ## Step 1: Create App Structure |
| 35 | |
| 36 | ```bash |
| 37 | cd ~/frappe-bench |
| 38 | bench new-app my_app |
| 39 | # Prompts: Title, Description, Publisher, Email, License |
| 40 | ``` |
| 41 | |
| 42 | ALWAYS verify immediately: |
| 43 | ```python |
| 44 | # my_app/my_app/__init__.py MUST have: |
| 45 | __version__ = "0.0.1" |
| 46 | ``` |
| 47 | |
| 48 | --- |
| 49 | |
| 50 | ## Step 2: Configure pyproject.toml (v15+) |
| 51 | |
| 52 | ```toml |
| 53 | [build-system] |
| 54 | requires = ["flit_core >=3.4,<4"] |
| 55 | build-backend = "flit_core.buildapi" |
| 56 | |
| 57 | [project] |
| 58 | name = "my_app" |
| 59 | authors = [{ name = "Your Company", email = "dev@example.com" }] |
| 60 | description = "Your app description" |
| 61 | requires-python = ">=3.10" |
| 62 | readme = "README.md" |
| 63 | dynamic = ["version"] |
| 64 | dependencies = [ |
| 65 | "requests>=2.28.0" # Only PyPI packages here |
| 66 | ] |
| 67 | |
| 68 | [tool.bench.frappe-dependencies] |
| 69 | frappe = ">=15.0.0,<16.0.0" |
| 70 | # erpnext = ">=15.0.0,<16.0.0" # Only if needed |
| 71 | ``` |
| 72 | |
| 73 | **Rule**: NEVER put frappe or erpnext in `[project].dependencies` -- they are NOT on PyPI. |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## Step 3: Configure hooks.py |
| 78 | |
| 79 | ```python |
| 80 | app_name = "my_app" |
| 81 | app_title = "My App" |
| 82 | app_publisher = "Your Company" |
| 83 | app_description = "Description" |
| 84 | app_email = "dev@example.com" |
| 85 | app_license = "MIT" |
| 86 | |
| 87 | required_apps = ["frappe"] # Or ["frappe", "erpnext"] |
| 88 | |
| 89 | fixtures = [] # Configured later |
| 90 | ``` |
| 91 | |
| 92 | **Rule**: ALWAYS declare `required_apps` with all dependencies. |
| 93 | |
| 94 | --- |
| 95 | |
| 96 | ## Step 4: Define Modules |
| 97 | |
| 98 | ```text |
| 99 | # my_app/my_app/modules.txt |
| 100 | My App |
| 101 | ``` |
| 102 | |
| 103 | | App Size | Module Strategy | |
| 104 | |----------|----------------| |
| 105 | | 1-5 DocTypes | ONE module with app name | |
| 106 | | 6-15 DocTypes | 2-4 modules by functional area | |
| 107 | | 15+ DocTypes | Modules by business domain | |
| 108 | |
| 109 | **Rule**: Each DocType belongs to EXACTLY one module. Module name in `modules.txt` maps to directory: `My Custom App` --> `my_custom_app/`. |
| 110 | |
| 111 | ### Adding a Module |
| 112 | ```bash |
| 113 | mkdir -p my_app/my_app/new_module/doctype |
| 114 | touch my_app/my_app/new_module/__init__.py |
| 115 | # Add "New Module" to modules.txt |
| 116 | bench --site mysite migrate |
| 117 | ``` |
| 118 | |
| 119 | --- |
| 120 | |
| 121 | ## Step 5: Install and Create DocTypes |
| 122 | |
| 123 | ```bash |
| 124 | # Install app on site |
| 125 | bench --site mysite install-app my_app |
| 126 | |
| 127 | # Create DocType (via UI recommended, or CLI) |
| 128 | bench --site mysite new-doctype "My Document" --module "My App" |
| 129 | ``` |
| 130 | |
| 131 | This creates: |
| 132 | ``` |
| 133 | my_app/my_app/doctype/my_document/ |
| 134 | +-- my_document.json # DocType definition |
| 135 | +-- my_document.py # Controller |
| 136 | +-- my_document.js # Client script |
| 137 | +-- test_my_document.py # Tests |
| 138 | ``` |
| 139 | |
| 140 | --- |
| 141 | |
| 142 | ## Step 6: Add Hooks |
| 143 | |
| 144 | ### doc_events (v14/v15/v16) |
| 145 | ```python |
| 146 | doc_events = { |
| 147 | "Sales Invoice": { |
| 148 | "validate": "my_app.events.sales_invoice.validate", |
| 149 | "on_submit": "my_app.events.sales_invoice.on_submit" |
| 150 | } |
| 151 | } |
| 152 | ``` |
| 153 | |
| 154 | ### extend_doctype_class (v16 ONLY -- preferred) |
| 155 | ```python |
| 156 | extend_doctype_class = { |
| 157 | "Sales Invoice": "my_app.overrides.sales_invoice.CustomSalesInvoice" |
| 158 | } |
| 159 | ``` |
| 160 | |
| 161 | **Rule**: ALWAYS call `super().method()` when overriding lifecycle methods in v16. |
| 162 | |
| 163 | ### Scheduler Events |
| 164 | ```python |
| 165 | scheduler_events = { |
| 166 | "daily": ["my_app.tasks.daily_cleanup"], |
| 167 | "cron": {"0 9 * * 1-5": ["my_app.tasks.morning_report"]} |
| 168 | } |
| 169 | ``` |
| 170 | |
| 171 | See `frappe-impl-hooks` and `frappe-impl-scheduler` for complete patterns. |
| 172 | |
| 173 | --- |
| 174 | |
| 175 | ## Step 7: Add Patches |
| 176 | |
| 177 | ### Create Patch File |
| 178 | ```bash |
| 179 | mkdir -p my_app/my_app/patches/v1_0 |
| 180 | touch my_app/my_app/patches/__init__.py |
| 181 | touch my_app/my_app/patches/v1_0/__init__.py |
| 182 | ``` |
| 183 | |
| 184 | ```python |
| 185 | # my_app/my_app/patches/v1_0/populate_defaults.py |
| 186 | import frappe |
| 187 | |
| 188 | def execute(): |
| 189 | if not frappe.db.has |