frappe-desk-customization

Frappe Desk Customization

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "frappe-desk-customization" with this command: npx skills add lubusin/agent-skills/lubusin-agent-skills-frappe-desk-customization

Frappe Desk Customization

Customize the Frappe Desk admin UI with form scripts, list views, dialogs, and client-side APIs.

When to use

  • Adding custom buttons or actions to forms

  • Filtering Link fields dynamically

  • Toggling field visibility based on conditions

  • Customizing list view indicators and bulk actions

  • Building interactive dialogs and prompts

  • Adding client-side validation before save

  • Injecting scripts into other apps' DocTypes via hooks

Inputs required

  • Target DocType for customization

  • Whether script is app-level (version controlled) or Client Script (site-specific)

  • Events to hook into (refresh, validate, field change, etc.)

  • UI behavior requirements (buttons, filters, visibility)

Procedure

  1. Choose script type

Type Location Version Controlled Use Case

App-level form script <app>/<module>/doctype/<doctype>/<doctype>.js

Yes Standard app behavior

Client Script DocType: Client Script No (DB) Site-specific customization

Hook-injected script Via doctype_js in hooks.py

Yes Extend other apps' DocTypes

  1. Write form scripts

frappe.ui.form.on("My DocType", { // Called once during form setup setup(frm) { frm.set_query("customer", function() { return { filters: { "status": "Active" } }; }); },

// Called every time form loads or refreshes
refresh(frm) {
    if (frm.doc.status === "Draft") {
        frm.add_custom_button(__("Submit for Review"), function() {
            frappe.call({
                method: "my_app.api.submit_for_review",
                args: { name: frm.doc.name },
                callback(r) {
                    frm.reload_doc();
                }
            });
        }, __("Actions"));
    }

    // Toggle field visibility
    frm.toggle_display("discount_section", frm.doc.grand_total > 1000);

    // Set field properties
    frm.set_df_property("notes", "read_only", frm.doc.docstatus === 1);
},

// Called before save — return false to cancel
validate(frm) {
    if (frm.doc.end_date &#x3C; frm.doc.start_date) {
        frappe.msgprint(__("End date must be after start date"));
        frappe.validated = false;
    }
},

// Field change handler (use fieldname as key)
customer(frm) {
    if (frm.doc.customer) {
        frappe.db.get_value("Customer", frm.doc.customer, "territory",
            function(r) {
                frm.set_value("territory", r.territory);
            }
        );
    }
},

// Before save hook
before_save(frm) {
    frm.doc.full_name = `${frm.doc.first_name} ${frm.doc.last_name}`;
},

// After save hook
after_save(frm) {
    frappe.show_alert({
        message: __("Document saved successfully"),
        indicator: "green"
    });
}

});

// Child table events frappe.ui.form.on("My DocType Item", { qty(frm, cdt, cdn) { let row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate); calculate_total(frm); },

items_remove(frm) {
    calculate_total(frm);
}

});

function calculate_total(frm) { let total = 0; (frm.doc.items || []).forEach(row => { total += row.amount || 0; }); frm.set_value("grand_total", total); }

  1. Build dialogs and prompts

// Simple prompt frappe.prompt( { fieldname: "reason", fieldtype: "Small Text", label: "Reason", reqd: 1 }, function(values) { frappe.call({ method: "my_app.api.reject", args: { name: frm.doc.name, reason: values.reason } }); }, __("Rejection Reason"), __("Reject") );

// Multi-field dialog let d = new frappe.ui.Dialog({ title: __("Configure Settings"), fields: [ { fieldname: "email", fieldtype: "Data", options: "Email", label: "Email", reqd: 1 }, { fieldname: "frequency", fieldtype: "Select", options: "Daily\nWeekly\nMonthly", label: "Frequency" }, { fieldname: "active", fieldtype: "Check", label: "Active", default: 1 } ], primary_action_label: __("Save"), primary_action(values) { frappe.call({ method: "my_app.api.save_settings", args: values, callback() { d.hide(); frappe.show_alert({ message: __("Settings saved"), indicator: "green" }); } }); } }); d.show();

// Confirmation dialog frappe.confirm( __("Are you sure you want to delete this?"), function() { /* Yes / }, function() { / No */ } );

  1. Make server calls

// Standard call (callback) frappe.call({ method: "my_app.api.get_stats", args: { customer: frm.doc.customer }, freeze: true, freeze_message: __("Loading..."), callback(r) { if (r.message) { frm.set_value("total_orders", r.message.total); } } });

// Promise-based call let result = await frappe.xcall("my_app.api.get_stats", { customer: frm.doc.customer });

  1. Customize list views

// my_app/public/js/sample_doc_list.js // or via hooks: doctype_list_js = {"Sample Doc": "public/js/sample_doc_list.js"}

frappe.listview_settings["Sample Doc"] = { // Status indicator colors get_indicator(doc) { if (doc.status === "Open") return [("Open"), "orange", "status,=,Open"]; if (doc.status === "Closed") return [("Closed"), "green", "status,=,Closed"]; return [__("Draft"), "grey", "status,=,Draft"]; },

// Add bulk actions
onload(listview) {
    listview.page.add_action_item(__("Mark as Closed"), function() {
        let names = listview.get_checked_items(true);
        frappe.call({
            method: "my_app.api.bulk_close",
            args: { names },
            callback() { listview.refresh(); }
        });
    });
},

// Hide default "New" button
hide_name_column: true

};

  1. Use realtime events

// Listen for server-side events frappe.realtime.on("export_complete", function(data) { frappe.show_alert({ message: __("Export complete: {0} records", [data.count]), indicator: "green" }); });

  1. Inject scripts via hooks

To extend a DocType from another app without modifying it:

hooks.py

doctype_js = { "Sales Order": "public/js/sales_order_custom.js" }

doctype_list_js = { "Sales Order": "public/js/sales_order_list_custom.js" }

Rebuild assets after adding hook scripts

bench build --app my_app

  1. Navigation and routing

// Navigate to a document frappe.set_route("Form", "Sales Order", "SO-001");

// Navigate to list with filters frappe.route_options = { "status": "Open" }; frappe.set_route("List", "Sales Order");

// Get current route let route = frappe.get_route();

Verification

  • Form script loads without JS console errors

  • Custom buttons appear in correct conditions

  • Field visibility toggles work

  • Link field filters return correct options

  • Validation prevents invalid saves

  • List view indicators display correctly

  • Dialogs open, collect input, and submit

Failure modes / debugging

  • Script not loading: Check file path matches DocType; run bench build

  • Button not appearing: Check condition logic in refresh ; verify frm.doc.docstatus

  • Event not firing: Verify event name matches exactly (case-sensitive)

  • Hook script ignored: Check hooks.py path; rebuild assets

  • frappe.call failing: Check method path; verify @frappe.whitelist() on server

Escalation

  • For server-side controller logic → frappe-doctype-development

  • For RPC endpoint implementation → frappe-api-development

  • For Frappe UI (Vue 3) frontends → frappe-frontend-development

References

  • references/desk.md — Desk UI views and scripting

  • references/js-api.md — JavaScript client API reference

Guardrails

  • Use frm.doc not doc directly: Always access document via frm.doc for consistency and reactivity

  • Validate before save: Use frm.validate() in validate event, not before_save

  • Async awareness: frappe.call() is async; use callbacks or async/await for sequential operations

  • Refresh after field changes: Call frm.refresh_field() or frm.refresh_fields() after programmatic changes

  • Check frm.is_new() appropriately: Some operations only make sense on saved documents

Common Mistakes

Mistake Why It Fails Fix

Missing frm.refresh_field() after set_value

UI doesn't update Call frm.refresh_field('fieldname') after frm.set_value()

Wrong event hook name Event never fires Use exact names: refresh , validate , onload , before_save

Blocking UI with sync calls Page freezes Use frappe.call() with async: true (default)

Using cur_frm instead of frm

Breaks in dialogs/multiple forms Always use the frm parameter passed to handlers

Not checking frm.doc.docstatus

Buttons appear on submitted docs Check frm.doc.docstatus == 0 before showing edit actions

console.log(frm.doc) showing stale data Debugging confusion Use frm.reload_doc() or check network responses

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

frappe-printing-templates

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

frappe-router

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

frappe-ui-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

frappe-testing

No summary provided by upstream source.

Repository SourceNeeds Review