Skip to content

Latest commit

 

History

History
489 lines (432 loc) · 15.8 KB

File metadata and controls

489 lines (432 loc) · 15.8 KB

Client Script Examples

Example 1: Complete Form Setup with Filters and Defaults

frappe.ui.form.on('Sales Order', {
    setup(frm) {
        // ALWAYS place set_query in setup — runs once
        frm.set_query('customer', () => ({
            filters: { disabled: 0 }
        }));

        frm.set_query('item_code', 'items', () => ({
            filters: { is_sales_item: 1, disabled: 0 }
        }));

        // Server-side query for complex filtering
        frm.set_query('delivery_warehouse', 'items', () => ({
            query: 'myapp.queries.get_warehouses_for_company',
            filters: { company: frm.doc.company }
        }));
    },

    onload(frm) {
        if (frm.is_new()) {
            frm.set_value('delivery_date',
                frappe.datetime.add_days(frappe.datetime.now_date(), 7));
        }
    }
});

Example 2: Conditional Field Visibility and Mandatory

frappe.ui.form.on('Sales Invoice', {
    refresh(frm) {
        let has_shipping = frm.doc.shipping_amount > 0;
        frm.toggle_display(['shipping_address', 'shipping_method'], has_shipping);
        frm.toggle_reqd('shipping_address', has_shipping);

        // Status-based read-only
        let is_submitted = frm.doc.docstatus === 1;
        frm.toggle_enable('customer', !is_submitted);
        frm.toggle_enable('posting_date', !is_submitted);
    },

    shipping_amount(frm) {
        // Re-trigger visibility when shipping amount changes
        frm.trigger('refresh');
    }
});

Example 3: Custom Buttons with State Guards

frappe.ui.form.on('Sales Order', {
    refresh(frm) {
        // ALWAYS guard buttons with document state checks
        if (frm.doc.docstatus === 1) {
            // Grouped buttons under "Create" dropdown
            frm.add_custom_button(__('Sales Invoice'), () => {
                frappe.model.open_mapped_doc({
                    method: 'erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice',
                    frm: frm
                });
            }, __('Create'));

            frm.add_custom_button(__('Delivery Note'), () => {
                frappe.model.open_mapped_doc({
                    method: 'erpnext.selling.doctype.sales_order.sales_order.make_delivery_note',
                    frm: frm
                });
            }, __('Create'));
        }

        // Button for draft documents only
        if (!frm.is_new() && frm.doc.docstatus === 0) {
            frm.add_custom_button(__('Validate Stock'), async () => {
                let r = await frappe.call({
                    method: 'myapp.api.check_stock',
                    args: { sales_order: frm.doc.name },
                    freeze: true,
                    freeze_message: __('Checking stock...')
                });
                if (r.message.all_available) {
                    frappe.show_alert({
                        message: __('All items available'),
                        indicator: 'green'
                    });
                } else {
                    frappe.msgprint({
                        title: __('Stock Warning'),
                        message: __('Unavailable: {0}',
                            [r.message.unavailable.join(', ')]),
                        indicator: 'orange'
                    });
                }
            });
        }
    }
});

Example 4: Form Validation with frappe.throw

frappe.ui.form.on('Sales Invoice', {
    validate(frm) {
        // Validate total
        if (frm.doc.grand_total <= 0) {
            frappe.throw(__('Grand total must be greater than zero'));
        }

        // Validate items exist
        if (!frm.doc.items || frm.doc.items.length === 0) {
            frappe.throw(__('At least one item is required'));
        }

        // Validate date logic
        if (frm.doc.due_date && frm.doc.due_date < frm.doc.posting_date) {
            frappe.throw(__('Due date cannot be before posting date'));
        }

        // Validate each row
        frm.doc.items.forEach((item, idx) => {
            if (item.qty <= 0) {
                frappe.throw(__('Row {0}: Quantity must be positive', [idx + 1]));
            }
            if (!item.rate) {
                frappe.throw(__('Row {0}: Rate is required', [idx + 1]));
            }
        });
    }
});

Example 5: Async Validation with Server Check

frappe.ui.form.on('Sales Order', {
    // CRITICAL: async validate with await — NEVER use callback pattern here
    async validate(frm) {
        if (frm.doc.customer) {
            let r = await frappe.call({
                method: 'myapp.api.check_credit_limit',
                args: {
                    customer: frm.doc.customer,
                    amount: frm.doc.grand_total
                }
            });
            if (r.message && !r.message.approved) {
                frappe.throw(__('Credit limit exceeded. Limit: {0}, Outstanding: {1}',
                    [r.message.limit, r.message.outstanding]));
            }
        }
    }
});

Example 6: Fetching Data on Field Change

frappe.ui.form.on('Sales Order', {
    customer(frm) {
        if (!frm.doc.customer) return;

        frappe.db.get_value('Customer', frm.doc.customer,
            ['customer_group', 'territory', 'default_currency', 'credit_limit'])
            .then(r => {
                if (r.message) {
                    frm.set_value({
                        customer_group: r.message.customer_group,
                        territory: r.message.territory,
                        currency: r.message.default_currency
                    });

                    if (r.message.credit_limit) {
                        frappe.show_alert({
                            message: __('Credit limit: {0}',
                                [format_currency(r.message.credit_limit)]),
                            indicator: 'blue'
                        });
                    }
                }
            });
    }
});

Example 7: Child Table Auto-Calculation

frappe.ui.form.on('Sales Invoice Item', {
    qty(frm, cdt, cdn) {
        calculate_row_amount(frm, cdt, cdn);
    },
    rate(frm, cdt, cdn) {
        calculate_row_amount(frm, cdt, cdn);
    },
    discount_percentage(frm, cdt, cdn) {
        calculate_row_amount(frm, cdt, cdn);
    },
    items_remove(frm) {
        calculate_totals(frm);
    }
});

function calculate_row_amount(frm, cdt, cdn) {
    let row = frappe.get_doc(cdt, cdn);
    let amount = row.qty * row.rate;

    if (row.discount_percentage) {
        amount = amount * (1 - row.discount_percentage / 100);
    }

    frappe.model.set_value(cdt, cdn, 'amount', amount);
    calculate_totals(frm);
}

function calculate_totals(frm) {
    let total = 0;
    (frm.doc.items || []).forEach(item => {
        total += item.amount || 0;
    });
    frm.set_value('net_total', total);
    frm.set_value('grand_total', total * 1.21);  // Example: 21% VAT
}

Example 8: Populating Child Table from Server

frappe.ui.form.on('Project', {
    refresh(frm) {
        if (!frm.is_new()) {
            frm.add_custom_button(__('Add Standard Tasks'), async () => {
                let r = await frappe.call({
                    method: 'myapp.api.get_standard_tasks',
                    args: { project_type: frm.doc.project_type }
                });

                if (r.message && r.message.length) {
                    frm.clear_table('tasks');

                    r.message.forEach(task => {
                        frm.add_child('tasks', {
                            title: task.title,
                            description: task.description,
                            expected_time: task.expected_time,
                            status: 'Open'
                        });
                    });

                    frm.refresh_field('tasks');  // ALWAYS after child table changes
                    frm.dirty();                 // Mark form as modified

                    frappe.show_alert({
                        message: __('Added {0} tasks', [r.message.length]),
                        indicator: 'green'
                    });
                }
            });
        }
    }
});

Example 9: Parallel API Calls with Promise.all

frappe.ui.form.on('Customer', {
    async refresh(frm) {
        if (frm.is_new()) return;

        try {
            let [order_count, invoice_count, balance] = await Promise.all([
                frappe.db.count('Sales Order', { customer: frm.doc.name }),
                frappe.db.count('Sales Invoice', { customer: frm.doc.name }),
                frappe.call({
                    method: 'erpnext.accounts.utils.get_balance_on',
                    args: { party_type: 'Customer', party: frm.doc.name }
                })
            ]);

            frm.dashboard.add_indicator(__('Orders: {0}', [order_count]), 'blue');
            frm.dashboard.add_indicator(__('Invoices: {0}', [invoice_count]), 'green');
            frm.dashboard.add_indicator(
                __('Balance: {0}', [format_currency(balance.message)]),
                balance.message > 0 ? 'orange' : 'green'
            );
        } catch (e) {
            console.error('Dashboard load error:', e);
        }
    }
});

Example 10: Confirmation Dialog Before Action

frappe.ui.form.on('Sales Invoice', {
    refresh(frm) {
        if (frm.doc.docstatus === 1 && frm.doc.outstanding_amount > 0) {
            frm.add_custom_button(__('Write Off'), () => {
                frappe.confirm(
                    __('Write off {0}?',
                        [format_currency(frm.doc.outstanding_amount, frm.doc.currency)]),
                    async () => {
                        let r = await frappe.call({
                            method: 'myapp.api.write_off_invoice',
                            args: { invoice: frm.doc.name },
                            freeze: true
                        });
                        if (r.message) {
                            frappe.show_alert({
                                message: __('Invoice written off'),
                                indicator: 'green'
                            });
                            frm.reload_doc();
                        }
                    }
                );
            });
        }
    }
});

Example 11: Dynamic Field Properties Based on Type

frappe.ui.form.on('Purchase Order', {
    refresh(frm) {
        let is_draft = frm.doc.docstatus === 0;
        frm.toggle_enable('supplier', is_draft);
    },

    order_type(frm) {
        let is_shopping_cart = frm.doc.order_type === 'Shopping Cart';
        frm.set_df_property('shipping_rule', 'reqd', is_shopping_cart ? 1 : 0);
        frm.set_df_property('shipping_rule', 'hidden', is_shopping_cart ? 0 : 1);
    },

    supplier(frm) {
        if (!frm.doc.supplier) return;

        frappe.db.get_value('Supplier', frm.doc.supplier, 'payment_terms')
            .then(r => {
                if (r.message.payment_terms) {
                    frm.set_value('payment_terms_template', r.message.payment_terms);
                }
            });
    }
});

Example 12: Custom Dialog with Table Field

frappe.ui.form.on('Sales Order', {
    refresh(frm) {
        if (frm.doc.docstatus === 0 && !frm.is_new()) {
            frm.add_custom_button(__('Bulk Add Items'), () => {
                let d = new frappe.ui.Dialog({
                    title: __('Add Items'),
                    fields: [
                        {
                            fieldname: 'item_list',
                            fieldtype: 'Table',
                            label: __('Items'),
                            in_place_edit: true,
                            fields: [
                                { fieldname: 'item_code', fieldtype: 'Link',
                                  options: 'Item', label: __('Item'), in_list_view: 1, reqd: 1 },
                                { fieldname: 'qty', fieldtype: 'Float',
                                  label: __('Qty'), in_list_view: 1, default: 1 }
                            ]
                        }
                    ],
                    size: 'large',
                    primary_action_label: __('Add'),
                    primary_action(values) {
                        (values.item_list || []).forEach(row => {
                            if (row.item_code) {
                                frm.add_child('items', {
                                    item_code: row.item_code,
                                    qty: row.qty || 1
                                });
                            }
                        });
                        frm.refresh_field('items');
                        frm.dirty();
                        d.hide();
                    }
                });
                d.show();
            });
        }
    }
});

Example 13: List View Customization

// In {doctype}_list.js or via Client Script
frappe.listview_settings['Task'] = {
    add_fields: ['status', 'priority', 'assigned_to'],
    filters: [['status', '!=', 'Cancelled']],
    hide_name_column: true,

    get_indicator(doc) {
        const indicators = {
            'Open': [__('Open'), 'orange', 'status,=,Open'],
            'Working': [__('Working'), 'blue', 'status,=,Working'],
            'Completed': [__('Completed'), 'green', 'status,=,Completed'],
            'Cancelled': [__('Cancelled'), 'red', 'status,=,Cancelled']
        };
        return indicators[doc.status] || [__(doc.status), 'grey'];
    },

    button: {
        show(doc) { return doc.status === 'Open'; },
        get_label() { return __('Start'); },
        get_description(doc) { return __('Mark {0} as Working', [doc.name]); },
        action(doc) {
            frappe.call({
                method: 'frappe.client.set_value',
                args: { doctype: 'Task', name: doc.name, fieldname: 'status', value: 'Working' }
            });
        }
    },

    formatters: {
        priority(val) {
            const colors = { 'High': 'red', 'Medium': 'orange', 'Low': 'green' };
            return `<span class="indicator-pill ${colors[val] || 'grey'}">${val || ''}</span>`;
        }
    },

    onload(listview) {
        // Runs once when list view loads
    }
};

Example 14: MultiSelectDialog for Linking Documents

frappe.ui.form.on('Purchase Order', {
    refresh(frm) {
        if (frm.doc.docstatus === 0 && !frm.is_new()) {
            frm.add_custom_button(__('Get Items from MR'), () => {
                new frappe.ui.form.MultiSelectDialog({
                    doctype: 'Material Request',
                    target: frm,
                    setters: {
                        schedule_date: null,
                        status: 'Pending'
                    },
                    date_field: 'transaction_date',
                    get_query() {
                        return { filters: { docstatus: 1, status: ['!=', 'Stopped'] } };
                    },
                    action(selections) {
                        if (selections.length) {
                            frappe.call({
                                method: 'myapp.api.get_items_from_mr',
                                args: { material_requests: selections },
                                callback(r) {
                                    if (r.message) {
                                        r.message.forEach(item => {
                                            frm.add_child('items', item);
                                        });
                                        frm.refresh_field('items');
                                        frm.dirty();
                                    }
                                }
                            });
                        }
                    }
                });
            }, __('Get Items'));
        }
    }
});