Permission Error on Expense Report DocType

Issue Summary

Getting “Not permitted” error when trying to access custom “Expense Report” DocType, despite having Administrator/System Manager roles assigned.


Environment Details

  • Frappe Version: [frappe: 15.91.2]

  • ERPNext Version: [v15.91.2]

  • Server: [Local]


Problem Description

I created a DocType called “Expense Report” with a client script for generating vehicle expense reports. When trying to access the form, I get a permission error:

Error Message:

Not permitted
You do not have enough permissions to access this resource. 
Please contact your manager to get access.


Steps to Reproduce

  1. Navigate to: /app/expense-report/new-expense-report-[docname]

  2. Form loads but immediately shows permission error dialog

  3. Background shows “Loading…” spinner

  4. User has Administrator role assigned


Current Permission Settings

Checked Role Permissions Manager for “Expense Report”:

  • :white_check_mark: Administrator role has: Select, Read, Write, Create, Delete, Print, Email, Report, Export, Share, Import

  • :white_check_mark: System Manager role has same permissions

  • Level: 0

  • “Only If Creator” is unchecked


Code Implementation

Client Script: expense_report.js

frappe.ui.form.on('Expense Report', {
    refresh: function(frm) {
        // Hide report initially
        frm.fields_dict.report_html.$wrapper.hide();
        
        // Clear default form buttons
        frm.disable_save();
        
        // Add custom buttons
        frm.page.set_primary_action(__('Search'), function() {
            search_expenses(frm);
        });
        
        frm.page.add_menu_item(__('Download'), function() {
            download_report(frm);
        });
        
        frm.page.add_menu_item(__('Preview'), function() {
            preview_report(frm);
        });
        
        // Set query for expense type
        frm.set_query('expense_type', function() {
            return {
                filters: {
                    'group': 'Expense'
                }
            };
        });
    },
    
    from_date: function(frm) {
        validate_dates(frm);
    },
    
    to_date: function(frm) {
        validate_dates(frm);
    }
});

function validate_dates(frm) {
    if (frm.doc.from_date && frm.doc.to_date) {
        if (frm.doc.from_date > frm.doc.to_date) {
            frappe.msgprint(__('From Date cannot be greater than To Date'));
            frm.set_value('to_date', '');
        }
    }
}

function search_expenses(frm) {
    // Validate required fields
    if (!frm.doc.from_date || !frm.doc.to_date || !frm.doc.vehicle_no || !frm.doc.expense_type) {
        frappe.msgprint({
            title: __('Required Fields Missing'),
            message: __('Please fill all required fields'),
            indicator: 'red'
        });
        return;
    }
    
    // Show loading
    frm.fields_dict.report_html.$wrapper.html('<div class="text-center" style="padding: 50px;"><i class="fa fa-spinner fa-spin fa-2x"></i><br><br>Loading...</div>');
    frm.fields_dict.report_html.$wrapper.show();
    
    // Fetch data
    frappe.call({
        method: 'frappe.client.get_list',
        args: {
            doctype: 'Vehicle Expense',
            fields: [
                'name',
                'voucher_date',
                'voucher_no',
                'vehicle_no',
                'driver',
                'km_start',
                'km_end',
                'km_total',
                'kpl'
            ],
            filters: [
                ['voucher_date', 'between', [frm.doc.from_date, frm.doc.to_date]],
                ['vehicle_no', '=', frm.doc.vehicle_no],
                ['docstatus', '<', 2]
            ],
            order_by: 'voucher_date desc',
            limit_page_length: 0
        },
        callback: function(r) {
            if (r.message && r.message.length > 0) {
                fetch_expense_items(frm, r.message);
            } else {
                frm.fields_dict.report_html.$wrapper.html('<div class="text-center text-muted" style="padding: 50px;">No records found</div>');
            }
        }
    });
}

function fetch_expense_items(frm, expenses) {
    let expense_names = expenses.map(e => e.name);
    
    frappe.call({
        method: 'frappe.client.get_list',
        args: {
            doctype: 'Vehicle Expense Item',
            fields: ['parent', 'vehicle_expense_type', 'qty', 'amount'],
            filters: [
                ['parent', 'in', expense_names],
                ['vehicle_expense_type', '=', frm.doc.expense_type]
            ],
            limit_page_length: 0
        },
        callback: function(r) {
            if (r.message && r.message.length > 0) {
                let merged_data = merge_data(expenses, r.message);
                display_report(frm, merged_data);
            } else {
                frm.fields_dict.report_html.$wrapper.html('<div class="text-center text-muted" style="padding: 50px;">No matching expense items found</div>');
            }
        }
    });
}

function merge_data(expenses, items) {
    let result = [];
    
    expenses.forEach(expense => {
        let matching_items = items.filter(item => item.parent === expense.name);
        
        matching_items.forEach(item => {
            result.push({
                voucher_date: expense.voucher_date,
                voucher_no: expense.voucher_no,
                vehicle_no: expense.vehicle_no,
                driver: expense.driver || '',
                km_start: expense.km_start || 0,
                km_end: expense.km_end || 0,
                km_total: expense.km_total || 0,
                kpl: expense.kpl || 0,
                qty: item.qty || 0,
                amount: item.amount || 0,
                name: expense.name
            });
        });
    });
    
    return result;
}

function display_report(frm, data) {
    let html = `
        <style>
            .expense-report-container {
                padding: 0;
                margin: 0;
            }
            .expense-table {
                width: 100%;
                border-collapse: collapse;
                font-size: 13px;
                background: white;
            }
            .expense-table thead tr {
                background-color: #f8f9fa;
            }
            .expense-table th {
                border: 1px solid #d1d8dd;
                padding: 8px 10px;
                text-align: left;
                font-weight: 600;
                color: #36414c;
            }
            .expense-table td {
                border: 1px solid #d1d8dd;
                padding: 8px 10px;
                color: #36414c;
            }
            .expense-table tbody tr:hover {
                background-color: #f8f9fa;
            }
            .text-right {
                text-align: right !important;
            }
            .text-center {
                text-align: center !important;
            }
            .voucher-link {
                color: #2490ef;
                text-decoration: none;
            }
            .voucher-link:hover {
                text-decoration: underline;
            }
            .report-footer {
                padding: 10px;
                color: #6c757d;
                font-size: 12px;
            }
        </style>
        
        <div class="expense-report-container">
            <table class="expense-table">
                <thead>
                    <tr>
                        <th class="text-center">No</th>
                        <th>Date</th>
                        <th>Voucher No</th>
                        <th>Vehicle No</th>
                        <th>Driver</th>
                        <th class="text-right">KM-Start</th>
                        <th class="text-right">KM-End</th>
                        <th class="text-right">KM-Total</th>
                        <th class="text-right">KPL</th>
                        <th class="text-right">Qty / Ltr</th>
                        <th class="text-right">Amount</th>
                    </tr>
                </thead>
                <tbody>
    `;
    
    data.forEach((row, index) => {
        html += `
            <tr>
                <td class="text-center">${index + 1}</td>
                <td>${frappe.datetime.str_to_user(row.voucher_date)}</td>
                <td>
                    <a href="/app/vehicle-expense/${row.name}" class="voucher-link" target="_blank">
                        ${row.voucher_no}
                    </a>
                </td>
                <td>${row.vehicle_no}</td>
                <td>${row.driver || '-'}</td>
                <td class="text-right">${row.km_start}</td>
                <td class="text-right">${row.km_end}</td>
                <td class="text-right">${row.km_total}</td>
                <td class="text-right">${format_float(row.kpl)}</td>
                <td class="text-right">${format_float(row.qty)}</td>
                <td class="text-right">${format_currency(row.amount)}</td>
            </tr>
        `;
    });
    
    html += `
                </tbody>
            </table>
            <div class="report-footer">
                Showing 1 to ${data.length} of ${data.length} entries
            </div>
        </div>
    `;
    
    frm.fields_dict.report_html.$wrapper.html(html);
    frm.fields_dict.report_html.$wrapper.show();
    
    // Store data for download/preview
    frm._report_data = data;
}

function format_float(value) {
    return parseFloat(value || 0).toFixed(2);
}

function format_currency(value) {
    return parseFloat(value || 0).toLocaleString('en-IN', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
    });
}

function download_report(frm) {
    if (!frm._report_data || frm._report_data.length === 0) {
        frappe.msgprint(__('Please search for data first'));
        return;
    }
    
    let csv = 'No,Date,Voucher No,Vehicle No,Driver,KM-Start,KM-End,KM-Total,KPL,Qty/Ltr,Amount\n';
    
    frm._report_data.forEach((row, index) => {
        csv += `${index + 1},`;
        csv += `${frappe.datetime.str_to_user(row.voucher_date)},`;
        csv += `${row.voucher_no},`;
        csv += `${row.vehicle_no},`;
        csv += `${row.driver || ''},`;
        csv += `${row.km_start},`;
        csv += `${row.km_end},`;
        csv += `${row.km_total},`;
        csv += `${row.kpl},`;
        csv += `${row.qty},`;
        csv += `${row.amount}\n`;
    });
    
    // Create and download file
    let blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    let link = document.createElement('a');
    let url = URL.createObjectURL(blob);
    
    link.setAttribute('href', url);
    link.setAttribute('download', `expense_report_${frappe.datetime.now_date()}.csv`);
    link.style.visibility = 'hidden';
    
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    
    frappe.show_alert({
        message: __('Report downloaded successfully'),
        indicator: 'green'
    }, 3);
}

function preview_report(frm) {
    if (!frm._report_data || frm._report_data.length === 0) {
        frappe.msgprint(__('Please search for data first'));
        return;
    }
    
    let print_html = generate_print_html(frm);
    
    let preview_window = window.open('', '_blank', 'width=1200,height=800');
    preview_window.document.write(print_html);
    preview_window.document.close();
}

function generate_print_html(frm) {
    let html = `
        <!DOCTYPE html>
        <html>
        <head>
            <title>Expense Report - Preview</title>
            <style>
                body {
                    font-family: Arial, sans-serif;
                    margin: 20px;
                    background: white;
                }
                .header {
                    text-align: center;
                    margin-bottom: 30px;
                    border-bottom: 2px solid #333;
                    padding-bottom: 15px;
                }
                .header h2 {
                    margin: 0;
                    color: #333;
                }
                .filters {
                    margin-bottom: 20px;
                    padding: 15px;
                    background: #f8f9fa;
                    border-radius: 5px;
                }
                .filters p {
                    margin: 5px 0;
                    font-size: 13px;
                }
                .filters strong {
                    display: inline-block;
                    width: 120px;
                }
                table {
                    width: 100%;
                    border-collapse: collapse;
                    font-size: 12px;
                }
                thead tr {
                    background-color: #f8f9fa;
                }
                th, td {
                    border: 1px solid #d1d8dd;
                    padding: 8px;
                    text-align: left;
                }
                th {
                    font-weight: 600;
                    color: #36414c;
                }
                .text-right {
                    text-align: right;
                }
                .text-center {
                    text-align: center;
                }
                .footer {
                    margin-top: 20px;
                    text-align: center;
                    font-size: 11px;
                    color: #666;
                }
                .no-print {
                    margin-bottom: 20px;
                }
                .no-print button {
                    padding: 10px 20px;
                    font-size: 14px;
                    margin-right: 10px;
                    cursor: pointer;
                    border: 1px solid #ccc;
                    background: white;
                    border-radius: 4px;
                }
                .no-print button:hover {
                    background: #f8f9fa;
                }
                @media print {
                    .no-print { display: none; }
                    body { margin: 0; }
                }
            </style>
        </head>
        <body>
            <div class="no-print">
                <button onclick="window.print()">🖨️ Print</button>
                <button onclick="window.close()">✖️ Close</button>
            </div>
            
            <div class="header">
                <h2>EXPENSE REPORT</h2>
            </div>
            
            <div class="filters">
                <p><strong>Period:</strong> ${frappe.datetime.str_to_user(frm.doc.from_date)} to ${frappe.datetime.str_to_user(frm.doc.to_date)}</p>
                <p><strong>Vehicle:</strong> ${frm.doc.vehicle_no}</p>
                <p><strong>Expense Type:</strong> ${frm.doc.expense_type}</p>
                <p><strong>Generated On:</strong> ${frappe.datetime.now_datetime()}</p>
            </div>
            
            <table>
                <thead>
                    <tr>
                        <th class="text-center">No</th>
                        <th>Date</th>
                        <th>Voucher No</th>
                        <th>Vehicle No</th>
                        <th>Driver</th>
                        <th class="text-right">KM-Start</th>
                        <th class="text-right">KM-End</th>
                        <th class="text-right">KM-Total</th>
                        <th class="text-right">KPL</th>
                        <th class="text-right">Qty / Ltr</th>
                        <th class="text-right">Amount</th>
                    </tr>
                </thead>
                <tbody>
    `;
    
    frm._report_data.forEach((row, index) => {
        html += `
            <tr>
                <td class="text-center">${index + 1}</td>
                <td>${frappe.datetime.str_to_user(row.voucher_date)}</td>
                <td>${row.voucher_no}</td>
                <td>${row.vehicle_no}</td>
                <td>${row.driver || '-'}</td>
                <td class="text-right">${row.km_start}</td>
                <td class="text-right">${row.km_end}</td>
                <td class="text-right">${row.km_total}</td>
                <td class="text-right">${format_float(row.kpl)}</td>
                <td class="text-right">${format_float(row.qty)}</td>
                <td class="text-right">${format_currency(row.amount)}</td>
            </tr>
        `;
    });
    
    html += `
                </tbody>
            </table>
            
            <div class="footer">
                <p>Report generated from ERPNext - ${frappe.datetime.now_datetime()}</p>
                <p>Total Records: ${frm._report_data.length}</p>
            </div>
        </body>
        </html>
    `;
    
    return html;
}




Key Functions:

  • search_expenses() - Fetches Vehicle Expense records

  • fetch_expense_items() - Fetches related expense items

  • display_report() - Shows HTML table with results

  • download_report() - Exports to CSV

  • preview_report() - Opens print preview


What I’ve Tried

  1. :white_check_mark: Verified user has Administrator and System Manager roles

  2. :white_check_mark: Checked Role Permissions Manager - all permissions granted

  3. :white_check_mark: Cleared browser cache and reloaded

  4. :white_check_mark: Tried with different user accounts

  5. :white_check_mark: Ran bench clear-cache and bench migrate

  6. :cross_mark: Issue persists


Questions

  1. Is there a permission check in the client script that might be blocking access?

  2. Do I need special permissions for the report_html field?

  3. Could frm.disable_save() be causing permission issues?

  4. Are there any server-side permission checks I need to configure?


Expected Behavior

Form should load normally, allowing users with appropriate roles to:

  • Select date range, vehicle, and expense type

  • Click “Search” to generate report

  • View results in HTML table

  • Download CSV or preview print version


Actual Behavior

Permission error dialog appears immediately on form load, blocking all access to the form.


Additional Context

  • This is a report-type DocType (not for data entry)

  • Uses HTML field report_html to display results

  • Fetches data from existing “Vehicle Expense” DocType

  • No custom Python controller - only client-side JavaScript


Attachments



Any guidance would be greatly appreciated! Thank you! :folded_hands:

  • @Harsh_Uvtech

    Check the System Manager Role has the permission to access this report via Role Permission for Page and Report. If not then enable it.

  • Also check This role has access to every doctype that you are using for this report.

  • Additional Check:
    Ensure all your AJAX calls are whitelisted