seitime-frappe/frappe/public/js/frappe/form/form.js

2350 lines
59 KiB
JavaScript

frappe.provide("frappe.ui.form");
frappe.provide("frappe.model.docinfo");
import "./quick_entry";
import "./toolbar";
import "./dashboard";
import "./workflow";
import "./save";
import "./print_utils";
import "./success_action";
import "./script_manager";
import "./script_helpers";
import "./sidebar/form_sidebar";
import "./footer/footer";
import "./form_tour";
import { UndoManager } from "./undo_manager";
frappe.ui.form.Controller = class FormController {
constructor(opts) {
$.extend(this, opts);
}
};
frappe.ui.form.Form = class FrappeForm {
constructor(doctype, parent, in_form, doctype_layout_name) {
this.docname = "";
this.doctype = doctype;
this.doctype_layout_name = doctype_layout_name;
this.in_form = in_form ? true : false;
this.hidden = false;
this.refresh_if_stale_for = 120;
this.opendocs = {};
this.custom_buttons = {};
this.sections = [];
this.grids = [];
this.cscript = new frappe.ui.form.Controller({ frm: this });
this.events = {};
this.fetch_dict = {};
this.parent = parent;
this.doctype_layout = frappe.get_meta(doctype_layout_name);
this.undo_manager = new UndoManager({ frm: this });
this.setup_meta(doctype);
this.debounced_reload_doc = frappe.utils.debounce(this.reload_doc.bind(this), 1000);
this.beforeUnloadListener = (event) => {
event.preventDefault();
// A String is returned for compatability with older Browsers. Return Value has to be truthy to trigger "Leave Site" Dialog
return (event.returnValue =
"There are unsaved changes, are you sure you want to exit?");
};
}
setup_meta() {
this.meta = frappe.get_meta(this.doctype);
if (this.meta.istable) {
this.meta.in_dialog = 1;
}
this.perm = frappe.perm.get_perm(this.doctype); // for create
this.action_perm_type_map = {
Create: "create",
Save: "write",
Submit: "submit",
Update: "submit",
Cancel: "cancel",
Amend: "amend",
Delete: "delete",
Mask: "mask",
};
}
setup() {
this.fields = [];
this.fields_dict = {};
this.state_fieldname = frappe.workflow.get_state_fieldname(this.doctype);
// wrapper
this.wrapper = this.parent;
this.$wrapper = $(this.wrapper);
let is_single_column = this.doctype === "DocType" ? true : this.meta.hide_toolbar;
frappe.ui.make_app_page({
parent: this.wrapper,
single_column: is_single_column,
sidebar_position: "Right",
});
this.page = this.wrapper.page;
this.layout_main = this.page.main.get(0);
this.$wrapper.on("hide", () => {
this.script_manager.trigger("on_hide");
});
this.toolbar = new frappe.ui.form.Toolbar({
frm: this,
page: this.page,
});
this.viewers = new frappe.ui.form.FormViewers({
frm: this,
parent: $('<div class="form-viewers d-flex"></div>').prependTo(this.page.page_actions),
});
// navigate records keyboard shortcuts
this.add_form_keyboard_shortcuts();
// 2 column layout
this.setup_std_layout();
// client script must be called after "setup" - there are no fields_dict attached to the frm otherwise
this.script_manager = new frappe.ui.form.ScriptManager({
frm: this,
});
this.script_manager.setup();
this.watch_model_updates();
if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) {
// this.footer_tab = new frappe.ui.form.Tab(this.layout, {
// label: __("Activity"),
// fieldname: 'timeline'
// });
this.footer = new frappe.ui.form.Footer({
frm: this,
parent: $("<div>").appendTo(this.page.main.parent()),
});
$("body").attr("data-sidebar", 1);
}
this.setup_file_drop();
this.setup_doctype_actions();
this.setup_notify_on_rename();
this.setup_done = true;
}
add_form_keyboard_shortcuts() {
// Navigate to next record
frappe.ui.keys.add_shortcut({
shortcut: "shift+ctrl+>",
action: () => this.navigate_records(0),
page: this.page,
description: __("Go to next record"),
ignore_inputs: true,
condition: () => !this.is_new(),
});
// Navigate to previous record
frappe.ui.keys.add_shortcut({
shortcut: "shift+ctrl+<",
action: () => this.navigate_records(1),
page: this.page,
description: __("Go to previous record"),
ignore_inputs: true,
condition: () => !this.is_new(),
});
// Alternate for redo, main shortcut are present in toolbar.js
frappe.ui.keys.add_shortcut({
shortcut: "shift+ctrl+z",
action: () => this.undo_manager.redo(),
page: this.page,
description: __("Redo last action"),
condition: () => !this.is_form_builder(),
});
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+p",
action: () => this.print_doc(),
page: this.page,
description: __("Print document"),
condition: () => frappe.model.can_print(this.doctype, this) && !this.meta.issingle,
});
let grid_shortcut_keys = [
{
shortcut: "Up Arrow",
description: __("Move cursor to above row"),
},
{
shortcut: "Down Arrow",
description: __("Move cursor to below row"),
},
{
shortcut: "tab",
description: __("Move cursor to next column"),
},
{
shortcut: "shift+tab",
description: __("Move cursor to previous column"),
},
{
shortcut: "Ctrl+up",
description: __("Add a row above the current row"),
},
{
shortcut: "Ctrl+down",
description: __("Add a row below the current row"),
},
{
shortcut: "Ctrl+shift+up",
description: __("Add a row at the top"),
},
{
shortcut: "Ctrl+shift+down",
description: __("Add a row at the bottom"),
},
{
shortcut: "shift+alt+down",
description: __("Duplicate current row"),
},
];
grid_shortcut_keys.forEach((row) => {
frappe.ui.keys.add_shortcut({
shortcut: row.shortcut,
page: this.page,
description: __(row.description),
ignore_inputs: true,
condition: () => !this.is_new(),
});
});
}
setup_std_layout() {
this.form_wrapper = $("<div></div>").appendTo(this.layout_main);
this.body = $('<div class="std-form-layout"></div>').appendTo(this.form_wrapper);
// only tray
this.meta.section_style = "Simple"; // always simple!
// layout
this.layout = new frappe.ui.form.Layout({
parent: this.body,
doctype: this.doctype,
doctype_layout: this.doctype_layout,
frm: this,
with_dashboard: true,
card_layout: true,
});
this.layout.make();
this.fields_dict = this.layout.fields_dict;
this.fields = this.layout.fields_list;
let dashboard_parent = $('<div class="form-dashboard">');
let dashboard_added = false;
if (this.layout.tabs.length) {
this.layout.tabs.every((tab) => {
if (tab.df.show_dashboard) {
tab.wrapper.prepend(dashboard_parent);
dashboard_added = true;
return false;
}
return true;
});
if (!dashboard_added) {
this.layout.tabs[0].wrapper.prepend(dashboard_parent);
}
} else {
this.layout.wrapper.find(".form-page").prepend(dashboard_parent);
}
this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this);
this.tour = new frappe.ui.form.FormTour({
frm: this,
});
// workflow state
this.states = new frappe.ui.form.States({
frm: this,
});
}
watch_model_updates() {
// watch model updates
var me = this;
// on main doc
frappe.model.on(
me.doctype,
"*",
function (fieldname, value, doc, skip_dirty_trigger = false) {
// set input
if (doc.name == me.docname) {
if (!skip_dirty_trigger) {
me.dirty();
}
let field = me.fields_dict[fieldname];
field && field.refresh(fieldname);
// Validate value for link field explicitly
field &&
["Link", "Dynamic Link"].includes(field.df.fieldtype) &&
field.validate &&
field.validate(value);
me.layout.refresh_dependency();
me.layout.refresh_sections();
return me.script_manager.trigger(fieldname, doc.doctype, doc.name);
}
}
);
// on table fields
var table_fields = frappe.get_children("DocType", me.doctype, "fields", {
fieldtype: ["in", frappe.model.table_fields],
});
// using $.each to preserve df via closure
$.each(table_fields, function (i, df) {
frappe.model.on(
df.options,
"*",
function (fieldname, value, doc, skip_dirty_trigger = false) {
if (doc.parent == me.docname && doc.parentfield === df.fieldname) {
if (!skip_dirty_trigger) {
me.dirty();
}
me.fields_dict[df.fieldname].grid.set_value(fieldname, value, doc);
return me.script_manager.trigger(fieldname, doc.doctype, doc.name);
}
}
);
});
}
setup_notify_on_rename() {
$(document).on("rename", (ev, dt, old_name, new_name) => {
if (dt == this.doctype) this.rename_notify(dt, old_name, new_name);
});
frappe.realtime.on("doc_rename", (data) => {
// the current form has been renamed by some backend process
if (data.doctype == this.doctype && data.old == this.docname) {
// the current form does not exist anymore, route to the new one
frappe.set_route("Form", this.doctype, data.new);
}
});
}
setup_file_drop() {
var me = this;
this.$wrapper.on("dragenter dragover", false).on("drop", function (e) {
var dataTransfer = e.originalEvent.dataTransfer;
if (!(dataTransfer && dataTransfer.files && dataTransfer.files.length > 0)) {
return;
}
e.stopPropagation();
e.preventDefault();
if (me.doc.__islocal) {
frappe.msgprint(__("Please save before attaching."));
throw "attach error";
}
new frappe.ui.FileUploader({
doctype: me.doctype,
docname: me.docname,
frm: me,
files: dataTransfer.files,
folder: "Home/Attachments",
on_success(file_doc) {
me.attachments.attachment_uploaded(file_doc);
},
});
});
}
setup_image_autocompletions_in_markdown() {
this.fields.map((field) => {
if (field.df.fieldtype === "Markdown Editor") {
this.set_df_property(field.df.fieldname, "autocompletions", () => {
let attachments = this.attachments.get_attachments();
return attachments
.filter((file) => frappe.utils.is_image_file(file.file_url))
.map((file) => {
return {
caption: "image: " + file.file_name,
value: `![](${file.file_url})`,
meta: "image",
};
});
});
}
});
}
// REFRESH
refresh(docname) {
var switched = docname ? true : false;
removeEventListener("beforeunload", this.beforeUnloadListener, { capture: true });
if (docname) {
this.switch_doc(docname);
}
cur_frm = this;
this.undo_manager.erase_history();
if (this.docname) {
// document to show
this.save_disabled = false;
// set the doc
this.doc = frappe.get_doc(this.doctype, this.docname);
// check permissions
this.fetch_permissions();
if (!this.has_read_permission()) {
frappe.show_not_permitted(__(this.doctype) + " " + __(cstr(this.docname)));
return;
}
// update grids with new permissions
this.grids.forEach((table) => {
table.grid.refresh();
});
// read only (workflow)
this.read_only = frappe.workflow.is_read_only(this.doctype, this.docname);
if (this.read_only) {
this.set_read_only();
}
// check if doctype is already open
if (!this.opendocs[this.docname]) {
this.check_doctype_conflict(this.docname);
} else {
if (this.check_reload()) {
return;
}
}
// do setup
if (!this.setup_done) {
this.setup();
}
// load the record for the first time, if not loaded (call 'onload')
this.trigger_onload(switched);
if (switched) {
if (this.show_print_first && this.doc.docstatus === 1) {
// show print view
this.print_doc();
}
}
// set status classes
this.$wrapper
.removeClass("validated-form")
.toggleClass("editable-form", this.doc.docstatus === 0)
.toggleClass("submitted-form", this.doc.docstatus === 1)
.toggleClass("cancelled-form", this.doc.docstatus === 2);
this.show_conflict_message();
this.show_submission_queue_banner();
this.mark_mask_fields_readonly();
if (frappe.boot.read_only) {
this.disable_form();
}
}
}
// sets up the refresh event for custom buttons
// added via configuration
setup_doctype_actions() {
if (this.meta.actions) {
for (let action of this.meta.actions) {
frappe.ui.form.on(this.doctype, "refresh", () => {
if (!this.is_new()) {
if (!action.hidden) {
this.add_custom_button(
action.label,
() => {
this.execute_action(action);
},
action.group
);
}
}
});
}
}
}
execute_action(action) {
if (typeof action === "string") {
// called by label - maybe via custom script
// frm.execute_action('Action')
for (let _action of this.meta.actions) {
if (_action.label === action) {
action = _action;
break;
}
}
if (typeof action === "string") {
frappe.throw(`Action ${action} not found`);
}
}
if (action.action_type === "Server Action") {
return frappe.xcall(action.action, { doc: this.doc }).then((doc) => {
if (doc.doctype) {
// document is returned by the method,
// apply the changes locally and refresh
frappe.model.sync(doc);
this.refresh();
}
// feedback
frappe.msgprint({
message: __("{} Complete", [__(action.label)]),
alert: true,
});
});
} else if (action.action_type === "Route") {
return frappe.set_route(action.action);
}
}
switch_doc(docname) {
// reset visible columns, since column headings can change in different docs
this.grids.forEach((grid_obj) => {
grid_obj.grid.visible_columns = null;
// reset page number to 1
grid_obj.grid.grid_pagination.go_to_page(1, true);
});
frappe.ui.form.close_grid_form();
this.viewers && this.viewers.parent.empty();
this.docname = docname;
this.setup_docinfo_change_listener();
}
check_reload() {
if (
this.doc &&
!this.doc.__unsaved &&
this.doc.__last_sync_on &&
new Date() - this.doc.__last_sync_on > this.refresh_if_stale_for * 1000
) {
this.debounced_reload_doc();
return true;
}
}
trigger_onload(switched) {
if (!this.opendocs[this.docname]) {
var me = this;
this.cscript.is_onload = true;
this.initialize_new_doc();
$(document).trigger("form-load", [this]);
$(this.page.wrapper).on("hide", function () {
$(document).trigger("form-unload", [me]);
});
} else {
this.render_form(switched);
if (this.doc.localname) {
// trigger form-rename and remove .localname
delete this.doc.localname;
$(document).trigger("form-rename", [this]);
}
}
}
initialize_new_doc() {
var me = this;
// hide any open grid
this.script_manager.trigger("before_load", this.doctype, this.docname).then(() => {
me.script_manager.trigger("onload");
me.opendocs[me.docname] = true;
me.render_form();
frappe.after_ajax(function () {
me.trigger_link_fields();
});
frappe.breadcrumbs.add(me.meta.module, me.doctype);
});
// update seen
if (this.meta.track_seen) {
$('.list-id[data-name="' + me.docname + '"]').addClass("seen");
}
}
render_form(switched) {
if (!this.meta.istable) {
this.layout.doc = this.doc;
this.layout.attach_doc_and_docfields();
if (frappe.boot.desk_settings.form_sidebar) {
this.sidebar = new frappe.ui.form.Sidebar({
frm: this,
page: this.page,
toolbar: this.toolbar,
});
this.sidebar.make();
} else {
this.page.sidebar.hide();
}
// clear layout message
this.layout.show_message();
frappe.run_serially([
// header must be refreshed before client methods
// because add_custom_button
() => this.refresh_header(switched),
// trigger global trigger
// to use this
() => $(document).trigger("form-refresh", [this]),
// fields
() => this.refresh_fields(),
// call trigger
() => this.script_manager.trigger("refresh"),
// call onload post render for callbacks to be fired
() => {
if (this.cscript.is_onload) {
this.onload_post_render();
return this.script_manager.trigger("onload_post_render");
}
},
() => this.cscript.is_onload && this.is_new() && this.focus_on_first_input(),
() => this.run_after_load_hook(),
() => this.dashboard.after_refresh(),
() => (this.cscript.is_onload = false),
() => this.configure_breadcrumb_width(),
]);
} else {
this.refresh_header(switched);
}
this.$wrapper.trigger("render_complete");
frappe.after_ajax(() => {
$(document).ready(() => {
this.scroll_to_element();
});
});
}
onload_post_render() {
this.setup_image_autocompletions_in_markdown();
}
configure_breadcrumb_width() {
let el = this.page.page_actions[0];
const rect = el.getBoundingClientRect();
let is_outside = cint(rect.right) > cint(document.documentElement.clientWidth);
if (is_outside) {
// check if the default actions are outside of the screen
const overflow = Math.max(0, rect.right - document.documentElement.clientWidth);
if (!overflow) return;
let max_breadcrumb_width = Math.max(
290,
this.page.$title_area.find("ul").width() - overflow
);
this.page.$title_area.parent().css("max-width", `${max_breadcrumb_width}px`);
let breadcrumb = this.page.$title_area.find("ul li.ellipsis");
if (cint(breadcrumb[0]?.clientWidth) <= 30) {
// if workspce sodebar is not visible
$(breadcrumb[0]).hide();
if (cint(breadcrumb[1]?.clientWidth) <= 30) {
// if doctype sodebar is not visible
$(breadcrumb[1]).hide();
// add elipsis to the name/title breadcrumb
this.page.$title_area.find(".title-text-form").parent().addClass("ellipsis");
}
}
}
}
focus_on_first_input() {
const layout_wrapper = this.layout.wrapper;
// dont do anything if the current active element is inside the form
// user must have clicked on some element before this function trigerred
if (!layout_wrapper || layout_wrapper.has(":focus").length) {
return;
}
layout_wrapper
.find(":input:visible:first")
.not("[data-fieldtype^='Date']")
.trigger("focus");
}
run_after_load_hook() {
if (frappe.route_hooks.after_load) {
let route_callback = frappe.route_hooks.after_load;
delete frappe.route_hooks.after_load;
route_callback(this);
}
}
refresh_fields() {
if (this.layout === undefined) {
return;
}
this.layout.refresh(this.doc);
this.layout.primary_button = this.$wrapper.find(".btn-primary");
// cleanup activities after refresh
this.cleanup_refresh(this);
}
cleanup_refresh() {
if (this.fields_dict["amended_from"]) {
if (this.doc.amended_from) {
unhide_field("amended_from");
if (this.fields_dict["amendment_date"]) unhide_field("amendment_date");
} else {
hide_field("amended_from");
if (this.fields_dict["amendment_date"]) hide_field("amendment_date");
}
}
if (this.fields_dict["trash_reason"]) {
if (this.doc.trash_reason && this.doc.docstatus == 2) {
unhide_field("trash_reason");
} else {
hide_field("trash_reason");
}
}
if (
this.meta.autoname &&
this.meta.autoname.substr(0, 6) == "field:" &&
!this.doc.__islocal
) {
var fn = this.meta.autoname.substr(6);
if (this.doc[fn]) {
this.toggle_display(fn, false);
}
}
if (this.meta.autoname == "naming_series:" && !this.doc.__islocal) {
this.toggle_display("naming_series", false);
}
}
refresh_header(switched) {
// set title
// main title
if (!this.meta.in_dialog || this.in_form) {
frappe.utils.set_title(this.meta.issingle ? this.doctype : this.docname);
}
// show / hide buttons
if (this.toolbar) {
if (switched) {
this.toolbar.current_status = undefined;
}
this.toolbar.refresh();
}
this.viewers.refresh();
this.dashboard.refresh();
frappe.breadcrumbs.update();
this.show_submit_message();
this.clear_custom_buttons();
this.show_web_link();
this.show_report_bug_link();
this.show_workflow_read_only_banner();
}
// SAVE
save_or_update() {
if (this.save_disabled) return;
if (this.doc.docstatus === 0) {
this.save();
} else if (this.doc.docstatus === 1 && this.doc.__unsaved) {
this.save("Update");
}
}
save(save_action, callback, btn, on_error) {
let me = this;
return new Promise((resolve, reject) => {
btn && $(btn).prop("disabled", true);
frappe.ui.form.close_grid_form();
me.validate_and_save(save_action, callback, btn, on_error, resolve, reject);
})
.then(() => {
me.show_success_action();
})
.catch((e) => {
console.error(e);
});
}
validate_and_save(save_action, callback, btn, on_error, resolve, reject) {
var me = this;
if (!save_action) save_action = "Save";
this.validate_form_action(save_action, resolve);
var after_save = function (r) {
// to remove hash from URL to avoid scroll after save
history.replaceState(null, null, " ");
if (!r.exc) {
if (["Save", "Update", "Amend"].indexOf(save_action) !== -1) {
frappe.utils.play_sound("click");
}
me.script_manager.trigger("after_save");
if (frappe.route_hooks.after_save) {
let route_callback = frappe.route_hooks.after_save;
delete frappe.route_hooks.after_save;
route_callback(me);
}
// submit comment if entered
if (me.comment_box) {
me.comment_box.submit();
}
me.refresh();
} else {
if (on_error) {
on_error();
reject();
}
}
callback && callback(r);
resolve();
};
var fail = (e) => {
if (e) {
console.error(e);
}
btn && $(btn).prop("disabled", false);
if (on_error) {
on_error();
reject();
}
};
if (save_action != "Update") {
// validate
frappe.validated = true;
frappe
.run_serially([
() => this.script_manager.trigger("validate"),
() => this.script_manager.trigger("before_save"),
() => {
if (!frappe.validated) {
fail();
return;
}
frappe.ui.form.save(me, save_action, after_save, btn);
},
])
.catch(fail);
} else {
frappe.ui.form.save(me, save_action, after_save, btn);
}
}
discard(btn, callback, on_error) {
const me = this;
return new Promise((resolve) => {
frappe.confirm(__("Discard {0}", [this.docname]), function () {
me.script_manager.trigger("before_discard").then(function () {
return me._discard(btn, callback, on_error, false); // ?
});
});
});
}
savesubmit(btn, callback, on_error) {
var me = this;
return new Promise((resolve) => {
this.validate_form_action("Submit");
frappe.confirm(
__("Permanently Submit {0}?", [this.docname]),
function () {
frappe.validated = true;
me.script_manager.trigger("before_submit").then(function () {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
}
me.save(
"Submit",
function (r) {
if (r.exc) {
me.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("submit");
callback && callback();
me.script_manager
.trigger("on_submit")
.then(() => resolve(me))
.then(() => {
if (frappe.route_hooks.after_submit) {
let route_callback =
frappe.route_hooks.after_submit;
delete frappe.route_hooks.after_submit;
route_callback(me);
}
});
}
},
btn,
() => me.handle_save_fail(btn, on_error),
resolve
);
});
},
() => me.handle_save_fail(btn, on_error)
);
});
}
savecancel(btn, callback, on_error) {
const me = this;
this.validate_form_action("Cancel");
me.ignore_doctypes_on_cancel_all = me.ignore_doctypes_on_cancel_all || [];
frappe
.call({
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
args: {
doctype: me.doc.doctype,
name: me.doc.name,
ignore_doctypes_on_cancel_all: me.ignore_doctypes_on_cancel_all,
},
freeze: true,
})
.then((r) => {
if (!r.exc) {
let doctypes_to_cancel = (r.message.docs || []).map((value) => {
return value.doctype;
});
if (doctypes_to_cancel.length) {
return me._cancel_all(r, btn, callback, on_error);
}
}
return me._cancel(btn, callback, on_error, false);
});
}
_cancel_all(r, btn, callback, on_error) {
const me = this;
// add confirmation message for cancelling all linked docs
let links_text = "";
let links = r.message.docs;
const doctypes = Array.from(new Set(links.map((link) => link.doctype)));
me.ignore_doctypes_on_cancel_all = me.ignore_doctypes_on_cancel_all || [];
for (let doctype of doctypes) {
if (!me.ignore_doctypes_on_cancel_all.includes(doctype)) {
let docnames = links
.filter((link) => link.doctype == doctype)
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
.join(", ");
links_text += `<li><strong>${__(doctype)}</strong>: ${docnames}</li>`;
}
}
links_text = `<ul>${links_text}</ul>`;
let confirm_message = __("{0} {1} is linked with the following submitted documents: {2}", [
__(me.doc.doctype).bold(),
me.doc.name,
links_text,
]);
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
if (can_cancel) {
confirm_message += __("Do you want to cancel all linked documents?");
} else {
confirm_message += __("You do not have permissions to cancel all linked documents.");
}
// generate dialog box to cancel all linked docs
let d = new frappe.ui.Dialog(
{
title: __("Cancel All Documents"),
fields: [
{
fieldtype: "HTML",
options: `<p class="frappe-confirm-message">${confirm_message}</p>`,
},
],
},
() => me.handle_save_fail(btn, on_error)
);
// if user can cancel all linked docs, add action to the dialog
if (can_cancel) {
d.set_primary_action(__("Cancel All"), () => {
d.hide();
frappe.call({
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
args: {
docs: links,
ignore_doctypes_on_cancel_all: me.ignore_doctypes_on_cancel_all || [],
},
freeze: true,
callback: (resp) => {
if (!resp.exc) {
me.reload_doc();
me._cancel(btn, callback, on_error, true);
}
},
});
});
}
d.show();
}
_cancel(btn, callback, on_error, skip_confirm) {
const me = this;
const cancel_doc = () => {
frappe.validated = true;
me.script_manager.trigger("before_cancel").then(() => {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
}
var after_cancel = function (r) {
if (r.exc) {
me.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("cancel");
me.refresh();
callback && callback();
me.script_manager.trigger("after_cancel");
}
};
frappe.ui.form.save(me, "cancel", after_cancel, btn);
});
};
if (skip_confirm) {
cancel_doc();
} else {
frappe.confirm(
__("Permanently Cancel {0}?", [this.docname]),
cancel_doc,
me.handle_save_fail(btn, on_error)
);
}
}
_discard(btn, on_error, skip_confirm) {
const me = this;
const discard_doc = () => {
frappe.validated = true;
me.script_manager.trigger("before_discard").then(() => {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
}
var after_discard = function (r) {
if (r.exc) {
me.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("cancel");
me.refresh();
me.script_manager.trigger("after_discard");
}
me.reload_doc();
};
//frappe.ui.form.discard(me, after_discard, btn);
frappe.call({
freeze: true,
method: "frappe.desk.form.save.discard",
args: {
doctype: me.doc.doctype,
name: me.doc.name,
},
btn: btn,
callback: function (r) {
after_discard(r);
},
});
});
};
if (skip_confirm) {
discard_doc();
} else {
frappe.confirm(
__("Permanently Discard {0}?", [this.docname]),
discard_doc,
me.handle_save_fail(btn, on_error)
);
}
}
savetrash() {
this.validate_form_action("Delete");
frappe.model.delete_doc(this.doctype, this.docname, function () {
window.history.back();
});
}
amend_doc() {
if (!this.fields_dict["amended_from"]) {
frappe.msgprint(__('"amended_from" field must be present to do an amendment.'));
return;
}
frappe
.xcall("frappe.client.is_document_amended", {
doctype: this.doc.doctype,
docname: this.doc.name,
})
.then((is_amended) => {
if (is_amended) {
frappe.throw(
__("This document is already amended, you cannot ammend it again")
);
}
this.validate_form_action("Amend");
var me = this;
var fn = function (newdoc) {
newdoc.amended_from = me.docname;
if (me.fields_dict && me.fields_dict["amendment_date"])
newdoc.amendment_date = frappe.datetime.obj_to_str(new Date());
};
this.copy_doc(fn, 1);
frappe.utils.play_sound("click");
});
}
validate_form_action(action, resolve) {
var perm_to_check = this.action_perm_type_map[action];
var allowed_for_workflow = false;
var perms = frappe.perm.get_perm(this.doc.doctype)[0];
// Allow submit, write, cancel and create permissions for read only documents that are assigned by
// workflows if the user already have those permissions. This is to allow for users to
// continue through the workflow states and to allow execution of functions like Duplicate.
if (
(frappe.workflow.is_read_only(this.doctype, this.docname) &&
(perms["write"] || perms["create"] || perms["submit"] || perms["cancel"])) ||
!frappe.workflow.is_read_only(this.doctype, this.docname)
) {
allowed_for_workflow = true;
}
if (!this.perm[0][perm_to_check] && !allowed_for_workflow) {
if (resolve) {
// re-enable buttons
resolve();
}
frappe.throw(
__(
"No permission to '{0}' {1}",
[__(action), __(this.doc.doctype)],
"{0} = verb, {1} = object"
)
);
}
}
// HELPERS
enable_save() {
this.save_disabled = false;
this.toolbar.set_primary_action();
}
disable_save(set_dirty = false) {
// IMPORTANT: this function should be called in refresh event
this.save_disabled = true;
this.toolbar.current_status = null;
// field changes should make form dirty
this.set_dirty = set_dirty;
this.page.clear_primary_action();
}
disable_form() {
this.set_read_only();
this.fields.forEach((field) => {
this.set_df_property(field.df.fieldname, "read_only", "1");
});
this.disable_save();
}
mark_mask_fields_readonly() {
const masked_fields = this.meta.masked_fields || [];
masked_fields.forEach((fieldname) => {
this.set_df_property(fieldname, "read_only", 1);
this.set_df_property(fieldname, "fieldtype", "Data");
});
}
handle_save_fail(btn, on_error) {
$(btn).prop("disabled", false);
if (on_error) {
on_error();
}
}
trigger_link_fields() {
// trigger link fields which have default values set
if (this.is_new() && this.doc.__run_link_triggers) {
$.each(this.fields_dict, function (fieldname, field) {
if (field.df.fieldtype == "Link" && this.doc[fieldname]) {
// triggers add fetch, sets value in model and runs triggers
field.set_value(this.doc[fieldname], true);
}
});
delete this.doc.__run_link_triggers;
}
}
show_conflict_message() {
if (this.doc.__needs_refresh) {
if (this.doc.__unsaved) {
this.dashboard.clear_headline();
this.dashboard.set_headline_alert(
__("This form has been modified after you have loaded it") +
'<button class="btn btn-xs btn-primary pull-right" onclick="cur_frm.reload_doc()">' +
__("Refresh") +
"</button>",
"alert-warning"
);
} else {
this.debounced_reload_doc();
}
}
}
show_submit_message() {
if (
this.meta.is_submittable &&
this.perm[0] &&
this.perm[0].submit &&
!this.is_dirty() &&
!this.is_new() &&
!frappe.model.has_workflow(this.doctype) && // show only if no workflow
this.doc.docstatus === 0
) {
this.dashboard.add_comment(__("Submit this document to confirm"), "blue", true);
}
}
show_web_link() {
if (!this.doc.__islocal && this.doc.__onload && this.doc.__onload.is_website_generator) {
this.web_link && this.web_link.remove();
if (this.doc.__onload.published) {
this.add_web_link("/" + this.doc.route);
}
}
}
show_report_bug_link() {
if (this.meta.beta) {
this.add_web_link(
"https://github.com/frappe/" +
frappe.boot.module_app[frappe.scrub(this.meta.module)] +
"/issues/new",
__("Report bug")
);
}
}
add_web_link(path, label) {
label = __(label) || __("See on Website");
this.web_link = this.sidebar
.add_user_action(__(label), function () {})
.attr("href", path || this.doc.route)
.attr("target", "_blank");
}
fetch_permissions() {
let dt = this.parent_doctype ? this.parent_doctype : this.doctype;
this.perm = frappe.perm.get_perm(dt, this.doc);
}
has_read_permission() {
if (!this.perm[0].read) {
return 0;
}
return 1;
}
check_doctype_conflict(docname) {
if (this.doctype == "DocType" && docname == "DocType") {
frappe.msgprint(__("Allowing DocType, DocType. Be careful!"));
} else if (this.doctype == "DocType") {
if (frappe.views.formview[docname] || frappe.pages["List/" + docname]) {
window.location.reload();
}
} else {
if (
frappe.views.formview.DocType &&
frappe.views.formview.DocType.frm.opendocs[this.doctype]
) {
window.location.reload();
}
}
}
// rename the form
// notify this form of renamed records
rename_notify(dt, old, name) {
// from form
if (this.meta.istable) return;
if (this.docname == old) this.docname = name;
else return;
// cleanup
if (this && this.opendocs[old] && frappe.meta.docfield_copy[dt]) {
// delete docfield copy
frappe.meta.docfield_copy[dt][name] = frappe.meta.docfield_copy[dt][old];
delete frappe.meta.docfield_copy[dt][old];
}
delete this.opendocs[old];
this.opendocs[name] = true;
if (this.meta.in_dialog || !this.in_form) {
return;
}
frappe.re_route[frappe.router.get_sub_path()] = `${encodeURIComponent(
frappe.router.slug(this.doctype)
)}/${encodeURIComponent(name)}`;
// Skip routing only when the document is created from a Form view's Link field
if (!frappe._from_link?.field_obj?.frm) {
frappe.set_route("Form", this.doctype, name);
}
}
// ACTIONS
print_doc() {
if (this.is_dirty()) {
frappe.toast({
message: __(
"This document has unsaved changes which might not appear in final PDF. <br> Consider saving the document before printing."
),
indicator: "yellow",
});
}
frappe.route_options = {
frm: this,
};
frappe.set_route("print", this.doctype, this.doc.name);
}
navigate_records(prev) {
let filters, sort_field, sort_order;
let list_view = frappe.get_list_view(this.doctype);
if (list_view) {
filters = list_view.get_filters_for_args();
sort_field = list_view.sort_by;
sort_order = list_view.sort_order;
} else {
let list_settings = frappe.get_user_settings(this.doctype)["List"];
if (list_settings) {
filters = list_settings.filters;
sort_field = list_settings.sort_by;
sort_order = list_settings.sort_order;
}
}
let args = {
doctype: this.doctype,
value: this.docname,
filters,
sort_order,
sort_field,
prev,
};
frappe
.call({ method: "frappe.desk.form.utils.get_next", args, freeze: true })
.then((r) => {
if (r.message) {
frappe.set_route("Form", this.doctype, r.message);
this.focus_on_first_input();
}
});
}
rename_doc() {
frappe.model.rename_doc(this.doctype, this.docname, () => this.refresh_header());
}
share_doc() {
this.shared.show();
}
email_doc(message) {
return new frappe.views.CommunicationComposer({
doc: this.doc,
frm: this,
subject: __(this.meta.name) + ": " + this.docname,
recipients: this.doc.email || this.doc.email_id || this.doc.contact_email,
attach_document_print: true,
message: message,
});
}
copy_doc(onload, from_amend) {
this.validate_form_action("Create");
var newdoc = frappe.model.copy_doc(this.doc, from_amend);
newdoc.idx = null;
newdoc.__run_link_triggers = false;
if (onload) {
onload(newdoc);
}
frappe.set_route("Form", newdoc.doctype, newdoc.name);
}
reload_doc() {
this.check_doctype_conflict(this.docname);
if (!this.doc.__islocal) {
frappe.model.remove_from_locals(this.doctype, this.docname);
return frappe.model.with_doc(this.doctype, this.docname, () => {
this.refresh();
});
}
}
refresh_field(fname) {
if (this.fields_dict[fname] && this.fields_dict[fname].refresh) {
this.fields_dict[fname].refresh();
this.layout.refresh_dependency();
this.layout.refresh_sections();
}
}
// UTILITIES
add_fetch(link_field, source_field, target_field, target_doctype) {
/*
Example fetch dict to get sender_email from email_id field in sender:
{
"Notification": {
"sender": {
"sender_email": "email_id"
}
}
}
*/
if (!target_doctype) target_doctype = "*";
// Target field kept as key because source field could be non-unique
this.fetch_dict.setDefault(target_doctype, {}).setDefault(link_field, {})[target_field] =
source_field;
}
has_perm(ptype) {
return frappe.perm.has_perm(this.doctype, 0, ptype, this.doc);
}
dirty() {
this.doc.__unsaved = 1;
this.$wrapper.trigger("dirty");
if (!frappe.boot.developer_mode) {
addEventListener("beforeunload", this.beforeUnloadListener, { capture: true });
}
}
get_docinfo() {
return frappe.model.docinfo[this.doctype][this.docname];
}
is_dirty() {
return !!this.doc.__unsaved;
}
is_new() {
return this.doc.__islocal;
}
is_form_builder() {
return (
["DocType", "Customize Form"].includes(this.doctype) &&
this.get_active_tab().label == "Form"
);
}
get_perm(permlevel, access_type) {
return this.perm[permlevel] ? this.perm[permlevel][access_type] : null;
}
set_intro(txt, color) {
this.dashboard.set_headline_alert(txt, color);
}
set_footnote(txt) {
this.footnote_area = frappe.utils.set_footnote(this.footnote_area, this.body, txt);
}
add_custom_button(label, fn, group) {
// temp! old parameter used to be icon
if (group && group.indexOf("fa fa-") !== -1) group = null;
let btn = this.page.add_inner_button(label, fn, group);
if (btn) {
this.custom_buttons[label] = btn;
}
return btn;
}
change_custom_button_type(label, group, type) {
this.page.change_inner_button_type(label, group, type);
}
clear_custom_buttons() {
this.page.clear_inner_toolbar();
this.page.clear_user_actions();
this.custom_buttons = {};
}
// Remove specific custom button by button Label
remove_custom_button(label, group) {
this.page.remove_inner_button(label, group);
// Remove actions from menu
delete this.custom_buttons[label];
let menu_item_label = group ? `${group} > ${label}` : label;
let $btn = this.page.is_in_group_button_dropdown(
this.page.menu,
"li > a.grey-link > span",
menu_item_label
);
if ($btn) {
let $linkBody = $btn.parent().parent();
if ($linkBody) {
// If last button, remove divider too
let $divider = $linkBody.next(".dropdown-divider");
if ($divider) $divider.remove();
$linkBody.remove();
}
}
}
scroll_to_element() {
if (frappe.route_options && frappe.route_options.scroll_to) {
var scroll_to = frappe.route_options.scroll_to;
delete frappe.route_options.scroll_to;
if (this.scroll_to_field(scroll_to)) {
const url = new URL(window.location);
url.searchParams.delete("scroll_to");
history.replaceState(null, null, url);
}
} else if (window.location.hash) {
if ($(window.location.hash).length) {
frappe.utils.scroll_to(window.location.hash, true, 200, null, null, true);
} else {
this.scroll_to_field(window.location.hash.replace("#", "")) &&
history.replaceState(null, null, " ");
}
}
}
show_success_action() {
const route = frappe.get_route();
if (route[0] !== "Form") return;
if (this.meta.is_submittable && this.doc.docstatus !== 1) return;
const success_action = new frappe.ui.form.SuccessAction(this);
success_action.show();
}
get_doc() {
return locals[this.doctype][this.docname];
}
set_currency_labels(fields_list, currency, parentfield) {
// To set the currency in the label
// For example Total Cost(INR), Total Cost(USD)
if (!currency) return;
var me = this;
var doctype = parentfield ? this.fields_dict[parentfield].grid.doctype : this.doc.doctype;
var field_label_map = {};
var grid_field_label_map = {};
$.each(fields_list, function (i, fname) {
var docfield = frappe.meta.docfield_map[doctype][fname];
if (docfield) {
var label = __(docfield.label || "", null, docfield.parent).replace(
/\([^\)]*\)/g,
""
); // eslint-disable-line
if (parentfield) {
grid_field_label_map[doctype + "-" + fname] =
label.trim() + " (" + __(currency) + ")";
} else {
field_label_map[fname] = label.trim() + " (" + currency + ")";
}
}
});
$.each(field_label_map, function (fname, label) {
me.fields_dict[fname].set_label(label);
});
$.each(grid_field_label_map, function (fname, label) {
fname = fname.split("-");
me.fields_dict[parentfield].grid.update_docfield_property(fname[1], "label", label);
});
}
field_map(fnames, fn) {
if (typeof fnames === "string") {
if (fnames == "*") {
fnames = Object.keys(this.fields_dict);
} else {
fnames = [fnames];
}
}
for (var i = 0, l = fnames.length; i < l; i++) {
var fieldname = fnames[i];
var field = frappe.meta.get_docfield(this.doctype, fieldname, this.docname);
if (field) {
fn(field);
this.refresh_field(fieldname);
}
}
}
get_docfield(fieldname1, fieldname2) {
if (fieldname2) {
// for child
var doctype = this.get_docfield(fieldname1).options;
return frappe.meta.get_docfield(doctype, fieldname2, this.docname);
} else {
// for parent
return frappe.meta.get_docfield(this.doctype, fieldname1, this.docname);
}
}
set_df_property(fieldname, property, value, docname, table_field, table_row_name = null) {
let df;
if (!docname || !table_field) {
df = this.get_docfield(fieldname);
} else {
const grid = this.fields_dict[fieldname].grid;
const filtered_fields = frappe.utils.filter_dict(grid.docfields, {
fieldname: table_field,
});
if (filtered_fields.length) {
df = frappe.meta.get_docfield(
filtered_fields[0].parent,
table_field,
table_row_name
);
}
}
if (df && df[property] != value) {
df[property] = value;
if (table_field && table_row_name) {
if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) {
this.fields_dict[fieldname].grid.grid_rows_by_docname[
table_row_name
].refresh_field(table_field);
}
} else {
this.refresh_field(fieldname);
}
}
}
toggle_enable(fnames, enable) {
this.field_map(fnames, function (field) {
field.read_only = enable ? 0 : 1;
});
}
toggle_reqd(fnames, mandatory) {
this.field_map(fnames, function (field) {
field.reqd = mandatory ? true : false;
});
}
toggle_display(fnames, show) {
this.field_map(fnames, function (field) {
field.hidden = show ? 0 : 1;
});
}
get_files() {
return this.attachments
? frappe.utils.sort(this.attachments.get_attachments(), "file_name", "string")
: [];
}
set_query(fieldname, opt1, opt2) {
if (opt2) {
// on child table
// set_query(fieldname, parent fieldname, query)
this.fields_dict[opt1].grid.get_field(fieldname).get_query = opt2;
} else {
// on parent table
// set_query(fieldname, query)
if (this.fields_dict[fieldname]) {
this.fields_dict[fieldname].get_query = opt1;
}
}
}
clear_table(fieldname) {
frappe.model.clear_table(this.doc, fieldname);
}
add_child(fieldname, values) {
var doc = frappe.model.add_child(
this.doc,
frappe.meta.get_docfield(this.doctype, fieldname).options,
fieldname
);
if (values) {
// Values of unique keys should not be overridden
var d = {};
var unique_keys = ["idx", "name"];
Object.keys(values).map((key) => {
if (!unique_keys.includes(key)) {
d[key] = values[key];
}
});
$.extend(doc, d);
}
return doc;
}
set_value(field, value, if_missing, skip_dirty_trigger = false) {
var me = this;
var _set = function (f, v) {
var fieldobj = me.fields_dict[f];
if (fieldobj) {
if (!if_missing || !frappe.model.has_value(me.doctype, me.doc.name, f)) {
if (
frappe.model.table_fields.includes(fieldobj.df.fieldtype) &&
$.isArray(v)
) {
// set entire child table from specified array as value
frappe.model.clear_table(me.doc, fieldobj.df.fieldname);
const standard_fields = [
...frappe.model.std_fields_list,
...frappe.model.child_table_field_list,
];
v.forEach((d, idx) => {
let child = frappe.model.add_child(
me.doc,
fieldobj.df.options,
fieldobj.df.fieldname,
idx + 1
);
// Don't set standard field, avoid mutating input too.
let doc_copy = { ...d };
standard_fields.forEach((field) => {
delete doc_copy[field];
});
$.extend(child, doc_copy);
});
me.refresh_field(f);
return Promise.resolve();
} else {
return frappe.model.set_value(
me.doctype,
me.doc.name,
f,
v,
me.fieldtype,
skip_dirty_trigger
);
}
}
} else {
frappe.msgprint(__("Field {0} not found.", [f]));
throw `frm.set_value: '${f}' does not exist in the form`;
}
};
if (typeof field == "string") {
return _set(field, value);
} else if ($.isPlainObject(field)) {
let tasks = [];
for (let f in field) {
let v = field[f];
if (me.get_field(f)) {
tasks.push(() => _set(f, v));
}
}
return frappe.run_serially(tasks);
}
}
call(opts, args, callback) {
var me = this;
if (typeof opts === "string") {
// called as frm.call('do_this', {with_arg: 'arg'});
opts = {
method: opts,
doc: this.doc,
args: args,
callback: callback,
};
}
if (!opts.doc) {
if (opts.method.indexOf(".") === -1)
opts.method = frappe.model.get_server_module_name(me.doctype) + "." + opts.method;
opts.original_callback = opts.callback;
opts.callback = function (r) {
if ($.isPlainObject(r.message)) {
if (opts.child) {
// update child doc
opts.child = locals[opts.child.doctype][opts.child.name];
// if child row is deleted, don't update
if (opts.child) {
var std_field_list = ["doctype"]
.concat(frappe.model.std_fields_list)
.concat(frappe.model.child_table_field_list);
for (var key in r.message) {
if (std_field_list.indexOf(key) === -1) {
opts.child[key] = r.message[key];
}
}
me.fields_dict[opts.child.parentfield].refresh();
}
} else {
// update parent doc
me.set_value(r.message);
}
}
opts.original_callback && opts.original_callback(r);
};
} else {
opts.original_callback = opts.callback;
opts.callback = function (r) {
if (!r.exc) me.refresh_fields();
opts.original_callback && opts.original_callback(r);
};
}
return frappe.call(opts);
}
get_field(field) {
return this.fields_dict[field];
}
set_read_only() {
const docperms = frappe.perm.get_perm(this.doc.doctype);
this.perm = docperms.map((p) => {
return {
read: p.read,
cancel: p.cancel,
share: p.share,
print: p.print,
email: p.email,
mask: p.mask,
};
});
this.refresh_fields();
}
trigger(event, doctype, docname) {
return this.script_manager.trigger(event, doctype, docname);
}
get_formatted(fieldname) {
return frappe.format(
this.doc[fieldname],
frappe.meta.get_docfield(this.doctype, fieldname, this.docname),
{ no_icon: true },
this.doc
);
}
open_grid_row() {
return frappe.ui.form.get_open_grid_form();
}
get_title() {
return frappe.model.get_doc_title(this.doc);
}
get_selected() {
// returns list of children that are selected. returns [parentfield, name] for each
var selected = {},
me = this;
frappe.meta.get_table_fields(this.doctype, true).forEach(function (df) {
// handle TableMultiselect child fields
let _selected = [];
if (me.fields_dict[df.fieldname].grid) {
_selected = me.fields_dict[df.fieldname].grid.get_selected();
}
if (_selected.length) {
selected[df.fieldname] = _selected;
}
});
return selected;
}
set_indicator_formatter(fieldname, get_color, get_text) {
// get doctype from parent
var doctype;
if (frappe.meta.docfield_map[this.doctype][fieldname]) {
doctype = this.doctype;
} else {
frappe.meta.get_table_fields(this.doctype, true).every(function (df) {
if (frappe.meta.docfield_map[df.options][fieldname]) {
doctype = df.options;
return false;
} else {
return true;
}
});
}
frappe.meta.docfield_map[doctype][fieldname].formatter = function (
value,
df,
options,
doc
) {
if (value) {
var label;
if (get_text) {
label = get_text(doc);
} else if (frappe.form.link_formatters[df.options]) {
label = frappe.form.link_formatters[df.options](value, doc, df);
} else {
label = value;
}
const escaped_name = encodeURIComponent(value);
return `
<a class="indicator ${get_color(doc || {})}"
href="/desk/${frappe.router.slug(df.options)}/${escaped_name}"
data-doctype="${df.options}"
data-name="${frappe.utils.escape_html(value)}">
${label}
</a>
`;
} else {
return "";
}
};
}
can_create(doctype) {
// return true or false if the user can make a particlar doctype
// will check permission, `can_make_methods` if exists, or will decided on
// basis of whether the document is submittable
if (!frappe.model.can_create(doctype)) {
return false;
}
if (this.custom_make_buttons && this.custom_make_buttons[doctype]) {
// custom buttons are translated and so are the keys
const key = __(this.custom_make_buttons[doctype]);
// if the button is present, then show make
return !!this.custom_buttons[key];
}
if (this.can_make_methods && this.can_make_methods[doctype]) {
return this.can_make_methods[doctype](this);
} else {
if (this.meta.is_submittable && this.doc.docstatus !== 1) {
return false;
} else {
return true;
}
}
}
make_new(doctype, fieldname) {
// make new doctype from the current form
// will handover to `make_methods` if defined
// or will create and match link fields
let me = this;
if (this.make_methods && this.make_methods[doctype]) {
return this.make_methods[doctype](this);
} else if (this.custom_make_buttons && this.custom_make_buttons[doctype]) {
this.custom_buttons[__(this.custom_make_buttons[doctype])].trigger("click");
} else {
frappe.model.with_doctype(doctype, function () {
let new_doc = frappe.model.get_new_doc(doctype, null, null, true);
// set link fields (if found)
me.set_link_field(doctype, new_doc, fieldname);
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
// frappe.set_route('Form', doctype, new_doc.name);
});
}
}
set_link_field(doctype, new_doc, fieldname) {
let me = this;
frappe.get_meta(doctype).fields.forEach(function (df) {
const isLinkToParent = df.fieldtype === "Link" && df.options === me.doctype;
if (fieldname) {
if (df.fieldname === fieldname && isLinkToParent) {
new_doc[df.fieldname] = me.doc.name;
}
if (df.fieldtype === "Table" && df.options && df.reqd) {
me.set_link_field(df.options, new_doc[df.fieldname][0]);
}
return;
}
if (isLinkToParent) {
new_doc[df.fieldname] = me.doc.name;
} else if (["Link", "Dynamic Link"].includes(df.fieldtype) && me.doc[df.fieldname]) {
new_doc[df.fieldname] = me.doc[df.fieldname];
} else if (df.fieldtype === "Table" && df.options && df.reqd) {
me.set_link_field(df.options, new_doc[df.fieldname][0]);
}
});
}
update_in_all_rows(table_fieldname, fieldname, value) {
// Update the `value` of the field named `fieldname` in all rows of the
// child table named `table_fieldname`.
// Do not overwrite existing values.
if (value === undefined) return;
frappe.model
.get_children(this.doc, table_fieldname)
.filter((child) => !frappe.model.has_value(child.doctype, child.name, fieldname))
.forEach((child) =>
frappe.model.set_value(child.doctype, child.name, fieldname, value)
);
}
get_sum(table_fieldname, fieldname) {
let sum = 0;
for (let d of this.doc[table_fieldname] || []) {
sum += d[fieldname];
}
return sum;
}
scroll_to_field(fieldname, focus = true) {
let field = this.get_field(fieldname);
if (!field) return false;
let $el = field.$wrapper;
if (!$el || !$el.length) return false;
// set tab as active
if (field.tab && !field.tab.is_active()) {
field.tab.set_active();
}
// uncollapse section
if (field.section?.is_collapsed()) {
field.section.collapse(false);
}
// scroll to input
frappe.utils.scroll_to($el, true, 15, $(".main-section"));
// focus if text field
if (focus) {
setTimeout(() => {
$el.find("input, select, textarea").focus();
}, 500);
}
// highlight control inside field
let control_element = $el.closest(".frappe-control");
if (control_element.length) {
control_element.addClass("highlight");
setTimeout(() => {
control_element.removeClass("highlight");
}, 2000);
}
return true;
}
setup_docinfo_change_listener() {
let doctype = this.doctype;
let docname = this.docname;
if (this.doc && !this.is_new()) {
frappe.realtime.doc_subscribe(doctype, docname);
}
frappe.realtime.off("docinfo_update");
frappe.realtime.on("docinfo_update", ({ doc, key, action = "update" }) => {
if (
!doc.reference_doctype ||
!doc.reference_name ||
doc.reference_doctype !== doctype ||
doc.reference_name !== docname
) {
return;
}
let doc_list = frappe.model.docinfo[doctype][docname][key] || [];
let docindex = doc_list.findIndex((old_doc) => {
return old_doc.name === doc.name;
});
if (action === "add") {
frappe.model.docinfo[doctype][docname][key].push(doc);
}
if (docindex > -1) {
if (action === "update") {
frappe.model.docinfo[doctype][docname][key].splice(docindex, 1, doc);
}
if (action === "delete") {
frappe.model.docinfo[doctype][docname][key].splice(docindex, 1);
}
}
this.timeline && this.timeline.refresh();
if (["add", "delete"].includes(action) && doc.doctype === "Comment") {
this.footer.refresh_comments_count();
}
});
}
// Filters fields from the reference doctype and sets them as options for a Select field
set_fields_as_options(
fieldname,
reference_doctype,
filter_function,
default_options = [],
table_fieldname
) {
if (!reference_doctype) return Promise.resolve();
let options = default_options || [];
if (!filter_function) filter_function = (f) => f;
return new Promise((resolve) => {
frappe.model.with_doctype(reference_doctype, () => {
frappe.get_meta(reference_doctype).fields.map((df) => {
filter_function(df) &&
options.push({ label: df.label || df.fieldname, value: df.fieldname });
});
options &&
this.set_df_property(
fieldname,
"options",
options,
this.doc.name,
table_fieldname
);
resolve(options);
});
});
}
set_active_tab(tab) {
const previous_tab_name = this.active_tab_map?.[this.docname]?.df?.fieldname || "";
const next_tab_name = tab?.df?.fieldname || "";
const has_changed = previous_tab_name !== next_tab_name;
// A change is always detected on first render, because next_tab_name is always set (= fieldname)
// but the previous_tab_name is always empty.
if (!has_changed) {
return; // No change in tab, don't trigger on_tab_change, don't update URL hash
}
this.active_tab_map ??= {};
this.active_tab_map[this.docname] = tab;
// Update URL hash to reflect the active tab
const new_hash = next_tab_name.replace("__details", "");
const url = new URL(window.location.href);
url.hash = new_hash;
if (url.href !== window.location.href) {
history.replaceState(null, null, url);
}
this.script_manager.trigger("on_tab_change");
// When switching tabs, we should tell fields to update their display if needed (e.g. Geolocation and Signature fields).
// This is done using the already existing on_section_collapse optional method.
let in_tab = false;
for (const df of this.layout.fields) {
const field = this.get_field(df.fieldname);
if (df?.fieldtype == "Tab Break") {
in_tab = df === tab?.df;
} else if (typeof field?.on_section_collapse == "function") {
field.on_section_collapse(!in_tab); // hide = !in_tab
}
}
}
get_active_tab() {
return this.active_tab_map && this.active_tab_map[this.docname];
}
get_involved_users() {
let user_fields = this.meta.fields
.filter((d) => d.fieldtype === "Link" && d.options === "User")
.map((d) => d.fieldname);
user_fields = [...user_fields, "owner", "modified_by"];
let involved_users = user_fields.map((field) => this.doc[field]);
const docinfo = this.get_docinfo();
involved_users = involved_users.concat(
docinfo.communications.map((d) => d.sender && d.delivery_status === "sent"),
docinfo.comments.map((d) => d.owner),
docinfo.versions.map((d) => d.owner),
docinfo.assignments.map((d) => d.owner)
);
return involved_users
.uniqBy((u) => u)
.filter((user) => !["Administrator", frappe.session.user].includes(user))
.filter(Boolean);
}
show_submission_queue_banner() {
let wrapper = this.layout.wrapper.find(".submission-queue-banner");
if (
!(
this.meta.is_submittable &&
this.meta.queue_in_background &&
!this.doc.__islocal &&
this.doc.docstatus <= 1
)
) {
wrapper.length && wrapper.remove();
return;
}
if (!wrapper.length) {
wrapper = $('<div class="submission-queue-banner form-message">');
this.layout.wrapper.prepend(wrapper);
}
frappe
.call({
method: "frappe.core.doctype.submission_queue.submission_queue.get_latest_submissions",
args: { doctype: this.doctype, docname: this.docname },
})
.then((r) => {
if (r.message?.latest_submission && r.message.status !== "Finished") {
// if we are here that means some submission(s) were queued and are in queued/failed state
let submission_label = __("Previous Submission");
let secondary = "";
let div_class = "col-md-12";
if (r.message.exc) {
secondary = `: <span>${r.message.exc}</span>`;
} else {
div_class = "col-md-6";
secondary = `
</div>
<div class="col-md-6">
<a href='/desk/submission-queue?ref_doctype=${encodeURIComponent(
this.doctype
)}&ref_docname=${encodeURIComponent(this.docname)}'>${__(
"All Submissions"
)}</a>
`;
}
let html = `
<div class="row">
<div class="${div_class}">
<a href='/desk/submission-queue/${r.message.latest_submission}'>${submission_label} (${r.message.status})</a>${secondary}
</div>
</div>
`;
wrapper.removeClass("red").removeClass("yellow");
wrapper.addClass(r.message.status == "Failed" ? "red" : "yellow");
wrapper.html(html);
} else {
wrapper.remove();
}
});
}
show_workflow_read_only_banner() {
if (!this.read_only) {
return;
}
const _show_read_only_banner = () => {
this.dashboard.set_headline(
__("This form is not editable due to a Workflow."),
"blue",
true
);
};
if (this.dashboard) {
_show_read_only_banner();
} else {
frappe.after_ajax(_show_read_only_banner);
}
}
};
frappe.validated = 0;