Merge branch 'frappe:develop' into chore/add-brazilian-portuguese-language

This commit is contained in:
Flavia de Castro 2026-03-02 11:13:33 -03:00 committed by GitHub
commit 547278c6a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 2537 additions and 1733 deletions

View file

@ -27,7 +27,9 @@ context("Web Form", () => {
cy.wait("@save_form");
cy.get('.frappe-control[data-fieldname="route"]').scrollIntoView();
cy.get_field("route").should("have.value", "note");
cy.get(".title-area .indicator-pill")
.should("contain.text", "Published")
.should("have.class", "green");

View file

@ -683,7 +683,10 @@ def validate_oauth(authorization_header):
uri, http_method, body, headers, required_scopes
)
if valid:
frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
user = frappe.db.get_value("OAuth Bearer Token", token, "user")
if not frappe.db.get_value("User", user, "enabled"):
frappe.throw(_("User {0} is disabled").format(user), frappe.AuthenticationError)
frappe.set_user(user)
frappe.local.form_dict = form_dict
except AttributeError:
pass

View file

@ -373,6 +373,7 @@ def run_tests(
@click.option("--use-orchestrator", is_flag=True, help="Use orchestrator to run parallel tests")
@click.option("--dry-run", is_flag=True, default=False, help="Dont actually run tests")
@click.option("--lightmode", is_flag=True, default=False, help="Skips all before test setup")
@click.option("--failfast", is_flag=True, default=False, help="Exit on first failure occurred")
@pass_context
def run_parallel_tests(
context: CliCtxObj,
@ -383,6 +384,7 @@ def run_parallel_tests(
use_orchestrator=False,
dry_run=False,
lightmode=False,
failfast=False,
):
from traceback_with_variables import activate_by_import
@ -404,6 +406,7 @@ def run_parallel_tests(
total_builds=total_builds,
dry_run=dry_run,
lightmode=lightmode,
failfast=failfast,
)
mode = "Orchestrator" if use_orchestrator else "Parallel"
banner = f"""

View file

@ -211,8 +211,7 @@ frappe.ui.form.on("Communication", {
],
primary_action_label: __("Move"),
primary_action(values) {
d.hide();
frappe.call({
return frappe.call({
method: "frappe.email.inbox.move_email",
args: {
communication: frm.doc.name,
@ -220,6 +219,7 @@ frappe.ui.form.on("Communication", {
},
freeze: true,
callback: function () {
d.hide();
window.history.back();
},
});

View file

@ -103,7 +103,7 @@ frappe.listview_settings["DocType"] = {
primary_action_label: __("Create & Continue"),
primary_action(values) {
if (!values.istable) values.editable_grid = 0;
frappe.db
return frappe.db
.insert({
doctype: "DocType",
...values,

View file

@ -890,6 +890,14 @@ def has_permission(doc, ptype=None, user=None, debug=False):
if user != "Guest" and doc.owner == user:
return True
if (
user != "Guest"
and ptype in ["read", "write", "share", "submit"]
and frappe.share.get_shared(
"File", filters=[["share_name", "=", doc.name]], rights=[ptype], user=user
)
):
return True
if doc.attached_to_doctype and doc.attached_to_name:
attached_to_doctype = doc.attached_to_doctype

View file

@ -427,6 +427,29 @@ def relink_mismatched_files(doc: "Document") -> None:
for df in attach_fields:
if doc.get(df.fieldname):
relink_files(doc, df.fieldname, doc.__temporary_name)
# Relink files in child table Attach fields
table_fields = doc.meta.get("fields", {"fieldtype": "Table"})
for table_df in table_fields:
child_rows = doc.get(table_df.fieldname) or []
if not child_rows:
continue
child_meta = frappe.get_meta(table_df.options)
child_attach_fields = child_meta.get("fields", {"fieldtype": ["in", ["Attach", "Attach Image"]]})
if not child_attach_fields:
continue
for child_row in child_rows:
for child_df in child_attach_fields:
file_url = child_row.get(child_df.fieldname)
if file_url:
frappe.db.set_value(
"File",
{"file_url": file_url, "attached_to_name": doc.__temporary_name},
{"attached_to_name": doc.name},
)
# delete temporary name after relinking is done
doc.delete_key("__temporary_name")

View file

@ -11,7 +11,9 @@
"settings_dropdown",
"help_dropdown",
"announcements_section",
"announcement_widget"
"announcement_widget",
"announcement_widget_color",
"dismissible_announcement_widget"
],
"fields": [
{
@ -47,16 +49,27 @@
"label": "Announcements"
},
{
"description": "These announcements will appear inside a dismissible alert below the Navbar.",
"description": "These announcements will appear inside an alert below the Navbar.",
"fieldname": "announcement_widget",
"fieldtype": "Text Editor",
"label": "Announcement Widget",
"max_height": "10em"
},
{
"fieldname": "announcement_widget_color",
"fieldtype": "Color",
"label": "Widget Color"
},
{
"default": "1",
"fieldname": "dismissible_announcement_widget",
"fieldtype": "Check",
"label": "Is Dismissible"
}
],
"issingle": 1,
"links": [],
"modified": "2024-05-01 14:09:54.587137",
"modified": "2026-02-05 15:19:55.524034",
"modified_by": "Administrator",
"module": "Core",
"name": "Navbar Settings",
@ -74,8 +87,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -17,7 +17,9 @@ class NavbarSettings(Document):
from frappe.types import DF
announcement_widget: DF.TextEditor | None
announcement_widget_color: DF.Color | None
app_logo: DF.AttachImage | None
dismissible_announcement_widget: DF.Check
help_dropdown: DF.Table[NavbarItem]
settings_dropdown: DF.Table[NavbarItem]
# end: auto-generated types

View file

@ -24,7 +24,7 @@ frappe.ui.form.on("Recorder", {
});
let index_grid = frm.fields_dict.suggested_indexes.grid;
index_grid.wrapper.find(".grid-footer").toggle(true);
index_grid.wrapper.find(".grid-footer").toggleClass("hidden", false);
index_grid.toggle_checkboxes(true);
index_grid.df.cannot_delete_rows = true;
index_grid.add_custom_button(__("Add Indexes"), function () {

View file

@ -201,18 +201,19 @@ frappe.ui.form.on("User", {
},
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password",
{
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions,
}
);
return frappe
.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password",
{
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions,
}
)
.then(() => d.hide());
},
});
d.show();

View file

@ -25,7 +25,7 @@ frappe.query_reports["Database Storage Usage By Tables"] = {
size: "small",
primary_action_label: "Optimize",
primary_action(values) {
frappe.call({
return frappe.call({
method: "frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables.optimize_doctype",
args: {
doctype_name: values.doctype_name,
@ -38,9 +38,9 @@ frappe.query_reports["Database Storage Usage By Tables"] = {
)
);
}
d.hide();
},
});
d.hide();
},
});
d.show();

View file

@ -22,6 +22,7 @@ def execute(filters=None):
round((data_length / 1024 / 1024), 2) as data_size,
round((index_length / 1024 / 1024), 2) as index_size
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC;
""",
"postgres": """

View file

@ -488,7 +488,13 @@ frappe.ui.form.on("Dashboard Chart", {
});
dialog.show();
dialog.set_values(frm.dynamic_filters);
if (frm.dynamic_filters) {
let filter_values = {};
frm.dynamic_filters.forEach((f) => {
filter_values[f[0] + ":" + f[1]] = f[3];
});
dialog.set_values(filter_values);
}
});
},

View file

@ -124,11 +124,6 @@ frappe.ui.form.on("Number Card", {
frappe.model.with_doctype(doctype, () => {
frappe.get_meta(doctype).fields.map((df) => {
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
if (df.fieldtype == "Currency") {
if (!df.options || df.options !== "Company:company:default_currency") {
return;
}
}
aggregate_based_on_fields.push({ label: df.label, value: df.fieldname });
}
});
@ -202,7 +197,6 @@ frappe.ui.form.on("Number Card", {
render_filters_table: function (frm) {
frm.set_df_property("filters_section", "hidden", 0);
let is_document_type = frm.doc.type == "Document Type";
let is_dynamic_filter = (f) => ["Date", "DateRange"].includes(f.fieldtype) && f.default;
let wrapper = $(frm.get_field("filters_json").wrapper).empty();
let table = $(`<table class="table table-bordered" style="cursor:${
@ -219,24 +213,14 @@ frappe.ui.form.on("Number Card", {
</table>`).appendTo(wrapper);
if (frm.has_perm("write")) {
$(`<p class="text-muted small">${__("Click table to edit")}</p>`).appendTo(wrapper);
$(`<p class="text-muted small mt-2">${__("Click table to edit")}</p>`).appendTo(
wrapper
);
}
let filters = JSON.parse(frm.doc.filters_json || "[]");
let filters_set = false;
// Set dynamic filters for reports
if (frm.doc.type == "Report") {
let set_filters = false;
frm.filters.forEach((f) => {
if (is_dynamic_filter(f)) {
filters[f.fieldname] = f.default;
set_filters = true;
}
});
set_filters && frm.set_value("filters_json", JSON.stringify(filters));
}
let fields = [];
if (is_document_type) {
fields = [
@ -290,7 +274,7 @@ frappe.ui.form.on("Number Card", {
}
let dialog = new frappe.ui.Dialog({
title: __("Set Filters"),
fields: fields.filter((f) => !is_dynamic_filter(f)),
fields,
primary_action: function () {
let values = this.get_values();
if (values) {
@ -304,7 +288,7 @@ frappe.ui.form.on("Number Card", {
frm.trigger("render_filters_table");
}
},
primary_action_label: __("Set"),
primary_action_label: __("Update"),
});
if (is_document_type) {
@ -340,8 +324,6 @@ frappe.ui.form.on("Number Card", {
frm.set_df_property("dynamic_filters_section", "hidden", 0);
let is_document_type = frm.doc.type == "Document Type";
let wrapper = $(frm.get_field("dynamic_filters_json").wrapper).empty();
frm.dynamic_filter_table = $(`<table class="table table-bordered" style="cursor:${
@ -349,29 +331,21 @@ frappe.ui.form.on("Number Card", {
}; margin:0px;">
<thead>
<tr>
<th style="width: 20%">${__("Filter")}</th>
<th style="width: 20%">${__("Condition")}</th>
<th>${__("Value")}</th>
<th style="width: 30%">${__("Filter")}</th>
<th>${__("Expression")}</th>
</tr>
</thead>
<tbody></tbody>
</table>`).appendTo(wrapper);
frm.dynamic_filters =
frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: null;
if (frm.has_perm("write")) {
$(`<p class="text-muted small mt-2">${__("Click table to edit")}</p>`).appendTo(
wrapper
);
}
frm.trigger("set_dynamic_filters_in_table");
let filters = JSON.parse(frm.doc.filters_json || "[]");
let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog(
is_document_type,
filters,
frm.dynamic_filters
);
frm.dynamic_filter_table.on("click", () => {
if (!frm.has_perm("write")) {
return;
@ -380,65 +354,215 @@ frappe.ui.form.on("Number Card", {
if (!frappe.boot.developer_mode && frm.doc.is_standard) {
frappe.throw(__("Cannot edit filters for standard number cards"));
}
let dialog = new frappe.ui.Dialog({
title: __("Set Dynamic Filters"),
fields: fields,
primary_action: () => {
let values = dialog.get_values();
dialog.hide();
let dynamic_filters = [];
for (let key of Object.keys(values)) {
if (is_document_type) {
let [doctype, fieldname] = key.split(":");
dynamic_filters.push([doctype, fieldname, "=", values[key]]);
}
}
if (is_document_type) {
frm.set_value("dynamic_filters_json", JSON.stringify(dynamic_filters));
} else {
frm.set_value("dynamic_filters_json", JSON.stringify(values));
}
frm.trigger("set_dynamic_filters_in_table");
},
primary_action_label: __("Set"),
});
dialog.show();
dialog.set_values(frm.dynamic_filters);
frm.trigger("show_dynamic_filter_dialog");
});
},
set_dynamic_filters_in_table: function (frm) {
frm.dynamic_filters =
frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: null;
show_dynamic_filter_dialog: function (frm) {
if (frm.doc.type === "Document Type") {
if (!frm.doc.document_type) {
frappe.msgprint(__("Please select a Document Type first"));
return;
}
frappe.model.with_doctype(frm.doc.document_type, () => {
frm.trigger("show_doctype_dynamic_filter_dialog");
});
return;
}
if (!frm.dynamic_filters) {
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
if (!frm.doc.report_name) {
frappe.msgprint(__("Please select a Report first"));
return;
}
if (!frm.filters?.length) {
frappe.msgprint(__("No filters available for this report"));
return;
}
frm.trigger("show_report_dynamic_filter_dialog");
},
show_doctype_dynamic_filter_dialog: function (frm) {
const meta = frappe.get_meta(frm.doc.document_type);
const field_options = meta.fields
.filter((df) => df.fieldname && !frappe.model.no_value_type.includes(df.fieldtype))
.map((df) => ({ label: df.label || df.fieldname, value: df.fieldname }));
frappe.model.std_fields.forEach((df) => {
field_options.push({ label: df.label, value: df.fieldname });
});
frm.events.show_dynamic_filter_dialog_common(frm, field_options, frm.doc.document_type);
},
show_report_dynamic_filter_dialog: function (frm) {
const field_options = frm.filters
.filter((f) => f.fieldname)
.map((f) => ({ label: f.label || f.fieldname, value: f.fieldname }));
frm.events.show_dynamic_filter_dialog_common(frm, field_options, frm.doc.report_name);
},
show_dynamic_filter_dialog_common: function (frm, field_options, doctype_or_report) {
let dynamic_filters =
frm.doc.dynamic_filters_json?.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: [];
const dialog = new frappe.ui.Dialog({
title: __("Set Dynamic Filters"),
fields: [
{
fieldtype: "HTML",
fieldname: "help_text",
options: frm.events.get_dynamic_filter_help_text(),
},
{ fieldtype: "HTML", fieldname: "filter_area" },
],
size: "large",
primary_action: () => {
const filters = [];
dialog.$wrapper.find(".dynamic-filter-row").each(function () {
const $row = $(this);
const fieldname = $row.data("selected_fieldname");
const expression = $row.find(".filter-expression").val();
if (fieldname && expression) {
filters.push([doctype_or_report, fieldname, "=", expression]);
}
});
dialog.hide();
frm.set_value("dynamic_filters_json", JSON.stringify(filters));
frm.trigger("set_dynamic_filters_in_table");
},
primary_action_label: __("Update"),
});
const add_filter_row = frm.events.build_dynamic_filter_interface(
dialog.fields_dict.filter_area.$wrapper,
field_options,
doctype_or_report
);
if (dynamic_filters?.length) {
dynamic_filters.forEach((filter) => {
add_filter_row(filter[1], filter[3]);
});
} else {
add_filter_row();
}
dialog.show();
},
get_dynamic_filter_help_text: function () {
return `<p class="text-muted small">
${__("Enter expressions that will be evaluated when the card is displayed. For example:")}<br>
<code>frappe.defaults.get_user_default("Company")</code><br>
<code>frappe.datetime.get_today()</code><br>
</p>`;
},
build_dynamic_filter_interface: function ($filter_area, field_options, doctype_or_report) {
const filter_html = `
<div>
<table class="table table-bordered" style="margin-bottom: 12px;">
<thead>
<tr>
<th style="width: 35%">${__("Field")}</th>
<th style="width: 60%">${__("Expression")}</th>
<th style="width: 5%"></th>
</tr>
</thead>
<tbody class="filter-rows"></tbody>
</table>
<div style="display: flex; justify-content: space-between; align-items: center;">
<button class="text-muted add-filter btn btn-xs">
+ ${__("Add Filter")}
</button>
<button class="btn btn-secondary btn-xs clear-filters">
${__("Clear Filters")}
</button>
</div>
</div>
`;
$filter_area.html(filter_html);
const filter_fields = field_options.map((opt) => ({
fieldname: opt.value,
label: opt.label,
parent: doctype_or_report,
}));
const add_filter_row = (fieldname = "", expression = "") => {
const row_html = `
<tr class="dynamic-filter-row">
<td class="fieldname-select-area"></td>
<td>
<input type="text" class="form-control input-xs filter-expression">
</td>
<td class="text-center">
<a class="remove-filter text-muted" style="cursor: pointer;">
<svg class="icon icon-sm">
<use href="#icon-close" class="close"></use>
</svg>
</a>
</td>
</tr>
`;
const $row = $(row_html);
const field_select = new frappe.ui.FieldSelect({
parent: $row.find(".fieldname-select-area"),
doctype: doctype_or_report,
filter_fields: filter_fields,
input_class: "input-xs",
select: (_, selected_fieldname) => {
$row.data("selected_fieldname", selected_fieldname);
},
});
if (fieldname) {
field_select.set_value(doctype_or_report, fieldname);
$row.data("selected_fieldname", fieldname);
}
$row.find(".filter-expression").val(expression);
$row.data("field_select", field_select);
$filter_area.find(".filter-rows").append($row);
};
$filter_area.on("click", ".add-filter", () => add_filter_row());
$filter_area.on("click", ".remove-filter", function () {
$(this).closest("tr").remove();
});
$filter_area.on("click", ".clear-filters", () => {
$filter_area.find(".filter-rows").empty();
add_filter_row();
});
return add_filter_row;
},
set_dynamic_filters_in_table: function (frm) {
let dynamic_filters =
frm.doc.dynamic_filters_json?.length > 2
? JSON.parse(frm.doc.dynamic_filters_json)
: [];
if (!dynamic_filters?.length) {
const filter_row = $(`<tr><td colspan="2" class="text-muted text-center">
${__("Click to Set Dynamic Filters")}</td></tr>`);
frm.dynamic_filter_table.find("tbody").html(filter_row);
} else {
let filter_rows = "";
if ($.isArray(frm.dynamic_filters)) {
frm.dynamic_filters.forEach((filter) => {
filter_rows += `<tr>
<td>${filter[1]}</td>
<td>${filter[2] || ""}</td>
<td>${filter[3]}</td>
</tr>`;
});
} else {
let condition = "=";
for (let [key, val] of Object.entries(frm.dynamic_filters)) {
filter_rows += `<tr>
<td>${key}</td>
<td>${condition}</td>
<td>${val || ""}</td>
</tr>`;
}
}
dynamic_filters.forEach((filter) => {
filter_rows += `<tr>
<td>${filter[1]}</td>
<td><code>${filter[3] || ""}</code></td>
</tr>`;
});
frm.dynamic_filter_table.find("tbody").html(filter_rows);
}

View file

@ -1,6 +1,7 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:label",
"creation": "2020-04-15 18:06:39.444683",
"doctype": "DocType",
"editable_grid": 1,
@ -72,7 +73,8 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
"reqd": 1,
"unique": 1
},
{
"fieldname": "color",
@ -229,10 +231,11 @@
}
],
"links": [],
"modified": "2025-09-17 21:00:11.351605",
"modified": "2026-02-25 16:33:09.032056",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{

View file

@ -437,37 +437,19 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
is_target_doctype_table = frappe.get_meta(doctype).istable
for linked_doctype, link_context in linkinfo.items():
# Don't try to fetch linked documents if the user can't read the doctype
if not frappe.has_permission(linked_doctype):
continue
linked_doctype_meta = frappe.get_meta(linked_doctype)
if linked_doctype_meta.issingle:
continue
has_permission = frappe.has_permission(linked_doctype)
filters = []
or_filters = []
ret = None
parent_info = None
fields = [
d.fieldname
for d in linked_doctype_meta.get(
"fields",
{
"in_list_view": 1,
"fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)],
},
)
] + ["name", "modified", "docstatus"]
if add_fields := link_context.get("add_fields"):
fields += add_fields
fields = [sf.strip() for sf in fields if sf]
if filters_ctx := link_context.get("filters"):
ret = frappe.get_list(doctype=linked_doctype, fields=fields, filters=filters_ctx, order_by=None)
filters = filters_ctx
elif link_context.get("get_parent"):
# check for child table
@ -478,13 +460,10 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
doctype, name, ["parenttype", "parent"], as_dict=True, order_by=None
)
if parent_info and parent_info.parenttype == linked_doctype:
ret = frappe.get_list(
doctype=linked_doctype,
fields=fields,
filters=[[linked_doctype, "name", "=", parent_info.parent]],
order_by=None,
)
if not (parent_info and parent_info.parenttype == linked_doctype):
continue
filters = [[linked_doctype, "name", "=", parent_info.parent]]
elif child_doctype := link_context.get("child_doctype"):
or_filters = [
@ -495,15 +474,6 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
if doctype_fieldname := link_context.get("doctype_fieldname"):
filters.append([child_doctype, doctype_fieldname, "=", doctype])
ret = frappe.get_list(
doctype=linked_doctype,
fields=fields,
filters=filters,
or_filters=or_filters,
distinct=True,
order_by=None,
)
elif link_fieldnames := link_context.get("fieldname"):
if isinstance(link_fieldnames, str):
link_fieldnames = [link_fieldnames]
@ -518,12 +488,51 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
or frappe.db.exists(linked_doctype, {"parenttype": doctype, "parent": name})
):
continue
total_count = len(
frappe.get_all(
linked_doctype,
filters=filters,
or_filters=or_filters,
fields=["name"],
order_by=None,
)
)
if not total_count:
continue
if has_permission:
fields = [
d.fieldname
for d in linked_doctype_meta.get(
"fields",
{
"in_list_view": 1,
"fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)],
},
)
] + ["name", "modified", "docstatus"]
if add_fields := link_context.get("add_fields"):
fields += add_fields
fields = [sf.strip() for sf in fields if sf]
ret = frappe.get_list(
doctype=linked_doctype, fields=fields, filters=filters, or_filters=or_filters, order_by=None
doctype=linked_doctype,
fields=fields,
filters=filters,
or_filters=or_filters,
distinct=True,
order_by=None,
)
if ret:
results[linked_doctype] = ret
permitted_count = len(ret or [])
results[linked_doctype] = {
"docs": ret or [],
"hidden_count": total_count - permitted_count,
}
return results

View file

@ -87,12 +87,17 @@
}
}
.modal
.modal-body .icons-container,.folder-icon .icons-container {
.modal-body .icons-container, .folder-icon .icons-container {
padding:0px;
margin: 0px;
height: 100%;
overflow: auto;
}
.folder-icon .icons-container {
overflow: hidden;
}
.icons{
gap: 16px;
display: grid;

View file

@ -548,7 +548,6 @@ class DesktopPage {
frappe.router.on("change", function () {
if (frappe.get_route()[0] == "desktop" || frappe.get_route()[0] == "") {
me.setup_navbar();
me.setup_edit_button();
} else {
$(".navbar").show();
frappe.desktop_utils.close_desktop_modal();

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2026-02-22 09:42+0000\n"
"PO-Revision-Date: 2026-02-23 22:07\n"
"PO-Revision-Date: 2026-02-25 23:15\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Bosnian\n"
"MIME-Version: 1.0\n"
@ -2870,7 +2870,7 @@ msgstr "Dodjela je Završena"
#. Label of the assignment_days (Table) field in DocType 'Assignment Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Assignment Days"
msgstr "Dani Dodjeljivanja"
msgstr "Dani Dodjele"
#. Name of a DocType
#. Label of the assignment_rule (Link) field in DocType 'ToDo'
@ -2888,7 +2888,7 @@ msgstr "Dan Dodjele Pravila"
#. Name of a DocType
#: frappe/automation/doctype/assignment_rule_user/assignment_rule_user.json
msgid "Assignment Rule User"
msgstr "Korisnik Dodjele Pravila"
msgstr "Korisnik Pravila Dodjele"
#: frappe/automation/doctype/assignment_rule/assignment_rule.py:55
msgid "Assignment Rule is not allowed on document type {0}"

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2026-02-22 09:42+0000\n"
"PO-Revision-Date: 2026-02-23 22:07\n"
"PO-Revision-Date: 2026-02-25 23:14\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@ -6314,7 +6314,7 @@ msgstr "سفارشی‌سازی"
#: frappe/custom/doctype/customize_form/customize_form.js:89
msgid "Customize Child Table"
msgstr "سفارشی کردن جدول فرزند"
msgstr "سفارشی‌سازی جدول فرزند"
#: frappe/public/js/frappe/views/dashboard/dashboard_view.js:38
msgid "Customize Dashboard"
@ -6339,7 +6339,7 @@ msgstr "سفارشی‌سازی فرم - {0}"
#. Name of a DocType
#: frappe/custom/doctype/customize_form_field/customize_form_field.json
msgid "Customize Form Field"
msgstr "سفارشی کردن فیلد فرم"
msgstr "سفارشی‌سازی فیلد فرم"
#: frappe/public/js/frappe/list/list_view.js:1994
msgctxt "Customize qucik filters of List View"
@ -18808,7 +18808,7 @@ msgstr ""
#: frappe/core/doctype/doctype/doctype.py:1699
msgid "Options for Rating field can range from 3 to 10"
msgstr "گزینه‌های فیلد رتبه بندی می‌تواند از 3 تا 10 باشد"
msgstr "گزینه‌های فیلد رتبهبندی می‌تواند از 3 تا 10 باشد"
#: frappe/custom/doctype/custom_field/custom_field.js:96
msgid "Options for select. Each option on a new line."

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2026-02-22 09:42+0000\n"
"PO-Revision-Date: 2026-02-23 22:07\n"
"PO-Revision-Date: 2026-02-25 23:14\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Croatian\n"
"MIME-Version: 1.0\n"
@ -2870,7 +2870,7 @@ msgstr "Dodjela je Završena"
#. Label of the assignment_days (Table) field in DocType 'Assignment Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Assignment Days"
msgstr "Dani Dodjeljivanja"
msgstr "Dani Dodjele"
#. Name of a DocType
#. Label of the assignment_rule (Link) field in DocType 'ToDo'

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2026-02-22 09:42+0000\n"
"PO-Revision-Date: 2026-02-23 22:07\n"
"PO-Revision-Date: 2026-02-26 23:27\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Serbian (Cyrillic)\n"
"MIME-Version: 1.0\n"
@ -1357,7 +1357,7 @@ msgstr "Додај параметре упита"
#. Label of the add_reply_to_header (Check) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Add Reply-To header"
msgstr ""
msgstr "Додај заглавље адресе за одговор"
#: frappe/core/doctype/user/user.py:860
msgid "Add Roles"
@ -1523,7 +1523,7 @@ msgstr "Додај на контролну таблу"
#: frappe/desk/doctype/workspace/workspace.js:49
msgid "Add to Desktop"
msgstr ""
msgstr "Додај на радну површину"
#: frappe/public/js/frappe/form/sidebar/assign_to.js:110
msgid "Add to ToDo"
@ -1646,7 +1646,7 @@ msgstr "Адресе и контакти"
#. Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Addresses added here will be used as the Reply-To header for outgoing emails sent from this account."
msgstr ""
msgstr "Адресе додате овде користиће се као адреса за одговор за излазне имејлове послате са овог налога."
#. Description of a DocType
#: frappe/custom/doctype/client_script/client_script.json
@ -3082,7 +3082,7 @@ msgstr "Историја измена"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/users.json
msgid "Audits"
msgstr ""
msgstr "Ревизије"
#. Label of the auth_url_data (Code) field in DocType 'Social Login Key'
#: frappe/integrations/doctype/social_login_key/social_login_key.json
@ -3516,7 +3516,7 @@ msgstr "Слика позадине"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/system.json
msgid "Background Job"
msgstr ""
msgstr "Позадински задатак"
#. Label of a Link in the Build Workspace
#. Label of the background_jobs_section (Section Break) field in DocType
@ -4934,7 +4934,7 @@ msgstr "Кликните да поставите филтере"
#: frappe/desk/page/desktop/desktop.js:1261
msgid "Click to edit"
msgstr ""
msgstr "Кликните за уређивање"
#: frappe/public/js/frappe/list/list_view.js:754
msgid "Click to sort by {0}"
@ -6537,7 +6537,7 @@ msgstr "Цијан"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "DELAY"
msgstr ""
msgstr "ОДЛАГАЊЕ"
#. Option for the 'Method' (Select) field in DocType 'Recorder'
#. Option for the 'Request Method' (Select) field in DocType 'Webhook'
@ -7365,7 +7365,7 @@ msgstr "Статус"
#. Label of the dsn_notify_type (Select) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Delivery Status Notification Type"
msgstr ""
msgstr "Врста обавештења о статусу испоруке"
#. Option for the 'Sign ups' (Select) field in DocType 'Social Login Key'
#: frappe/integrations/doctype/social_login_key/social_login_key.json
@ -9430,7 +9430,7 @@ msgstr "Планер омогућен"
#. Label of the enabled (Check) field in DocType 'Notification Settings'
#: frappe/desk/doctype/notification_settings/notification_settings.json
msgid "Enabled System Notification"
msgstr ""
msgstr "Омогућено системско обавештење"
#: frappe/email/doctype/email_account/email_account.py:1101
msgid "Enabled email inbox for user {0}"
@ -10128,7 +10128,7 @@ msgstr "Додатни параметри"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "FAILURE"
msgstr ""
msgstr "НЕУСПЕХ"
#. Option for the 'Social Login Provider' (Select) field in DocType 'Social
#. Login Key'
@ -10272,7 +10272,7 @@ msgstr "Неуспешан покушај пријаве на Frappe Cloud"
#: frappe/email/doctype/email_account/email_account.py:232
msgid "Failed to retrieve the list of IMAP folders from the server. Please ensure the mailbox is accessible and the account has permission to list folders."
msgstr ""
msgstr "Неуспешно преузимање листе IMAP директоријума са сервера. Проверите да ли је поштанско сандуче доступно и да ли налог има дозволу за приказ директоријума."
#: frappe/email/doctype/email_queue/email_queue.py:311
msgid "Failed to send email with subject:"
@ -11339,7 +11339,7 @@ msgstr "Јединица фракције"
#. Label of a Desktop Icon
#: frappe/desktop_icon/framework.json
msgid "Framework"
msgstr ""
msgstr "Framework"
#. Option for the 'Social Login Provider' (Select) field in DocType 'Social
#. Login Key'
@ -12235,7 +12235,7 @@ msgstr "Наслов"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/system.json
msgid "Health Report"
msgstr ""
msgstr "Извештај о стању система"
#. Option for the 'Type' (Select) field in DocType 'Dashboard Chart'
#: frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@ -12654,7 +12654,7 @@ msgstr "IMAP датотека"
#: frappe/email/doctype/email_account/email_account.py:235
#: frappe/email/doctype/email_account/email_account.py:263
msgid "IMAP Folder Not Found"
msgstr ""
msgstr "IMAP директоријум није пронађен"
#. Label of the ip_address (Data) field in DocType 'Activity Log'
#. Label of the ip_address (Data) field in DocType 'Comment'
@ -13414,7 +13414,7 @@ msgstr "Погрешан верификациони код"
#: frappe/public/js/frappe/views/gantt/gantt_view.js:88
msgid "Incorrect configuration"
msgstr ""
msgstr "Неисправна конфигурација"
#: frappe/model/document.py:1733
msgid "Incorrect value in row {0}:"
@ -16979,7 +16979,7 @@ msgstr "MyISAM"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "NEVER"
msgstr ""
msgstr "НИКАДА"
#: frappe/workflow/doctype/workflow/workflow.js:19
msgid "NOTE: If you add states or transitions in the table, it will be reflected in the Workflow Builder but you will have to position them manually. Also Workflow Builder is currently in <b>BETA</b>."
@ -17750,7 +17750,7 @@ msgstr "Нема података за извоз"
#: frappe/public/js/frappe/views/reports/query_report.js:1543
msgid "No data to perform this action"
msgstr ""
msgstr "Нема података за извршавање ове радње"
#: frappe/contacts/doctype/address/address.py:247
msgid "No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."
@ -17795,7 +17795,7 @@ msgstr "Нема додатних записа"
#: frappe/public/js/frappe/views/reports/report_view.js:337
msgid "No matching entries in the current results"
msgstr ""
msgstr "Нема подударних записа у тренутним резултатима"
#: frappe/templates/includes/search_template.html:49
msgid "No matching records. Search something new"
@ -18429,7 +18429,7 @@ msgstr "OAuth грешка"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/integrations.json
msgid "OAuth Provider"
msgstr ""
msgstr "OAuth провајдер"
#. Name of a DocType
#. Label of a Link in the Integrations Workspace
@ -18698,7 +18698,7 @@ msgstr "Дозволи уређивање само за"
#: frappe/core/doctype/module_def/module_def.py:95
msgid "Only Custom Modules can be renamed."
msgstr ""
msgstr "Искључиво прилагођени модули могу бити преименовани."
#: frappe/core/doctype/doctype/doctype.py:1652
msgid "Only Options allowed for Data field are:"
@ -19969,7 +19969,7 @@ msgstr "Молимо Вас да додате валидан коментар."
#: frappe/public/js/frappe/views/reports/query_report.js:1544
msgid "Please adjust filters to include some data"
msgstr ""
msgstr "Прилагодите филтере како бисте укључили неке податке"
#: frappe/core/doctype/user/user.py:1122
msgid "Please ask your administrator to verify your sign-up"
@ -20029,7 +20029,7 @@ msgstr "Молимо Вас да кликнете на следећи линк
#: frappe/public/js/frappe/views/gantt/gantt_view.js:89
msgid "Please configure the start field for this Doctype in the controller file."
msgstr ""
msgstr "Молимо Вас да конфигуришете почетно поље за овај DocType у датотеци контролера."
#: frappe/www/confirm_workflow_action.html:4
msgid "Please confirm your action to {0} this document."
@ -21132,7 +21132,7 @@ msgstr "Љубичасто"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/integrations.json
msgid "Push Notification"
msgstr ""
msgstr "Push обавештење"
#. Name of a DocType
#. Label of a Link in the Integrations Workspace
@ -22225,16 +22225,16 @@ msgstr "Одговори свима"
#. Name of a DocType
#: frappe/email/doctype/reply_to_address/reply_to_address.json
msgid "Reply To Address"
msgstr ""
msgstr "Адреса за одговор"
#: frappe/email/doctype/email_account/email_account.py:278
msgid "Reply To email is required"
msgstr ""
msgstr "Адреса за одговор је обавезна"
#. Label of the reply_to_addresses (Table) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Reply-To Addresses"
msgstr ""
msgstr "Адреса за одговор"
#. Label of the report (Check) field in DocType 'Custom DocPerm'
#. Label of the report (Link) field in DocType 'Custom Role'
@ -23276,19 +23276,19 @@ msgstr "SSL/TLS режим"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "SUCCESS"
msgstr ""
msgstr "УСПЕХ"
#. Option for the 'Delivery Status Notification Type' (Select) field in DocType
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "SUCCESS,FAILURE"
msgstr ""
msgstr "УСПЕХ, НЕУСПЕХ"
#. Option for the 'Delivery Status Notification Type' (Select) field in DocType
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "SUCCESS,FAILURE,DELAY"
msgstr ""
msgstr "УСПЕХ, НЕУСПЕХ, ОДЛАГАЊЕ"
#: frappe/public/js/frappe/color_picker/color_picker.js:20
msgid "SWATCHES"
@ -24115,7 +24115,7 @@ msgstr "Изабери две верзије за приказ разлика."
#. DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Select which delivery events should trigger a delivery status notification (DSN) from the SMTP server."
msgstr ""
msgstr "Изаберите који догађаји испоруке треба да покрену обавештење о статусу испоруке (DNS) са SMTP сервера."
#: frappe/public/js/frappe/form/link_selector.js:24
#: frappe/public/js/frappe/form/multi_select_dialog.js:80
@ -27098,7 +27098,7 @@ msgstr "Коментар не може бити празан"
#: frappe/email/doctype/email_account/email_account.py:290
msgid "The configured SMTP server does not support DSN (Delivery Status Notification)."
msgstr ""
msgstr "Конфигурисани SMTP сервер не подржава DNS (обавештење о статусу испоруке)."
#: frappe/templates/emails/workflow_action.html:9
msgid "The contents of this email are strictly confidential. Please do not forward this email to anyone."
@ -27160,7 +27160,7 @@ msgstr "Следећа скрипта заглавља ће додати тре
#: frappe/email/doctype/email_account/email_account.py:257
msgid "The following configured IMAP folder(s) were not found on the server:<br><ul>{0}</ul>Please verify the folder names exactly as they appear on the server (folder names are case-sensitive)."
msgstr ""
msgstr "Следећи конфигурисани IMAP директоријуми нису пронађени на серверу:<br><ul>{0}</ul>Молимо Вас да проверите називе директоријума тачно онако како су приказани на серверу (велика и мала слова су битна за називе датотека)."
#: frappe/core/doctype/data_import/importer.py:1092
msgid "The following values are invalid: {0}. Values must be one of {1}"
@ -27252,7 +27252,7 @@ msgstr "Изабрани документ {0} није {1}."
#: frappe/email/doctype/email_account/email_account.py:247
msgid "The server did not return any IMAP folders for this account."
msgstr ""
msgstr "Сервер није вратио ниједан IMAP директоријум за овај налог."
#: frappe/utils/response.py:343
msgid "The system is being updated. Please refresh again after a few moments."
@ -29089,7 +29089,7 @@ msgstr "Отпреми"
#: frappe/public/js/frappe/file_uploader/FileUploader.vue:663
msgid "Upload Failed"
msgstr ""
msgstr "Отпремање је неуспешно"
#: frappe/public/js/print_format_builder/LetterHeadEditor.vue:93
msgid "Upload Image"
@ -30783,7 +30783,7 @@ msgstr "Ставка бочне траке радног простора"
#: frappe/desk/doctype/workspace/workspace.js:58
msgid "Workspace added to desktop"
msgstr ""
msgstr "Радни простор је додат на радну површину"
#: frappe/public/js/frappe/views/workspace/workspace.js:558
msgid "Workspace {0} created"
@ -31698,7 +31698,7 @@ msgstr "нпр. \"Подршка\", \"Продаја\", \"Петар Петро
#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:230
msgid "e.g. (55 + 434) / 4"
msgstr ""
msgstr "на пример (55 + 434) / 4"
#. Description of the 'Incoming Server' (Data) field in DocType 'Email Account'
#. Description of the 'Incoming Server' (Data) field in DocType 'Email Domain'
@ -32860,7 +32860,7 @@ msgstr "{0} од {1} ({2} редова са зависним подацима)"
#: frappe/public/js/frappe/views/reports/report_view.js:456
msgid "{0} of {1} records match (filtered on visible rows only)"
msgstr ""
msgstr "{0} од {1} записа одговара критеријуму (филтрирано само по видљивим редовима)"
#: frappe/utils/data.py:1571
msgctxt "Money in words"

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2026-02-22 09:42+0000\n"
"PO-Revision-Date: 2026-02-23 22:07\n"
"PO-Revision-Date: 2026-02-26 23:27\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Serbian (Latin)\n"
"MIME-Version: 1.0\n"
@ -1358,7 +1358,7 @@ msgstr "Dodaj parametre upita"
#. Label of the add_reply_to_header (Check) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Add Reply-To header"
msgstr ""
msgstr "Dodaj zaglavlje adrese za odgovor"
#: frappe/core/doctype/user/user.py:860
msgid "Add Roles"
@ -1524,7 +1524,7 @@ msgstr "Dodaj na kontrolnu tablu"
#: frappe/desk/doctype/workspace/workspace.js:49
msgid "Add to Desktop"
msgstr ""
msgstr "Dodaj na radnu površinu"
#: frappe/public/js/frappe/form/sidebar/assign_to.js:110
msgid "Add to ToDo"
@ -1647,7 +1647,7 @@ msgstr "Adrese i kontakti"
#. Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Addresses added here will be used as the Reply-To header for outgoing emails sent from this account."
msgstr ""
msgstr "Adrese dodate ovde koristiće se kao adresa za odgovor za izlazne imejlove poslate sa ovog naloga."
#. Description of a DocType
#: frappe/custom/doctype/client_script/client_script.json
@ -3083,7 +3083,7 @@ msgstr "Istorija izmena"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/users.json
msgid "Audits"
msgstr ""
msgstr "Revizije"
#. Label of the auth_url_data (Code) field in DocType 'Social Login Key'
#: frappe/integrations/doctype/social_login_key/social_login_key.json
@ -3517,7 +3517,7 @@ msgstr "Slika pozadine"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/system.json
msgid "Background Job"
msgstr ""
msgstr "Pozadinski zadatak"
#. Label of a Link in the Build Workspace
#. Label of the background_jobs_section (Section Break) field in DocType
@ -4935,7 +4935,7 @@ msgstr "Kliknite da postavite filtere"
#: frappe/desk/page/desktop/desktop.js:1261
msgid "Click to edit"
msgstr ""
msgstr "Kliknite za uređivanje"
#: frappe/public/js/frappe/list/list_view.js:754
msgid "Click to sort by {0}"
@ -6538,7 +6538,7 @@ msgstr "Cijan"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "DELAY"
msgstr ""
msgstr "ODLAGANJE"
#. Option for the 'Method' (Select) field in DocType 'Recorder'
#. Option for the 'Request Method' (Select) field in DocType 'Webhook'
@ -7366,7 +7366,7 @@ msgstr "Status"
#. Label of the dsn_notify_type (Select) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Delivery Status Notification Type"
msgstr ""
msgstr "Vrsta obaveštenja o statusu isporuke"
#. Option for the 'Sign ups' (Select) field in DocType 'Social Login Key'
#: frappe/integrations/doctype/social_login_key/social_login_key.json
@ -9431,7 +9431,7 @@ msgstr "Planer omogućen"
#. Label of the enabled (Check) field in DocType 'Notification Settings'
#: frappe/desk/doctype/notification_settings/notification_settings.json
msgid "Enabled System Notification"
msgstr ""
msgstr "Omogućeno sistemsko obaveštenje"
#: frappe/email/doctype/email_account/email_account.py:1101
msgid "Enabled email inbox for user {0}"
@ -10129,7 +10129,7 @@ msgstr "Dodatni parametri"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "FAILURE"
msgstr ""
msgstr "NEUSPEH"
#. Option for the 'Social Login Provider' (Select) field in DocType 'Social
#. Login Key'
@ -10273,7 +10273,7 @@ msgstr "Neuspešan pokušaj prijave na Frappe Cloud"
#: frappe/email/doctype/email_account/email_account.py:232
msgid "Failed to retrieve the list of IMAP folders from the server. Please ensure the mailbox is accessible and the account has permission to list folders."
msgstr ""
msgstr "Neuspešno preuzimanje liste IMAP direktorijuma sa servera. Proverite da li je poštansko sanduče dostupno i da li nalog ima dozvolu za prikaz direktorijuma."
#: frappe/email/doctype/email_queue/email_queue.py:311
msgid "Failed to send email with subject:"
@ -11340,7 +11340,7 @@ msgstr "Jedinica frakcije"
#. Label of a Desktop Icon
#: frappe/desktop_icon/framework.json
msgid "Framework"
msgstr ""
msgstr "Framework"
#. Option for the 'Social Login Provider' (Select) field in DocType 'Social
#. Login Key'
@ -12236,7 +12236,7 @@ msgstr "Naslov"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/system.json
msgid "Health Report"
msgstr ""
msgstr "Izveštaj o stanju sistema"
#. Option for the 'Type' (Select) field in DocType 'Dashboard Chart'
#: frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@ -12655,7 +12655,7 @@ msgstr "IMAP datoteka"
#: frappe/email/doctype/email_account/email_account.py:235
#: frappe/email/doctype/email_account/email_account.py:263
msgid "IMAP Folder Not Found"
msgstr ""
msgstr "IMAP direktorijum nije pronađen"
#. Label of the ip_address (Data) field in DocType 'Activity Log'
#. Label of the ip_address (Data) field in DocType 'Comment'
@ -13415,7 +13415,7 @@ msgstr "Pogrešan verifikacioni kod"
#: frappe/public/js/frappe/views/gantt/gantt_view.js:88
msgid "Incorrect configuration"
msgstr ""
msgstr "Neispravna konfiguracija"
#: frappe/model/document.py:1733
msgid "Incorrect value in row {0}:"
@ -16980,7 +16980,7 @@ msgstr "MyISAM"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "NEVER"
msgstr ""
msgstr "NIKADA"
#: frappe/workflow/doctype/workflow/workflow.js:19
msgid "NOTE: If you add states or transitions in the table, it will be reflected in the Workflow Builder but you will have to position them manually. Also Workflow Builder is currently in <b>BETA</b>."
@ -17751,7 +17751,7 @@ msgstr "Nema podataka za izvoz"
#: frappe/public/js/frappe/views/reports/query_report.js:1543
msgid "No data to perform this action"
msgstr ""
msgstr "Nema podataka za izvršavanje ove radnje"
#: frappe/contacts/doctype/address/address.py:247
msgid "No default Address Template found. Please create a new one from Setup > Printing and Branding > Address Template."
@ -17796,7 +17796,7 @@ msgstr "Nema dodatnih zapisa"
#: frappe/public/js/frappe/views/reports/report_view.js:337
msgid "No matching entries in the current results"
msgstr ""
msgstr "Nema podudarnih zapisa u trenutnim rezultatima"
#: frappe/templates/includes/search_template.html:49
msgid "No matching records. Search something new"
@ -18430,7 +18430,7 @@ msgstr "OAuth greška"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/integrations.json
msgid "OAuth Provider"
msgstr ""
msgstr "OAuth provajder"
#. Name of a DocType
#. Label of a Link in the Integrations Workspace
@ -18699,7 +18699,7 @@ msgstr "Dozvoli uređivanje samo za"
#: frappe/core/doctype/module_def/module_def.py:95
msgid "Only Custom Modules can be renamed."
msgstr ""
msgstr "Isključivo prilagođeni moduli mogu biti preimenovani."
#: frappe/core/doctype/doctype/doctype.py:1652
msgid "Only Options allowed for Data field are:"
@ -19970,7 +19970,7 @@ msgstr "Molimo Vas da dodate validan komentar."
#: frappe/public/js/frappe/views/reports/query_report.js:1544
msgid "Please adjust filters to include some data"
msgstr ""
msgstr "Prilagodite filtere kako biste uključili neke podatke"
#: frappe/core/doctype/user/user.py:1122
msgid "Please ask your administrator to verify your sign-up"
@ -20030,7 +20030,7 @@ msgstr "Molimo Vas da kliknete na sledeći link da biste postavili novu lozinku"
#: frappe/public/js/frappe/views/gantt/gantt_view.js:89
msgid "Please configure the start field for this Doctype in the controller file."
msgstr ""
msgstr "Molimo Vas da konfigurišete početno polje za ovaj DocType u datoteci kontrolera."
#: frappe/www/confirm_workflow_action.html:4
msgid "Please confirm your action to {0} this document."
@ -21133,7 +21133,7 @@ msgstr "Ljubičasto"
#. Label of a Workspace Sidebar Item
#: frappe/workspace_sidebar/integrations.json
msgid "Push Notification"
msgstr ""
msgstr "Push obaveštenje"
#. Name of a DocType
#. Label of a Link in the Integrations Workspace
@ -22226,16 +22226,16 @@ msgstr "Odgovori svima"
#. Name of a DocType
#: frappe/email/doctype/reply_to_address/reply_to_address.json
msgid "Reply To Address"
msgstr ""
msgstr "Adresa za odgovor"
#: frappe/email/doctype/email_account/email_account.py:278
msgid "Reply To email is required"
msgstr ""
msgstr "Adresa za odgovor je obavezna"
#. Label of the reply_to_addresses (Table) field in DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Reply-To Addresses"
msgstr ""
msgstr "Adrese za odgovor"
#. Label of the report (Check) field in DocType 'Custom DocPerm'
#. Label of the report (Link) field in DocType 'Custom Role'
@ -23277,19 +23277,19 @@ msgstr "SSL/TLS režim"
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "SUCCESS"
msgstr ""
msgstr "USPEH"
#. Option for the 'Delivery Status Notification Type' (Select) field in DocType
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "SUCCESS,FAILURE"
msgstr ""
msgstr "USPEH, NEUSPEH"
#. Option for the 'Delivery Status Notification Type' (Select) field in DocType
#. 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "SUCCESS,FAILURE,DELAY"
msgstr ""
msgstr "USPEH, NEUSPEH, ODLAGANJE"
#: frappe/public/js/frappe/color_picker/color_picker.js:20
msgid "SWATCHES"
@ -24116,7 +24116,7 @@ msgstr "Izaberi dve verzije za prikaz razlika."
#. DocType 'Email Account'
#: frappe/email/doctype/email_account/email_account.json
msgid "Select which delivery events should trigger a delivery status notification (DSN) from the SMTP server."
msgstr ""
msgstr "Izaberite koji događaji isporuke treba da pokrenu obaveštenje o statusu isporuke (DNS) sa SMTP servera."
#: frappe/public/js/frappe/form/link_selector.js:24
#: frappe/public/js/frappe/form/multi_select_dialog.js:80
@ -27099,7 +27099,7 @@ msgstr "Komentar ne može biti prazan"
#: frappe/email/doctype/email_account/email_account.py:290
msgid "The configured SMTP server does not support DSN (Delivery Status Notification)."
msgstr ""
msgstr "Konfigurisani SMTP server ne podržava DSN (obaveštenje o statusu isporuke)."
#: frappe/templates/emails/workflow_action.html:9
msgid "The contents of this email are strictly confidential. Please do not forward this email to anyone."
@ -27161,7 +27161,7 @@ msgstr "Sledeća skripta zaglavlja će dodati trenutni datum u element klase 'he
#: frappe/email/doctype/email_account/email_account.py:257
msgid "The following configured IMAP folder(s) were not found on the server:<br><ul>{0}</ul>Please verify the folder names exactly as they appear on the server (folder names are case-sensitive)."
msgstr ""
msgstr "Sledeći konfigurisani IMAP direktorijumi nisu pronađeni na serveru:<br><ul>{0}</ul>Molimo Vas da proverite nazive direktorijuma tačno onako kako su prikazani na serveru (velika i mala slova su bitna za nazive datoteka)."
#: frappe/core/doctype/data_import/importer.py:1092
msgid "The following values are invalid: {0}. Values must be one of {1}"
@ -27253,7 +27253,7 @@ msgstr "Izabrani dokument {0} nije {1}."
#: frappe/email/doctype/email_account/email_account.py:247
msgid "The server did not return any IMAP folders for this account."
msgstr ""
msgstr "Server nije vratio nijedan IMAP direktorijum za ovaj nalog."
#: frappe/utils/response.py:343
msgid "The system is being updated. Please refresh again after a few moments."
@ -29089,7 +29089,7 @@ msgstr "Otpremi"
#: frappe/public/js/frappe/file_uploader/FileUploader.vue:663
msgid "Upload Failed"
msgstr ""
msgstr "Otpremanje je neuspešno"
#: frappe/public/js/print_format_builder/LetterHeadEditor.vue:93
msgid "Upload Image"
@ -30783,7 +30783,7 @@ msgstr "Stavka bočne trake radnog prostora"
#: frappe/desk/doctype/workspace/workspace.js:58
msgid "Workspace added to desktop"
msgstr ""
msgstr "Radni prostor je dodat na radnu površinu"
#: frappe/public/js/frappe/views/workspace/workspace.js:558
msgid "Workspace {0} created"
@ -31698,7 +31698,7 @@ msgstr "npr. \"Podrška\", \"Prodaja\", \"Petar Petrović\""
#: frappe/public/js/frappe/ui/toolbar/awesome_bar.js:230
msgid "e.g. (55 + 434) / 4"
msgstr ""
msgstr "na primer (55 + 434) / 4"
#. Description of the 'Incoming Server' (Data) field in DocType 'Email Account'
#. Description of the 'Incoming Server' (Data) field in DocType 'Email Domain'
@ -32860,7 +32860,7 @@ msgstr "{0} od {1} ({2} redova sa zavisnim podacima)"
#: frappe/public/js/frappe/views/reports/report_view.js:456
msgid "{0} of {1} records match (filtered on visible rows only)"
msgstr ""
msgstr "{0} od {1} zapisa odgovara kriterijumu (filtrirano samo po vidljivim redovima)"
#: frappe/utils/data.py:1571
msgctxt "Money in words"

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: developers@frappe.io\n"
"POT-Creation-Date: 2026-02-22 09:42+0000\n"
"PO-Revision-Date: 2026-02-23 22:07\n"
"PO-Revision-Date: 2026-02-28 23:51\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Swedish\n"
"MIME-Version: 1.0\n"
@ -2868,7 +2868,7 @@ msgstr "Tilldelning Klar"
#. Label of the assignment_days (Table) field in DocType 'Assignment Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Assignment Days"
msgstr "Automation Dagar"
msgstr "Tilldelning Dagar"
#. Name of a DocType
#. Label of the assignment_rule (Link) field in DocType 'ToDo'
@ -2876,7 +2876,7 @@ msgstr "Automation Dagar"
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
#: frappe/desk/doctype/todo/todo.json frappe/workspace_sidebar/automation.json
msgid "Assignment Rule"
msgstr "Automation Regel"
msgstr "Tilldelning Regel"
#. Name of a DocType
#: frappe/automation/doctype/assignment_rule_day/assignment_rule_day.json
@ -2890,13 +2890,13 @@ msgstr "Automation Regel Användare"
#: frappe/automation/doctype/assignment_rule/assignment_rule.py:55
msgid "Assignment Rule is not allowed on document type {0}"
msgstr "Automation Regel är ej tillåten på dokument typ {0}"
msgstr "Tilldelning Regel är ej tillåten på dokument typ {0}"
#. Label of the assignment_rules_section (Section Break) field in DocType
#. 'Assignment Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Assignment Rules"
msgstr "Automation Regler"
msgstr "Tilldelning Regler"
#: frappe/desk/doctype/notification_log/notification_log.py:153
msgid "Assignment Update on {0}"
@ -8565,7 +8565,7 @@ msgstr "Förfallo Datum"
#. Label of the due_date_based_on (Select) field in DocType 'Assignment Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Due Date Based On"
msgstr "Förfallo Datum Baserad På"
msgstr "Förfallodatum baserat på"
#: frappe/public/js/frappe/form/grid_row_form.js:44
#: frappe/public/js/frappe/form/toolbar.js:445
@ -9859,7 +9859,7 @@ msgstr "Exempel: Anges detta till 24:00 loggas användare ut om de inte är akti
#. Rule'
#: frappe/automation/doctype/assignment_rule/assignment_rule.json
msgid "Example: {{ subject }}"
msgstr "Exempel: {{subject}}"
msgstr "Exempel: {{ subject }}"
#. Option for the 'File Type' (Select) field in DocType 'Data Export'
#: frappe/core/doctype/data_export/data_export.json
@ -11577,7 +11577,7 @@ msgstr "Könsartad"
#: frappe/www/contact.html:29
msgid "General"
msgstr "Allmän"
msgstr "Allmänt"
#. Label of the generate_keys (Button) field in DocType 'User'
#: frappe/core/doctype/user/user.json
@ -14260,7 +14260,7 @@ msgstr "Är Privat"
#: frappe/desk/doctype/dashboard_chart/dashboard_chart.json
#: frappe/desk/doctype/number_card/number_card.json
msgid "Is Public"
msgstr "Är Allmän"
msgstr "Är Publik"
#. Label of the is_published_field (Data) field in DocType 'DocType'
#: frappe/core/doctype/doctype/doctype.json
@ -15978,7 +15978,7 @@ msgstr "Skapa {0}"
#: frappe/website/doctype/web_page/web_page.js:77
msgid "Makes the page public"
msgstr "Allmän Sida"
msgstr "Publik Sida"
#: frappe/desk/page/setup_wizard/install_fixtures.py:28
msgid "Male"
@ -21025,17 +21025,17 @@ msgstr "Leverantör Namn"
#: frappe/public/js/frappe/views/interaction.js:78
#: frappe/public/js/frappe/views/workspace/workspace.js:458
msgid "Public"
msgstr "Allmän"
msgstr "Publik"
#. Label of the public_files_size (Float) field in DocType 'System Health
#. Report'
#: frappe/desk/doctype/system_health_report/system_health_report.json
msgid "Public Files (MB)"
msgstr "Allmänna Filer (MB)"
msgstr "Publika Filer (MB)"
#: frappe/templates/emails/file_backup_notification.html:5
msgid "Public Files Backup:"
msgstr "Allmänna Filer Säkerhetskopiering:"
msgstr "Publika Filer Säkerhetskopiering:"
#. Label of the publish (Check) field in DocType 'Package Release'
#: frappe/core/doctype/package_release/package_release.json
@ -27569,7 +27569,7 @@ msgstr "Den här filen är offentlig och kan nås av vem som helst, även utan a
#: frappe/core/doctype/file/file.js:22
msgid "This file is public. It can be accessed without authentication."
msgstr "Denna fil är allmän fil. Den kan nås utan autentisering."
msgstr "Denna fil är publik. Den kan nås utan autentisering."
#: frappe/public/js/frappe/form/form.js:1248
msgid "This form has been modified after you have loaded it"
@ -29789,7 +29789,7 @@ msgstr "Värde måste vara ett av {0}"
#. 'OAuth Client'
#: frappe/integrations/doctype/oauth_client/oauth_client.json
msgid "Value of \"None\" implies a public client. In such a case Client Secret is not given to the client and token exchange makes use of PKCE."
msgstr "Värdet \"None\" innebär allmän klient. I ett sådant fall ges inte Klient Hemlighet till klient och token utbyte använder PKCE."
msgstr "Värdet \"None\" innebär publik klient. I ett sådant fall ges inte Klient Hemlighet till klient och token utbyte använder PKCE."
#. Label of the value_to_validate (Data) field in DocType 'Onboarding Step'
#: frappe/desk/doctype/onboarding_step/onboarding_step.json
@ -31299,7 +31299,7 @@ msgstr "Du behöver '{0}' behörighet på {1} {2} för att utföra denna åtgär
#: frappe/desk/doctype/workspace/workspace.py:132
#: frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py:74
msgid "You need to be Workspace Manager to delete a public workspace."
msgstr "Du måste vara Arbetsyta Ansvarig för att ta bort allmän arbetsyta."
msgstr "Du måste vara Arbetsyta Ansvarig för att ta bort publik arbetsyta."
#: frappe/desk/doctype/workspace/workspace.py:78
msgid "You need to be Workspace Manager to edit this document"

View file

@ -894,6 +894,12 @@ def get_field_currency(df, doc=None):
if frappe.get_meta(doc.parenttype).has_field(df.get("options")):
# only get_value if parent has currency field
currency = frappe.db.get_value(doc.parenttype, doc.parent, df.get("options"))
if not currency:
# Parent may not be in DB yet (new document being saved).
# Use the in-memory parent document reference if available.
parent = getattr(doc, "parent_doc", None)
if parent:
currency = parent.get(df.get("options"))
if currency:
frappe.local.field_currency.setdefault((doc.doctype, ref_docname), frappe._dict()).setdefault(

View file

@ -5,10 +5,10 @@ from typing import TYPE_CHECKING
import frappe
import frappe.permissions
from frappe import _, bold
from frappe import _, bold, scrub
from frappe.model.document import Document
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import validate_name
from frappe.model.naming import is_autoincremented, validate_name
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
from frappe.query_builder import Field
from frappe.utils.data import cint, cstr, sbool
@ -412,6 +412,11 @@ def rename_doctype(doctype: str, old: str, new: str) -> None:
# change parenttype for fieldtype Table
update_parenttype_values(old, new)
# if autoincrement is enabled, update sequence name
meta = frappe.get_meta(new)
if is_autoincremented(new, meta):
update_sequence_name(old, new)
def update_child_docs(old: str, new: str, meta: "Meta") -> None:
# update "parent"
@ -656,6 +661,15 @@ def update_parenttype_values(old: str, new: str):
frappe.qb.update(table).set(table.parenttype, new).where(table.parenttype == old).run()
def update_sequence_name(old: str, new: str, slug: str = "_id_seq"):
old_sequence_name = scrub(old + slug)
new_sequence_name = scrub(new + slug)
if frappe.db.db_type == "mariadb":
frappe.db.sql_ddl(f"RENAME TABLE `{old_sequence_name}` TO `{new_sequence_name}`")
else:
frappe.db.sql_ddl(f'ALTER SEQUENCE "{old_sequence_name}" RENAME TO "{new_sequence_name}"')
def rename_dynamic_links(doctype: str, old: str, new: str):
Singles = frappe.qb.DocType("Singles")
for df in get_dynamic_link_map().get(doctype, []):

View file

@ -29,13 +29,16 @@ TEST_WEIGHT_OVERRIDES = {
class ParallelTestRunner:
def __init__(self, app, site, build_number=1, total_builds=1, dry_run=False, lightmode=False):
def __init__(
self, app, site, build_number=1, total_builds=1, dry_run=False, lightmode=False, failfast=False
):
self.app = app
self.site = site
self.build_number = frappe.utils.cint(build_number) or 1
self.total_builds = frappe.utils.cint(total_builds)
self.dry_run = dry_run
self.lightmode = lightmode
self.failfast = failfast
self.test_file_list = []
self.total_test_weight = 0
self.test_result = None
@ -81,7 +84,9 @@ class ParallelTestRunner:
self.total_test_weight = sum(self.get_test_weight(test) for test in self.test_file_list)
def run_tests(self):
self.test_result = TestResult(stream=sys.stderr, descriptions=True, verbosity=2)
self.test_result = TestResult(
stream=sys.stderr, descriptions=True, verbosity=2, failfast=self.failfast
)
for test_file_info in self.test_file_list:
self.run_tests_for_file(test_file_info)

View file

@ -127,12 +127,12 @@
{
"fieldname": "image_height",
"fieldtype": "Float",
"label": "Image Height"
"label": "Image Height (px)"
},
{
"fieldname": "image_width",
"fieldtype": "Float",
"label": "Image Width"
"label": "Image Width (px)"
},
{
"depends_on": "eval:doc.footer_source==='Image' && doc.letter_head_name",
@ -148,12 +148,12 @@
{
"fieldname": "footer_image_height",
"fieldtype": "Float",
"label": "Image Height"
"label": "Image Height (px)"
},
{
"fieldname": "footer_image_width",
"fieldtype": "Float",
"label": "Image Width"
"label": "Image Width (px)"
},
{
"fieldname": "footer_align",
@ -203,7 +203,7 @@
"links": [],
"make_attachments_public": 1,
"max_attachments": 3,
"modified": "2026-02-24 20:53:14.297567",
"modified": "2026-02-25 14:37:57.061516",
"modified_by": "Administrator",
"module": "Printing",
"name": "Letter Head",

View file

@ -35,6 +35,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
this.show_start();
} else {
this.page.set_title(this.print_format.name);
this.page.sidebar.toggle(true);
this.setup_print_format();
}
}
@ -65,6 +66,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
this.page.main.html(frappe.render_template("print_format_builder_start", {}));
this.page.clear_actions();
this.page.set_title(__("Print Format Builder"));
this.page.sidebar.toggle(false);
this.start_edit_print_format();
this.start_new_print_format();
}

View file

@ -33,7 +33,14 @@ $(document).ready(function () {
!frappe.is_mobile() &&
frappe.user.has_role("System Manager");
if (visiblity_condition && isFCUser) {
addChatBubble();
frappe.router.on("change", function () {
if (frappe.get_route()[0] == "") {
addChatBubble();
toggleChatBubble(true);
} else {
toggleChatBubble(false);
}
});
}
if (isFCUser) {
$.extend(card_args, {
@ -89,19 +96,40 @@ function openFrappeCloudDashboard() {
}
function addChatBubble() {
if (checkBusinessHours()) {
const all_apps = frappe.utils.get_installed_apps();
const desk_apps = ["erpnext", "hrms"];
const apps_allowed = desk_apps.some((app) => all_apps.includes(app));
if (checkBusinessHours() && apps_allowed) {
let chat_banner = document.createElement("script");
chat_banner.setAttribute("id", "chat_widget_trigger");
chat_banner.innerHTML =
'(function(d,t){var BASE_URL="https://chat.frappe.cloud";var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src=BASE_URL+"/packs/js/sdk.js";g.async=true;s.parentNode.insertBefore(g,s);g.onload=function(){window.chatwootSDK.run({websiteToken:"LdmfJzftdJGEcFjoTqk8CrSq",baseUrl:BASE_URL})}})(document,"script");';
document.body.append(chat_banner);
const root = document.documentElement;
root.style.setProperty("--s-700", "var(--gray-50)");
root.style.setProperty("--s-700", "var(--gray-500)");
}
}
function checkBusinessHours() {
let currentTime = new Date();
const istTime = new Date(currentTime.toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
let current_time = new Date();
const ist_time = new Date(current_time.toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
return istTime.getHours() >= 11 && istTime.getHours() <= 18;
const hours = ist_time.getHours();
const day = ist_time.getDay();
const is_weekend = day === 0 || day === 6;
const is_business_hour = hours >= 11 && hours < 18;
return !is_weekend && is_business_hour;
}
function toggleChatBubble(toggle) {
if (toggle) {
$(".woot-widget-holder").show();
$("#cw-bubble-holder").show();
} else {
$(".woot-widget-holder").hide();
$("#cw-bubble-holder").hide();
}
}

View file

@ -514,22 +514,7 @@ function check_restrictions(file) {
return is_correct_type && valid_file_size;
}
function set_loading_state(dialog, loading) {
let $btn = dialog?.get_primary_btn();
if (loading) {
$btn?.css("width", $btn.outerWidth());
$btn?.html(`<i class="fa fa-spinner fa-spin"></i>`);
$btn?.prop("disabled", true);
dialog?.get_secondary_btn().prop("disabled", true);
} else {
$btn?.css("width", "");
$btn?.html(__("Upload"));
$btn?.prop("disabled", false);
dialog?.get_secondary_btn().prop("disabled", false);
}
}
function upload_files(dialog) {
set_loading_state(dialog, true);
function upload_files() {
if (show_file_browser.value) {
promise = upload_via_file_browser();
} else if (show_web_link.value) {
@ -542,7 +527,7 @@ function upload_files(dialog) {
} else {
promise = frappe.run_serially(files.value.map((file, i) => () => upload_file(file, i)));
}
return promise.finally(() => set_loading_state(dialog, false));
return promise;
}
function upload_via_file_browser() {
let selected_file = file_browser.value.selected_node;

View file

@ -151,6 +151,7 @@ class FileUploader {
const dialog_opts = {
title: title || __("Upload"),
primary_action_label: __("Upload"),
primary_action_loading_label: __("Uploading"),
primary_action: () => this.upload_files(),
on_page_show: () => {
this.uploader.wrapper_ready = true;

View file

@ -37,24 +37,35 @@ export default class Column {
}
resize_all_columns() {
// distribute all columns equally
let columns = this.section.wrapper.find(".form-column").length;
// distribute visible columns equally
let all_columns = this.section.wrapper.find(".form-column");
let visible_columns = all_columns.filter(":not(.hide-control)");
let columns = visible_columns.length || all_columns.length;
let colspan = cint(12 / columns);
if (columns == 5) {
colspan = 20;
}
this.section.wrapper
.find(".form-column")
.removeClass()
.addClass("form-column")
.addClass("col-sm-" + colspan);
all_columns.each(function () {
const $col = $(this);
const is_hidden = $col.hasClass("hide-control");
$col.removeClass()
.addClass("form-column")
.addClass("col-sm-" + colspan);
if (is_hidden) {
$col.addClass("hide-control");
}
});
}
add_field() {}
refresh() {
if (!this.df) return;
const hide = this.df.hidden || this.df.hidden_due_to_dependency;
this.wrapper.toggleClass("hide-control", !!hide);
this.resize_all_columns();
this.section.refresh();
}
}

View file

@ -2,7 +2,7 @@ import Picker from "../../color_picker/color_picker";
frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlData {
make_input() {
this.df.placeholder = this.df.placeholder || __("Choose a color");
this.df.placeholder = __(this.df.placeholder) || __("Choose a color");
super.make_input();
this.make_color_input();
}

View file

@ -3,6 +3,7 @@ import "./base_input";
import "./data";
import "./int";
import "./float";
import "./percent";
import "./currency";
import "./date";
import "./time";

View file

@ -241,7 +241,7 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
this.$input
.attr("data-fieldtype", this.df.fieldtype)
.attr("data-fieldname", this.df.fieldname)
.attr("placeholder", this.df.placeholder || "");
.attr("placeholder", __(this.df.placeholder || ""));
if (this.doctype) {
this.$input.attr("data-doctype", this.doctype);
}

View file

@ -32,5 +32,3 @@ frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlI
return this.df.precision || cint(frappe.boot.sysdefaults.float_precision, null);
}
};
frappe.ui.form.ControlPercent = frappe.ui.form.ControlFloat;

View file

@ -2,7 +2,7 @@ import Picker from "../../icon_picker/icon_picker";
frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlData {
make_input() {
this.df.placeholder = this.df.placeholder || __("Choose an icon");
this.df.placeholder = __(this.df.placeholder) || __("Choose an icon");
super.make_input();
this.get_all_icons();
this.make_icon_input();

View file

@ -0,0 +1,13 @@
frappe.ui.form.ControlPercent = class ControlPercent extends frappe.ui.form.ControlFloat {
format_for_input(value) {
if (value === null || value === undefined || isNaN(Number(value))) {
return "";
}
const precision = value.toString().split(".")[1]?.length || 0;
return format_number(
value,
this.get_number_format(),
Math.min(this.get_precision(), precision)
);
}
};

View file

@ -28,7 +28,7 @@ frappe.ui.form.ControlSelect = class ControlSelect extends frappe.ui.form.Contro
const placeholder_html = `<div class="placeholder ellipsis text-extra-muted ${
is_xs_input ? "xs" : ""
}">
<span>${this.df.placeholder}</span>
<span>${__(this.df.placeholder)}</span>
</div>`;
if (this.only_input) {
this.$wrapper.append(placeholder_html);

View file

@ -235,7 +235,7 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
theme: this.df.theme || "snow",
readOnly: this.disabled || this.df.read_only,
bounds: this.quill_container[0],
placeholder: this.df.placeholder || "",
placeholder: __(this.df.placeholder || ""),
};
// In a grid row where space is constrained, hide the toolbar.

View file

@ -1574,15 +1574,10 @@ frappe.ui.form.Form = class FrappeForm {
var scroll_to = frappe.route_options.scroll_to;
delete frappe.route_options.scroll_to;
var selector = [];
for (var key in scroll_to) {
var value = scroll_to[key];
selector.push(repl('[data-%(key)s="%(value)s"]', { key: key, value: value }));
}
selector = $(selector.join(" "));
if (selector.length) {
frappe.utils.scroll_to(selector);
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) {
@ -2106,7 +2101,7 @@ frappe.ui.form.Form = class FrappeForm {
}
// scroll to input
frappe.utils.scroll_to($el, true, 15);
frappe.utils.scroll_to($el, true, 15, $(".main-section"));
// focus if text field
if (focus) {

View file

@ -94,12 +94,15 @@ frappe.form.formatters = {
if (value === null) {
return "";
}
const valuePrecision = value.toString().split(".")[1]?.length || 0;
const precision =
docfield.precision ||
cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) ||
2;
return frappe.form.formatters._right(format_number(value, null, precision) + "%", options);
return frappe.form.formatters._right(
format_number(value, null, Math.min(precision, valuePrecision)) + "%",
options
);
},
Rating: function (value, docfield) {
let rating_html = "";

View file

@ -671,9 +671,11 @@ export default class Grid {
this.wrapper.find(".grid-footer").addClass("hidden");
}
// don't be tempted to use the `.hidden` class here
// it is used in other logic for the same buttons and will cause conflicts
this.wrapper
.find(".grid-add-row, .grid-add-multiple-rows, .grid-upload")
.toggleClass("hidden", !is_editable);
.toggleClass("d-none", !is_editable);
}
setup_fields() {

View file

@ -1033,7 +1033,7 @@ export default class GridRow {
let is_focused = false;
var $col = $(
`<div class="col grid-static-col col-xs-${colsize} ${add_class}" style="${add_style}"></div>`
`<div class="col grid-static-col flex col-xs-${colsize} ${add_class}" style="${add_style}"></div>`
)
.attr("data-fieldname", df.fieldname)
.attr("data-fieldtype", df.fieldtype)
@ -1095,7 +1095,9 @@ export default class GridRow {
return out;
});
$col.field_area = $('<div class="field-area"></div>').appendTo($col).toggle(false);
$col.field_area = $('<div class="field-area flex flex-grow-1"></div>')
.appendTo($col)
.toggle(false);
$col.static_area = $('<div class="static-area ellipsis"></div>').appendTo($col).html(txt);
// set title attribute to see full label for columns in the heading row

View file

@ -745,7 +745,7 @@ frappe.ui.form.Layout = class Layout {
if (f.df.fieldtype === "Table") {
for (const row of f.grid?.grid_rows || []) {
row.refresh_dependency();
row?.refresh_dependency();
}
}
}

View file

@ -20,7 +20,8 @@ frappe.ui.form.LinkedWith = class LinkedWith {
make_dialog() {
this.dialog = new frappe.ui.Dialog({
title: __("Linked With"),
title: __("Links"),
minimizable: true,
});
this.dialog.on_page_show = () => {
@ -39,22 +40,40 @@ frappe.ui.form.LinkedWith = class LinkedWith {
make_html() {
let html = "";
const linked_docs = this.frm.__linked_docs;
const linked_doctypes = Object.keys(linked_docs);
const linked_doctypes = Object.keys(linked_docs).filter((dt) => {
const entry = linked_docs[dt];
return (entry.docs && entry.docs.length) || entry.hidden_count > 0;
});
if (linked_doctypes.length === 0) {
html = __("Not Linked to any record");
} else {
html = linked_doctypes
.map((doctype) => {
const docs = linked_docs[doctype];
return `
<div class="list-item-table margin-bottom">
${this.make_doc_head(doctype)}
${docs.map((doc) => this.make_doc_row(doc, doctype)).join("")}
html = `
<div class="margin-bottom">
${__("Following documents are linked with {0}", [
frappe.utils
.get_form_link(this.frm.doctype, this.frm.docname, true)
.bold(),
])}
</div>
`;
})
.join("");
${linked_doctypes
.map((doctype) => {
const { docs, hidden_count } = linked_docs[doctype];
let rows = (docs || [])
.map((doc) => this.make_doc_row(doc, doctype))
.join("");
if (hidden_count > 0) {
rows += this.make_hidden_count_row(hidden_count);
}
return `
<div class="list-item-table margin-bottom">
${this.make_doc_head(doctype)}
${rows}
</div>
`;
})
.join("")}
`;
}
$(this.dialog.body).html(html);
@ -68,6 +87,16 @@ frappe.ui.form.LinkedWith = class LinkedWith {
`;
}
make_hidden_count_row(count) {
return `<div class="list-row-container">
<div class="level list-row small text-muted">
<div class="level-left">
${count == 1 ? __("{0} restricted document", [count]) : __("{0} restricted documents", [count])}
</div>
</div>
</div>`;
}
make_doc_row(doc, doctype) {
return `<div class="list-row-container">
<div class="level list-row small">

View file

@ -48,8 +48,7 @@ export class ReminderManager {
],
primary_action_label: __("Create"),
primary_action: () => {
this.create_reminder();
this.dialog.hide();
return this.create_reminder().then(() => this.dialog.hide());
},
secondary_action_label: __("Cancel"),
secondary_action: () => {
@ -84,7 +83,7 @@ export class ReminderManager {
}
create_reminder() {
frappe
return frappe
.xcall("frappe.automation.doctype.reminder.reminder.create_new_reminder", {
remind_at: this.dialog.get_value("remind_at"),
description: this.dialog.get_value("description"),

View file

@ -231,7 +231,7 @@ frappe.ui.form.check_mandatory = function (frm) {
}
function scroll_to(fieldname) {
if (frm.scroll_to_field(fieldname)) {
if (frm.scroll_to_field(fieldname, false)) {
frm.scroll_set = true;
}
}

View file

@ -114,9 +114,7 @@ frappe.ui.form.AssignToDialog = class AssignToDialog {
let args = me.dialog.get_values();
if (args && args.assign_to) {
me.dialog.set_message("Assigning...");
frappe.call({
return frappe.call({
method: me.method,
args: $.extend(args, {
doctype: me.doctype,
@ -125,15 +123,12 @@ frappe.ui.form.AssignToDialog = class AssignToDialog {
bulk_assign: me.bulk_assign || false,
re_assign: me.re_assign || false,
}),
btn: me.dialog.get_primary_btn(),
callback: function (r) {
if (!r.exc) {
if (me.callback) {
me.callback(r);
}
me.dialog && me.dialog.hide();
} else {
me.dialog.clear_message();
}
},
});

View file

@ -180,8 +180,18 @@ frappe.ui.form.Attachments = class Attachments {
file_url = "/files/" + attachment.file_name;
}
}
const is_web_url = /^(https?:)?\/\//i.test(file_url);
file_url = encodeURI(file_url);
// hash is not escaped, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
return encodeURI(file_url).replace(/#/g, "%23");
// only encode hash if it's a local file path, not a web URL
if (!is_web_url) {
file_url = file_url.replace(/#/g, "%23");
}
return file_url;
}
get_file_id_from_file_url(file_url) {
var fid;

View file

@ -452,9 +452,7 @@ export default class BulkOperations {
primary_action: () => {
let args = dialog.get_values();
if (args && args.tags) {
dialog.set_message("Adding Tags...");
frappe.call({
return frappe.call({
method: "frappe.desk.doctype.tag.tag.add_tags",
args: {
tags: args.tags,

View file

@ -120,7 +120,7 @@ export default class ListFilter {
fields: fields,
primary_action_label: __("Create"),
primary_action: (values) => {
this.bind_save_filter(dialog, values.filter_name, values?.is_global);
return this.bind_save_filter(dialog, values.filter_name, values?.is_global);
},
});
dialog.show();
@ -138,7 +138,7 @@ export default class ListFilter {
dialog.fields_dict.filter_name.set_description(__("Duplicate Filter Name"));
return;
}
this.save_filter(value, is_global).then(() => {
return this.save_filter(value, is_global).then(() => {
this.refresh_list_filter();
dialog.hide();
});

View file

@ -332,7 +332,8 @@ $.extend(frappe.meta, {
} else if (df && df.fieldtype === "Currency") {
precision = cint(frappe.defaults.get_default("currency_precision"));
if (!precision) {
var number_format = get_number_format();
var currency = frappe.meta.get_field_currency(df, doc);
var number_format = get_number_format(currency);
var number_format_info = get_number_format_info(number_format);
precision = number_format_info.precision;
}

View file

@ -48,9 +48,6 @@ frappe.ui.AddressAutocompleteDialog = class AddressAutocompleteDialog {
],
primary_action_label: __("Create Address"),
primary_action: () => {
// Insert the address into the database
dialog.hide();
const address = this.parse_selected_value();
address["doctype"] = "Address";
address["links"] = [
@ -59,7 +56,8 @@ frappe.ui.AddressAutocompleteDialog = class AddressAutocompleteDialog {
link_name: this.link_name,
},
];
frappe.db.insert(address).then((doc) => {
return frappe.db.insert(address).then((doc) => {
dialog.hide();
this.after_insert && this.after_insert(doc);
});
},

View file

@ -207,6 +207,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.has_primary_action = true;
var me = this;
const primary_btn = this.get_primary_btn().removeClass("hide").html(label);
const spinner = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" style="width: 13px; height: 13px; animation: spin 1s linear infinite;"><circle opacity=".25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path opacity=".25" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/></svg>`;
if (typeof click == "function") {
primary_btn.off("click").on("click", function () {
me.primary_action_fulfilled = true;
@ -215,7 +216,35 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
// if no values then return
var values = me.get_values();
if (!values) return;
click && click.apply(me, [values]);
const action = click.apply(me, [values]);
if (action && typeof action.then === "function") {
const loading_label = me.primary_action_loading_label;
primary_btn
.css({
"min-width": primary_btn.outerWidth(),
"min-height": primary_btn.outerHeight(),
})
.prop("disabled", true)
.addClass("btn-primary-dark")
.html(
`<div class="d-flex align-items-center justify-content-center" style="gap: 0.45rem;">
${spinner}
${
loading_label
? `<span class="text-muted" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${loading_label}</span>`
: ""
}
</div>`
);
Promise.resolve(action).finally(() => {
primary_btn
.css({ "min-width": "", "min-height": "" })
.prop("disabled", false)
.removeClass("btn-primary-dark")
.html(label);
});
}
});
}
return primary_btn;

View file

@ -18,12 +18,35 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
}
}
resolve_date_default_keywords(def_value, fieldtype) {
if (!def_value || typeof def_value !== "string") return def_value;
def_value = def_value.toLowerCase();
if (def_value == "today" && fieldtype == "Date") {
return frappe.datetime.get_today();
}
if (def_value == "now") {
if (fieldtype == "Datetime") {
return frappe.datetime.now_datetime();
}
if (fieldtype == "Time") {
return frappe.datetime.now_time();
}
}
return def_value;
}
make() {
let me = this;
if (this.fields) {
super.make();
this.refresh();
// set default
let defaults = {};
$.each(this.fields_list, function (i, field) {
let def_value = field.df["default"];
// loose equality check matches undefined also
@ -33,12 +56,14 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
)
return;
if (def_value == "Today" && field.df["fieldtype"] == "Date") {
def_value = frappe.datetime.get_today();
if (["Date", "Datetime", "Time"].includes(field.df.fieldtype)) {
def_value = me.resolve_date_default_keywords(def_value, field.df.fieldtype);
}
field.set_input(def_value);
// if default and has depends_on, render its fields.
defaults[field.df.fieldname] = def_value;
});
this.set_values(defaults).then(() => {
me.refresh_dependency();
});

View file

@ -206,7 +206,7 @@ frappe.msgprint = function (msg, title, is_minimizable, re_route) {
typeof data.primary_action.server_action === "string"
) {
data.primary_action.action = () => {
frappe.call({
return frappe.call({
method: data.primary_action.server_action,
args: data.primary_action.args,
callback() {

View file

@ -242,7 +242,7 @@ frappe.ui.Page = class Page {
}
setup_main_sidebar_toggle() {
$(".sidebar-toggle-btn.navbar-brand").on("click", (event) => {
this.wrapper.find(".sidebar-toggle-btn.navbar-brand").on("click", (event) => {
frappe.app.sidebar.set_height();
frappe.app.sidebar.toggle_width();
frappe.app.sidebar.prevent_scroll();

View file

@ -1,10 +1,12 @@
<div class="sticky-top">
{% if !localStorage.getItem("dismissed_announcement_widget") && strip_html(navbar_settings.announcement_widget) != '' %}
<div class="announcement-widget form-message p-2 m-0" style="position: relative; z-index: -1; border-radius: 0; background-color: var(--bg-blue);">
{% if (!navbar_settings.dismissible_announcement_widget || !localStorage.getItem("dismissed_announcement_widget")) && strip_html(navbar_settings.announcement_widget) != '' %}
<div class="announcement-widget form-message p-2 m-0" style="position: relative; z-index: -1; border-radius: 0; background-color: {{ navbar_settings.announcement_widget_color || 'var(--bg-blue)' }};">
<div class="container flex justify-between align-center mx-auto">
{{ navbar_settings.announcement_widget }}
{% if navbar_settings.dismissible_announcement_widget %}
<div class="close-message p-0 mr-2" style="position: relative;">
{{ frappe.utils.icon("close") }}
{% endif %}
</div>
</div>
</div>

View file

@ -9,7 +9,8 @@ frappe.ui.toolbar.Toolbar = class {
if (
frappe.boot.read_only ||
frappe.boot.user.impersonated_by ||
(!localStorage.getItem("dismissed_announcement_widget") &&
((!localStorage.getItem("dismissed_announcement_widget") ||
!frappe.boot.navbar_settings.dismissible_announcement_widget) &&
strip_html(frappe.boot.navbar_settings.announcement_widget) != "") ||
frappe.is_mobile()
) {

View file

@ -306,7 +306,7 @@ function markReset(step) {
</div>
<div v-else>
<span
class="text-base onb-step-text"
class="text-base onb-step-text text-extra-muted"
style="text-decoration-line: line-through"
>
{{ __(step.action_label) }}

View file

@ -132,7 +132,7 @@ frappe.dashboard_utils = {
remove_common_static_filter_values(static_filters, dynamic_filters) {
if (dynamic_filters) {
if ($.isArray(static_filters)) {
if (Array.isArray(static_filters)) {
static_filters = static_filters.filter((static_filter) => {
for (let dynamic_filter of dynamic_filters) {
if (
@ -207,29 +207,26 @@ frappe.dashboard_utils = {
? JSON.parse(doc.dynamic_filters_json)
: null;
if (!dynamic_filters || !Object.keys(dynamic_filters).length) {
if (!dynamic_filters?.length) {
return filters;
}
if (Array.isArray(dynamic_filters)) {
dynamic_filters.forEach((f) => {
try {
f[3] = eval(f[3]);
} catch (e) {
frappe.throw(__("Invalid expression set in filter {0} ({1})", [f[1], f[0]]));
}
});
dynamic_filters.forEach((f) => {
try {
f[3] = eval(f[3]);
} catch (e) {
frappe.throw(__("Invalid expression set in filter {0} ({1})", [f[1], f[0]]));
}
});
if (!filters) {
filters = dynamic_filters;
} else if (Array.isArray(filters)) {
filters = [...filters, ...dynamic_filters];
} else {
for (let key of Object.keys(dynamic_filters)) {
try {
const val = eval(dynamic_filters[key]);
dynamic_filters[key] = val;
} catch (e) {
frappe.throw(__("Invalid expression set in filter {0}", [key]));
}
}
Object.assign(filters, dynamic_filters);
dynamic_filters.forEach((f) => {
filters[f[1]] = f[3];
});
}
return filters;
@ -264,7 +261,7 @@ frappe.dashboard_utils = {
primary_action: (values) => {
values.name = docname;
values.set_standard = frappe.boot.developer_mode;
frappe.xcall(method, { args: values }).then(() => {
return frappe.xcall(method, { args: values }).then(() => {
let dashboard_route_html = `<a href = "/desk/dashboard/${values.dashboard}">${values.dashboard}</a>`;
let message = __("{0} {1} added to Dashboard {2}", [
doctype,
@ -273,9 +270,8 @@ frappe.dashboard_utils = {
]);
frappe.msgprint(message);
dialog.hide();
});
dialog.hide();
},
});

View file

@ -328,7 +328,7 @@ Object.assign(frappe.utils, {
scroll_top =
typeof element == "number"
? element - cint(additional_offset)
: this.get_scroll_position(element, additional_offset);
: this.get_scroll_position(element, additional_offset, element_to_be_scrolled);
}
if (scroll_top < 0) {
@ -366,10 +366,33 @@ Object.assign(frappe.utils, {
element_to_be_scrolled.scrollTop(scroll_top);
}
},
get_scroll_position: function (element, additional_offset) {
let header_offset =
$(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height();
return $(element).offset().top - header_offset - cint(additional_offset);
get_scroll_position: function (element, additional_offset, element_to_be_scrolled) {
const get_offset_relative_to_container = () => {
let offset = 0;
let el = element instanceof HTMLElement ? element : element[0];
const container = element_to_be_scrolled ? element_to_be_scrolled[0] : null;
while (el && el !== container && el.offsetParent) {
offset += el.offsetTop;
el = el.offsetParent;
}
return offset;
};
const get_header_offset = () => {
const navbar_height = $(".navbar").height() || 0;
const page_head_height = $(".page-head:visible").height() || 0;
const tabs_container_height = $(".form-tabs-list:visible").height() || 0;
return navbar_height + page_head_height + tabs_container_height;
};
const element_offset_top = get_offset_relative_to_container();
const header_offset = get_header_offset();
return element_offset_top - header_offset - cint(additional_offset);
},
filter_dict: function (dict, filters) {
var ret = [];
@ -2229,4 +2252,16 @@ Object.assign(frappe.utils, {
}
return value;
},
get_installed_apps() {
return frappe.boot.app_data.map((app) => {
return app.app_name;
});
},
is_sub_array(big, small) {
let i = 0;
for (let num of big) {
if (num === small[i]) i++;
}
return i === small.length;
},
});

View file

@ -449,7 +449,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
: chart.chart_type;
chart.document_type = this.doctype;
chart.filters_json = "[]";
frappe
return frappe
.xcall(
"frappe.desk.doctype.dashboard_chart.dashboard_chart.create_dashboard_chart",
{ args: chart }
@ -460,6 +460,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
name: doc.chart_name,
label: chart.label,
});
dialog.hide();
});
} else {
this.chart_group.new_widget.on_create({
@ -467,8 +468,8 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
label: __(chart.chart),
name: chart.chart,
});
dialog.hide();
}
dialog.hide();
},
});
dialog.show();

View file

@ -17,7 +17,7 @@ frappe.views.InteractionComposer = class InteractionComposer {
fields: me.get_fields(),
primary_action_label: __("Create"),
primary_action: function () {
me.create_action();
return me.create_action();
},
});

View file

@ -762,9 +762,39 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
let data = r.message;
this.hide_status();
clearInterval(this.interval);
clearInterval(this.stale_report_interval);
this.refreshed_at = frappe.datetime.now_datetime();
this.execution_time = data.execution_time || 0.1;
const check_if_report_is_stale = () => {
let generated_at = this.prepared_report
? this.prepared_report_document.report_end_time
: this.refreshed_at;
let pretty_diff = frappe.datetime.comment_when(generated_at);
const days_old = frappe.datetime.get_day_diff(
frappe.datetime.now_datetime(),
generated_at
);
const minutes_old = frappe.datetime.get_minute_diff(
frappe.datetime.now_datetime(),
generated_at
);
if (days_old > 1) {
pretty_diff = `<span style="color:var(--red-600)">${pretty_diff}</span>`;
}
if (minutes_old >= 1) {
this.show_status(`
<div class="indicator orange pl-1">
<span>
${__("This report was generated {0}.", [pretty_diff])}
</span>
</div>
`);
}
};
this.stale_report_interval = setInterval(check_if_report_is_stale, 60000);
if (data.custom_filters) {
this.set_filters(data.custom_filters);
this.previous_filters = data.custom_filters;
@ -787,6 +817,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
});
}
this.add_prepared_report_buttons(data.doc);
check_if_report_is_stale();
}
if (data.report_summary) {
@ -865,28 +896,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
},
__("Actions")
);
let pretty_diff = frappe.datetime.comment_when(doc.report_end_time);
const days_old = frappe.datetime.get_day_diff(
frappe.datetime.now_datetime(),
doc.report_end_time
);
if (days_old > 1) {
pretty_diff = `<span style="color:var(--red-600)">${pretty_diff}</span>`;
}
const part1 = __("This report was generated {0}.", [pretty_diff]);
const part2 = __("To get the updated report, click on {0}.", [__("Rebuild")]);
const part3 = __("See all past reports.");
this.show_status(`
<div class="indicator orange">
<span>
${part1}
${part2}
<a href="/desk/List/Prepared%20Report?report_name=${this.report_name}"> ${part3}</a>
</span>
</div>
`);
}
// Three cases
@ -2109,7 +2118,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
},
],
primary_action: (values) => {
frappe.call({
return frappe.call({
method: "frappe.desk.query_report.save_report",
args: {
reference_report: this.report_name,

View file

@ -99,16 +99,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
});
}
setup_paging_area() {
super.setup_paging_area();
const message = __(
"For comparison, use >5, <10 or =324. For ranges, use 5:10 (for values between 5 & 10)."
);
this.$paging_area.before(
`<span class="comparison-message text-extra-muted">${message}</span>`
);
}
setup_sort_selector() {
this.sort_selector = new frappe.ui.SortSelector({
parent: this.filter_area.$filter_list_wrapper,
@ -430,6 +420,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
setup_inline_filter_observer() {
this.setup_inline_filter_help_icons();
this.$datatable_wrapper.on(
"keyup",
".dt-filter",
@ -439,6 +431,29 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
);
}
setup_inline_filter_help_icons() {
const message = __(
"For comparison, use >5, <10 or =324.\nFor ranges, use 5:10 (for values between 5 & 10)."
);
this.$datatable_wrapper.find(".dt-filter").each((_, input) => {
const $input = $(input);
if ($input.siblings(".comparison-help-icon").length) {
return;
}
const $icon = $(
`<span class="comparison-help-icon text-muted" title="${message}">${frappe.utils.icon(
"info",
"xs"
)}</span>`
);
$input.after($icon);
});
}
update_count_for_inline_filter() {
if (!this.datatable) return;

View file

@ -570,8 +570,14 @@ export default class ChartWidget extends Widget {
let setup_dashboard_chart = () => {
const chart_args = this.get_chart_args();
const is_circular_chart = ["Pie", "Donut", "Percentage"].includes(this.chart_doc.type);
if (!this.dashboard_chart) {
this.dashboard_chart = frappe.utils.make_chart(this.chart_wrapper[0], chart_args);
} else if (is_circular_chart) {
this.chart_wrapper.empty();
delete this.dashboard_chart;
this.dashboard_chart = frappe.utils.make_chart(this.chart_wrapper[0], chart_args);
} else {
this.dashboard_chart.update(this.data);
}
@ -619,6 +625,7 @@ export default class ChartWidget extends Widget {
colors: colors,
height: this.height,
maxSlices: this.chart_doc.number_of_groups || max_slices,
truncateLegends: 0,
axisOptions: {
xIsSeries: this.chart_doc.timeseries,
shortenYAxisNumbers: 1,

View file

@ -1,11 +1,20 @@
<script setup>
import { ref, computed, nextTick } from "vue";
import { ref, computed, nextTick, watch } from "vue";
import { useStore } from "../store";
let store = useStore();
let title = ref("Workflow Details");
watch(
() => store.workflow_doc?.document_type,
async (newDocType) => {
if (!newDocType) return;
await store.update_is_submittable();
store.reset_non_submittable_states();
}
);
let doc = computed(() => {
return store.workflow.selected ? store.workflow.selected.data : store.workflow_doc;
});
@ -61,6 +70,7 @@ let properties = computed(() => {
v-model="doc[df.fieldname]"
:data-fieldname="df.fieldname"
:data-fieldtype="df.fieldtype"
:read_only="df.fieldname === 'doc_status' ? !store.is_submittable : false"
/>
</div>
</div>

View file

@ -12,6 +12,7 @@ export const useStore = defineStore("workflow-builder-store", () => {
let statefields = ref([]);
let transitionfields = ref([]);
let ref_history = ref(null);
let is_submittable = ref(true);
async function fetch() {
await frappe.model.clear_doc("Workflow", workflow_name.value);
@ -61,6 +62,9 @@ export const useStore = defineStore("workflow-builder-store", () => {
workflow.value.elements = get_workflow_elements(workflow_doc.value, workflow_data);
await update_is_submittable();
reset_non_submittable_states();
setup_undo_redo();
setup_breadcrumbs();
}
@ -135,13 +139,46 @@ export const useStore = defineStore("workflow-builder-store", () => {
frappe.breadcrumbs.$breadcrumbs.append(breadcrumbs);
}
async function update_is_submittable() {
if (!workflow_doc.value?.document_type) {
is_submittable.value = true;
return;
}
await frappe.model.with_doctype(workflow_doc.value.document_type);
is_submittable.value =
frappe.get_meta(workflow_doc.value.document_type)?.is_submittable || false;
}
function reset_non_submittable_states() {
if (is_submittable.value) return;
let has_affected_states = false;
workflow.value.elements.forEach((el) => {
if (el.type === "state" && el.data.doc_status && el.data.doc_status !== "Draft") {
has_affected_states = true;
el.data.doc_status = "Draft";
}
});
if (has_affected_states) {
frappe.msgprint({
title: __("Doc Status Reset"),
message: __(
"The <strong>Doc Status</strong> for all states has been reset to <strong>Draft</strong> because <strong>{0}</strong> is not submittable",
[workflow_doc.value.document_type]
),
indicator: "orange",
});
}
}
function get_state_df(data) {
let doc_status_map = {
Draft: 0,
Submitted: 1,
Cancelled: 2,
};
data.doc_status = doc_status_map[data.doc_status];
data.doc_status = is_submittable.value ? doc_status_map[data.doc_status] : 0;
return data;
}
@ -234,5 +271,8 @@ export const useStore = defineStore("workflow-builder-store", () => {
reset_changes,
save_changes,
setup_undo_redo,
is_submittable,
update_is_submittable,
reset_non_submittable_states,
};
});

View file

@ -142,10 +142,6 @@ body {
-webkit-transform: translate(-50%, -50%);
}
.hide {
display: none !important;
}
.btn-link {
box-shadow: none !important;
outline: none;
@ -159,6 +155,10 @@ body {
@extend .d-none;
}
.hide {
@extend .d-none;
}
.margin {
margin: var(--margin-sm);
}

View file

@ -270,6 +270,7 @@
.col:last-child {
border: none;
background-color: var(--fg-color);
}
.btn-open-row {
@ -309,8 +310,8 @@
border-radius: 0px;
border: 0px;
padding-top: 10px;
padding-bottom: calc(var(--padding-md) - 3px);
height: auto;
padding-bottom: 10px;
height: 100%;
}
.link-btn {
@ -430,6 +431,7 @@
.frappe-control {
margin-bottom: 0px !important;
position: relative;
flex-grow: 1;
}
.col-sm-6 {
@ -779,7 +781,7 @@
.data-row.row {
flex-wrap: nowrap;
}
.frappe-control[data-fieldtype="Table"].form-group:has(.column-limit-reached) {
.frappe-control[data-fieldtype="Table"].form-group:has(.column-limit-reached):not(.highlight) {
overflow-x: clip;
}
.column-limit-reached {

View file

@ -2,6 +2,12 @@ h5.modal-title {
margin: 0px !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
// Hack to fix incorrect padding applied by Bootstrap
body.modal-open[style^="padding-right"] {
padding-right: 12px !important;
@ -103,6 +109,11 @@ body.modal-open[style^="padding-right"] {
button:not(:last-child) {
margin-right: var(--margin-xs);
}
.btn-primary-dark {
min-width: 80px;
max-width: 200px;
}
}
& > * {

View file

@ -371,6 +371,27 @@ input.list-header-checkbox {
.list-item-table {
border: 1px solid $border-color;
border-radius: 3px;
.list-row-head {
border-radius: unset;
}
.list-row-container {
border-bottom: 1px solid $border-color;
border-radius: unset;
&:last-child {
border-bottom: none;
}
}
.list-row-container:hover {
border-radius: unset;
}
.list-row-container .list-row {
border-bottom: none;
}
}
.list-item {

View file

@ -93,6 +93,37 @@
border-radius: var(--border-radius);
}
}
.report-view {
.layout-main-section {
height: calc(100vh - var(--page-head-height));
display: flex;
flex-direction: column;
overflow: hidden;
.page-form {
flex-shrink: 0;
}
.frappe-list {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.result,
.no-result {
flex-grow: 1;
overflow: auto;
}
.comparison-message {
display: none;
}
}
}
}
@include media-breakpoint-up(sm) {
.report-view {
width: calc(100% - 220px);
@ -129,6 +160,36 @@
@include get_textstyle("base", "regular");
}
.report-view {
.datatable .dt-row-filter .dt-cell__content {
position: relative;
}
.datatable .dt-row-filter .dt-filter.dt-input {
padding-inline-end: 1.5rem;
}
.datatable .dt-row-filter .comparison-help-icon {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
display: inline-flex;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
.icon {
stroke: currentColor;
}
}
.datatable .dt-row-filter .dt-filter.dt-input:focus + .comparison-help-icon {
opacity: 1;
pointer-events: auto;
}
}
.list-count {
margin-right: var(--margin-sm);
@include get_textstyle("base", "regular");

View file

@ -4,7 +4,7 @@
{% if image or user_info.image %}
<img
class="avatar-frame standard-image"
src="{{ image or user_info.image }}"
src="{{ (image or user_info.image) |e }}"
title="{{ full_name|e or user_info.name|e }}">
{% else %}
<span

View file

@ -2,6 +2,6 @@
{%- block value -%}
<div class="value">
<img class="w-100" src="{{ value }}" alt="{{ df.label }}">
<img class="w-100" src="{{ value }}" alt="{{ _(df.label) }}">
</div>
{%- endblock -%}

View file

@ -1,7 +1,7 @@
{% if value %}
<div class="field {{ df.section.field_orientation or '' }}" {{ field_attributes(df) }}>
{%- block label -%}
<div class="label">{{ df.label }}</div>
<div class="label">{{ _(df.label) }}</div>
{%- endblock -%}
{%- block value -%}
<div class="value">{{ doc.get_formatted(df.fieldname) }}</div>

View file

@ -2,6 +2,6 @@
{%- block value -%}
<div class="value">
<img src="{{ value }}" alt="{{ df.label }}">
<img src="{{ value }}" alt="{{ _(df.label) }}">
</div>
{%- endblock -%}

View file

@ -1,7 +1,7 @@
{% if doc.get(df.fieldname) %}
<div class="child-table" {{ field_attributes(df) }}>
<div class="label">
{{ df.label }}
{{ _(df.label) }}
</div>
<table class="table table-bordered">
{% set columns = df.table_columns %}
@ -9,7 +9,7 @@
<tr class="table-row">
{% for column in columns %}
<th class="column-header" width="{{ column.width }}%" {{ field_attributes(column) }}>
{{ column.label }}
{{ _(column.label) }}
</th>
{% endfor %}
</tr>

View file

@ -21,7 +21,7 @@
{% for section in layout.sections %}
<div class="section {{ resolve_class({'page-break': section.page_break}) }}">
{% if section.label %}
<div class="section-label">{{ section.label }}</div>
<div class="section-label">{{ _(section.label) }}</div>
{% endif %}
<div class="section-columns row">

View file

@ -29,8 +29,9 @@ logger = logging.getLogger(__name__)
class TestResult(unittest.TextTestResult):
def __init__(self, stream, descriptions, verbosity):
def __init__(self, stream, descriptions, verbosity, failfast=False):
super().__init__(stream, descriptions, verbosity)
self.failfast = failfast
self._old_stdout = []
self._old_stderr = []

View file

@ -44,6 +44,7 @@ def clean_html(html):
"tbody",
"td",
"tr",
"a",
},
clean_content_tags=REMOVE_CONTENT_TAGS,
strip_comments=True,

View file

@ -259,7 +259,7 @@ def get_context(context):
context.boot = get_boot_data()
context.boot["link_title_doctypes"] = frappe.boot.get_link_title_doctypes()
context.webform_banner_image = self.banner_image
context.webform_banner_image = context.get("banner_image") or self.banner_image
context.pop("banner_image", None)
def add_metatags(self, context):

View file

@ -16,6 +16,10 @@ frappe.ui.form.on("Website Settings", {
frm.add_custom_button(__("View Website"), () => {
window.open("/", "_blank");
});
// Check if templates have fields and show/hide edit button
frm.events.check_template_has_fields(frm, "navbar_template");
frm.events.check_template_has_fields(frm, "footer_template");
},
set_banner_from_image: function (frm) {
@ -100,11 +104,36 @@ frappe.ui.form.on("Website Settings", {
frappe.show_alert(__("Please select {0}", [frm.get_docfield(template_field).label]));
return;
}
let values = JSON.parse(frm.doc[values_field] || "{}");
open_web_template_values_editor(template, values).then((new_values) => {
frm.set_value(values_field, JSON.stringify(new_values));
});
},
check_template_has_fields(frm, template_field) {
let template = frm.doc[template_field];
let button_field = "edit_" + template_field + "_values";
if (!template || template === "Standard Navbar" || template === "Standard Footer") {
frm.toggle_display(button_field, false);
return;
}
frappe.model.with_doc("Web Template", template, () => {
let doc = frappe.model.get_doc("Web Template", template);
let has_fields = doc.fields && doc.fields.length > 0;
frm.toggle_display(button_field, has_fields);
});
},
navbar_template(frm) {
frm.events.check_template_has_fields(frm, "navbar_template");
},
footer_template(frm) {
frm.events.check_template_has_fields(frm, "footer_template");
},
});
frappe.ui.form.on("Top Bar Item", {

View file

@ -31,7 +31,7 @@ frappe.ui.form.on("Website Slideshow", {
],
primary_action_label: __("Add to table"),
primary_action: ({ reference_doctype, reference_name }) => {
frappe.db
return frappe.db
.get_list("File", {
fields: ["file_url"],
filters: {

View file

@ -108,17 +108,21 @@ class TestWorkflow(IntegrationTestCase):
self.assertEqual(workflow_actions[0].status, "Completed")
def test_if_workflow_set_on_action(self):
self.workflow._update_state_docstatus = True
self.workflow.states[1].doc_status = 1
self.workflow.save()
todo = create_new_todo()
self.assertEqual(todo.docstatus, 0)
todo.submit()
self.assertEqual(todo.docstatus, 1)
self.assertEqual(todo.workflow_state, "Approved")
dt = create_new_submittable_doctype()
workflow = create_submittable_workflow(dt.name)
doc = frappe.get_doc({"doctype": dt.name, "test_field": "test"}).insert()
self.workflow.states[1].doc_status = 0
self.workflow.save()
workflow._update_state_docstatus = True
workflow.states[1].doc_status = 1
workflow.save()
self.assertEqual(doc.docstatus, 0)
doc.submit()
self.assertEqual(doc.docstatus, 1)
self.assertEqual(doc.workflow_state, "Approved")
workflow.states[1].doc_status = 0
workflow.save()
def test_syntax_error_in_transition_rule(self):
self.workflow.transitions[0].condition = 'doc.status =! "Closed"'
@ -350,6 +354,55 @@ def create_new_todo():
return frappe.get_doc(doctype="ToDo", description="workflow " + random_string(10)).insert()
def create_new_submittable_doctype():
return frappe.get_doc(
{
"doctype": "DocType",
"module": "Core",
"name": "Test Submittable Doc",
"custom": 1,
"is_submittable": 1,
"fields": [
{"label": "Field", "fieldname": "test_field", "fieldtype": "Data"},
{
"label": "Workflow State",
"fieldname": "workflow_state",
"fieldtype": "Link",
"options": "Workflow State",
},
],
"permissions": [{"role": "System Manager", "read": 1, "write": 1, "submit": 1, "cancel": 1}],
}
).insert(ignore_if_duplicate=True)
def create_submittable_workflow(doctype):
workflow = frappe.get_doc(
{
"doctype": "Workflow",
"workflow_name": "Submittable Workflow",
"document_type": doctype,
"workflow_state_field": "workflow_state",
"is_active": 1,
"states": [
{"state": "Pending", "allow_edit": "All"},
{"state": "Approved", "allow_edit": "System Manager", "doc_status": 0},
],
"transitions": [
{
"state": "Pending",
"action": "Approve",
"next_state": "Approved",
"allowed": "System Manager",
"allow_self_approval": 1,
}
],
}
).insert(ignore_permissions=True, ignore_if_duplicate=True)
return workflow
def create_new_note(doc):
note = frappe.new_doc("Note")
note.title = "workflow - " + doc.name

View file

@ -109,9 +109,10 @@ frappe.ui.form.on("Workflow", {
return;
}
frappe.model.with_doctype(doc.document_type, () => {
const fieldnames = frappe
.get_meta(doc.document_type)
.fields.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype))
const meta = frappe.get_meta(doc.document_type);
const is_submittable = meta.is_submittable;
const fieldnames = meta.fields
.filter((field) => !frappe.model.no_value_type.includes(field.fieldtype))
.map((field) => field.fieldname);
frm.fields_dict.states.grid.update_docfield_property(
@ -119,6 +120,33 @@ frappe.ui.form.on("Workflow", {
"options",
[""].concat(fieldnames)
);
frm.fields_dict.states.grid.update_docfield_property(
"doc_status",
"read_only",
!is_submittable
);
if (!is_submittable) {
let has_affected_states = false;
frm.doc.states.forEach((row) => {
if (parseInt(row.doc_status || 0) !== 0) {
has_affected_states = true;
row.doc_status = "0";
}
});
if (has_affected_states) {
frm.refresh_field("states");
frappe.msgprint({
title: __("Doc Status Reset"),
message: __(
"The <strong>Doc Status</strong> for all states has been reset to <strong>0</strong> because <strong>{0}</strong> is not submittable",
[frm.doc.document_type]
),
indicator: "orange",
});
}
}
});
},
create_warning_dialog: function (frm) {

View file

@ -90,23 +90,38 @@ class Workflow(Document):
frappe.throw(frappe._("{0} not a valid State").format(state))
meta = frappe.get_meta(self.document_type)
is_submittable = meta.is_submittable
if not is_submittable:
for state in self.states:
if cint(state.doc_status) != 0:
frappe.throw(
frappe._(
"Workflow State '{0}' has Document Status {1}, but DocType '{2}' is not submittable. "
"Only Document Status 0 (Draft) is allowed for non-submittable DocTypes."
).format(state.state, state.doc_status, self.document_type)
)
for t in self.transitions:
state = get_state(t.state)
next_state = get_state(t.next_state)
state_docstatus = cint(state.doc_status)
next_state_docstatus = cint(next_state.doc_status)
if state.doc_status == "2":
if state_docstatus == 2:
frappe.throw(
frappe._("Cannot change state of Cancelled Document. Transition row {0}").format(t.idx)
)
if state.doc_status == "1" and next_state.doc_status == "0":
if state_docstatus == 1 and next_state_docstatus == 0:
frappe.throw(
frappe._(
"Submitted Document cannot be converted back to draft. Transition row {0}"
).format(t.idx)
)
if state.doc_status == "0" and next_state.doc_status == "2":
if state_docstatus == 0 and next_state_docstatus == 2:
frappe.throw(frappe._("Cannot cancel before submitting. See Transition {0}").format(t.idx))
def set_active(self):

View file

@ -20,7 +20,7 @@ dependencies = [
# We depend on internal attributes,
# do NOT add loose requirements on PyMySQL versions.
"PyMySQL==1.1.2",
"pypdf==6.7.1",
"pypdf==6.7.4",
"PyPika @ git+https://github.com/frappe/pypika@2c50e6142b2d61d2d243e466fdd5dc03b3d918f2",
"mysqlclient==2.2.7",
"PyQRCode~=1.2.1",

View file

@ -2055,9 +2055,9 @@ mime@^1.4.1:
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
minimatch@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624"
integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"