You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

792 lines
20 KiB

  1. frappe.pages['print'].on_page_load = function(wrapper) {
  2. frappe.ui.make_app_page({
  3. parent: wrapper,
  4. });
  5. let print_view = new frappe.ui.form.PrintView(wrapper);
  6. $(wrapper).bind('show', () => {
  7. const route = frappe.get_route();
  8. const doctype = route[1];
  9. const docname = route[2];
  10. if (!frappe.route_options || !frappe.route_options.frm) {
  11. frappe.model.with_doc(doctype, docname, () => {
  12. let frm = { doctype: doctype, docname: docname };
  13. frm.doc = frappe.get_doc(doctype, docname);
  14. frappe.model.with_doctype(doctype, () => {
  15. frm.meta = frappe.get_meta(route[1]);
  16. print_view.show(frm);
  17. });
  18. });
  19. } else {
  20. print_view.frm = frappe.route_options.frm;
  21. frappe.route_options.frm = null;
  22. print_view.show(print_view.frm);
  23. }
  24. });
  25. };
  26. frappe.ui.form.PrintView = class {
  27. constructor(wrapper) {
  28. this.wrapper = $(wrapper);
  29. this.page = wrapper.page;
  30. this.make();
  31. }
  32. make() {
  33. this.print_wrapper = this.page.main.empty().html(
  34. `<div class="print-preview-wrapper"><div class="print-preview">
  35. ${frappe.render_template('print_skeleton_loading')}
  36. <iframe class="print-format-container" width="100%" height="0" frameBorder="0" scrolling="no"">
  37. </iframe>
  38. </div>
  39. <div class="page-break-message text-muted text-center text-medium margin-top"></div>
  40. </div>`
  41. );
  42. this.print_settings = frappe.model.get_doc(
  43. ':Print Settings',
  44. 'Print Settings'
  45. );
  46. this.setup_toolbar();
  47. this.setup_menu();
  48. this.setup_sidebar();
  49. this.setup_keyboard_shortcuts();
  50. }
  51. set_title() {
  52. this.page.set_title(this.frm.docname);
  53. }
  54. setup_toolbar() {
  55. this.page.set_primary_action(
  56. __('Print'),
  57. () => this.printit(), 'printer'
  58. );
  59. this.page.add_button(
  60. __('Full Page'),
  61. () => this.render_page('/printview?'),
  62. { icon: 'full-page' }
  63. );
  64. this.page.add_button(
  65. __('PDF'),
  66. () => this.render_page('/api/method/frappe.utils.print_format.download_pdf?'),
  67. { icon: 'small-file' }
  68. );
  69. this.page.add_button(
  70. frappe.utils.icon('refresh'),
  71. () => this.refresh_print_format()
  72. );
  73. }
  74. setup_sidebar() {
  75. this.sidebar = this.page.sidebar.addClass('print-preview-sidebar');
  76. this.print_sel = this.add_sidebar_item(
  77. {
  78. fieldtype: 'Select',
  79. fieldname: 'print_format',
  80. label: 'Print Format',
  81. options: [this.get_default_option_for_select(__('Select Print Format'))],
  82. change: () => this.refresh_print_format(),
  83. default: __('Select Print Format')
  84. },
  85. ).$input;
  86. this.language_sel = this.add_sidebar_item(
  87. {
  88. fieldtype: 'Select',
  89. fieldname: 'language',
  90. placeholder: 'Language',
  91. options: [
  92. this.get_default_option_for_select(__('Select Language')),
  93. ...this.get_language_options()
  94. ],
  95. default: __('Select Language'),
  96. change: () => {
  97. this.set_user_lang();
  98. this.preview();
  99. },
  100. },
  101. ).$input;
  102. this.letterhead_selector_df = this.add_sidebar_item(
  103. {
  104. fieldtype: 'Autocomplete',
  105. fieldname: 'letterhead',
  106. label: __('Select Letterhead'),
  107. placeholder: __('Select Letterhead'),
  108. options: [__('No Letterhead')],
  109. change: () => this.preview(),
  110. default: this.print_settings.with_letterhead
  111. ? __('No Letterhead')
  112. : __('Select Letterhead')
  113. },
  114. );
  115. this.letterhead_selector = this.letterhead_selector_df.$input;
  116. this.sidebar_dynamic_section = $(
  117. `<div class="dynamic-settings"></div>`
  118. ).appendTo(this.sidebar);
  119. }
  120. add_sidebar_item(df, is_dynamic) {
  121. if (df.fieldtype == 'Select') {
  122. df.input_class = 'btn btn-default btn-sm';
  123. }
  124. let field = frappe.ui.form.make_control({
  125. df: df,
  126. parent: is_dynamic ? this.sidebar_dynamic_section : this.sidebar,
  127. render_input: 1,
  128. });
  129. if (df.default != null) {
  130. field.set_input(df.default);
  131. }
  132. return field;
  133. }
  134. get_default_option_for_select(value) {
  135. return {
  136. label: value,
  137. value: value,
  138. disabled: true
  139. };
  140. }
  141. setup_menu() {
  142. this.page.clear_menu();
  143. this.page.add_menu_item(__('Print Settings'), () => {
  144. frappe.set_route('Form', 'Print Settings');
  145. });
  146. if (this.print_settings.enable_raw_printing == '1') {
  147. this.page.add_menu_item(__('Raw Printing Setting'), () => {
  148. this.printer_setting_dialog();
  149. });
  150. }
  151. if (frappe.model.can_create('Print Format')) {
  152. this.page.add_menu_item(__('Customize'), () =>
  153. this.edit_print_format()
  154. );
  155. }
  156. if (cint(this.print_settings.enable_print_server)) {
  157. this.page.add_menu_item(__('Select Network Printer'), () =>
  158. this.network_printer_setting_dialog()
  159. );
  160. }
  161. }
  162. show(frm) {
  163. this.frm = frm;
  164. this.set_title();
  165. this.set_breadcrumbs();
  166. this.setup_customize_dialog();
  167. let tasks = [
  168. this.refresh_print_options,
  169. this.set_default_print_language,
  170. this.set_letterhead_options,
  171. this.preview,
  172. ].map((fn) => fn.bind(this));
  173. this.setup_additional_settings();
  174. return frappe.run_serially(tasks);
  175. }
  176. set_breadcrumbs() {
  177. frappe.breadcrumbs.add(this.frm.meta.module, this.frm.doctype);
  178. }
  179. setup_additional_settings() {
  180. this.additional_settings = {};
  181. this.sidebar_dynamic_section.empty();
  182. frappe
  183. .xcall('frappe.printing.page.print.print.get_print_settings_to_show', {
  184. doctype: this.frm.doc.doctype,
  185. docname: this.frm.doc.name
  186. })
  187. .then((settings) => this.add_settings_to_sidebar(settings));
  188. }
  189. add_settings_to_sidebar(settings) {
  190. for (let df of settings) {
  191. let field = this.add_sidebar_item({
  192. ...df,
  193. change: () => {
  194. const val = field.get_value();
  195. this.additional_settings[field.df.fieldname] = val;
  196. this.preview();
  197. },
  198. }, true);
  199. }
  200. }
  201. edit_print_format() {
  202. let print_format = this.get_print_format();
  203. let is_custom_format =
  204. print_format.name &&
  205. print_format.print_format_builder &&
  206. print_format.standard === 'No';
  207. let is_standard_but_editable =
  208. print_format.name && print_format.custom_format;
  209. if (is_standard_but_editable) {
  210. frappe.set_route('Form', 'Print Format', print_format.name);
  211. return;
  212. }
  213. if (is_custom_format) {
  214. frappe.set_route('print-format-builder', print_format.name);
  215. return;
  216. }
  217. // start a new print format
  218. frappe.prompt(
  219. [
  220. {
  221. label: __('New Print Format Name'),
  222. fieldname: 'print_format_name',
  223. fieldtype: 'Data',
  224. reqd: 1,
  225. },
  226. {
  227. label: __('Based On'),
  228. fieldname: 'based_on',
  229. fieldtype: 'Read Only',
  230. default: print_format.name || 'Standard',
  231. },
  232. ],
  233. (data) => {
  234. frappe.route_options = {
  235. make_new: true,
  236. doctype: this.frm.doctype,
  237. name: data.print_format_name,
  238. based_on: data.based_on,
  239. };
  240. frappe.set_route('print-format-builder');
  241. this.print_sel.val(data.print_format_name);
  242. },
  243. __('New Custom Print Format'),
  244. __('Start')
  245. );
  246. }
  247. refresh_print_format() {
  248. this.set_default_print_language();
  249. this.toggle_raw_printing();
  250. this.preview();
  251. }
  252. // bind_events () {
  253. // // // hide print view on pressing escape, only if there is no focus on any input
  254. // // $(document).on("keydown", function (e) {
  255. // // if (e.which === 27 && me.frm && e.target === document.body) {
  256. // // me.hide();
  257. // // }
  258. // // });
  259. // }
  260. setup_customize_dialog() {
  261. let print_format = this.get_print_format();
  262. $(document).on('new-print-format', (e) => {
  263. this.refresh_print_options();
  264. if (e.print_format) {
  265. this.print_sel.val(e.print_format);
  266. }
  267. // start a new print format
  268. frappe.prompt(
  269. [
  270. {
  271. label: __('New Print Format Name'),
  272. fieldname: 'print_format_name',
  273. fieldtype: 'Data',
  274. reqd: 1,
  275. },
  276. {
  277. label: __('Based On'),
  278. fieldname: 'based_on',
  279. fieldtype: 'Read Only',
  280. default: print_format.name || 'Standard',
  281. },
  282. ],
  283. (data) => {
  284. frappe.route_options = {
  285. make_new: true,
  286. doctype: this.frm.doctype,
  287. name: data.print_format_name,
  288. based_on: data.based_on,
  289. };
  290. frappe.set_route('print-format-builder');
  291. },
  292. __('New Custom Print Format'),
  293. __('Start')
  294. );
  295. });
  296. }
  297. setup_keyboard_shortcuts() {
  298. this.wrapper.find('.print-toolbar a.btn-default').each((i, el) => {
  299. frappe.ui.keys.get_shortcut_group(this.frm.page).add($(el));
  300. });
  301. }
  302. set_letterhead_options() {
  303. let letterhead_options = [__('No Letterhead')];
  304. let default_letterhead;
  305. let doc_letterhead = this.frm.doc.letter_head;
  306. return frappe.db
  307. .get_list('Letter Head', { fields: ['name', 'is_default'], limit: 0 })
  308. .then((letterheads) => {
  309. letterheads.map((letterhead) => {
  310. if (letterhead.is_default) default_letterhead = letterhead.name;
  311. return letterhead_options.push(letterhead.name);
  312. });
  313. this.letterhead_selector_df.set_data(letterhead_options);
  314. let selected_letterhead = doc_letterhead || default_letterhead;
  315. if (selected_letterhead)
  316. this.letterhead_selector.val(selected_letterhead);
  317. });
  318. }
  319. set_user_lang() {
  320. this.lang_code = this.language_sel.val();
  321. }
  322. get_language_options() {
  323. return frappe.get_languages();
  324. }
  325. set_default_print_language() {
  326. let print_format = this.get_print_format();
  327. this.lang_code =
  328. print_format.default_print_language ||
  329. this.frm.doc.language ||
  330. frappe.boot.lang;
  331. this.language_sel.val(this.lang_code);
  332. }
  333. toggle_raw_printing() {
  334. const is_raw_printing = this.is_raw_printing();
  335. this.wrapper.find('.btn-print-preview').toggle(!is_raw_printing);
  336. this.wrapper.find('.btn-download-pdf').toggle(!is_raw_printing);
  337. }
  338. preview() {
  339. const $print_format = this.print_wrapper.find('iframe');
  340. this.$print_format_body = $print_format.contents();
  341. this.get_print_html((out) => {
  342. if (!out.html) {
  343. out.html = this.get_no_preview_html();
  344. }
  345. this.setup_print_format_dom(out, $print_format);
  346. const print_height = $print_format.get(0).offsetHeight;
  347. const $message = this.wrapper.find('.page-break-message');
  348. const print_height_inches = frappe.dom.pixel_to_inches(print_height);
  349. // if contents are large enough, indicate that it will get printed on multiple pages
  350. // Maximum height for an A4 document is 11.69 inches
  351. if (print_height_inches > 11.69) {
  352. $message.text(__('This may get printed on multiple pages'));
  353. } else {
  354. $message.text('');
  355. }
  356. });
  357. }
  358. setup_print_format_dom(out, $print_format) {
  359. this.print_wrapper.find('.print-format-skeleton').remove();
  360. let base_url = frappe.urllib.get_base_url();
  361. let print_css = frappe.assets.bundled_asset('print.bundle.css', frappe.utils.is_rtl(this.lang_code));
  362. this.$print_format_body.find('html').attr('dir', frappe.utils.is_rtl(this.lang_code) ? 'rtl': 'ltr');
  363. this.$print_format_body.find('html').attr('lang', this.lang_code);
  364. this.$print_format_body.find('head').html(
  365. `<style type="text/css">${out.style}</style>
  366. <link href="${base_url}${print_css}" rel="stylesheet">`
  367. );
  368. this.$print_format_body.find('body').html(
  369. `<div class="print-format print-format-preview">${out.html}</div>`
  370. );
  371. this.show_footer();
  372. this.$print_format_body.find('.print-format').css({
  373. display: 'flex',
  374. flexDirection: 'column',
  375. });
  376. this.$print_format_body.find('.page-break').css({
  377. display: 'flex',
  378. 'flex-direction': 'column',
  379. flex: '1',
  380. });
  381. setTimeout(() => {
  382. $print_format.height(this.$print_format_body.find('.print-format').outerHeight());
  383. }, 500);
  384. }
  385. hide() {
  386. if (this.frm.setup_done && this.frm.page.current_view_name === 'print') {
  387. this.frm.page.set_view(
  388. this.frm.page.previous_view_name === 'print'
  389. ? 'main'
  390. : this.frm.page.previous_view_name || 'main'
  391. );
  392. }
  393. }
  394. show_footer() {
  395. // footer is hidden by default as reqd by pdf generation
  396. // simple hack to show it in print preview
  397. this.$print_format_body.find('#footer-html').attr(
  398. 'style',
  399. `
  400. display: block !important;
  401. order: 1;
  402. margin-top: auto;
  403. padding-top: var(--padding-xl)
  404. `
  405. );
  406. }
  407. printit() {
  408. let me = this;
  409. if (cint(me.print_settings.enable_print_server)) {
  410. if (localStorage.getItem('network_printer')) {
  411. me.print_by_server();
  412. } else {
  413. me.network_printer_setting_dialog(() => me.print_by_server());
  414. }
  415. } else if (me.get_mapped_printer().length === 1) {
  416. // printer is already mapped in localstorage (applies for both raw and pdf )
  417. if (me.is_raw_printing()) {
  418. me.get_raw_commands(function(out) {
  419. frappe.ui.form
  420. .qz_connect()
  421. .then(function() {
  422. let printer_map = me.get_mapped_printer()[0];
  423. let data = [out.raw_commands];
  424. let config = qz.configs.create(printer_map.printer);
  425. return qz.print(config, data);
  426. })
  427. .then(frappe.ui.form.qz_success)
  428. .catch((err) => {
  429. frappe.ui.form.qz_fail(err);
  430. });
  431. });
  432. } else {
  433. frappe.show_alert(
  434. {
  435. message: __('PDF printing via "Raw Print" is not supported.'),
  436. subtitle: __(
  437. 'Please remove the printer mapping in Printer Settings and try again.'
  438. ),
  439. indicator: 'info',
  440. },
  441. 14
  442. );
  443. //Note: need to solve "Error: Cannot parse (FILE)<URL> as a PDF file" to enable qz pdf printing.
  444. }
  445. } else if (me.is_raw_printing()) {
  446. // printer not mapped in localstorage and the current print format is raw printing
  447. frappe.show_alert(
  448. {
  449. message: __('Printer mapping not set.'),
  450. subtitle: __(
  451. 'Please set a printer mapping for this print format in the Printer Settings'
  452. ),
  453. indicator: 'warning',
  454. },
  455. 14
  456. );
  457. me.printer_setting_dialog();
  458. } else {
  459. me.render_page('/printview?', true);
  460. }
  461. }
  462. print_by_server() {
  463. let me = this;
  464. if (localStorage.getItem('network_printer')) {
  465. frappe.call({
  466. method: 'frappe.utils.print_format.print_by_server',
  467. args: {
  468. doctype: me.frm.doc.doctype,
  469. name: me.frm.doc.name,
  470. printer_setting: localStorage.getItem('network_printer'),
  471. print_format: me.selected_format(),
  472. no_letterhead: me.with_letterhead(),
  473. letterhead: me.get_letterhead(),
  474. },
  475. callback: function() {},
  476. });
  477. }
  478. }
  479. network_printer_setting_dialog(callback) {
  480. frappe.call({
  481. method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings',
  482. callback: function(r) {
  483. if (r.message) {
  484. let d = new frappe.ui.Dialog({
  485. title: __('Select Network Printer'),
  486. fields: [
  487. {
  488. "label": "Printer",
  489. "fieldname": "printer",
  490. "fieldtype": "Select",
  491. "reqd": 1,
  492. "options": r.message
  493. }
  494. ],
  495. primary_action: function() {
  496. localStorage.setItem('network_printer', d.get_values().printer);
  497. if (typeof callback == "function") {
  498. callback();
  499. }
  500. d.hide();
  501. },
  502. primary_action_label: __('Select')
  503. });
  504. d.show();
  505. }
  506. },
  507. });
  508. }
  509. render_page(method, printit = false) {
  510. let w = window.open(
  511. frappe.urllib.get_full_url(
  512. method +
  513. 'doctype=' +
  514. encodeURIComponent(this.frm.doc.doctype) +
  515. '&name=' +
  516. encodeURIComponent(this.frm.doc.name) +
  517. (printit ? '&trigger_print=1' : '') +
  518. '&format=' +
  519. encodeURIComponent(this.selected_format()) +
  520. '&no_letterhead=' +
  521. (this.with_letterhead() ? '0' : '1') +
  522. '&letterhead=' +
  523. encodeURIComponent(this.get_letterhead()) +
  524. '&settings=' +
  525. encodeURIComponent(JSON.stringify(this.additional_settings)) +
  526. (this.lang_code ? '&_lang=' + this.lang_code : '')
  527. )
  528. );
  529. if (!w) {
  530. frappe.msgprint(__('Please enable pop-ups'));
  531. return;
  532. }
  533. }
  534. get_print_html(callback) {
  535. let print_format = this.get_print_format();
  536. if (print_format.raw_printing) {
  537. callback({
  538. html: this.get_no_preview_html(),
  539. });
  540. return;
  541. }
  542. if (this._req) {
  543. this._req.abort();
  544. }
  545. this._req = frappe.call({
  546. method: 'frappe.www.printview.get_html_and_style',
  547. args: {
  548. doc: this.frm.doc,
  549. print_format: this.selected_format(),
  550. no_letterhead: !this.with_letterhead() ? 1 : 0,
  551. letterhead: this.get_letterhead(),
  552. settings: this.additional_settings,
  553. _lang: this.lang_code,
  554. },
  555. callback: function(r) {
  556. if (!r.exc) {
  557. callback(r.message);
  558. }
  559. },
  560. });
  561. }
  562. get_letterhead() {
  563. return this.letterhead_selector.val();
  564. }
  565. get_no_preview_html() {
  566. return `<div class="text-muted text-center" style="font-size: 1.2em;">
  567. ${__('No Preview Available')}
  568. </div>`;
  569. }
  570. get_raw_commands(callback) {
  571. // fetches rendered raw commands from the server for the current print format.
  572. frappe.call({
  573. method: 'frappe.www.printview.get_rendered_raw_commands',
  574. args: {
  575. doc: this.frm.doc,
  576. print_format: this.selected_format(),
  577. _lang: this.lang_code,
  578. },
  579. callback: function(r) {
  580. if (!r.exc) {
  581. callback(r.message);
  582. }
  583. },
  584. });
  585. }
  586. get_mapped_printer() {
  587. // returns a list of "print format: printer" mapping filtered by the current print format
  588. let print_format_printer_map = this.get_print_format_printer_map();
  589. if (print_format_printer_map[this.frm.doctype]) {
  590. return print_format_printer_map[this.frm.doctype].filter(
  591. (printer_map) => printer_map.print_format == this.selected_format()
  592. );
  593. } else {
  594. return [];
  595. }
  596. }
  597. get_print_format_printer_map() {
  598. // returns the whole object "print_format_printer_map" stored in the localStorage.
  599. try {
  600. let print_format_printer_map = JSON.parse(
  601. localStorage.print_format_printer_map
  602. );
  603. return print_format_printer_map;
  604. } catch (e) {
  605. return {};
  606. }
  607. }
  608. refresh_print_options() {
  609. this.print_formats = frappe.meta.get_print_formats(this.frm.doctype);
  610. const print_format_select_val = this.print_sel.val();
  611. this.print_sel.empty().add_options([
  612. this.get_default_option_for_select(__('Select Print Format')),
  613. ...this.print_formats
  614. ]);
  615. return this.print_formats.includes(print_format_select_val)
  616. && this.print_sel.val(print_format_select_val);
  617. }
  618. selected_format() {
  619. return (
  620. this.print_sel.val() || this.frm.meta.default_print_format || 'Standard'
  621. );
  622. }
  623. is_raw_printing(format) {
  624. return this.get_print_format(format).raw_printing === 1;
  625. }
  626. get_print_format(format) {
  627. let print_format = {};
  628. if (!format) {
  629. format = this.selected_format();
  630. }
  631. if (locals['Print Format'] && locals['Print Format'][format]) {
  632. print_format = locals['Print Format'][format];
  633. }
  634. return print_format;
  635. }
  636. with_letterhead() {
  637. return cint(this.get_letterhead() !== __('No Letterhead'));
  638. }
  639. set_style(style) {
  640. frappe.dom.set_style(style || frappe.boot.print_css, 'print-style');
  641. }
  642. printer_setting_dialog() {
  643. // dialog for the Printer Settings
  644. this.print_format_printer_map = this.get_print_format_printer_map();
  645. this.data = this.print_format_printer_map[this.frm.doctype] || [];
  646. this.printer_list = [];
  647. frappe.ui.form.qz_get_printer_list().then((data) => {
  648. this.printer_list = data;
  649. const dialog = new frappe.ui.Dialog({
  650. title: __('Printer Settings'),
  651. fields: [
  652. {
  653. fieldtype: 'Section Break',
  654. },
  655. {
  656. fieldname: 'printer_mapping',
  657. fieldtype: 'Table',
  658. label: __('Printer Mapping'),
  659. in_place_edit: true,
  660. data: this.data,
  661. get_data: () => {
  662. return this.data;
  663. },
  664. fields: [
  665. {
  666. fieldtype: 'Select',
  667. fieldname: 'print_format',
  668. default: 0,
  669. options: this.print_formats,
  670. read_only: 0,
  671. in_list_view: 1,
  672. label: __('Print Format'),
  673. },
  674. {
  675. fieldtype: 'Select',
  676. fieldname: 'printer',
  677. default: 0,
  678. options: this.printer_list,
  679. read_only: 0,
  680. in_list_view: 1,
  681. label: __('Printer'),
  682. },
  683. ],
  684. },
  685. ],
  686. primary_action: () => {
  687. let printer_mapping = dialog.get_values()['printer_mapping'];
  688. if (printer_mapping && printer_mapping.length) {
  689. let print_format_list = printer_mapping.map((a) => a.print_format);
  690. let has_duplicate = print_format_list.some(
  691. (item, idx) => print_format_list.indexOf(item) != idx
  692. );
  693. if (has_duplicate)
  694. frappe.throw(
  695. __(
  696. 'Cannot have multiple printers mapped to a single print format.'
  697. )
  698. );
  699. } else {
  700. printer_mapping = [];
  701. }
  702. dialog.print_format_printer_map = this.get_print_format_printer_map();
  703. dialog.print_format_printer_map[this.frm.doctype] = printer_mapping;
  704. localStorage.print_format_printer_map = JSON.stringify(
  705. dialog.print_format_printer_map
  706. );
  707. dialog.hide();
  708. },
  709. primary_action_label: __('Save'),
  710. });
  711. dialog.show();
  712. if (!(this.printer_list && this.printer_list.length)) {
  713. frappe.throw(__('No Printer is Available.'));
  714. }
  715. });
  716. }
  717. };