').hide().appendTo(this.page.main);
+
+ this.$loading = $(this.message_div('')).hide().appendTo(this.page.main);
this.$report = $('
').appendTo(this.page.main);
this.$message = $(this.message_div('')).hide().appendTo(this.page.main);
}
@@ -1738,11 +1756,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.refresh();
}
- toggle_loading(flag) {
- this.toggle_message(flag, __('Loading') + '...');
- }
-
-
toggle_nothing_to_show(flag) {
let message = this.prepared_report
? __('This is a background report. Please set the appropriate filters and then generate a new one.')
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index 2a92d93e30..8866a4b2af 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -50,8 +50,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.setup_columns();
super.setup_new_doc_event();
this.page.main.addClass('report-view');
- this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height'));
- this.page.body.parent().css('margin-bottom', 'unset');
}
toggle_side_bar() {
diff --git a/frappe/public/js/frappe/views/workspace/blocks/card.js b/frappe/public/js/frappe/views/workspace/blocks/card.js
index 15e27fed40..9b4a2ed14f 100644
--- a/frappe/public/js/frappe/views/workspace/blocks/card.js
+++ b/frappe/public/js/frappe/views/workspace/blocks/card.js
@@ -30,7 +30,7 @@ export default class Card extends Block {
this.new('card', 'links');
if (this.data && this.data.card_name) {
- let has_data = this.make('card', this.data.card_name, 'links');
+ let has_data = this.make('card', __(this.data.card_name), 'links');
if (!has_data) return;
}
diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js
index e41063e6fc..02e6a66e6f 100644
--- a/frappe/public/js/frappe/views/workspace/blocks/chart.js
+++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js
@@ -30,7 +30,7 @@ export default class Chart extends Block {
this.new('chart');
if (this.data && this.data.chart_name) {
- let has_data = this.make('chart', this.data.chart_name);
+ let has_data = this.make('chart', __(this.data.chart_name));
if (!has_data) return;
}
diff --git a/frappe/public/js/frappe/views/workspace/blocks/header.js b/frappe/public/js/frappe/views/workspace/blocks/header.js
index 356f9c3244..d88bc42af9 100644
--- a/frappe/public/js/frappe/views/workspace/blocks/header.js
+++ b/frappe/public/js/frappe/views/workspace/blocks/header.js
@@ -27,7 +27,7 @@ export default class Header extends Block {
data = {};
}
- newData.text = data.text || '';
+ newData.text = (data.text && __(data.text.replace(/(\n|\t)/gm, ""))) || '';
newData.level = parseInt(data.level) || this.defaultLevel.number;
newData.col = parseInt(data.col) || 12;
diff --git a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js
index 26afa65d51..9e5dfb68ff 100644
--- a/frappe/public/js/frappe/views/workspace/blocks/paragraph.js
+++ b/frappe/public/js/frappe/views/workspace/blocks/paragraph.js
@@ -177,7 +177,7 @@ export default class Paragraph extends Block {
set data(data) {
this._data = data || {};
- this._element.innerHTML = this._data.text || '';
+ this._element.innerHTML = __(this._data.text) || '';
}
static get pasteConfig() {
diff --git a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js
index f7482a06f3..96b8f47484 100644
--- a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js
+++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js
@@ -29,7 +29,7 @@ export default class Shortcut extends Block {
this.new('shortcut');
if (this.data && this.data.shortcut_name) {
- let has_data = this.make('shortcut', this.data.shortcut_name);
+ let has_data = this.make('shortcut', __(this.data.shortcut_name));
if (!has_data) return;
}
diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js
index 8989814349..e6248f66cf 100644
--- a/frappe/public/js/frappe/views/workspace/workspace.js
+++ b/frappe/public/js/frappe/views/workspace/workspace.js
@@ -51,38 +51,37 @@ frappe.views.Workspace = class Workspace {
this.body = this.wrapper.find(".layout-main-section");
}
- setup_pages(reload) {
- this.get_pages().then(pages => {
- this.all_pages = pages.pages;
- this.has_access = pages.has_access;
+ async setup_pages(reload) {
+ this.sidebar_pages = !this.discard ? await this.get_pages() : this.sidebar_pages;
+ this.all_pages = this.sidebar_pages.pages;
+ this.has_access = this.sidebar_pages.has_access;
- this.all_pages.forEach(page => {
- page.is_editable = !page.public || pages.has_access;
- });
-
- this.public_pages = this.all_pages.filter(page => page.public);
- this.private_pages = this.all_pages.filter(page => !page.public);
-
- if (this.all_pages) {
- frappe.workspaces = {};
- for (let page of this.all_pages) {
- frappe.workspaces[frappe.router.slug(page.title)] = {title: page.title};
- }
- if (this.new_page && this.new_page.name) {
- if (!frappe.workspaces[frappe.router.slug(this.new_page.name)]) {
- this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public };
- }
- if (this.new_page.public) {
- frappe.set_route(`${frappe.router.slug(this.new_page.name)}`);
- } else {
- frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`);
- }
- this.new_page = null;
- }
- this.make_sidebar();
- reload && this.show();
- }
+ this.all_pages.forEach(page => {
+ page.is_editable = !page.public || this.has_access;
});
+
+ this.public_pages = this.all_pages.filter(page => page.public);
+ this.private_pages = this.all_pages.filter(page => !page.public);
+
+ if (this.all_pages) {
+ frappe.workspaces = {};
+ for (let page of this.all_pages) {
+ frappe.workspaces[frappe.router.slug(page.name)] = {title: page.title};
+ }
+ if (this.new_page && this.new_page.name) {
+ if (!frappe.workspaces[frappe.router.slug(this.new_page.label)]) {
+ this.new_page = { name: this.all_pages[0].title, public: this.all_pages[0].public };
+ }
+ if (this.new_page.public) {
+ frappe.set_route(`${frappe.router.slug(this.new_page.name)}`);
+ } else {
+ frappe.set_route(`private/${frappe.router.slug(this.new_page.name)}`);
+ }
+ this.new_page = null;
+ }
+ this.make_sidebar();
+ reload && this.show();
+ }
}
get_pages() {
@@ -95,10 +94,10 @@ frappe.views.Workspace = class Workspace {
@@ -152,8 +151,8 @@ frappe.views.Workspace = class Workspace {
append_item(item, container) {
let is_current_page = frappe.router.slug(item.title) == frappe.router.slug(this.get_page_to_show().name)
&& item.public == this.get_page_to_show().public;
+ item.selected = is_current_page;
if (is_current_page) {
- item.selected = true;
this.current_page = { name: item.title, public: item.public };
}
@@ -219,14 +218,14 @@ frappe.views.Workspace = class Workspace {
if (!this.page_data || Object.keys(this.page_data).length === 0) return;
+ if (this.page_data.charts && this.page_data.charts.items.length === 0) return;
+
return frappe.dashboard_utils.get_dashboard_settings().then(settings => {
if (settings) {
let chart_config = settings.chart_config ? JSON.parse(settings.chart_config) : {};
- if (this.page_data.charts && this.page_data.charts.items) {
- this.page_data.charts.items.map(chart => {
- chart.chart_settings = chart_config[chart.chart_name] || {};
- });
- }
+ this.page_data.charts.items.map(chart => {
+ chart.chart_settings = chart_config[chart.chart_name] || {};
+ });
this.pages[page.name] = this.page_data;
}
});
@@ -272,8 +271,7 @@ frappe.views.Workspace = class Workspace {
`).appendTo(this.body);
}
- this.$page.prepend(frappe.render_template('workspace_loading_skeleton'));
- this.$page.find('.codex-editor').addClass('hidden');
+ this.create_skeleton();
if (this.all_pages) {
let pages = page.public ? this.public_pages : this.private_pages;
@@ -293,8 +291,7 @@ frappe.views.Workspace = class Workspace {
this.prepare_editorjs();
$('.item-anchor').removeClass('disable-click');
- this.$page.find('.codex-editor').removeClass('hidden');
- this.$page.find('.workspace-skeleton').remove();
+ this.remove_skeleton();
}
}
@@ -336,10 +333,10 @@ frappe.views.Workspace = class Workspace {
this.page.clear_secondary_action();
this.page.clear_inner_toolbar();
- current_page.is_editable && this.page.set_secondary_action(__("Edit"), () => {
+ current_page.is_editable && this.page.set_secondary_action(__("Edit"), async () => {
if (!this.editor || !this.editor.readOnly) return;
this.is_read_only = false;
- this.editor.readOnly.toggle();
+ await this.editor.readOnly.toggle();
this.editor.isReady.then(() => {
this.initialize_editorjs_undo();
this.setup_customization_buttons(current_page);
@@ -383,13 +380,13 @@ frappe.views.Workspace = class Workspace {
this.page.set_secondary_action(
__("Discard"),
- () => {
+ async () => {
+ this.discard = true;
this.page.clear_primary_action();
this.page.clear_secondary_action();
this.page.clear_inner_toolbar();
- this.editor.readOnly.toggle();
+ await this.editor.readOnly.toggle();
this.is_read_only = true;
- this.deleted_sidebar_items = [];
this.reload();
frappe.show_alert({ message: __("Customizations Discarded"), indicator: "info" });
}
@@ -568,10 +565,10 @@ frappe.views.Workspace = class Workspace {
}
}
]
- }).then(() => {
+ }).then(async () => {
if (this.editor.configuration.readOnly) {
this.is_read_only = false;
- this.editor.readOnly.toggle();
+ await this.editor.readOnly.toggle();
}
this.add_page_to_sidebar(values);
this.show_sidebar_actions();
@@ -646,7 +643,10 @@ frappe.views.Workspace = class Workspace {
this.tools = {
header: {
class: this.blocks['header'],
- inlineToolbar: true
+ inlineToolbar: true,
+ config: {
+ defaultLevel: 4
+ }
},
paragraph: {
class: this.blocks['paragraph'],
@@ -693,6 +693,7 @@ frappe.views.Workspace = class Workspace {
save_page() {
frappe.dom.freeze();
+ this.create_skeleton();
let save = true;
if (!this.title && this.current_page) {
let pages = this.current_page.public ? this.public_pages : this.private_pages;
@@ -740,13 +741,6 @@ frappe.views.Workspace = class Workspace {
if (res.message) {
me.new_page = res.message;
me.pages[res.message.label] && delete me.pages[res.message.label];
- me.title = '';
- me.icon = '';
- me.parent = '';
- me.public = false;
- me.sorted_public_items = [];
- me.sorted_private_items = [];
- me.deleted_sidebar_items = [];
me.reload();
frappe.show_alert({ message: __("Page Saved Successfully"), indicator: "green" });
}
@@ -759,9 +753,26 @@ frappe.views.Workspace = class Workspace {
}
reload() {
- this.$page.prepend(frappe.render_template('workspace_loading_skeleton'));
- this.$page.find('.codex-editor').addClass('hidden');
+ this.title = '';
+ this.icon = '';
+ this.parent = '';
+ this.public = false;
+ this.sorted_public_items = [];
+ this.sorted_private_items = [];
+ this.deleted_sidebar_items = [];
+ this.create_skeleton();
this.setup_pages(true);
+ this.discard = false;
this.undo.readOnly = true;
}
+
+ create_skeleton() {
+ this.$page.prepend(frappe.render_template('workspace_loading_skeleton'));
+ this.$page.find('.codex-editor').addClass('hidden');
+ }
+
+ remove_skeleton() {
+ this.$page.find('.codex-editor').removeClass('hidden');
+ this.$page.find('.workspace-skeleton').remove();
+ }
};
diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js
index d8f92edc5d..cb3fc58ea3 100644
--- a/frappe/public/js/frappe/widgets/widget_group.js
+++ b/frappe/public/js/frappe/widgets/widget_group.js
@@ -191,7 +191,6 @@ export class SingleWidgetGroup {
Object.assign(this, opts);
this.widgets_list = [];
this.widgets_dict = {};
- this.widget_order = [];
this.make();
}
diff --git a/frappe/public/js/print_format_builder/ConfigureColumns.vue b/frappe/public/js/print_format_builder/ConfigureColumns.vue
new file mode 100644
index 0000000000..da10f99e40
--- /dev/null
+++ b/frappe/public/js/print_format_builder/ConfigureColumns.vue
@@ -0,0 +1,111 @@
+
+
+
+ {{ help_message }}
+
+
+
+ {{ __("Column") }}
+
+
+ {{ __("Width") }}
+ ({{ __("Total:") }} {{ total_width }})
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/Field.vue b/frappe/public/js/print_format_builder/Field.vue
new file mode 100644
index 0000000000..ca53402083
--- /dev/null
+++ b/frappe/public/js/print_format_builder/Field.vue
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+ {{ df.label }}
+
+
+
{{ df.label }}
+
+ {{ __("No Label") }} ({{ df.fieldname }})
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/HTMLEditor.vue b/frappe/public/js/print_format_builder/HTMLEditor.vue
new file mode 100644
index 0000000000..17024da503
--- /dev/null
+++ b/frappe/public/js/print_format_builder/HTMLEditor.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/LetterHeadEditor.vue b/frappe/public/js/print_format_builder/LetterHeadEditor.vue
new file mode 100644
index 0000000000..1eae56f81a
--- /dev/null
+++ b/frappe/public/js/print_format_builder/LetterHeadEditor.vue
@@ -0,0 +1,341 @@
+
+
+
+
+
+
+
+
+ (letterhead[range_input_field] = parseFloat(
+ e.target.value
+ ))
+ "
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/Preview.vue b/frappe/public/js/print_format_builder/Preview.vue
new file mode 100644
index 0000000000..35105dee6c
--- /dev/null
+++ b/frappe/public/js/print_format_builder/Preview.vue
@@ -0,0 +1,132 @@
+
+
+
+
Generating preview...
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/PrintFormat.vue b/frappe/public/js/print_format_builder/PrintFormat.vue
new file mode 100644
index 0000000000..1857bae47e
--- /dev/null
+++ b/frappe/public/js/print_format_builder/PrintFormat.vue
@@ -0,0 +1,136 @@
+
+
+
{{ __("1 of 2") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/PrintFormatBuilder.vue b/frappe/public/js/print_format_builder/PrintFormatBuilder.vue
new file mode 100644
index 0000000000..bcc3f8300f
--- /dev/null
+++ b/frappe/public/js/print_format_builder/PrintFormatBuilder.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/PrintFormatControls.vue b/frappe/public/js/print_format_builder/PrintFormatControls.vue
new file mode 100644
index 0000000000..2eefc22409
--- /dev/null
+++ b/frappe/public/js/print_format_builder/PrintFormatControls.vue
@@ -0,0 +1,336 @@
+
+
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/PrintFormatSection.vue b/frappe/public/js/print_format_builder/PrintFormatSection.vue
new file mode 100644
index 0000000000..9a065e5e26
--- /dev/null
+++ b/frappe/public/js/print_format_builder/PrintFormatSection.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
diff --git a/frappe/public/js/print_format_builder/print_format_builder.bundle.js b/frappe/public/js/print_format_builder/print_format_builder.bundle.js
new file mode 100644
index 0000000000..b2d3372daf
--- /dev/null
+++ b/frappe/public/js/print_format_builder/print_format_builder.bundle.js
@@ -0,0 +1,64 @@
+import PrintFormatBuilderComponent from "./PrintFormatBuilder.vue";
+import { getStore } from "./store";
+
+class PrintFormatBuilder {
+ constructor({ wrapper, page, print_format }) {
+ this.$wrapper = $(wrapper);
+ this.page = page;
+ this.print_format = print_format;
+
+ this.page.clear_actions();
+ this.page.clear_icons();
+ this.page.clear_custom_actions();
+
+ this.page.set_title(__("Editing {0}", [this.print_format]));
+ this.page.set_primary_action(__("Save"), () => {
+ this.$component.$store.save_changes();
+ });
+ let $toggle_preview_btn = this.page.add_button(
+ __("Show Preview"),
+ () => {
+ this.$component.toggle_preview();
+ }
+ );
+ this.page.add_button(__("Reset Changes"), () =>
+ this.$component.$store.reset_changes()
+ );
+ this.page.add_menu_item(__("Edit Print Format"), () => {
+ frappe.set_route("Form", "Print Format", this.print_format);
+ });
+ this.page.add_menu_item(__("Change Print Format"), () => {
+ frappe.set_route("print-format-builder-beta");
+ });
+
+ let $vm = new Vue({
+ el: this.$wrapper.get(0),
+ render: h =>
+ h(PrintFormatBuilderComponent, {
+ props: {
+ print_format_name: print_format
+ }
+ })
+ });
+ this.$component = $vm.$children[0];
+ let store = getStore(print_format);
+ store.$watch("dirty", value => {
+ if (value) {
+ this.page.set_indicator("Not Saved", "orange");
+ $toggle_preview_btn.hide();
+ } else {
+ this.page.clear_indicator();
+ $toggle_preview_btn.show();
+ }
+ });
+ this.$component.$watch("show_preview", value => {
+ $toggle_preview_btn.text(
+ value ? __("Hide Preview") : __("Show Preview")
+ );
+ });
+ }
+}
+
+frappe.provide("frappe.ui");
+frappe.ui.PrintFormatBuilder = PrintFormatBuilder;
+export default PrintFormatBuilder;
diff --git a/frappe/public/js/print_format_builder/store.js b/frappe/public/js/print_format_builder/store.js
new file mode 100644
index 0000000000..f531a4a7e0
--- /dev/null
+++ b/frappe/public/js/print_format_builder/store.js
@@ -0,0 +1,177 @@
+import { create_default_layout, pluck } from "./utils";
+
+let stores = {};
+
+export function getStore(print_format_name) {
+ if (stores[print_format_name]) {
+ return stores[print_format_name];
+ }
+
+ let options = {
+ data() {
+ return {
+ print_format_name,
+ letterhead_name: null,
+ print_format: null,
+ letterhead: null,
+ doctype: null,
+ meta: null,
+ layout: null,
+ dirty: false,
+ edit_letterhead: false
+ };
+ },
+ watch: {
+ layout: {
+ deep: true,
+ handler() {
+ this.dirty = true;
+ }
+ },
+ print_format: {
+ deep: true,
+ handler() {
+ this.dirty = true;
+ }
+ }
+ },
+ methods: {
+ fetch() {
+ return new Promise(resolve => {
+ frappe.model.clear_doc(
+ "Print Format",
+ this.print_format_name
+ );
+ frappe.model.with_doc(
+ "Print Format",
+ this.print_format_name,
+ () => {
+ let print_format = frappe.get_doc(
+ "Print Format",
+ this.print_format_name
+ );
+ frappe.model.with_doctype(
+ print_format.doc_type,
+ () => {
+ this.meta = frappe.get_meta(
+ print_format.doc_type
+ );
+ this.print_format = print_format;
+ this.layout = this.get_layout();
+ this.$nextTick(() => (this.dirty = false));
+ this.edit_letterhead = false;
+ resolve();
+ }
+ );
+ }
+ );
+ });
+ },
+ update({ fieldname, value }) {
+ this.$set(this.print_format, fieldname, value);
+ },
+ save_changes() {
+ frappe.dom.freeze(__("Saving..."));
+
+ this.layout.sections = this.layout.sections
+ .filter(section => !section.remove)
+ .map(section => {
+ section.columns = section.columns.map(column => {
+ column.fields = column.fields
+ .filter(df => !df.remove)
+ .map(df => {
+ if (df.table_columns) {
+ df.table_columns = df.table_columns.map(
+ tf => {
+ return pluck(tf, [
+ "label",
+ "fieldname",
+ "fieldtype",
+ "options",
+ "width",
+ "field_template"
+ ]);
+ }
+ );
+ }
+ return pluck(df, [
+ "label",
+ "fieldname",
+ "fieldtype",
+ "options",
+ "table_columns",
+ "html",
+ "field_template"
+ ]);
+ });
+ return column;
+ });
+ return section;
+ });
+
+ this.print_format.format_data = JSON.stringify(this.layout);
+
+ frappe
+ .call("frappe.client.save", {
+ doc: this.print_format
+ })
+ .then(() => {
+ if (this.letterhead && this.letterhead._dirty) {
+ return frappe
+ .call("frappe.client.save", {
+ doc: this.letterhead
+ })
+ .then(r => (this.letterhead = r.message));
+ }
+ })
+ .then(() => this.fetch())
+ .always(() => {
+ frappe.dom.unfreeze();
+ this.$emit("after_save");
+ });
+ },
+ reset_changes() {
+ this.fetch();
+ },
+ get_layout() {
+ if (this.print_format) {
+ if (typeof this.print_format.format_data == "string") {
+ return JSON.parse(this.print_format.format_data);
+ }
+ return this.print_format.format_data;
+ }
+ return null;
+ },
+ get_default_layout() {
+ return create_default_layout(this.meta, this.print_format);
+ },
+ change_letterhead(letterhead) {
+ return frappe.db
+ .get_doc("Letter Head", letterhead)
+ .then(doc => {
+ this.letterhead = doc;
+ });
+ }
+ }
+ };
+ stores[print_format_name] = new Vue(options);
+ return stores[print_format_name];
+}
+
+export let storeMixin = {
+ inject: ["$store"],
+ computed: {
+ print_format() {
+ return this.$store.print_format;
+ },
+ layout() {
+ return this.$store.layout;
+ },
+ letterhead() {
+ return this.$store.letterhead;
+ },
+ meta() {
+ return this.$store.meta;
+ }
+ }
+};
diff --git a/frappe/public/js/print_format_builder/utils.js b/frappe/public/js/print_format_builder/utils.js
new file mode 100644
index 0000000000..879fe9efd2
--- /dev/null
+++ b/frappe/public/js/print_format_builder/utils.js
@@ -0,0 +1,159 @@
+export function create_default_layout(meta, print_format) {
+ let layout = {
+ header: get_default_header(meta),
+ sections: []
+ };
+
+ let section = null,
+ column = null;
+
+ function set_column(df) {
+ if (!section) {
+ set_section();
+ }
+ column = get_new_column(df);
+ section.columns.push(column);
+ }
+
+ function set_section(df) {
+ section = get_new_section(df);
+ column = null;
+ layout.sections.push(section);
+ }
+
+ function get_new_section(df) {
+ if (!df) {
+ df = { label: "" };
+ }
+ return {
+ label: df.label || "",
+ columns: []
+ };
+ }
+
+ function get_new_column(df) {
+ if (!df) {
+ df = { label: "" };
+ }
+ return {
+ label: df.label || "",
+ fields: []
+ };
+ }
+
+ for (let df of meta.fields) {
+ if (df.fieldname) {
+ // make a copy to avoid mutation bugs
+ df = JSON.parse(JSON.stringify(df));
+ } else {
+ continue;
+ }
+
+ if (df.fieldtype === "Section Break") {
+ set_section(df);
+ } else if (df.fieldtype === "Column Break") {
+ set_column(df);
+ } else if (df.label) {
+ if (!column) set_column();
+
+ if (!df.print_hide) {
+ let field = {
+ label: df.label,
+ fieldname: df.fieldname,
+ fieldtype: df.fieldtype,
+ options: df.options
+ };
+
+ let field_template = get_field_template(
+ print_format,
+ df.fieldname
+ );
+ if (field_template) {
+ field.label = `${__(df.label)} (${__("Field Template")})`;
+ field.fieldtype = "Field Template";
+ field.field_template = field_template.name;
+ field.fieldname = df.fieldname = "_template";
+ }
+
+ if (df.fieldtype === "Table") {
+ field.table_columns = get_table_columns(df);
+ }
+
+ column.fields.push(field);
+ section.has_fields = true;
+ }
+ }
+ }
+
+ // remove empty sections
+ layout.sections = layout.sections.filter(section => section.has_fields);
+
+ return layout;
+}
+
+export function get_table_columns(df) {
+ let table_columns = [];
+ let table_fields = frappe.get_meta(df.options).fields;
+ let total_width = 0;
+ for (let tf of table_fields) {
+ if (
+ !in_list(["Section Break", "Column Break"], tf.fieldtype) &&
+ !tf.print_hide &&
+ df.label &&
+ total_width < 100
+ ) {
+ let width =
+ typeof tf.width == "number" && tf.width < 100
+ ? tf.width
+ : tf.width
+ ? 20
+ : 10;
+ table_columns.push({
+ label: tf.label,
+ fieldname: tf.fieldname,
+ fieldtype: tf.fieldtype,
+ options: tf.options,
+ width
+ });
+ total_width += width;
+ }
+ }
+ return table_columns;
+}
+
+function get_field_template(print_format, fieldname) {
+ let templates = print_format.__onload.print_templates || {};
+ for (let template of templates) {
+ if (template.field === fieldname) {
+ return template;
+ }
+ }
+ return null;
+}
+
+function get_default_header(meta) {
+ return ``;
+}
+
+export function pluck(object, keys) {
+ let out = {};
+ for (let key of keys) {
+ if (key in object) {
+ out[key] = object[key];
+ }
+ }
+ return out;
+}
+
+export function get_image_dimensions(src) {
+ return new Promise(resolve => {
+ let img = new Image();
+ img.onload = function() {
+ resolve({ width: this.width, height: this.height });
+ };
+ img.src = src;
+ });
+}
diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss
index a10cd454a6..954916c911 100644
--- a/frappe/public/scss/common/controls.scss
+++ b/frappe/public/scss/common/controls.scss
@@ -231,6 +231,13 @@ textarea.form-control {
background-color: var(--control-bg);
border-radius: var(--border-radius);
padding: var(--padding-md);
+
+ svg > rect {
+ fill: var(--control-bg) !important;
+ }
+ svg > g {
+ fill: var(--text-color) !important;
+ }
}
@media (min-width: 768px) {
diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss
index 4456acabb3..a49b5a463e 100644
--- a/frappe/public/scss/desk/list.scss
+++ b/frappe/public/scss/desk/list.scss
@@ -147,7 +147,6 @@
.list-row-head {
@extend .list-row;
- padding: 15px;
cursor: default;
.list-subject {
@@ -214,6 +213,10 @@ input.list-check-all, input.list-row-checkbox {
--checkbox-right-margin: calc(var(--checkbox-size) / 2 + #{$level-margin-right});
}
+input.list-check-all {
+ margin-left: 15px;
+}
+
.render-list-checkbox {
margin-left: 15px;
}
diff --git a/frappe/public/scss/desk/print_preview.scss b/frappe/public/scss/desk/print_preview.scss
index 3c0acc68b8..468b37fe5a 100644
--- a/frappe/public/scss/desk/print_preview.scss
+++ b/frappe/public/scss/desk/print_preview.scss
@@ -14,6 +14,11 @@
}
}
+.preview-beta-wrapper {
+ border-radius: var(--border-radius);
+ overflow: hidden;
+}
+
.print-toolbar {
margin: 0px;
padding: var(--padding-md) 0;
diff --git a/frappe/public/scss/desk/report.scss b/frappe/public/scss/desk/report.scss
index 2389a4f8f6..f8666602ff 100644
--- a/frappe/public/scss/desk/report.scss
+++ b/frappe/public/scss/desk/report.scss
@@ -84,39 +84,17 @@
margin-bottom: 10px;
}
-.layout-main-section {
- --report-filter-height: 0px;
- --report-total-height: 275px;
-}
-
.report-wrapper {
overflow: auto;
-
- .datatable {
- height: calc(100vh - var(--report-filter-height) - 205px);
-
- .dt-scrollable {
- height: calc(100vh - var(--report-filter-height) - var(--report-total-height));
- }
- }
}
.report-view {
.result {
- min-height: 50vh !important;
.dt-row:last-child:not(.dt-row-filter) {
.dt-cell {
border-bottom: 1px solid var(--border-color);
}
}
-
- .datatable {
- height: calc(100vh - var(--report-filter-height) - 225px);
-
- .dt-scrollable {
- height: calc(100vh - var(--report-filter-height) - 295px);
- }
- }
}
}
diff --git a/frappe/public/scss/print_format.bundle.scss b/frappe/public/scss/print_format.bundle.scss
new file mode 100644
index 0000000000..b01e669d71
--- /dev/null
+++ b/frappe/public/scss/print_format.bundle.scss
@@ -0,0 +1,5 @@
+@import "./desk/variables.scss";
+@import "./common/mixins.scss";
+@import "./common/global.scss";
+@import "./common/icons.scss";
+@import "~bootstrap/scss/bootstrap";
diff --git a/frappe/search/full_text_search.py b/frappe/search/full_text_search.py
index 560ad55bf3..1d4f3fef32 100644
--- a/frappe/search/full_text_search.py
+++ b/frappe/search/full_text_search.py
@@ -7,7 +7,7 @@ from frappe.utils import update_progress_bar
from whoosh.index import create_in, open_dir, EmptyIndexError
from whoosh.fields import TEXT, ID, Schema
from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin
-from whoosh.query import Prefix
+from whoosh.query import Prefix, FuzzyTerm
from whoosh.writing import AsyncWriter
@@ -23,6 +23,9 @@ class FullTextSearch:
def get_schema(self):
return Schema(name=ID(stored=True), content=TEXT(stored=True))
+ def get_fields_to_search(self):
+ return ["name", "content"]
+
def get_id(self):
return "name"
@@ -120,8 +123,15 @@ class FullTextSearch:
results = None
out = []
+ search_fields = self.get_fields_to_search()
+ fieldboosts = {}
+
+ # apply reducing boost on fields based on order. 1.0, 0.5, 0.33 and so on
+ for idx, field in enumerate(search_fields, start=1):
+ fieldboosts[field] = 1.0 / idx
+
with ix.searcher() as searcher:
- parser = MultifieldParser(["title", "content"], ix.schema)
+ parser = MultifieldParser(search_fields, ix.schema, termclass=FuzzyTermExtended, fieldboosts=fieldboosts)
parser.remove_plugin_class(FieldsPlugin)
parser.remove_plugin_class(WildcardPlugin)
query = parser.parse(text)
@@ -136,5 +146,13 @@ class FullTextSearch:
return out
+
+class FuzzyTermExtended(FuzzyTerm):
+ def __init__(self, fieldname, text, boost=1.0, maxdist=2, prefixlength=1,
+ constantscore=True):
+ super().__init__(fieldname, text, boost=boost, maxdist=maxdist,
+ prefixlength=prefixlength, constantscore=constantscore)
+
+
def get_index_path(index_name):
return frappe.get_site_path("indexes", index_name)
diff --git a/frappe/search/test_full_text_search.py b/frappe/search/test_full_text_search.py
index 348a0ec72a..0dbc7e775b 100644
--- a/frappe/search/test_full_text_search.py
+++ b/frappe/search/test_full_text_search.py
@@ -125,4 +125,4 @@ def get_documents():
deploy business applications with Rich Admin Interface. CommonSearchTerm"""
})
- return docs
\ No newline at end of file
+ return docs
diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py
index 0bc06d1a9b..30eadae6f1 100644
--- a/frappe/search/website_search.py
+++ b/frappe/search/website_search.py
@@ -21,6 +21,9 @@ class WebsiteSearch(FullTextSearch):
title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True)
)
+ def get_fields_to_search(self):
+ return ["title", "content"]
+
def get_id(self):
return "path"
diff --git a/frappe/sessions.py b/frappe/sessions.py
index ce104968ad..9a0f19df80 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -16,9 +16,11 @@ import frappe.translate
import redis
from urllib.parse import unquote
from frappe.cache_manager import clear_user_cache
+from frappe.query_builder import Order, DocType
-@frappe.whitelist(allow_guest=True)
-def clear(user=None):
+
+@frappe.whitelist()
+def clear():
frappe.local.session_obj.update(force=True)
frappe.local.db.commit()
clear_user_cache(frappe.session.user)
@@ -61,18 +63,14 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None):
simultaneous_sessions = frappe.db.get_value('User', user, 'simultaneous_sessions') or 1
offset = simultaneous_sessions - 1
- condition = ''
+ session = DocType("Sessions")
+ session_id = frappe.qb.from_(session).where((session.user == user) & (session.device.isin(device)))
if keep_current:
- condition = ' AND sid != {0}'.format(frappe.db.escape(frappe.session.sid))
+ session_id = session_id.where(session.sid != frappe.db.escape(frappe.session.sid))
- return frappe.db.sql_list("""
- SELECT `sid` FROM `tabSessions`
- WHERE `tabSessions`.user=%(user)s
- AND device in %(device)s
- {condition}
- ORDER BY `lastupdate` DESC
- LIMIT 100 OFFSET {offset}""".format(condition=condition, offset=offset),
- {"user": user, "device": device})
+ query = session_id.select(session.sid).offset(offset).limit(100).orderby(session.lastupdate, order=Order.desc)
+
+ return query.run(pluck=True)
def delete_session(sid=None, user=None, reason="Session Expired"):
from frappe.core.doctype.activity_log.feed import logout_feed
@@ -80,7 +78,10 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
frappe.cache().hdel("session", sid)
frappe.cache().hdel("last_db_session_update", sid)
if sid and not user:
- user_details = frappe.db.sql("""select user from tabSessions where sid=%s""", sid, as_dict=True)
+ table = DocType("Sessions")
+ user_details = frappe.qb.from_(table).where(
+ table.sid == sid
+ ).select(table.user).run(as_dict=True)
if user_details: user = user_details[0].get("user")
logout_feed(user, reason)
@@ -91,7 +92,7 @@ def clear_all_sessions(reason=None):
"""This effectively logs out all users"""
frappe.only_for("Administrator")
if not reason: reason = "Deleted All Active Session"
- for sid in frappe.db.sql_list("select sid from `tabSessions`"):
+ for sid in frappe.qb.from_("Sessions").select("sid").run(pluck=True):
delete_session(sid, reason=reason)
def get_expired_sessions():
@@ -159,6 +160,10 @@ def get():
return bootinfo
+@frappe.whitelist()
+def get_boot_assets_json():
+ return get_assets_json()
+
def get_csrf_token():
if not frappe.local.session.data.csrf_token:
generate_csrf_token()
diff --git a/frappe/share.py b/frappe/share.py
index 030feea8fa..9b33198c9b 100644
--- a/frappe/share.py
+++ b/frappe/share.py
@@ -128,8 +128,11 @@ def get_shared_doctypes(user=None):
"""Return list of doctypes in which documents are shared for the given user."""
if not user:
user = frappe.session.user
-
- return frappe.db.sql_list("select distinct share_doctype from tabDocShare where (user=%s or everyone=1)", user)
+ table = frappe.qb.DocType("DocShare")
+ query = frappe.qb.from_(table).where(
+ (table.user == user) | (table.everyone == 1)
+ ).select(table.share_doctype).distinct()
+ return query.run(pluck=True)
def get_share_name(doctype, name, user, everyone):
if cint(everyone):
diff --git a/frappe/templates/print_format/macros.html b/frappe/templates/print_format/macros.html
new file mode 100644
index 0000000000..ace992e88d
--- /dev/null
+++ b/frappe/templates/print_format/macros.html
@@ -0,0 +1,13 @@
+{% macro render_field(df, doc) %}
+{%- set value = doc.get(df.fieldname) -%}
+{% include ['templates/print_format/macros/' + df.renderer + '.html', 'templates/print_format/macros/Data.html'] ignore missing %}
+{% endmacro %}
+
+{% macro field_attributes(df) %}
+{%- if df.fieldname -%}
+data-fieldname="{{ df.fieldname }}"
+{%- endif %}
+{% if df.fieldtype -%}
+data-fieldtype="{{ df.fieldtype }}"
+{%- endif -%}
+{% endmacro %}
diff --git a/frappe/templates/print_format/macros/Attach.html b/frappe/templates/print_format/macros/Attach.html
new file mode 100644
index 0000000000..523d9e057a
--- /dev/null
+++ b/frappe/templates/print_format/macros/Attach.html
@@ -0,0 +1,7 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/AttachImage.html b/frappe/templates/print_format/macros/AttachImage.html
new file mode 100644
index 0000000000..796662f67a
--- /dev/null
+++ b/frappe/templates/print_format/macros/AttachImage.html
@@ -0,0 +1,7 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+

+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/Check.html b/frappe/templates/print_format/macros/Check.html
new file mode 100644
index 0000000000..fbc43608a5
--- /dev/null
+++ b/frappe/templates/print_format/macros/Check.html
@@ -0,0 +1,9 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/Code.html b/frappe/templates/print_format/macros/Code.html
new file mode 100644
index 0000000000..e83457808a
--- /dev/null
+++ b/frappe/templates/print_format/macros/Code.html
@@ -0,0 +1,7 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/Color.html b/frappe/templates/print_format/macros/Color.html
new file mode 100644
index 0000000000..ef7a2226c6
--- /dev/null
+++ b/frappe/templates/print_format/macros/Color.html
@@ -0,0 +1,8 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/Data.html b/frappe/templates/print_format/macros/Data.html
new file mode 100644
index 0000000000..722c42ce1a
--- /dev/null
+++ b/frappe/templates/print_format/macros/Data.html
@@ -0,0 +1,10 @@
+{% if value %}
+
+ {%- block label -%}
+
{{ df.label }}
+ {%- endblock -%}
+ {%- block value -%}
+
{{ doc.get_formatted(df.fieldname) }}
+ {%- endblock -%}
+
+{% endif %}
diff --git a/frappe/templates/print_format/macros/Divider.html b/frappe/templates/print_format/macros/Divider.html
new file mode 100644
index 0000000000..49fdf3f547
--- /dev/null
+++ b/frappe/templates/print_format/macros/Divider.html
@@ -0,0 +1,2 @@
+
+
diff --git a/frappe/templates/print_format/macros/FieldTemplate.html b/frappe/templates/print_format/macros/FieldTemplate.html
new file mode 100644
index 0000000000..9ea7fabb22
--- /dev/null
+++ b/frappe/templates/print_format/macros/FieldTemplate.html
@@ -0,0 +1,4 @@
+
+ {% set template = frappe.db.get_value('Print Format Field Template', df.field_template, ['template', 'template_file', 'standard'], as_dict=1) %}
+ {{ frappe.render_template(template.template_file if template.standard else template.template, {'doc': doc}) }}
+
diff --git a/frappe/templates/print_format/macros/HTML.html b/frappe/templates/print_format/macros/HTML.html
new file mode 100644
index 0000000000..6bd3659902
--- /dev/null
+++ b/frappe/templates/print_format/macros/HTML.html
@@ -0,0 +1,3 @@
+
+ {{ frappe.render_template(df.html, {'doc': doc}) }}
+
diff --git a/frappe/templates/print_format/macros/Markdown.html b/frappe/templates/print_format/macros/Markdown.html
new file mode 100644
index 0000000000..b692283fa0
--- /dev/null
+++ b/frappe/templates/print_format/macros/Markdown.html
@@ -0,0 +1,9 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+ {{ frappe.utils.md_to_html(doc.get(df.fieldname)) }}
+
+{%- endblock -%}
+
+
diff --git a/frappe/templates/print_format/macros/Rating.html b/frappe/templates/print_format/macros/Rating.html
new file mode 100644
index 0000000000..2e001fb58f
--- /dev/null
+++ b/frappe/templates/print_format/macros/Rating.html
@@ -0,0 +1,22 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{% macro star(is_active=false) %}
+
+{% endmacro %}
+
+{%- block value -%}
+
+ {%- for i in range(value) -%}
+ {{ star(true) }}
+ {%- endfor -%}
+ {%- for i in range(5 - value) -%}
+ {{ star() }}
+ {%- endfor -%}
+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/Signature.html b/frappe/templates/print_format/macros/Signature.html
new file mode 100644
index 0000000000..128ff2a927
--- /dev/null
+++ b/frappe/templates/print_format/macros/Signature.html
@@ -0,0 +1,7 @@
+{% extends "templates/print_format/macros/Data.html" %}
+
+{%- block value -%}
+
+

+
+{%- endblock -%}
diff --git a/frappe/templates/print_format/macros/Spacer.html b/frappe/templates/print_format/macros/Spacer.html
new file mode 100644
index 0000000000..1a7336e17b
--- /dev/null
+++ b/frappe/templates/print_format/macros/Spacer.html
@@ -0,0 +1,2 @@
+
+
diff --git a/frappe/templates/print_format/macros/Table.html b/frappe/templates/print_format/macros/Table.html
new file mode 100644
index 0000000000..27c0be961c
--- /dev/null
+++ b/frappe/templates/print_format/macros/Table.html
@@ -0,0 +1,30 @@
+{% if doc.get(df.fieldname) %}
+
+
+ {{ df.label }}
+
+
+ {% set columns = df.table_columns %}
+
+
+ {% for column in columns %}
+ |
+ {{ column.label }}
+ |
+ {% endfor %}
+
+
+
+ {% for row in doc.get(df.fieldname) %}
+
+ {% for column in columns %}
+ |
+ {{ row.get_formatted(column.fieldname) }}
+ |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+{% endif %}
diff --git a/frappe/templates/print_format/print_footer.html b/frappe/templates/print_format/print_footer.html
new file mode 100644
index 0000000000..bd64c0b1b2
--- /dev/null
+++ b/frappe/templates/print_format/print_footer.html
@@ -0,0 +1,24 @@
+
+
diff --git a/frappe/templates/print_format/print_format.css b/frappe/templates/print_format/print_format.css
new file mode 100644
index 0000000000..480cd19439
--- /dev/null
+++ b/frappe/templates/print_format/print_format.css
@@ -0,0 +1,131 @@
+{% include "templates/print_format/print_format_font.css" %}
+
+{% macro render_margin_text(position, content) %}
+@{{ position.replace('_', '-') }} {
+ content: {{ content }}
+}
+{% endmacro %}
+
+@page {
+ size: {{ print_settings.pdf_page_size or 'A4' }} portrait;
+ margin-top: {{ print_format.margin_top | int }}mm;
+ margin-bottom: {{ print_format.margin_bottom | int }}mm;
+ margin-left: {{ print_format.margin_left | int }}mm;
+ margin-right: {{ print_format.margin_right | int }}mm;
+ padding-top: {{ (header_height or 0) + 8 }}px;
+ padding-bottom: {{ (footer_height or 0) + 8 }}px;
+
+ /* page number */
+ {% set page_number_position = print_format.page_number.lower().replace(' ', '_') %}
+ {% if page_number_position in ['top_left', 'top_center', 'top_right', 'bottom_left', 'bottom_center', 'bottom_right'] %}
+ {{ render_margin_text(page_number_position, 'counter(page) " of " counter(pages)') }}
+ {% endif %}
+}
+
+html, body {
+ font-size: {{ print_format.font_size }}px;
+}
+
+body {
+ min-width: {{ body_width | int }}mm !important;
+ max-width: {{ body_width | int }}mm !important;
+}
+
+/* CSS rules to fix bootstrap column rendering in PDF
+ https://github.com/Kozea/WeasyPrint/issues/697#issuecomment-542338732
+*/
+@media print {
+ .col, *[class^="col-"] {
+ max-width: none !important;
+ }
+}
+
+@media screen {
+ html {
+ background-color: var(--gray-200);
+ }
+ body {
+ background-color: white;
+ box-shadow: var(--shadow-md);
+ margin: 2rem auto;
+ min-height: 297mm;
+ height: min-content;
+ min-width: {{ body_width | int }}mm !important;
+ max-width: {{ body_width | int }}mm !important;
+ padding-top: {{ print_format.margin_top | int }}mm;
+ padding-right: {{ print_format.margin_right | int }}mm;
+ padding-left: {{ print_format.margin_left | int }}mm;
+ padding-bottom: {{ print_format.margin_bottom | int }}mm;
+ }
+}
+
+.section:not(:first-child) {
+ margin-top: 1rem;
+}
+
+.section-label {
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+.field + .field {
+ margin-top: 0.5rem;
+}
+
+.field .label {
+ font-weight: bold;
+}
+
+.field.left-right {
+ display: flex;
+}
+
+.field.left-right .label {
+ width: 50%;
+}
+
+.field.left-right .value {
+ width: 50%;
+}
+
+.child-table [data-fieldtype="Currency"] {
+ text-align: right;
+}
+
+.table-row {
+ page-break-inside: avoid;
+}
+
+.page-break {
+ page-break-after: always;
+}
+
+.document-header {
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ border-bottom-color: var(--gray-300);
+}
+
+.field[data-fieldtype="Rating"] .rating-star {
+ width: 1.5rem;
+}
+
+.field[data-fieldtype="Long Text"] .value, .field[data-fieldtype="Text"] .value {
+ white-space: pre-line;
+}
+
+.field[data-fieldtype="Color"] .value {
+ display: flex;
+ align-items: center;
+}
+
+.field[data-fieldtype="Color"] .color-square {
+ width: 1rem;
+ height: 1rem;
+ margin-right: 0.3rem;
+ border-radius: var(--border-radius);
+}
+
+.field[data-fieldtype="Check"] #icon-tick {
+ width: 1rem;
+}
diff --git a/frappe/templates/print_format/print_format.html b/frappe/templates/print_format/print_format.html
new file mode 100644
index 0000000000..b9fb95a9d3
--- /dev/null
+++ b/frappe/templates/print_format/print_format.html
@@ -0,0 +1,40 @@
+{% import "templates/print_format/macros.html" as macros %}
+
+
+
+
+
+
+
+
{{ doc.doctype }}: {{ doc.name }}
+ {{ include_style('print_format.bundle.css') }}
+
+ {%- if print_style and print_style.css -%}
+
+ {%- endif -%}
+ {%- if print_format.css -%}
+
+ {%- endif -%}
+
+
+ {{ header or '' }}
+ {% for section in layout.sections %}
+
+ {% if section.label %}
+
{{ section.label }}
+ {% endif %}
+
+
+ {% for column in section.columns %}
+
+ {% for df in column.fields %}
+ {{ macros.render_field(df, doc) }}
+ {% endfor %}
+
+ {% endfor %}
+
+
+ {% endfor %}
+ {{ footer or '' }}
+
+
diff --git a/frappe/templates/print_format/print_format_font.css b/frappe/templates/print_format/print_format_font.css
new file mode 100644
index 0000000000..6103dbf5a4
--- /dev/null
+++ b/frappe/templates/print_format/print_format_font.css
@@ -0,0 +1,9 @@
+@charset "UTF-8";
+{% if print_format.font %}
+{% set font_family = print_format.font.replace(' ', '+') %}
+@import url("https://fonts.googleapis.com/css?family={{ font_family }}:400,500,600,700");
+{% endif %}
+
+html, body {
+ font-family: {{ print_format.font or 'Inter' }}, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+}
diff --git a/frappe/templates/print_format/print_header.html b/frappe/templates/print_format/print_header.html
new file mode 100644
index 0000000000..9b1357e08c
--- /dev/null
+++ b/frappe/templates/print_format/print_header.html
@@ -0,0 +1,24 @@
+
+
+ {%- if letterhead -%}
+ {{ frappe.render_template(letterhead.content, {'doc': doc}) }}
+ {%- endif -%}
+
+ {%- if layout.header -%}
+ {{ frappe.render_template(layout.header, {'doc': doc}) }}
+ {%- endif -%}
+
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 0d32a72756..978e3f4f7f 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -11,6 +11,7 @@ import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.utils import random_string
from frappe.utils.testutils import clear_custom_fields
+from frappe.query_builder import Field
from .test_query_builder import run_only_if, db_type_is
@@ -24,6 +25,7 @@ class TestDB(unittest.TestCase):
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"]), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
self.assertEqual(frappe.db.get_value("User", {}, "Min(name)"), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
+ self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower())
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
frappe.db.get_value("User", {"name": [">", "s"]}))
diff --git a/frappe/tests/test_global_search.py b/frappe/tests/test_global_search.py
index 41d6427b77..9a86baa4e5 100644
--- a/frappe/tests/test_global_search.py
+++ b/frappe/tests/test_global_search.py
@@ -88,13 +88,13 @@ class TestGlobalSearch(unittest.TestCase):
event = frappe.get_doc('Event', event_name)
test_subject = event.subject
results = global_search.search(test_subject)
- self.assertEqual(len(results), 1)
+ self.assertTrue(any(r["name"] == event_name for r in results), msg="Failed to search document by exact name")
frappe.delete_doc('Event', event_name)
global_search.sync_global_search()
results = global_search.search(test_subject)
- self.assertEqual(len(results), 0)
+ self.assertTrue(all(r["name"] != event_name for r in results), msg="Deleted documents appearing in global search.")
def test_insert_child_table(self):
frappe.db.delete("Event")
diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py
index 48510f55f6..f4276b2d59 100644
--- a/frappe/tests/test_permissions.py
+++ b/frappe/tests/test_permissions.py
@@ -493,6 +493,34 @@ class TestPermissions(unittest.TestCase):
frappe.set_user("test2@example.com")
self.assertRaises(frappe.PermissionError, getdoc, 'Blog Post', doc.name)
+ def test_if_owner_permission_on_get_list(self):
+ doc = frappe.get_doc({
+ "doctype": "Blog Post",
+ "blog_category": "-test-blog-category",
+ "blogger": "_Test Blogger 1",
+ "title": "_Test If Owner Permissions on Get List",
+ "content": "_Test Blog Post Content"
+ })
+
+ doc.insert(ignore_if_duplicate=True)
+
+ update('Blog Post', 'Blogger', 0, 'if_owner', 1)
+ update('Blog Post', 'Blogger', 0, 'read', 1)
+ user = frappe.get_doc("User", "test2@example.com")
+ user.add_roles("Website Manager")
+ frappe.clear_cache(doctype="Blog Post")
+
+ frappe.set_user("test2@example.com")
+ self.assertIn(doc.name, frappe.get_list("Blog Post", pluck="name"))
+
+ # Become system manager to remove role
+ frappe.set_user("test1@example.com")
+ user.remove_roles("Website Manager")
+ frappe.clear_cache(doctype="Blog Post")
+
+ frappe.set_user("test2@example.com")
+ self.assertNotIn(doc.name, frappe.get_list("Blog Post", pluck="name"))
+
def test_if_owner_permission_on_delete(self):
update('Blog Post', 'Blogger', 0, 'if_owner', 1)
update('Blog Post', 'Blogger', 0, 'read', 1)
diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py
index 818dc8bce6..25aa7b31ce 100644
--- a/frappe/tests/test_website.py
+++ b/frappe/tests/test_website.py
@@ -5,12 +5,12 @@ from frappe.utils import set_request
from frappe.website.serve import get_response, get_response_content
from frappe.website.utils import (build_response, clear_website_cache, get_home_page)
-
class TestWebsite(unittest.TestCase):
def setUp(self):
frappe.set_user('Guest')
def tearDown(self):
+ frappe.db.value_cache = {}
frappe.set_user('Administrator')
def test_home_page(self):
@@ -197,7 +197,7 @@ class TestWebsite(unittest.TestCase):
frappe.cache().delete_key('app_hooks')
def test_printview_page(self):
- content = get_response_content('/Language/en')
+ content = get_response_content('/Language/ru')
self.assertIn('
', content)
self.assertIn('
Language
', content)
diff --git a/frappe/translate.py b/frappe/translate.py
index 6f3ed81dc2..e5c1c9ef10 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -20,6 +20,8 @@ from typing import List, Union, Tuple
import frappe
from frappe.model.utils import InvalidIncludePath, render_include
from frappe.utils import get_bench_path, is_html, strip, strip_html_tags
+from frappe.query_builder import Field
+from pypika.terms import PseudoColumn
def get_language(lang_list: List = None) -> str:
@@ -119,7 +121,8 @@ def set_default_language(lang):
def get_lang_dict():
"""Returns all languages in dict format, full name is the key e.g. `{"english":"en"}`"""
- return dict(frappe.db.sql('select language_name, name from tabLanguage'))
+ result = dict(frappe.get_all("Language", fields=["language_name", "name"], order_by="modified", as_list=True))
+ return result
def get_dict(fortype, name=None):
"""Returns translation dict for a type of object.
@@ -151,12 +154,25 @@ def get_dict(fortype, name=None):
messages += get_messages_from_navbar()
messages += get_messages_from_include_files()
- messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`")
- messages += frappe.db.sql("select 'DocType:', name from tabDocType")
- messages += frappe.db.sql("select 'Role:', name from tabRole")
- messages += frappe.db.sql("select 'Module:', name from `tabModule Def`")
- messages += frappe.db.sql("select '', format from `tabWorkspace Shortcut` where format is not null")
- messages += frappe.db.sql("select '', title from `tabOnboarding Step`")
+ messages += (
+ frappe.qb.from_("Print Format")
+ .select(PseudoColumn("'Print Format:'"), "name")).run()
+ messages += (
+ frappe.qb.from_("DocType")
+ .select(PseudoColumn("'DocType:'"), "name")).run()
+ messages += (
+ frappe.qb.from_("Role").select(PseudoColumn("'Role:'"), "name").run()
+ )
+ messages += (
+ frappe.qb.from_("Module Def")
+ .select(PseudoColumn("'Module:'"), "name")).run()
+ messages += (
+ frappe.qb.from_("Workspace Shortcut")
+ .where(Field("format").isnotnull())
+ .select(PseudoColumn("''"), "format")).run()
+ messages += (
+ frappe.qb.from_("Onboarding Step")
+ .select(PseudoColumn("''"), "title")).run()
messages = deduplicate_messages(messages)
message_dict = make_dict_from_messages(messages, load_user_translation=False)
@@ -323,13 +339,17 @@ def get_messages_for_app(app, deduplicate=True):
# doctypes
if modules:
- for name in frappe.db.sql_list("""select name from tabDocType
- where module in ({})""".format(modules)):
+ filtered_doctypes = frappe.qb.from_("DocType").where(
+ Field("module").isin(modules)
+ ).select("name").run()
+ for name in filtered_doctypes:
messages.extend(get_messages_from_doctype(name))
# pages
- for name, title in frappe.db.sql("""select name, title from tabPage
- where module in ({})""".format(modules)):
+ filtered_pages = frappe.qb.from_("Page").where(
+ Field("module").isin(modules)
+ ).select("name", "title").run()
+ for name, title in filtered_pages:
messages.append((None, title or name))
messages.extend(get_messages_from_page(name))
@@ -898,7 +918,7 @@ def get_translator_url():
def get_all_languages(with_language_name=False):
"""Returns all language codes ar, ch etc"""
def get_language_codes():
- return frappe.db.sql_list('select name from tabLanguage')
+ return frappe.get_all("Language", pluck="name")
def get_all_language_with_name():
return frappe.db.get_all('Language', ['language_code', 'language_name'])
diff --git a/frappe/utils/weasyprint.py b/frappe/utils/weasyprint.py
new file mode 100644
index 0000000000..006bab2dd0
--- /dev/null
+++ b/frappe/utils/weasyprint.py
@@ -0,0 +1,244 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+
+import frappe
+from weasyprint import HTML, CSS
+
+
+@frappe.whitelist()
+def download_pdf(doctype, name, print_format, letterhead=None):
+ doc = frappe.get_doc(doctype, name)
+ generator = PrintFormatGenerator(print_format, doc, letterhead)
+ pdf = generator.render_pdf()
+
+ frappe.local.response.filename = "{name}.pdf".format(
+ name=name.replace(" ", "-").replace("/", "-")
+ )
+ frappe.local.response.filecontent = pdf
+ frappe.local.response.type = "pdf"
+
+
+def get_html(doctype, name, print_format, letterhead=None):
+ doc = frappe.get_doc(doctype, name)
+ generator = PrintFormatGenerator(print_format, doc, letterhead)
+ return generator.get_html_preview()
+
+
+class PrintFormatGenerator:
+ """
+ Generate a PDF of a Document, with repeatable header and footer if letterhead is provided.
+
+ This generator draws its inspiration and, also a bit of its implementation, from this
+ discussion in the library github issues: https://github.com/Kozea/WeasyPrint/issues/92
+ """
+
+ def __init__(self, print_format, doc, letterhead=None):
+ """
+ Parameters
+ ----------
+ print_format: str
+ Name of the Print Format
+ doc: str
+ Document to print
+ letterhead: str
+ Letter Head to apply (optional)
+ """
+ self.base_url = frappe.utils.get_url()
+ self.print_format = frappe.get_doc("Print Format", print_format)
+ self.doc = doc
+ self.letterhead = frappe.get_doc("Letter Head", letterhead) if letterhead else None
+ self.build_context()
+ self.layout = self.get_layout(self.print_format)
+ self.context.layout = self.layout
+
+ def build_context(self):
+ self.print_settings = frappe.get_doc("Print Settings")
+ page_width_map = {"A4": 210, "Letter": 216}
+ page_width = page_width_map.get(self.print_settings.pdf_page_size) or 210
+ body_width = (
+ page_width - self.print_format.margin_left - self.print_format.margin_right
+ )
+ print_style = (
+ frappe.get_doc("Print Style", self.print_settings.print_style)
+ if self.print_settings.print_style
+ else None
+ )
+ context = frappe._dict(
+ {
+ "doc": self.doc,
+ "print_format": self.print_format,
+ "print_settings": self.print_settings,
+ "print_style": print_style,
+ "letterhead": self.letterhead,
+ "page_width": page_width,
+ "body_width": body_width,
+ }
+ )
+ self.context = context
+
+ def get_html_preview(self):
+ header_html, footer_html = self.get_header_footer_html()
+ self.context.header = header_html
+ self.context.footer = footer_html
+ return self.get_main_html()
+
+ def get_main_html(self):
+ self.context.css = frappe.render_template(
+ "templates/print_format/print_format.css", self.context
+ )
+ return frappe.render_template(
+ "templates/print_format/print_format.html", self.context
+ )
+
+ def get_header_footer_html(self):
+ header_html = footer_html = None
+ if self.letterhead:
+ header_html = frappe.render_template(
+ "templates/print_format/print_header.html", self.context
+ )
+ if self.letterhead:
+ footer_html = frappe.render_template(
+ "templates/print_format/print_footer.html", self.context
+ )
+ return header_html, footer_html
+
+ def render_pdf(self):
+ """
+ Returns
+ -------
+ pdf: a bytes sequence
+ The rendered PDF.
+ """
+ self._make_header_footer()
+
+ self.context.update(
+ {"header_height": self.header_height, "footer_height": self.footer_height}
+ )
+ main_html = self.get_main_html()
+
+ html = HTML(string=main_html, base_url=self.base_url)
+ main_doc = html.render()
+
+ if self.header_html or self.footer_html:
+ self._apply_overlay_on_main(main_doc, self.header_body, self.footer_body)
+ pdf = main_doc.write_pdf()
+
+ return pdf
+
+ def _compute_overlay_element(self, element: str):
+ """
+ Parameters
+ ----------
+ element: str
+ Either 'header' or 'footer'
+
+ Returns
+ -------
+ element_body: BlockBox
+ A Weasyprint pre-rendered representation of an html element
+ element_height: float
+ The height of this element, which will be then translated in a html height
+ """
+ html = HTML(string=getattr(self, f"{element}_html"), base_url=self.base_url,)
+ element_doc = html.render(
+ stylesheets=[CSS(string="@page {size: A4 portrait; margin: 0;}")]
+ )
+ element_page = element_doc.pages[0]
+ element_body = PrintFormatGenerator.get_element(
+ element_page._page_box.all_children(), "body"
+ )
+ element_body = element_body.copy_with_children(element_body.all_children())
+ element_html = PrintFormatGenerator.get_element(
+ element_page._page_box.all_children(), element
+ )
+
+ if element == "header":
+ element_height = element_html.height
+ if element == "footer":
+ element_height = element_page.height - element_html.position_y
+
+ return element_body, element_height
+
+ def _apply_overlay_on_main(self, main_doc, header_body=None, footer_body=None):
+ """
+ Insert the header and the footer in the main document.
+
+ Parameters
+ ----------
+ main_doc: Document
+ The top level representation for a PDF page in Weasyprint.
+ header_body: BlockBox
+ A representation for an html element in Weasyprint.
+ footer_body: BlockBox
+ A representation for an html element in Weasyprint.
+ """
+ for page in main_doc.pages:
+ page_body = PrintFormatGenerator.get_element(page._page_box.all_children(), "body")
+
+ if header_body:
+ page_body.children += header_body.all_children()
+ if footer_body:
+ page_body.children += footer_body.all_children()
+
+ def _make_header_footer(self):
+ self.header_html, self.footer_html = self.get_header_footer_html()
+
+ if self.header_html:
+ header_body, header_height = self._compute_overlay_element("header")
+ else:
+ header_body, header_height = None, 0
+ if self.footer_html:
+ footer_body, footer_height = self._compute_overlay_element("footer")
+ else:
+ footer_body, footer_height = None, 0
+
+ self.header_body = header_body
+ self.header_height = header_height
+ self.footer_body = footer_body
+ self.footer_height = footer_height
+
+ def get_layout(self, print_format):
+ layout = frappe.parse_json(print_format.format_data)
+ layout = self.set_field_renderers(layout)
+ layout = self.process_margin_texts(layout)
+ return layout
+
+ def set_field_renderers(self, layout):
+ renderers = {"HTML Editor": "HTML", "Markdown Editor": "Markdown"}
+ for section in layout["sections"]:
+ for column in section["columns"]:
+ for df in column["fields"]:
+ fieldtype = df["fieldtype"]
+ renderer_name = fieldtype.replace(" ", "")
+ df["renderer"] = renderers.get(fieldtype) or renderer_name
+ df["section"] = section
+ return layout
+
+ def process_margin_texts(self, layout):
+ margin_texts = [
+ "top_left",
+ "top_center",
+ "top_right",
+ "bottom_left",
+ "bottom_center",
+ "bottom_right",
+ ]
+ for key in margin_texts:
+ text = layout.get("text_" + key)
+ if text and "{{" in text:
+ layout["text_" + key] = frappe.render_template(text, self.context)
+
+ return layout
+
+ @staticmethod
+ def get_element(boxes, element):
+ """
+ Given a set of boxes representing the elements of a PDF page in a DOM-like way, find the
+ box which is named `element`.
+
+ Look at the notes of the class for more details on Weasyprint insides.
+ """
+ for box in boxes:
+ if box.element_tag == element:
+ return box
+ return PrintFormatGenerator.get_element(box.all_children(), element)
diff --git a/frappe/website/workspace/website/website.json b/frappe/website/workspace/website/website.json
index 8d22f84b5e..bd06f0a131 100644
--- a/frappe/website/workspace/website/website.json
+++ b/frappe/website/workspace/website/website.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Website\", \"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Blog Post\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Blogger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Web Page\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Web Form\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Website Settings\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Setup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Blog\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Web Site\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Portal\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Knowledge Base\", \"col\": 4}}]",
"creation": "2020-03-02 14:13:51.089373",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "website",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Website",
"links": [
{
@@ -239,15 +232,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:03.154032",
+ "modified": "2021-08-05 12:16:03.154033",
"modified_by": "Administrator",
"module": "Website",
"name": "Website",
- "onboarding": "Website",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/frappe/www/printpreview.html b/frappe/www/printpreview.html
new file mode 100644
index 0000000000..3c3871ecce
--- /dev/null
+++ b/frappe/www/printpreview.html
@@ -0,0 +1,10 @@
+---
+no_cache: 1
+---
+
+
+{{
+ frappe
+ .get_doc('Print Format', frappe.form_dict.print_format)
+ .get_html(frappe.form_dict.name, frappe.form_dict.letterhead)
+}}
diff --git a/frappe/www/update-password.html b/frappe/www/update-password.html
index 0d66fe5ab5..cacbce35b3 100644
--- a/frappe/www/update-password.html
+++ b/frappe/www/update-password.html
@@ -10,7 +10,7 @@
{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}