mcpbeat Sign in

Frappe Impl Controllers Agent Skill

> file creation, lifecycle hooks, validation, autoname, submittable workflows, controller override, child table controllers, flags system, controller, which hook to use, validate vs on_update, override controller, submittable document, autoname, flags, extend_doctype_class, controller testing, child table controller, which hook to use, when does validate run, how to override save, document lifecycle.

11k tokens
context cost
the whole folder, loaded on every use
5
files
instructions only
0
copies elsewhere
how many repositories repackaged it
158
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-controllers

The instruction itself

22 sections, as written by the author

Document Controllers — Implementation Workflows

Step-by-step workflows for building server-side DocType logic with full Python power. For exact syntax, see frappe-syntax-controllers.

Version: v14/v15/v16 | v15+: Supports auto-generated type annotations

Quick Decision: Controller vs Server Script?

NEED full Python (imports, classes, generators)?     → Controller
NEED external libraries (requests, pandas)?          → Controller
NEED try/except with rollback?                       → Controller
NEED frappe.enqueue() for background jobs?           → Controller
NEED to extend standard ERPNext DocType?             → Controller
Quick validation without custom app?                 → Server Script
Simple auto-fill or notification?                    → Server Script

Rule: ALWAYS use Controllers when you need a custom app. ALWAYS use Server Scripts for no-code prototyping.

Workflow 1: Create a New Controller

Step 1: Create DocType via Frappe UI or bench new-doctype

Step 2: File is auto-generated at:

apps/myapp/myapp/{module}/doctype/{doctype_name}/{doctype_name}.py

Step 3: Implement the controller class:

import frappe
from frappe import _
from frappe.model.document import Document

class MyDocType(Document):
    def validate(self):
        self.validate_dates()
        self.calculate_totals()

    def validate_dates(self):
        if self.from_date and self.to_date and self.from_date > self.to_date:
            frappe.throw(_("From Date cannot be after To Date"))

    def calculate_totals(self):
        self.total = sum(item.amount for item in self.items)

Step 4: Run bench restart (or bench watch for hot-reload in dev)

Naming convention: DocType "Sales Order" → class SalesOrder, file sales_order.py

Workflow 2: Choose the Right Hook

WHAT DO YOU WANT?
├── Validate data / calculate fields before save?
│   └── validate — changes to self ARE saved
│
├── Action AFTER save (emails, linked docs, logs)?
│   └── on_update — changes to self NOT saved (use db_set)
│
├── Only for NEW documents?
│   └── after_insert
│
├── Before/after SUBMIT?
│   ├── Check before submit → before_submit
│   └── Ledger entries after → on_submit
│
├── Before/after CANCEL?
│   ├── Prevent cancel → before_cancel
│   └── Reverse entries → on_cancel
│
├── Before DELETE?
│   └── on_trash (throw to prevent)
│
├── Custom document naming?
│   └── autoname
│
└── Detect ANY change (including db_set)?
    └── on_change

> See references/decision-tree.md for all hooks with execution order.

CRITICAL: validate vs on_update

| Aspect | validate | on_update |

|--------|-----------|-------------|

| When | Before DB write | After DB write |

| self.x = y saved? | YES | NO — use db_set |

| Can abort with throw? | YES | Already saved |

| get_doc_before_save() | Available | Available |

| Use for | Validation, calculations | Notifications, linked docs |

# WRONG — changes in on_update are NOT saved
def on_update(self):
    self.status = "Completed"  # LOST!

# CORRECT — use db_set
def on_update(self):
    frappe.db.set_value(self.doctype, self.name, "status", "Completed")

Workflow 3: Validation with Error Collection

def validate(self):
    errors = []
    if not self.items:
        errors.append(_("At least one item is required"))
    for item in self.items:
        if item.qty <= 0:
            errors.append(_("Row {0}: Qty must be positive").format(item.idx))
    if self.from_date > self.to_date:
        errors.append(_("From Date cannot be after To Date"))
    if errors:
        frappe.throw("<br>".join(errors))

Workflow 4: Detect Field Changes

def validate(self):
    old = self.get_doc_before_save()
    if old and old.status != self.status:
        self.flags.status_changed = True
        self.status_changed_on = frappe.utils.now()

def on_update(self):
    if self.flags.get('status_changed'):
        self.notify_status_change()

Rule: ALWAYS use self.flags to pass data between hooks. NEVER rely on external state.

Workflow 5: Custom Naming (autoname)

from frappe.model.naming import getseries

def autoname(self):
    # Format: PRJ-CUST-2025-001
    code = (self.customer or "GEN")[:4].upper()
    year = frappe.utils.getdate(self.start_date or frappe.utils.today()).year
    prefix = f"PRJ-{code}-{year}-"
    self.name = getseries(prefix, 3)

Alternative — before_naming:

def before_naming(self):
    if self.is_priority:
        self.naming_series = "PRIORITY-.#####"
    else:
        self.naming_series = "STD-.#####"

Workflow 6: Submittable Document

DRAFT (docstatus=0) → submit() → SUBMITTED (docstatus=1) → cancel() → CANCELLED (docstatus=2)

submit():  validate → before_submit → [DB: docstatus=1] → on_update → on_submit
cancel():  before_cancel → [DB: docstatus=2] → on_cancel
class PurchaseOrder(Document):
    def validate(self):
        self.validate_items()
        self.calculate_totals()

    def before_submit(self):
        # ONLY submit-specific checks here
        if self.total > 100000 and not self.manager_approval:
            frappe.throw(_("Manager approval required for POs over 100,000"))

    def on_submit(self):
        self.update_ordered_qty()
        self.create_purchase_receipt_draft()

    def before_cancel(self):
        if frappe.db.exists("Purchase Invoice",
                {"purchase_order": self.name, "docstatus": 1}):
            frappe.throw(_("Cancel linked invoices first"))

    def on_cancel(self):
        self.reverse_ordered_qty()

Rule: NEVER duplicate validation between validate and before_submit. validate ALWAYS runs before before_submit.

Workflow 7: Override Standard ERPNext Controller

Method A: Full Override (hooks.py)

# hooks.py
override_doctype_class = {
    "Sales Invoice": "myapp.overrides.CustomSalesInvoice"
}

# myapp/overrides.py
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice

class CustomSalesInvoice(SalesInvoice):
    def validate(self):
        super().validate()  # ALWAYS call parent first
        self.custom_validation()

Method B: Event Handler (Safer, no class override)

# hooks.py
doc_events = {
    "Sales Invoice": {
        "validate": "myapp.events.validate_sales_invoice",
    }
}

# myapp/events.py
def validate_sales_invoice(doc, method=None):
    if doc.grand_total < 0:
        frappe.throw(_("Invalid total"))

Method C: extend_doctype_class (v16+)

# hooks.py
extend_doctype_class = {
    "Sales Invoice": "myapp.extends.SalesInvoiceExtend"
}

# myapp/extends.py — Only methods to add/override
class SalesInvoiceExtend:
    def custom_method(self):
        pass

Rule: ALWAYS call super().validate() in override. NEVER skip parent methods — standard ERPNext logic depends on it.

Workflow 8: Whitelisted Methods (Client-Callable)

class Quotation(Document):
    @frappe.whitelist()
    def apply_discount(self, discount_percent):
        if discount_percent < 0 or discount_percent > 100:
            frappe.throw(_("Discount must be 0-100"))
        self.discount_amount = self.total * (discount_percent / 100)
        self.grand_total = self.total - self.discount_amount
        self.save()
        return {"grand_total": self.grand_total}

Client-side call:

frm.call('apply_discount', { discount_percent: 10 }).then(r => {
    frm.reload_doc();
});

Workflow 9: Flags System

# Document-level flags (built-in)
doc.flags.ignore_permissions = True    # Bypass permission checks
doc.flags.ignore_validate = True       # Skip validate() hook
doc.flags.ignore_mandatory = True      # Skip required field check

# Custom flags for inter-hook communication
def validate(self):
    if self.is_urgent:
        self.flags.needs_notification = True

def on_update(self):
    if self.flags.get('needs_notification'):
        self.notify_team()

Workflow 10: Testing Controllers

# tests/test_my_doctype.py
import frappe
from frappe.tests.utils import FrappeTestCase

class TestMyDocType(FrappeTestCase):
    def test_validate_dates(self):
        doc = frappe.get_doc({
            "doctype": "My DocType",
            "from_date": "2025-01-10",
            "to_date": "2025-01-01"  # Before from_date
        })
        self.assertRaises(frappe.ValidationError, doc.insert)

    def test_calculate_totals(self):
        doc = frappe.get_doc({
            "doctype": "My DocType",
            "items": [
                {"item": "A", "qty": 2, "rate": 100},
                {"item": "B", "qty": 3, "rate": 50}
            ]
        })
        doc.insert()
        self.assertEqual(doc.total, 350)

Run: bench run-tests --module myapp.module.doctype.my_doctype.test_my_doctype

Execution Order Reference

INSERT

before_insert → before_naming → autoname → before_validate →
validate → before_save → [DB INSERT] → after_insert →
on_update → on_change

SAVE (existing)

before_validate → validate → before_save → [DB UPDATE] →
on_update → on_change

SUBMIT

validate → before_submit → [DB: docstatus=1] →
on_update → on_submit → on_change

Anti-Pattern Quick Check

| Do NOT | Do Instead |

|--------|------------|

| self.x = y in on_update | frappe.db.set_value(...) |

| self.save() in on_update | Causes infinite loop |

| frappe.db.commit() in hooks | Let framework handle |

| Heavy ops in validate | Use frappe.enqueue() in on_update |

| Skip super().validate() | ALWAYS call parent first |

| frappe.get_doc() in loops | Use frappe.get_cached_doc() |

| Hardcoded thresholds | Use Settings DocType |

> See references/anti-patterns.md for complete list.

  • frappe-syntax-controllers — Exact hook signatures and API
  • frappe-errors-controllers — Error handling patterns
  • frappe-impl-serverscripts — When Server Script suffices
  • frappe-syntax-hooks — hooks.py configuration
  • frappe-core-databasefrappe.db.* operations

> See references/decision-tree.md for all hooks.

> See references/workflows.md for extended patterns.

> See references/examples.md for complete working examples.

Other skills for the same job

different authors, same section of the catalogue
Analytical Method Validation
by K-Dense-AI
×1

Plan, execute, and document validation, verification, and transfer of analytical procedures under the governing framework - ICH Q2(R2) and Q14, USP <1220>/<1225>/<1226>, ICH M10 bioanalytical, CLSI EP, or ISO/IEC 17025. Use for HPLC, LC-MS/MS, GC, CE, ICP-MS, dissolution, qNMR, qPCR, NIR, and ligand binding or cell-based assays whenever the question is whether a procedure is fit for its intended purpose. Triggers include "method validation", "analytical method validation", "AMV", "validation protocol", "acceptance criteria", "linearity", "reportable range", "accuracy and precision", "repeatability", "intermediate precision", "recovery", "LOD", "LOQ", "detection limit", "quantitation limit", "specificity", "robustness", "method transfer", "method comparison", "Deming", "Passing-Bablok", "Bland-Altman", "equivalence testing", "OOS investigation", "ICH Q2", "Q2(R2)", "Q14", "USP 1225", "ICH M10", "incurred sample reanalysis", "ISR", "CLSI EP", and any request to show that an assay works.

52k tokens scripts
Doc Coauthoring
by lingxling
×1

This skill provides a structured workflow for guiding users through collaborative document creation. Act as an active guide, walking users through three stages: Context Gathering, Refinement & Structure, and Reader Testing.

4k tokens
Technical Design Doc Creator
by christophacham
×1

Creates comprehensive Technical Design Documents (TDD) with mandatory and optional sections through interactive discovery. Use when user asks to "write a design doc", "create a TDD", "technical spec", "architecture document", "RFC", "design proposal", or needs to document a technical decision before implementation. Do NOT use for README files, API docs, or general documentation (use docs-writer instead).

16k tokens
Linear Bug Triage
by langfuse
vendor

| Use this skill after a bug or regression candidate has measured evidence to document them within linear.

1k tokens
QA Quarto
by pedrohcgs

Adversarial Quarto-vs-Beamer parity QA. A critic agent compares the Quarto HTML render to the Beamer PDF benchmark for content/visual parity; a fixer agent applies fixes; loops until APPROVED (max 5 rounds). Use when user says "qa the quarto", "check parity", "does the html match the pdf?", "quarto matches beamer?", or after a translate-to-quarto run. Requires both the `.qmd` rendered and a `.pdf` benchmark.

714 tokens
Ambiguity Stress Test
by lawve-ai

>- Adversarially stress-tests a legal text — a contract, statute, regulation, or people governed by it will later disagree about what it means and turns each into a concrete dispute scenario with both sides' arguments, the likely outcome, and a fix. Use it whenever someone wants to pressure-test, red-team, audit, or find weak spots, loopholes, gaps, ambiguities, or drafting problems in a legal document; whenever a drafter wants to tighten a contract, statute, regulation, or opinion before it issues; whenever a litigator wants to mine an opinion or contract for arguments; or whenever someone hands over a legal text and asks where it will be fought over or for issue-spotting. Trigger it for contract review, statutory-ambiguity analysis, judicial-opinion scope analysis, and drafting QA — even if the user never says "stress-test" or "ambiguity."

18k tokens
Nw Distill
by nWave-ai

Acceptance test creation methodology for the DISTILL wave. Domain knowledge for the acceptance designer agent: port-to-port principle, prior wave reading, wave-decision reconciliation, graceful degradation, and document back-propagation.

17k tokens
Deliver Edge Cases
by product-on-purpose

Documents edge cases, error states, boundary conditions, race conditions, and recovery paths for a feature - the systematic catalog of what can go wrong and the failure modes to design for. Use during specification to map the failure surface and ensure comprehensive coverage, or during QA planning to identify boundary and limit scenarios to test. Distinct from deliver-acceptance-criteria, which writes story-level Given/When/Then checks; this skill produces the whole-feature edge-case catalog.

6k tokens

How to use it

Copy the folder

Take impertio-studio/frappe-impl-controllers from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.