v2.0 to v2.0

225 added, 174 removed. Audit A to A.

---
name: frappe-syntax-customapp
description: >
Use when building Frappe custom apps from scratch. Covers app structure,
pyproject.toml configuration, module creation, patches, and fixtures
for v14/v15/v16. Prevents common mistakes with app scaffolding and
module organization. Keywords: custom app, bench new-app, pyproject.toml,
patches, fixtures, modules, app structure.
license: MIT
compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."
metadata:
author: OpenAEC-Foundation
version: "2.0"
---
- # ERPNext Custom App Syntax Skill
+ # Frappe Custom App Syntax
- > Complete syntax for building Frappe custom apps in v14/v15, including build configuration, module organization, patches and fixtures.
+ Deterministic syntax reference for building Frappe custom apps — scaffolding, configuration, modules, patches, and fixtures.
- ---
+ ## Decision Tree
- ## When to Use This Skill
+ ```
+ What do you need?
+ ├─ Brand new app from scratch → bench new-app
+ ├─ Extend existing ERPNext behavior → bench new-app + required_apps = ["frappe", "erpnext"]
+ ├─ Install existing app from Git → bench get-app <url>
+ └─ Add functionality to an installed app
+ ├─ New data model → Add module to modules.txt + create DocType
+ ├─ New fields on existing DocType → Fixtures (Custom Field)
+ ├─ Modify field properties → Fixtures (Property Setter)
+ └─ Data migration → Patch in patches.txt
- USE this skill when you:
- - Create a new Frappe/ERPNext custom app
- - Configure pyproject.toml or setup.py
- - Organize modules within an app
- - Write database migration patches
- - Configure fixtures for data export/import
- - Manage app dependencies
+ New app vs extend existing?
+ ├─ Independent functionality → New app
+ ├─ Tightly coupled to one app → New app with required_apps dependency
+ └─ Small customization (fields, properties) → Extend via fixtures in existing custom app
+ ```
- DO NOT USE for:
- - DocType controllers (use frappe-syntax-controllers)
- - Client Scripts (use frappe-syntax-clientscripts)
- - Server Scripts (use frappe-syntax-serverscripts)
- - Hooks configuration (use frappe-syntax-hooks)
+ ## Creating an App
- ---
+ ```bash
+ # Create new app (interactive prompts for title, description, publisher, etc.)
+ bench new-app my_custom_app
- ## App Structure Overview
+ # Install on site
+ bench --site mysite install-app my_custom_app
- ### v15 (pyproject.toml - Primary)
+ # Get existing app from Git
+ bench get-app https://github.com/org/my_custom_app
+ # Build frontend assets
+ bench build --app my_custom_app
+
+ # Run migrations (patches + fixtures + schema sync)
+ bench --site mysite migrate
```
+
+ ## App Directory Structure
+
+ ### [v15+] pyproject.toml (Primary)
+
+ ```
apps/my_custom_app/
- ├── pyproject.toml # Build configuration
+ ├── pyproject.toml # Build configuration (flit)
├── README.md
- ├── my_custom_app/ # Main package
- │ ├── __init__.py # MUST contain __version__!
- │ ├── hooks.py # Frappe integration
+ ├── my_custom_app/ # Inner Python package
+ │ ├── __init__.py # MUST contain __version__
+ │ ├── hooks.py # Frappe integration hooks
│ ├── modules.txt # Module registration
│ ├── patches.txt # Migration scripts
│ ├── patches/ # Patch files
- │ ├── my_custom_app/ # Default module
+ │ │ └── __init__.py
+ │ ├── my_custom_app/ # Default module (same name as app)
+ │ │ ├── __init__.py
│ │ └── doctype/
- │ ├── public/ # Client assets
- │ └── templates/ # Jinja templates
+ │ ├── public/ # Static assets → /assets/my_custom_app/
+ │ │ ├── css/
+ │ │ └── js/
+ │ ├── templates/ # Jinja templates
+ │ │ └── includes/
+ │ └── www/ # Portal pages (URL = directory path)
└── .git/
```
- > **See**: `references/structure.md` for complete directory structure.
+ ### [v14] setup.py (Legacy)
- ---
+ ```
+ apps/my_custom_app/
+ ├── setup.py # Build configuration (setuptools)
+ ├── MANIFEST.in
+ ├── requirements.txt # Python dependencies
+ ├── dev-requirements.txt # Dev dependencies (developer_mode only)
+ ├── package.json # Node dependencies
+ ├── my_custom_app/
+ │ ├── __init__.py
+ │ ├── hooks.py
+ │ ├── modules.txt
+ │ ├── patches.txt
+ │ └── [same inner structure as v15]
+ └── .git/
+ ```
## Critical Files
### __init__.py (REQUIRED)
```python
# my_custom_app/__init__.py
__version__ = "0.0.1"
```
- **CRITICAL**: Without `__version__` the flit build fails!
+ **CRITICAL**: Without `__version__`, the flit build FAILS and the app CANNOT be installed.
- ### pyproject.toml (v15)
+ ### pyproject.toml [v15+]
```toml
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "my_custom_app"
authors = [
{ name = "Your Company", email = "dev@example.com" }
]
description = "Description of your app"
requires-python = ">=3.10"
readme = "README.md"
dynamic = ["version"]
- dependencies = []
+ dependencies = [] # Python packages ONLY — NEVER Frappe/ERPNext
[tool.bench.frappe-dependencies]
frappe = ">=15.0.0,<16.0.0"
- erpnext = ">=15.0.0,<16.0.0"
+ erpnext = ">=15.0.0,<16.0.0" # Only if app extends ERPNext
```
- > **See**: `references/pyproject-toml.md` for all configuration options.
+ **CRITICAL rules for pyproject.toml**:
+ - `name` MUST match the inner directory name exactly
+ - `dynamic = ["version"]` is REQUIRED — flit reads `__version__` from `__init__.py`
+ - NEVER put `frappe` or `erpnext` in `[project] dependencies` (they are not on PyPI)
+ - ALWAYS put Frappe app dependencies in `[tool.bench.frappe-dependencies]`
- ---
+ ### setup.py [v14] (Legacy)
+ ```python
+ from setuptools import setup, find_packages
+
+ setup(
+ name="my_custom_app",
+ version="0.0.1",
+ description="Description of your app",
+ author="Your Company",
+ author_email="dev@example.com",
+ packages=find_packages(),
+ zip_safe=False,
+ include_package_data=True,
+ install_requires=[],
+ )
+ ```
+
+ ### hooks.py (Minimal Skeleton)
+
+ ```python
+ app_name = "my_custom_app"
+ app_title = "My Custom App"
+ app_publisher = "Your Company"
+ app_description = "Description"
+ app_email = "dev@example.com"
+ app_license = "MIT"
+
+ required_apps = ["frappe"] # Or ["frappe", "erpnext"] if extending ERPNext
+
+ fixtures = [
+ {"dt": "Custom Field", "filters": [["module", "=", "My Custom App"]]},
+ {"dt": "Property Setter", "filters": [["module", "=", "My Custom App"]]},
+ ]
+ ```
+
## Modules
### modules.txt
```
My Custom App
Integrations
Settings
Reports
```
- **Rules:**
- - One module per line
- - Spaces in name → underscores in directory
- - Every DocType MUST belong to a module
+ **Rules**:
+ - One module name per line — NEVER leave empty lines or trailing spaces
+ - Module name uses spaces; directory name uses underscores (`My Custom App` → `my_custom_app/`)
+ - Every DocType MUST belong to a registered module
+ - ALWAYS include `__init__.py` in every module directory
- ### Module Directory
+ ### Module Directory Structure
```
my_custom_app/
├── my_custom_app/ # "My Custom App" module
- │ ├── __init__.py # REQUIRED
+ │ ├── __init__.py
│ └── doctype/
├── integrations/ # "Integrations" module
- │ ├── __init__.py # REQUIRED
+ │ ├── __init__.py
│ └── doctype/
- └── settings/ # "Settings" module
- ├── __init__.py # REQUIRED
- └── doctype/
+ ├── settings/ # "Settings" module
+ │ ├── __init__.py
+ │ └── doctype/
+ └── reports/ # "Reports" module
+ ├── __init__.py
+ └── report/
```
- > **See**: `references/modules.md` for module organization.
+ ### DocType Directory (within a module)
- ---
+ ```
+ doctype/my_doctype/
+ ├── __init__.py # Empty (REQUIRED)
+ ├── my_doctype.json # DocType definition (generated by UI)
+ ├── my_doctype.py # Python controller
+ ├── my_doctype.js # Client script
+ ├── test_my_doctype.py # Unit tests
+ └── my_doctype_dashboard.py # Dashboard config
+ ```
## Patches (Migration Scripts)
### patches.txt with INI Sections
```ini
[pre_model_sync]
- # Before schema sync - old fields still available
+ # Runs BEFORE schema sync — old fields still available
myapp.patches.v1_0.backup_old_data
[post_model_sync]
- # After schema sync - new fields available
+ # Runs AFTER schema sync — new fields available
myapp.patches.v1_0.populate_new_fields
myapp.patches.v1_0.cleanup_data
```
### Patch Implementation
```python
# myapp/patches/v1_0/populate_new_fields.py
import frappe
def execute():
"""Populate new fields with default values."""
-
batch_size = 1000
offset = 0
-
+
while True:
records = frappe.get_all(
"MyDocType",
filters={"new_field": ["is", "not set"]},
fields=["name"],
limit_page_length=batch_size,
- limit_start=offset
+ limit_start=offset,
)
-
if not records:
break
-
+
for record in records:
frappe.db.set_value(
- "MyDocType",
- record.name,
- "new_field",
- "default_value",
- update_modified=False
+ "MyDocType", record.name,
+ "new_field", "default_value",
+ update_modified=False,
)
-
+
frappe.db.commit()
offset += batch_size
```
- ### When Pre vs Post Model Sync?
+ ### Pre vs Post Model Sync
- | Situation | Section |
- |-----------|---------|
- | Migrate data from old field | `[pre_model_sync]` |
- | Populate new fields | `[post_model_sync]` |
- | Data cleanup | `[post_model_sync]` |
+ | Situation | Section | Reason |
+ |-----------|---------|--------|
+ | Migrate data from old field | `[pre_model_sync]` | Old field still exists |
+ | Rename field + preserve data | `[pre_model_sync]` | Old name still available |
+ | Populate new required fields | `[post_model_sync]` | New field already exists |
+ | General data cleanup | `[post_model_sync]` | No schema dependency |
- > **See**: `references/patches.md` for complete patch documentation.
+ ### Re-running a Patch
- ---
+ ```
+ # Patches run ONCE. To re-run, make the line unique with a comment:
+ myapp.patches.v1_0.my_patch #2024-01-15
+ ```
+ ### bench migrate Workflow
+
+ 1. `before_migrate` hooks execute
+ 2. `[pre_model_sync]` patches execute
+ 3. Database schema sync (DocType JSON → tables)
+ 4. `[post_model_sync]` patches execute
+ 5. Fixtures sync
+ 6. `after_migrate` hooks execute
+
## Fixtures
### hooks.py Configuration
```python
fixtures = [
- # All records
- "Category",
-
- # With filter
- {
- "dt": "Custom Field",
- "filters": [["module", "=", "My Custom App"]]
- },
-
- # Multiple filters
- {
- "dt": "Property Setter",
- "filters": [
- ["module", "=", "My Custom App"],
- ["doc_type", "in", ["Sales Invoice", "Sales Order"]]
- ]
- }
+ "Category", # All records
+ {"dt": "Custom Field", "filters": [["module", "=", "My Custom App"]]},
+ {"dt": "Property Setter", "filters": [["module", "=", "My Custom App"]]},
+ {"dt": "Role", "filters": [["name", "like", "MyApp%"]]},
]
```
- ### Exporting
+ ### Exporting and Importing
```bash
+ # Export fixtures to JSON files
bench --site mysite export-fixtures --app my_custom_app
- ```
- ### Common Fixture DocTypes
+ # Import happens automatically during bench migrate or install-app
+ ```
- | DocType | Usage |
- |---------|-------|
- | `Custom Field` | Custom fields on existing DocTypes |
- | `Property Setter` | Modify field properties |
- | `Role` | Custom roles |
- | `Workflow` | Workflow definitions |
+ ### Fixtures vs Patches
- > **See**: `references/fixtures.md` for fixture configuration.
+ | What | Fixtures | Patches |
+ |------|:--------:|:-------:|
+ | Custom Fields | YES | NO |
+ | Property Setters | YES | NO |
+ | Roles, Workflows | YES | NO |
+ | Data transformation | NO | YES |
+ | One-time migration | NO | YES |
+ | Seed configuration data | YES | NO |
- ---
+ ### Fixture Ordering
- ## Minimal hooks.py
+ ALWAYS order fixtures so dependencies come first:
```python
- app_name = "my_custom_app"
- app_title = "My Custom App"
- app_publisher = "Your Company"
- app_description = "Description"
- app_email = "dev@example.com"
- app_license = "MIT"
-
- required_apps = ["frappe"] # Or ["frappe", "erpnext"]
-
fixtures = [
- {"dt": "Custom Field", "filters": [["module", "=", "My Custom App"]]}
+ "Workflow State", # FIRST — Workflow depends on states
+ "Workflow", # SECOND
]
```
- ---
-
- ## Creating and Installing App
-
- ```bash
- # Create new app
- bench new-app my_custom_app
-
- # Install on site
- bench --site mysite install-app my_custom_app
-
- # Migrate (patches + fixtures)
- bench --site mysite migrate
-
- # Build assets
- bench build --app my_custom_app
- ```
-
- ---
-
## Version Differences
- | Aspect | v14 | v15 |
- |--------|-----|-----|
- | Build config | setup.py | pyproject.toml |
- | Dependencies | requirements.txt | In pyproject.toml |
- | Build backend | setuptools | flit_core |
- | Python minimum | >=3.10 | >=3.10 |
- | INI patches | ✅ | ✅ |
+ | Aspect | v14 | v15+ | v16+ |
+ |--------|-----|------|------|
+ | Build config | setup.py | pyproject.toml | pyproject.toml |
+ | Build backend | setuptools | flit_core | flit_core |
+ | Dependencies file | requirements.txt | pyproject.toml | pyproject.toml |
+ | Python minimum | >=3.10 | >=3.10 | >=3.14 |
+ | INI patches | YES | YES | YES |
- ---
+ ### Migration v14 to v15
+ 1. Create `pyproject.toml` with flit_core build-system
+ 2. Move dependencies from `requirements.txt` to `[project] dependencies`
+ 3. Verify `__version__` in `__init__.py`
+ 4. Optionally remove: `setup.py`, `MANIFEST.in`, `requirements.txt`
+ 5. Test with `bench get-app` and `bench install-app`
+
## Critical Rules
- ### ✅ ALWAYS
+ ### ALWAYS
- 1. Define `__version__` in `__init__.py`
+ 1. Define `__version__` in `__init__.py` — flit build fails without it
2. Add `dynamic = ["version"]` in pyproject.toml
- 3. Register modules in `modules.txt`
- 4. Include `__init__.py` in EVERY directory
- 5. Put Frappe dependencies in `[tool.bench.frappe-dependencies]`
- 6. Add error handling in patches
- 7. Use batch processing for large datasets
-
- ### ❌ NEVER
-
- 1. Put Frappe/ERPNext in project dependencies (not on PyPI)
- 2. Create patches without error handling
- 3. Include user/transactional data in fixtures
- 4. Hardcode site-specific values
- 5. Process large datasets without batching
-
- ---
-
- ## Fixtures vs Patches
+ 3. Register EVERY module in `modules.txt`
+ 4. Include `__init__.py` in EVERY Python directory
+ 5. Put Frappe dependencies in `[tool.bench.frappe-dependencies]`, NEVER in `[project] dependencies`
+ 6. Use batch processing and error handling in patches
+ 7. Set `module` field on Custom Fields and Property Setters for correct fixture export
+ 8. Order fixtures by dependency (states before workflows)
- | What | Fixtures | Patches |
- |------|:--------:|:-------:|
- | Custom Fields | ✅ | ❌ |
- | Property Setters | ✅ | ❌ |
- | Roles/Workflows | ✅ | ❌ |
- | Data transformation | ❌ | ✅ |
- | Data cleanup | ❌ | ✅ |
- | One-time migration | ❌ | ✅ |
+ ### NEVER
- ---
+ 1. Put `frappe` or `erpnext` in pip dependencies (not on PyPI — install fails)
+ 2. Create patches without try/except and logging
+ 3. Include user data or transactional data (Sales Invoice, User) in fixtures
+ 4. Hardcode site-specific values in patches
+ 5. Process large datasets without batching and periodic `frappe.db.commit()`
+ 6. Use spaces in directory names (spaces in `modules.txt` only)
+ 7. Change module names after DocTypes have been created in production
## Reference Files
| File | Contents |
|------|----------|
- | `references/structure.md` | Complete directory structure |
- | `references/pyproject-toml.md` | Build configuration options |
- | `references/modules.md` | Module organization |
- | `references/patches.md` | Migration scripts |
- | `references/fixtures.md` | Data export/import |
- | `references/examples.md` | Complete app examples |
- | `references/anti-patterns.md` | Mistakes to avoid |
-
- ---
+ | [structure.md](references/structure.md) | Complete directory structure for v14 and v15 |
+ | [pyproject-toml.md](references/pyproject-toml.md) | Full pyproject.toml and setup.py configuration |
+ | [modules.md](references/modules.md) | Module organization, naming, workspaces |
+ | [patches.md](references/patches.md) | Patch syntax, pre/post model sync, batch processing |
+ | [fixtures.md](references/fixtures.md) | Fixture configuration, filters, common DocTypes |
+ | [examples.md](references/examples.md) | Complete minimal and ERPNext extension app examples |
+ | [anti-patterns.md](references/anti-patterns.md) | Top 10 mistakes and corrections |
## See Also
- - `frappe-syntax-hooks` - For hooks.py configuration
- - `frappe-syntax-controllers` - For DocType controllers
- - `frappe-impl-customapp` - For implementation patterns
+ - `frappe-syntax-hooks` — Full hooks.py reference
+ - `frappe-syntax-controllers` — DocType controller methods
+ - `frappe-impl-customapp` — Implementation patterns and workflows