Claude
Skills
Sign in
Back

frappe-client-script-generator

Included with Lifetime
$97 forever

Generate JavaScript client-side form scripts for Frappe DocTypes. Use when creating form customizations, field validations, custom buttons, or client-side logic for Frappe/ERPNext forms.

General

What this skill does


# Frappe Client Script Generator

Generate production-ready JavaScript form scripts for Frappe DocTypes with proper event handlers, validations, and custom functionality.

## Global Rules

These Frappe conventions apply to everything this skill generates, and override any conflicting example below.

- **Bench commands:** use bare `bench` (never `./env/bin/bench` or a full path). Always pass `--site <site>` explicitly — never run a bare `bench migrate` / `bench run-tests`. Run `bench start` in the background and only if it isn't already running. Don't run discovery commands (`which bench`, `bench --version`).
- **DocType files** live at `apps/<app>/<app>/<module>/doctype/<name>/<name>.json` — the app name appears twice (directory + Python package) — with an empty `__init__.py` alongside. Never `mkdir` the folder; write the JSON and run `bench --site <site> migrate` to create the structure. Don't add `creation`, `modified`, `owner`, `modified_by`, or `docstatus` as fields — Frappe manages them.
- **Database & ORM:** prefer `frappe.qb.get_query()` over raw `frappe.db.sql()`. Use `frappe.db.get_all()` for server logic (ignores permissions) and `frappe.db.get_list()` for user-facing APIs (enforces them). Never use `frappe.db.set_value()` on a field with validation or lifecycle logic — load the doc and `doc.save()` so controller hooks run. Batch-fetch related records; never query inside a loop (N+1).
- **Never call `frappe.db.commit()`** in controllers, request handlers, background jobs, or patches — Frappe auto-commits on success and rolls back on uncaught errors. Flush manually only to make a write visible to a subsequent `frappe.enqueue()` (or pass `enqueue_after_commit=True`).
- **Permissions & APIs:** put permission checks inside controller methods (enforced on every call path), not in API wrappers. Type-hint every `@frappe.whitelist()` parameter so Frappe validates and casts it, and pass `methods=[...]` to pin the HTTP verb.

## When to Use This Skill

Claude should invoke this skill when:
- User wants to add client-side form customizations
- User needs field validations or calculations
- User requests custom buttons or actions on forms
- User wants to filter or fetch data dynamically
- User mentions form scripts, client scripts, or JavaScript for DocTypes
- User wants to show/hide fields conditionally
- User needs to set field values based on other fields

## Capabilities

### 1. Form Event Handlers

Generate event handlers for DocType forms following Frappe patterns from core apps.

**Refresh Event** (runs when form loads):
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    refresh: function(frm) {
        // Add custom buttons
        if (frm.doc.docstatus === 1) {
            frm.add_custom_button(__('Create Payment'), function() {
                frm.events.make_payment_entry(frm);
            });
        }

        // Set field properties
        frm.set_df_property('customer', 'reqd', 1);

        // Show/hide fields
        frm.toggle_display('discount_section', frm.doc.apply_discount);
    }
});
```

**Setup Event** (runs once when form is created):
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    setup: function(frm) {
        // Set query filters for Link fields
        frm.set_query('item_code', 'items', function() {
            return {
                filters: {
                    'is_stock_item': 1,
                    'has_serial_no': 0
                }
            };
        });
    }
});
```

**Onload Event** (runs on form load, before refresh):
```javascript
// Pattern from: erpnext/accounts/doctype/payment_entry/payment_entry.js
frappe.ui.form.on('Payment Entry', {
    onload: function(frm) {
        if (frm.is_new()) {
            frm.set_value('posting_date', frappe.datetime.get_today());
        }
    }
});
```

### 2. Field Change Handlers

**Single Field Change**:
```javascript
// Pattern from: erpnext/selling/doctype/sales_order/sales_order.js
frappe.ui.form.on('Sales Order', {
    customer: function(frm) {
        if (frm.doc.customer) {
            // Fetch customer details
            frappe.db.get_value('Customer', frm.doc.customer, 'customer_group')
                .then(r => {
                    if (r.message) {
                        frm.set_value('customer_group', r.message.customer_group);
                    }
                });
        }
    }
});
```

**Multiple Field Dependencies**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    customer: function(frm) {
        frm.events.set_dynamic_field_label(frm);
    },
    currency: function(frm) {
        frm.events.set_dynamic_field_label(frm);
    },
    set_dynamic_field_label: function(frm) {
        if (frm.doc.currency) {
            frm.set_currency_labels(['total', 'grand_total'], frm.doc.currency);
        }
    }
});
```

### 3. Child Table (Grid) Events

**Child Table Row Events**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice_item.js
frappe.ui.form.on('Sales Invoice Item', {
    item_code: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        if (row.item_code) {
            frappe.call({
                method: 'erpnext.stock.get_item_details.get_item_details',
                args: {
                    item_code: row.item_code,
                    company: frm.doc.company
                },
                callback: function(r) {
                    if (r.message) {
                        frappe.model.set_value(cdt, cdn, 'rate', r.message.price_list_rate);
                        frappe.model.set_value(cdt, cdn, 'uom', r.message.stock_uom);
                    }
                }
            });
        }
    },

    qty: function(frm, cdt, cdn) {
        frm.events.calculate_totals(frm, cdt, cdn);
    },

    rate: function(frm, cdt, cdn) {
        frm.events.calculate_totals(frm, cdt, cdn);
    }
});
```

**Grid Operations**:
```javascript
// Pattern from: erpnext/stock/doctype/stock_entry/stock_entry.js
frappe.ui.form.on('Stock Entry', {
    items_add: function(frm, cdt, cdn) {
        let row = locals[cdt][cdn];
        row.s_warehouse = frm.doc.from_warehouse;
        row.t_warehouse = frm.doc.to_warehouse;
    },

    items_remove: function(frm) {
        frm.events.calculate_totals(frm);
    }
});
```

### 4. Custom Buttons and Actions

**Standard Button Patterns**:
```javascript
// Pattern from: erpnext/accounts/doctype/sales_invoice/sales_invoice.js
frappe.ui.form.on('Sales Invoice', {
    refresh: function(frm) {
        if (frm.doc.docstatus === 1 && frm.doc.outstanding_amount > 0) {
            frm.add_custom_button(__('Payment'), function() {
                frm.events.make_payment_entry(frm);
            }, __('Create'));
        }

        // Add custom button in toolbar
        if (frm.doc.docstatus === 0) {
            frm.add_custom_button(__('Get Items from Sales Order'), function() {
                erpnext.utils.map_current_doc({
                    method: 'erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice',
                    source_doctype: 'Sales Order',
                    target: frm,
                    setters: {
                        customer: frm.doc.customer || undefined
                    },
                    get_query_filters: {
                        docstatus: 1,
                        status: ['not in', ['Closed', 'On Hold']]
                    }
                });
            });
        }
    },

    make_payment_entry: function(frm) {
        return frappe.call({
            method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
            args: {
                dt: frm.doc.doctype,
                dn: frm.doc.name
            },
            callback: function(r) {
           

Related in General