diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 3520e56078..18de95b40d 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -288,10 +288,24 @@ function get_watch_config() {
assets_json,
prev_assets_json
} = await write_assets_json(result.metafile);
+
+ let changed_files;
if (prev_assets_json) {
- log_rebuilt_assets(prev_assets_json, assets_json);
+ changed_files = get_rebuilt_assets(
+ prev_assets_json,
+ assets_json
+ );
+
+ let timestamp = new Date().toLocaleTimeString();
+ let message = `${timestamp}: Compiled ${changed_files.length} files...`;
+ log(chalk.yellow(message));
+ for (let filepath of changed_files) {
+ let filename = path.basename(filepath);
+ log(" " + filename);
+ }
+ log();
}
- notify_redis({ success: true });
+ notify_redis({ success: true, changed_files });
}
}
};
@@ -461,7 +475,7 @@ function run_build_command_for_apps(apps) {
process.chdir(cwd);
}
-async function notify_redis({ error, success }) {
+async function notify_redis({ error, success, changed_files }) {
// notify redis which in turns tells socketio to publish this to browser
let subscriber = get_redis_subscriber("redis_socketio");
subscriber.on("error", _ => {
@@ -484,6 +498,7 @@ async function notify_redis({ error, success }) {
if (success) {
payload = {
success: true,
+ changed_files,
live_reload: argv["live-reload"]
};
}
@@ -514,7 +529,7 @@ function open_in_editor() {
subscriber.subscribe("open_in_editor");
}
-function log_rebuilt_assets(prev_assets, new_assets) {
+function get_rebuilt_assets(prev_assets, new_assets) {
let added_files = [];
let old_files = Object.values(prev_assets);
let new_files = Object.values(new_assets);
@@ -524,17 +539,5 @@ function log_rebuilt_assets(prev_assets, new_assets) {
added_files.push(filepath);
}
}
-
- log(
- chalk.yellow(
- `${new Date().toLocaleTimeString()}: Compiled ${
- added_files.length
- } files...`
- )
- );
- for (let filepath of added_files) {
- let filename = path.basename(filepath);
- log(" " + filename);
- }
- log();
+ return added_files;
}
diff --git a/frappe/client.py b/frappe/client.py
index 2b80a17feb..0e9be0a7ee 100644
--- a/frappe/client.py
+++ b/frappe/client.py
@@ -258,6 +258,12 @@ def set_default(key, value, parent=None):
frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user)
+@frappe.whitelist()
+def get_default(key, parent=None):
+ """set a user default value"""
+ return frappe.db.get_default(key, parent)
+
+
@frappe.whitelist(methods=['POST', 'PUT'])
def make_width_property_setter(doc):
'''Set width Property Setter
diff --git a/frappe/data/google_fonts.json b/frappe/data/google_fonts.json
new file mode 100644
index 0000000000..232e509e77
--- /dev/null
+++ b/frappe/data/google_fonts.json
@@ -0,0 +1,56 @@
+[
+ "Alegreya Sans",
+ "Alegreya",
+ "Andada Pro",
+ "Anton",
+ "Archivo Narrow",
+ "Archivo",
+ "BioRhyme",
+ "Cardo",
+ "Chivo",
+ "Cormorant",
+ "Crimson Text",
+ "DM Sans",
+ "Eczar",
+ "Encode Sans",
+ "Epilogue ",
+ "Fira Sans",
+ "Hahmlet",
+ "IBM Plex Sans",
+ "Inconsolata",
+ "Inknut Antiqua",
+ "Inter",
+ "JetBrains Mono",
+ "Karla",
+ "Lato",
+ "Libre Baskerville",
+ "Libre Franklin",
+ "Lora",
+ "Manrope",
+ "Merriweather",
+ "Montserrat",
+ "Neuton",
+ "Nunito",
+ "Old Standard TT",
+ "Open Sans",
+ "Oswald",
+ "Oxygen",
+ "Playfair Display",
+ "Poppins",
+ "Proza Libre",
+ "PT Sans",
+ "PT Serif",
+ "Raleway",
+ "Roboto Slab",
+ "Roboto",
+ "Rubik",
+ "Sora",
+ "Source Sans Pro",
+ "Source Serif Pro",
+ "Space Grotesk",
+ "Space Mono",
+ "Spectral",
+ "Syne",
+ "Work Sans"
+]
+
diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json
index f6c9def567..f723a6b489 100644
--- a/frappe/printing/doctype/letter_head/letter_head.json
+++ b/frappe/printing/doctype/letter_head/letter_head.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_rename": 1,
"autoname": "field:letter_head_name",
"creation": "2012-11-22 17:45:46",
@@ -13,6 +14,9 @@
"is_default",
"letter_head_image_section",
"image",
+ "image_height",
+ "image_width",
+ "align",
"header_section",
"content",
"footer_section",
@@ -100,15 +104,34 @@
"fieldname": "footer",
"fieldtype": "HTML Editor",
"label": "Footer HTML"
+ },
+ {
+ "default": "Left",
+ "fieldname": "align",
+ "fieldtype": "Select",
+ "label": "Align",
+ "options": "Left\nRight\nCenter"
+ },
+ {
+ "fieldname": "image_height",
+ "fieldtype": "Float",
+ "label": "Image Height"
+ },
+ {
+ "fieldname": "image_width",
+ "fieldtype": "Float",
+ "label": "Image Width"
}
],
"icon": "fa fa-font",
"idx": 1,
+ "links": [],
"max_attachments": 3,
- "modified": "2019-11-11 18:46:43.375120",
+ "modified": "2021-10-03 14:37:58.314696",
"modified_by": "Administrator",
"module": "Printing",
"name": "Letter Head",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py
index eeaef28393..67c0d236e0 100644
--- a/frappe/printing/doctype/letter_head/letter_head.py
+++ b/frappe/printing/doctype/letter_head/letter_head.py
@@ -2,7 +2,7 @@
# License: MIT. See LICENSE
import frappe
-from frappe.utils import is_image
+from frappe.utils import is_image, flt
from frappe.model.document import Document
from frappe import _
@@ -26,7 +26,15 @@ class LetterHead(Document):
def set_image(self):
if self.source=='Image':
if self.image and is_image(self.image):
- self.content = '
'.format(self.image)
+ self.image_width = flt(self.image_width)
+ self.image_height = flt(self.image_height)
+ dimension = 'width' if self.image_width > self.image_height else 'height'
+ dimension_value = self.get('image_' + dimension)
+ self.content = f'''
+
+

+
+ '''
frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True)
else:
frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange')
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 7b7009dbaf..3fd1d9d148 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -30,7 +30,11 @@ frappe.ui.form.on("Print Format", {
frappe.msgprint(__("Please select DocType first"));
return;
}
- frappe.set_route("print-format-builder", frm.doc.name);
+ if (frm.doc.print_format_builder_beta) {
+ frappe.set_route("print-format-builder-beta", frm.doc.name);
+ } else {
+ frappe.set_route("print-format-builder", frm.doc.name);
+ }
});
}
else if (frm.doc.custom_format && !frm.doc.raw_printing) {
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 4032cef209..75ec0fa7fd 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -19,19 +19,26 @@
"html",
"raw_commands",
"section_break_9",
+ "margin_top",
+ "margin_bottom",
+ "margin_left",
+ "margin_right",
"align_labels_right",
"show_section_headings",
"line_breaks",
"absolute_value",
"column_break_11",
+ "font_size",
"font",
+ "page_number",
"css_section",
"css",
"custom_html_help",
"section_break_13",
"print_format_help",
"format_data",
- "print_format_builder"
+ "print_format_builder",
+ "print_format_builder_beta"
],
"fields": [
{
@@ -149,12 +156,10 @@
"options": "Language"
},
{
- "default": "Default",
"depends_on": "eval:!doc.custom_format",
"fieldname": "font",
- "fieldtype": "Select",
- "label": "Font",
- "options": "Default\nHelvetica Neue\nArial\nHelvetica\nVerdana\nMonospace"
+ "fieldtype": "Data",
+ "label": "Google Font"
},
{
"depends_on": "eval:!doc.raw_printing",
@@ -205,16 +210,60 @@
"fieldname": "absolute_value",
"fieldtype": "Check",
"label": "Show Absolute Values"
+ },
+ {
+ "default": "0",
+ "fieldname": "print_format_builder_beta",
+ "fieldtype": "Check",
+ "label": "Print Format Builder Beta"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_top",
+ "fieldtype": "Float",
+ "label": "Margin Top"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_bottom",
+ "fieldtype": "Float",
+ "label": "Margin Bottom"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_left",
+ "fieldtype": "Float",
+ "label": "Margin Left"
+ },
+ {
+ "default": "15",
+ "fieldname": "margin_right",
+ "fieldtype": "Float",
+ "label": "Margin Right"
+ },
+ {
+ "default": "14",
+ "fieldname": "font_size",
+ "fieldtype": "Int",
+ "label": "Font Size"
+ },
+ {
+ "default": "Hide",
+ "fieldname": "page_number",
+ "fieldtype": "Select",
+ "label": "Page Number",
+ "options": "Hide\nTop Left\nTop Center\nTop Right\nBottom Left\nBottom Center\nBottom Right"
}
],
"icon": "fa fa-print",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-01 15:25:46.578863",
+ "modified": "2021-10-12 17:52:41.167107",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py
index 878a864b38..f19c0af9bf 100644
--- a/frappe/printing/doctype/print_format/print_format.py
+++ b/frappe/printing/doctype/print_format/print_format.py
@@ -7,10 +7,24 @@ import frappe.utils
import json
from frappe import _
from frappe.utils.jinja import validate_template
-
+from frappe.utils.weasyprint import get_html, download_pdf
from frappe.model.document import Document
class PrintFormat(Document):
+ def onload(self):
+ templates = frappe.db.get_all(
+ "Print Format Field Template",
+ fields=["template", "field", "name"],
+ filters={"document_type": self.doc_type},
+ )
+ self.set_onload("print_templates", templates)
+
+ def get_html(self, docname, letterhead=None):
+ return get_html(self.doc_type, docname, self.name, letterhead)
+
+ def download_pdf(self, docname, letterhead=None):
+ return download_pdf(self.doc_type, docname, self.name, letterhead)
+
def validate(self):
if (self.standard=="Yes"
and not frappe.local.conf.get("developer_mode")
@@ -38,6 +52,10 @@ class PrintFormat(Document):
def extract_images(self):
from frappe.core.doctype.file.file import extract_images_from_html
+
+ if self.print_format_builder_beta:
+ return
+
if self.format_data:
data = json.loads(self.format_data)
for df in data:
diff --git a/frappe/printing/doctype/print_format_field_template/__init__.py b/frappe/printing/doctype/print_format_field_template/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.js b/frappe/printing/doctype/print_format_field_template/print_format_field_template.js
new file mode 100644
index 0000000000..7fbb0d7359
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Print Format Field Template', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.json b/frappe/printing/doctype/print_format_field_template/print_format_field_template.json
new file mode 100644
index 0000000000..3b79aae7e8
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.json
@@ -0,0 +1,101 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-10-05 14:23:56.508499",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "field",
+ "template_file",
+ "column_break_3",
+ "module",
+ "standard",
+ "section_break_5",
+ "template"
+ ],
+ "fields": [
+ {
+ "depends_on": "eval:!doc.multiple",
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Document Type",
+ "mandatory_depends_on": "eval:!doc.multiple",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "field",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Default Template For Field"
+ },
+ {
+ "depends_on": "eval:!doc.standard",
+ "fieldname": "template",
+ "fieldtype": "Code",
+ "label": "Template",
+ "mandatory_depends_on": "eval:!doc.standard",
+ "options": "HTML"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "hide_border": 1
+ },
+ {
+ "depends_on": "standard",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "options": "Module Def"
+ },
+ {
+ "default": "0",
+ "fieldname": "standard",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Standard"
+ },
+ {
+ "depends_on": "eval:doc.standard",
+ "fieldname": "template_file",
+ "fieldtype": "Data",
+ "label": "Template File",
+ "mandatory_depends_on": "eval:doc.standard"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-10-19 17:47:59.577949",
+ "modified_by": "Administrator",
+ "module": "Printing",
+ "name": "Print Format Field Template",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/printing/doctype/print_format_field_template/print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py
new file mode 100644
index 0000000000..b66afdb6b1
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/print_format_field_template.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+
+
+class PrintFormatFieldTemplate(Document):
+ def validate(self):
+ if self.standard and not (frappe.conf.developer_mode or frappe.flags.in_patch):
+ frappe.throw(_("Enable developer mode to create a standard Print Template"))
+
+ def before_insert(self):
+ self.validate_duplicate()
+
+ def on_update(self):
+ self.validate_duplicate()
+ self.export_doc()
+
+ def validate_duplicate(self):
+ if not self.standard:
+ return
+ if not self.field:
+ return
+
+ filters = {"document_type": self.document_type, "field": self.field}
+ if not self.is_new():
+ filters.update({"name": ("!=", self.name)})
+ result = frappe.db.get_all("Print Format Field Template", filters=filters, limit=1)
+ if result:
+ frappe.throw(
+ _("A template already exists for field {0} of {1}").format(
+ frappe.bold(self.field), frappe.bold(self.document_type)
+ ),
+ frappe.DuplicateEntryError,
+ title=_("Duplicate Entry"),
+ )
+
+ def export_doc(self):
+ from frappe.modules.utils import export_module_json
+
+ export_module_json(self, self.standard, self.module)
diff --git a/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py b/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py
new file mode 100644
index 0000000000..f0b1329763
--- /dev/null
+++ b/frappe/printing/doctype/print_format_field_template/test_print_format_field_template.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+class TestPrintFormatFieldTemplate(unittest.TestCase):
+ pass
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 7f40fd3127..f10c703589 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -41,7 +41,11 @@ frappe.ui.form.PrintView = class {
- `
+
+
+
+
+ `
);
this.print_settings = frappe.model.get_doc(
@@ -72,7 +76,7 @@ frappe.ui.form.PrintView = class {
this.page.add_button(
__('PDF'),
- () => this.render_page('/api/method/frappe.utils.print_format.download_pdf?'),
+ () => this.render_pdf(),
{ icon: 'small-file' }
);
@@ -190,6 +194,13 @@ frappe.ui.form.PrintView = class {
this.set_breadcrumbs();
this.setup_customize_dialog();
+ // print format builder beta
+ this.page.add_inner_message(`
+
+ ${__('Try the new Print Format Builder')}
+
+ `);
+
let tasks = [
this.refresh_print_options,
this.set_default_print_language,
@@ -233,7 +244,7 @@ frappe.ui.form.PrintView = class {
let print_format = this.get_print_format();
let is_custom_format =
print_format.name &&
- print_format.print_format_builder &&
+ (print_format.print_format_builder || print_format.print_format_builder_beta) &&
print_format.standard === 'No';
let is_standard_but_editable =
print_format.name && print_format.custom_format;
@@ -243,7 +254,11 @@ frappe.ui.form.PrintView = class {
return;
}
if (is_custom_format) {
- frappe.set_route('print-format-builder', print_format.name);
+ if (print_format.print_format_builder_beta) {
+ frappe.set_route('print-format-builder-beta', print_format.name);
+ } else {
+ frappe.set_route('print-format-builder', print_format.name);
+ }
return;
}
// start a new print format
@@ -261,6 +276,11 @@ frappe.ui.form.PrintView = class {
fieldtype: 'Read Only',
default: print_format.name || 'Standard',
},
+ {
+ label: __('Use the new Print Format Builder'),
+ fieldname: 'beta',
+ fieldtype: 'Check'
+ },
],
(data) => {
frappe.route_options = {
@@ -268,6 +288,7 @@ frappe.ui.form.PrintView = class {
doctype: this.frm.doctype,
name: data.print_format_name,
based_on: data.based_on,
+ beta: data.beta
};
frappe.set_route('print-format-builder');
this.print_sel.val(data.print_format_name);
@@ -380,6 +401,17 @@ frappe.ui.form.PrintView = class {
}
preview() {
+ let print_format = this.get_print_format();
+ if (print_format.print_format_builder_beta) {
+ this.print_wrapper.find('.print-preview-wrapper').hide();
+ this.print_wrapper.find('.preview-beta-wrapper').show();
+ this.preview_beta();
+ return;
+ }
+
+ this.print_wrapper.find('.preview-beta-wrapper').hide();
+ this.print_wrapper.find('.print-preview-wrapper').show();
+
const $print_format = this.print_wrapper.find('iframe');
this.$print_format_body = $print_format.contents();
this.get_print_html((out) => {
@@ -403,6 +435,21 @@ frappe.ui.form.PrintView = class {
});
}
+ preview_beta() {
+ let print_format = this.get_print_format();
+ const iframe = this.print_wrapper.find('.preview-beta-wrapper iframe');
+ let params = new URLSearchParams({
+ doctype: this.frm.doc.doctype,
+ name: this.frm.doc.name,
+ print_format: print_format.name
+ });
+ let letterhead = this.get_letterhead();
+ if (letterhead) {
+ params.append("letterhead", letterhead);
+ }
+ iframe.prop('src', `/printpreview?${params.toString()}`);
+ }
+
setup_print_format_dom(out, $print_format) {
this.print_wrapper.find('.print-format-skeleton').remove();
let base_url = frappe.urllib.get_base_url();
@@ -565,6 +612,26 @@ frappe.ui.form.PrintView = class {
},
});
}
+
+ render_pdf() {
+ let print_format = this.get_print_format();
+ if (print_format.print_format_builder_beta) {
+ let params = new URLSearchParams({
+ doctype: this.frm.doc.doctype,
+ name: this.frm.doc.name,
+ print_format: print_format.name,
+ letterhead: this.get_letterhead()
+ });
+ let w = window.open(`/api/method/frappe.utils.weasyprint.download_pdf?${params}`);
+ if (!w) {
+ frappe.msgprint(__('Please enable pop-ups'));
+ return;
+ }
+ } else {
+ this.render_page('/api/method/frappe.utils.print_format.download_pdf?');
+ }
+ }
+
render_page(method, printit = false) {
let w = window.open(
frappe.urllib.get_full_url(
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js
index b73ff31d32..313e8da539 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.js
+++ b/frappe/printing/page/print_format_builder/print_format_builder.js
@@ -12,9 +12,9 @@ frappe.pages['print-format-builder'].on_page_show = function(wrapper) {
});
} else if(frappe.route_options) {
if(frappe.route_options.make_new) {
- let { doctype, name, based_on } = frappe.route_options;
+ let { doctype, name, based_on, beta } = frappe.route_options;
frappe.route_options = null;
- frappe.print_format_builder.setup_new_print_format(doctype, name, based_on);
+ frappe.print_format_builder.setup_new_print_format(doctype, name, based_on, beta);
} else {
frappe.print_format_builder.print_format = frappe.route_options.doc;
frappe.route_options = null;
@@ -126,18 +126,22 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
});
}
- setup_new_print_format(doctype, name, based_on) {
+ setup_new_print_format(doctype, name, based_on, beta) {
frappe.call({
method: 'frappe.printing.page.print_format_builder.print_format_builder.create_custom_format',
args: {
doctype: doctype,
name: name,
- based_on: based_on
+ based_on: based_on,
+ beta: Boolean(beta)
},
callback: (r) => {
- if(!r.exc) {
- if(r.message) {
- this.print_format = r.message;
+ if (r.message) {
+ let print_format = r.message;
+ if (print_format.print_format_builder_beta) {
+ frappe.set_route('print-format-builder-beta', print_format.name);
+ } else {
+ this.print_format = print_format;
this.refresh();
}
}
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.py b/frappe/printing/page/print_format_builder/print_format_builder.py
index d9f57762b0..fae564d3c3 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.py
+++ b/frappe/printing/page/print_format_builder/print_format_builder.py
@@ -1,11 +1,16 @@
import frappe
@frappe.whitelist()
-def create_custom_format(doctype, name, based_on='Standard'):
+def create_custom_format(doctype, name, based_on='Standard', beta=False):
doc = frappe.new_doc('Print Format')
doc.doc_type = doctype
doc.name = name
- doc.print_format_builder = 1
+ beta = frappe.parse_json(beta)
+
+ if beta:
+ doc.print_format_builder_beta = 1
+ else:
+ doc.print_format_builder = 1
doc.format_data = frappe.db.get_value('Print Format', based_on, 'format_data') \
if based_on != 'Standard' else None
doc.insert()
diff --git a/frappe/printing/page/print_format_builder_beta/__init__.py b/frappe/printing/page/print_format_builder_beta/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css
new file mode 100644
index 0000000000..0bd8d9c0f3
--- /dev/null
+++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.css
@@ -0,0 +1,3 @@
+.layout-main-section-wrapper {
+ margin-bottom: 0;
+}
diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js
new file mode 100644
index 0000000000..e923bbcb00
--- /dev/null
+++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js
@@ -0,0 +1,122 @@
+frappe.pages["print-format-builder-beta"].on_page_load = function(wrapper) {
+ frappe.ui.make_app_page({
+ parent: wrapper,
+ title: __("Print Format Builder"),
+ single_column: true
+ });
+
+ // hot reload in development
+ if (frappe.boot.developer_mode) {
+ frappe.hot_update = frappe.hot_update || [];
+ frappe.hot_update.push(() => load_print_format_builder_beta(wrapper));
+ }
+};
+
+frappe.pages["print-format-builder-beta"].on_page_show = function(wrapper) {
+ load_print_format_builder_beta(wrapper);
+};
+
+function load_print_format_builder_beta(wrapper) {
+ let route = frappe.get_route();
+ let $parent = $(wrapper).find(".layout-main-section");
+ $parent.empty();
+
+ if (route.length > 1) {
+ frappe.require("print_format_builder.bundle.js").then(() => {
+ frappe.print_format_builder = new frappe.ui.PrintFormatBuilder({
+ wrapper: $parent,
+ page: wrapper.page,
+ print_format: route[1]
+ });
+ });
+ } else {
+ let d = new frappe.ui.Dialog({
+ title: __("Create or Edit Print Format"),
+ fields: [
+ {
+ label: __("Action"),
+ fieldname: "action",
+ fieldtype: "Select",
+ options: [
+ { label: __("Create New"), value: "Create" },
+ { label: __("Edit Existing"), value: "Edit" }
+ ],
+ change() {
+ let action = d.get_value("action");
+ d.get_primary_btn().text(
+ action === "Create" ? __("Create") : __("Edit")
+ );
+ }
+ },
+ {
+ label: __("Select Document Type"),
+ fieldname: "doctype",
+ fieldtype: "Link",
+ options: "DocType",
+ filters: {
+ istable: 0
+ },
+ reqd: 1,
+ default: frappe.route_options
+ ? frappe.route_options.doctype
+ : null
+ },
+ {
+ label: __("Print Format Name"),
+ fieldname: "print_format_name",
+ fieldtype: "Data",
+ depends_on: doc => doc.action === "Create",
+ mandatory_depends_on: doc => doc.action === "Create"
+ },
+ {
+ label: __("Select Print Format"),
+ fieldname: "print_format",
+ fieldtype: "Link",
+ options: "Print Format",
+ only_select: 1,
+ depends_on: doc => doc.action === "Edit",
+ get_query() {
+ return {
+ filters: {
+ doc_type: d.get_value("doctype"),
+ print_format_builder_beta: 1
+ }
+ };
+ },
+ mandatory_depends_on: doc => doc.action === "Edit"
+ }
+ ],
+ primary_action_label: __("Edit"),
+ primary_action({
+ action,
+ doctype,
+ print_format,
+ print_format_name
+ }) {
+ if (action === "Edit") {
+ frappe.set_route("print-format-builder-beta", print_format);
+ } else if (action === "Create") {
+ d.get_primary_btn().prop("disabled", true);
+ frappe.db
+ .insert({
+ doctype: "Print Format",
+ name: print_format_name,
+ doc_type: doctype,
+ print_format_builder_beta: 1
+ })
+ .then(doc => {
+ frappe.set_route(
+ "print-format-builder-beta",
+ doc.name
+ );
+ })
+ .finally(() => {
+ d.get_primary_btn().prop("disabled", false);
+ });
+ }
+ }
+ });
+ d.set_value("action", "Create");
+ d.show();
+ }
+}
diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json
new file mode 100644
index 0000000000..a5b1288bc0
--- /dev/null
+++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.json
@@ -0,0 +1,22 @@
+{
+ "content": null,
+ "creation": "2021-07-10 12:22:16.138485",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2021-07-10 12:22:16.138485",
+ "modified_by": "Administrator",
+ "module": "Printing",
+ "name": "print-format-builder-beta",
+ "owner": "Administrator",
+ "page_name": "Print Format Builder Beta",
+ "roles": [
+ {
+ "role": "System Manager"
+ }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0
+}
\ No newline at end of file
diff --git a/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.py b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.py
new file mode 100644
index 0000000000..e13412cd07
--- /dev/null
+++ b/frappe/printing/page/print_format_builder_beta/print_format_builder_beta.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+import functools
+
+
+@frappe.whitelist()
+def get_google_fonts():
+ return _get_google_fonts()
+
+
+@functools.lru_cache()
+def _get_google_fonts():
+ file_path = frappe.get_app_path("frappe", "data", "google_fonts.json")
+ return frappe.parse_json(frappe.read_file(file_path))
diff --git a/frappe/public/js/frappe/build_events/build_events.bundle.js b/frappe/public/js/frappe/build_events/build_events.bundle.js
index 13b9c7a334..21960f0b00 100644
--- a/frappe/public/js/frappe/build_events/build_events.bundle.js
+++ b/frappe/public/js/frappe/build_events/build_events.bundle.js
@@ -7,6 +7,34 @@ let error = null;
frappe.realtime.on("build_event", data => {
if (data.success) {
+ // remove executed cache for rebuilt files
+ let changed_files = data.changed_files;
+ if (Array.isArray(changed_files)) {
+ for (let file of changed_files) {
+ if (file.includes(".bundle.")) {
+ let parts = file.split(".bundle.");
+ if (parts.length === 2) {
+ let filename = parts[0].split("/").slice(-1)[0];
+
+ frappe.assets.executed_ = frappe.assets.executed_.filter(
+ asset => !asset.includes(`${filename}.bundle`)
+ );
+ }
+ }
+ }
+ }
+ // update assets json
+ frappe.call("frappe.sessions.get_boot_assets_json").then(r => {
+ if (r.message) {
+ frappe.boot.assets_json = r.message;
+
+ if (frappe.hot_update) {
+ frappe.hot_update.forEach(callback => {
+ callback();
+ });
+ }
+ }
+ });
show_build_success(data);
} else if (data.error) {
show_build_error(data);
diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js
index 04bdb3f19e..3a678b6a97 100644
--- a/frappe/public/js/frappe/form/controls/barcode.js
+++ b/frappe/public/js/frappe/form/controls/barcode.js
@@ -56,9 +56,6 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont
get_options(value) {
// get JsBarcode options
let options = {};
- options.background = "var(--control-bg)";
- options.lineColor = "var(--text-color)";
- options.font = "var(--font-stack)";
options.fontSize = "16";
options.width = "3";
options.height = "50";
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index d4dbf91bdb..7874ffcdde 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -58,6 +58,12 @@ $('body').on('click', 'a', function(e) {
if (frappe.router.is_app_route(e.currentTarget.pathname)) {
// target has "/app, this is a v2 style route.
+
+ frappe.route_options = {};
+ let params = new URLSearchParams(e.currentTarget.search);
+ for (const [key, value] of params) {
+ frappe.route_options[key] = value;
+ }
return override(e.currentTarget.pathname + e.currentTarget.hash);
}
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/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/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/sessions.py b/frappe/sessions.py
index c27c17a5cb..9a0f19df80 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -160,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/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/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/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/package.json b/package.json
index 7472a6b28b..d173b8218c 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,8 @@
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "2.6.12",
- "vue-router": "^2.0.0"
+ "vue-router": "^2.0.0",
+ "vuedraggable": "^2.24.3"
},
"devDependencies": {
"chalk": "^2.3.2",
diff --git a/requirements.txt b/requirements.txt
index 17f77efa5d..671f6ced11 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -73,3 +73,5 @@ wrapt~=1.12.1
xlrd~=2.0.1
zxcvbn-python~=4.4.24
tenacity~=8.0.1
+cairocffi==1.2.0
+WeasyPrint==52.5
diff --git a/yarn.lock b/yarn.lock
index 2cf748606c..584a56efa7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4353,6 +4353,11 @@ socket.io@^2.4.0:
socket.io-client "2.4.0"
socket.io-parser "~3.4.0"
+sortablejs@1.10.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.2.tgz#6e40364d913f98b85a14f6678f92b5c1221f5290"
+ integrity sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==
+
sortablejs@^1.7.0:
version "1.8.3"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.8.3.tgz#5ae908ef96300966e95440a143340f5dd565a0df"
@@ -4922,6 +4927,20 @@ vue@2.6.12:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.12.tgz#f5ebd4fa6bd2869403e29a896aed4904456c9123"
integrity sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==
+vuedraggable@^2.24.3:
+ version "2.24.3"
+ resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-2.24.3.tgz#43c93849b746a24ce503e123d5b259c701ba0d19"
+ integrity sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==
+ dependencies:
+ sortablejs "1.10.2"
+
+wcwidth@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+ integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
+ dependencies:
+ defaults "^1.0.3"
+
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"