Clientskript (Client Script)
Einleitung
Diese Code-Erweiterungen können dazu genutzt werden über die Browser-Sitzung die Oberfläche von ERPNext zu manipulieren. Sie können in ganz unterschiedlicher Weise eingesetzt werden.
https://frappeframework.com/docs/v13/user/en/desk/scripting/client-script
Beispiele
Timesheet Button auf Projekt Listenansicht
Dies wurde in diesem Video verwendet: https://youtu.be/yDwlsbh2SnA?si=G3IfDCyYyR-2K_q3
// Doctype: Project
// View: List
const FILTER_BY_OWNER = true; // only consider your own timesheets
const FALLBACK_EMPLOYEE = ''; // set if Employee is mandatory in your setup
const PARENT_SCAN_LIMIT = 25; // how many recent Timesheets to scan
async function find_running_timer_for_project(project_name) {
// 1) Get recent parent Timesheets
const tsListRes = await frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Timesheet',
fields: ['name', 'owner', 'modified'],
filters: [
['Timesheet', 'docstatus', '<', 2],
...(FILTER_BY_OWNER ? [['Timesheet', 'owner', '=', frappe.session.user]] : [])
],
order_by: 'modified desc',
limit_page_length: PARENT_SCAN_LIMIT
}
});
const parents = tsListRes?.message || [];
if (!parents.length) return null;
// 2) Load each parent and inspect time_logs on the client
for (const p of parents) {
const tsRes = await frappe.call({
method: 'frappe.client.get',
args: { doctype: 'Timesheet', name: p.name }
});
const ts = tsRes?.message;
if (!ts || !Array.isArray(ts.time_logs)) continue;
const runningRow = ts.time_logs.find(
tl => tl.project === project_name && !tl.to_time // running == no to_time
);
if (runningRow) {
return { parent: ts.name, child: runningRow.name };
}
}
return null;
}
frappe.listview_settings['Project'] = {
button: {
show() { return true; },
get_label() { return __('Timer'); },
get_description(doc) {
return __('Start or manage a running Timesheet timer for Project {0}', [doc.name]);
},
action(doc) {
(async () => {
try {
frappe.dom.freeze(__('Checking timers...'));
const running = await find_running_timer_for_project(doc.name);
frappe.dom.unfreeze();
if (running) {
// Running timer exists -> Open/Stop dialog
const dlg = new frappe.ui.Dialog({
title: __('Running Timer'),
fields: [{ fieldtype: 'HTML', fieldname: 'info' }],
primary_action_label: __('Stop Timer'),
primary_action: async () => {
try {
dlg.disable_primary_action();
dlg.set_message(__('Stopping timer...'));
const tsGet = await frappe.call({
method: 'frappe.client.get',
args: { doctype: 'Timesheet', name: running.parent }
});
const tsDoc = tsGet.message;
const row = (tsDoc.time_logs || []).find(r => r.name === running.child && !r.to_time);
if (!row) throw new Error('Running row not found');
row.to_time = frappe.datetime.now_datetime();
await frappe.call({
method: 'frappe.client.save',
args: { doc: tsDoc }
});
dlg.hide();
frappe.show_alert(
{ message: __('Timer stopped. <a href="#Form/Timesheet/{0}">Open Timesheet</a>', [running.parent]), indicator: 'green' },
8
);
} catch (e) {
console.error(e);
frappe.msgprint({ title: __('Error'), message: __('Could not stop the timer.'), indicator: 'red' });
}
}
});
dlg.get_field('info').$wrapper.html(`
<div>
${__('A timer is already running for this project.')}
<div class="mt-2">
<a class="btn btn-sm btn-secondary" href="#Form/Timesheet/${frappe.utils.escape_html(running.parent)}">
${__('Open Timesheet')}
</a>
</div>
</div>
`);
dlg.show();
return;
}
// No running timer -> Start flow
const d = new frappe.ui.Dialog({
title: __('Start Timer'),
fields: [
{ fieldtype: 'Link', label: __('Project'), fieldname: 'project', options: 'Project', read_only: 1, reqd: 1 },
{ fieldtype: 'Link', label: __('Task'), fieldname: 'task', options: 'Task',
get_query: () => ({ filters: { project: doc.name } })
},
{ fieldtype: 'Link', label: __('Activity Type'), fieldname: 'activity_type', options: 'Activity Type', reqd: 1 },
{ fieldtype: 'Small Text', label: __('Notes'), fieldname: 'notes' }
],
primary_action_label: __('Start'),
primary_action: async () => {
const v = d.get_values();
if (!v) return;
try {
frappe.dom.freeze(__('Starting timer...'));
const ts_doc = {
doctype: 'Timesheet',
...(FALLBACK_EMPLOYEE ? { employee: FALLBACK_EMPLOYEE } : {}),
time_logs: [
{
project: v.project,
task: v.task || undefined,
activity_type: v.activity_type,
description: v.notes || undefined,
from_time: frappe.datetime.now_datetime() // running
}
]
};
const ins = await frappe.call({
method: 'frappe.client.insert',
args: { doc: ts_doc }
});
frappe.dom.unfreeze();
d.hide();
const ts_name = ins?.message?.name;
frappe.show_alert(
{
message: __(
'Timer started for {0}. <a href="#Form/Timesheet/{1}">Open Timesheet</a>',
[frappe.utils.escape_html(v.project), ts_name]
),
indicator: 'green'
},
8
);
} catch (e) {
console.error(e);
frappe.dom.unfreeze();
d.hide();
frappe.msgprint({
title: __('Error'),
message: __('Could not start timer. Check mandatory fields or permissions.'),
indicator: 'red'
});
}
}
});
d.set_value('project', doc.name);
d.show();
} catch (err) {
console.error(err);
frappe.dom.unfreeze();
frappe.msgprint({ title: __('Error'), message: __('Unexpected error occurred.'), indicator: 'red' });
}
})();
}
}
};