frappe-syntax-customapp

$npx mdskill add Impertio-Studio/Frappe_Claude_Skill_Package/frappe-syntax-customapp

Build Frappe apps correctly using scaffolding and configuration.

  • Prevents common mistakes in app structure and module organization.
  • Integrates with bench new-app and Frappe v14-v16 environments.
  • Decides actions based on whether building new or extending apps.
  • Delivers deterministic syntax references for patches and fixtures.

SKILL.md

.github/skills/frappe-syntax-customappView on GitHub ↗
---
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,
  app boilerplate, bench new-app example, module setup, patch example.
license: MIT
compatibility: "Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16."
metadata:
  author: OpenAEC-Foundation
  version: "2.0"
---

# Frappe Custom App Syntax

Deterministic syntax reference for building Frappe custom apps — scaffolding, configuration, modules, patches, and fixtures.

## Decision Tree

```
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

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
```

## Creating an App

```bash
# Create new app (interactive prompts for title, description, publisher, etc.)
bench new-app my_custom_app

# Install on site
bench --site mysite install-app my_custom_app

# 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 (flit)
├── README.md
├── 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
│   │   └── __init__.py
│   ├── my_custom_app/                 # Default module (same name as app)
│   │   ├── __init__.py
│   │   └── doctype/
│   ├── public/                        # Static assets → /assets/my_custom_app/
│   │   ├── css/
│   │   └── js/
│   ├── templates/                     # Jinja templates
│   │   └── includes/
│   └── www/                           # Portal pages (URL = directory path)
└── .git/
```

### [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 and the app CANNOT be installed.

### 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 = []            # Python packages ONLY — NEVER Frappe/ERPNext

[tool.bench.frappe-dependencies]
frappe = ">=15.0.0,<16.0.0"
erpnext = ">=15.0.0,<16.0.0"  # Only if app extends ERPNext
```

**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 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 Structure

```
my_custom_app/
├── my_custom_app/       # "My Custom App" module
│   ├── __init__.py
│   └── doctype/
├── integrations/        # "Integrations" module
│   ├── __init__.py
│   └── doctype/
├── settings/            # "Settings" module
│   ├── __init__.py
│   └── doctype/
└── reports/             # "Reports" module
    ├── __init__.py
    └── report/
```

### 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]
# Runs BEFORE schema sync — old fields still available
myapp.patches.v1_0.backup_old_data

[post_model_sync]
# 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,
        )
        if not records:
            break

        for record in records:
            frappe.db.set_value(
                "MyDocType", record.name,
                "new_field", "default_value",
                update_modified=False,
            )

        frappe.db.commit()
        offset += batch_size
```

### Pre vs 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 |

### 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 = [
    "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 and Importing

```bash
# Export fixtures to JSON files
bench --site mysite export-fixtures --app my_custom_app

# Import happens automatically during bench migrate or install-app
```

### Fixtures vs Patches

| 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

ALWAYS order fixtures so dependencies come first:

```python
fixtures = [
    "Workflow State",   # FIRST — Workflow depends on states
    "Workflow",         # SECOND
]
```

## Version Differences

| 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

1. Define `__version__` in `__init__.py` — flit build fails without it
2. Add `dynamic = ["version"]` in pyproject.toml
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)

### 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 |
|------|----------|
| [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` — Full hooks.py reference
- `frappe-syntax-controllers` — DocType controller methods
- `frappe-impl-customapp` — Implementation patterns and workflows

More from Impertio-Studio/Frappe_Claude_Skill_Package