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.
 
 
 
 
 
 

679 lines
17 KiB

  1. // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
  2. // MIT License. See license.txt
  3. // default print style
  4. _p.def_print_style_body = "html, body, div, span, td, p { \
  5. font-family: inherit; \
  6. font-size: inherit; \
  7. }\
  8. .page-settings {\
  9. font-family: Arial, Helvetica Neue, Sans;\
  10. font-size: 9pt;\
  11. }\
  12. pre { margin:0; padding:0;}";
  13. _p.def_print_style_other = "\n.simpletable, .noborder { \
  14. border-collapse: collapse;\
  15. margin-bottom: 10px;\
  16. }\
  17. .simpletable td {\
  18. border: 1pt solid #777;\
  19. vertical-align: top;\
  20. padding: 4px;\
  21. }\
  22. .noborder td {\
  23. vertical-align: top;\
  24. }";
  25. _p.go = function(html) {
  26. var w = _p.preview(html);
  27. w.print();
  28. w.close();
  29. }
  30. _p.preview = function(html) {
  31. var w = window.open();
  32. if(!w) {
  33. msgprint(__("Please enable pop-ups"));
  34. return;
  35. }
  36. w.document.write(html);
  37. return w
  38. }
  39. // _p can be referenced as this inside $.extend
  40. $.extend(_p, {
  41. show_dialog: function() {
  42. if(!_p.dialog) {
  43. _p.make_dialog();
  44. }
  45. _p.dialog.show();
  46. },
  47. make_dialog: function() {
  48. // Prepare Dialog Box Layout
  49. var dialog = new frappe.ui.Dialog({
  50. title: "Print Formats",
  51. fields: [
  52. {fieldtype:"Select", label:"Print Format", fieldname:"print_format", reqd:1},
  53. {fieldtype:"Check", label:"No Letter Head", fieldname:"no_letterhead"},
  54. {fieldtype:"HTML", options: '<p style="text-align: right;">\
  55. <button class="btn btn-primary btn-print">Print</button>\
  56. <button class="btn btn-default btn-preview">Preview</button>\
  57. </p>'},
  58. ]
  59. });
  60. dialog.$wrapper.find(".btn-print").click(function() {
  61. var args = dialog.get_values();
  62. _p.build(
  63. args.print_format, // fmtname
  64. _p.go, // onload
  65. args.no_letterhead // no_letterhead
  66. );
  67. });
  68. dialog.$wrapper.find(".btn-preview").click(function() {
  69. var args = dialog.get_values();
  70. _p.build(
  71. args.print_format, // fmtname
  72. _p.preview, // onload
  73. args.no_letterhead // no_letterhead
  74. );
  75. });
  76. dialog.onshow = function() {
  77. var $print = dialog.fields_dict.print_format.$input;
  78. $print.empty().add_options(cur_frm.print_formats);
  79. if(cur_frm.$print_view_select && cur_frm.$print_view_select.val())
  80. $print.val(cur_frm.$print_view_select.val());
  81. }
  82. _p.dialog = dialog;
  83. },
  84. // Define formats dict
  85. formats: {},
  86. /* args dict can contain:
  87. + fmtname --> print format name
  88. + onload
  89. + no_letterhead
  90. + only_body
  91. */
  92. build: function(fmtname, onload, no_letterhead, only_body, no_heading) {
  93. if(!fmtname) {
  94. fmtname= "Standard";
  95. }
  96. args = {
  97. fmtname: fmtname,
  98. onload: onload,
  99. no_letterhead: no_letterhead,
  100. only_body: only_body
  101. };
  102. if(!cur_frm) {
  103. msgprint(__("No document selected"));
  104. return;
  105. }
  106. if(!frappe.model.can_print(cur_frm.doctype, cur_frm)) {
  107. msgprint(__("You are not allowed to print this document"));
  108. return;
  109. }
  110. // Get current doc (record)
  111. var doc = locals[cur_frm.doctype][cur_frm.docname];
  112. if(args.fmtname == 'Standard') {
  113. args.onload(_p.render({
  114. body: _p.print_std(args.no_letterhead, no_heading),
  115. style: _p.print_style,
  116. doc: doc,
  117. title: doc.name,
  118. no_letterhead: args.no_letterhead,
  119. no_heading: no_heading,
  120. only_body: args.only_body
  121. }));
  122. } else {
  123. var print_format_doc = locals["Print Format"][args.fmtname];
  124. if(!print_format_doc) {
  125. msgprint(__("Unknown Print Format: {0}", [args.fmtname]));
  126. return;
  127. }
  128. args.onload(_p.render({
  129. body: print_format_doc.html,
  130. style: '',
  131. doc: doc,
  132. title: doc.name,
  133. no_letterhead: args.no_letterhead,
  134. no_heading: no_heading,
  135. only_body: args.only_body
  136. }));
  137. }
  138. },
  139. render: function(args) {
  140. var container = document.createElement('div');
  141. var stat = '';
  142. if(!args.no_heading) {
  143. // if draft/archived, show draft/archived banner
  144. stat += _p.show_draft(args);
  145. stat += _p.show_archived(args);
  146. stat += _p.show_cancelled(args);
  147. }
  148. // Append args.body's content as a child of container
  149. container.innerHTML = args.body;
  150. // Show letterhead?
  151. _p.show_letterhead(container, args);
  152. _p.run_embedded_js(container, args.doc);
  153. var style = _p.consolidate_css(container, args);
  154. _p.render_header_on_break(container, args);
  155. return _p.render_final(style, stat, container, args);
  156. },
  157. head_banner_format: function() {
  158. return "\
  159. <div style = '\
  160. text-align: center; \
  161. padding: 8px; \
  162. background-color: #CCC;'> \
  163. <div style = '\
  164. font-size: 20px; \
  165. font-weight: bold;'>\
  166. {{HEAD}}\
  167. </div>\
  168. {{DESCRIPTION}}\
  169. </div>"
  170. },
  171. /*
  172. Check if doc's status is not submitted (docstatus == 0)
  173. and submission is pending
  174. Display draft in header if true
  175. */
  176. show_draft: function(args) {
  177. var is_doctype_submittable = 0;
  178. var plist = locals['DocPerm'];
  179. for(var perm in plist) {
  180. var p = plist[perm];
  181. if((p.parent==args.doc.doctype) && (p.submit==1)){
  182. is_doctype_submittable = 1;
  183. break;
  184. }
  185. }
  186. if(args.doc && cint(args.doc.docstatus)==0 && is_doctype_submittable) {
  187. draft = _p.head_banner_format();
  188. draft = draft.replace("{{HEAD}}", "DRAFT");
  189. draft = draft.replace("{{DESCRIPTION}}", "This box will go away after the document is submitted.");
  190. return draft;
  191. } else {
  192. return "";
  193. }
  194. },
  195. /*
  196. Check if doc is archived
  197. Display archived in header if true
  198. */
  199. show_archived: function(args) {
  200. if(args.doc && args.doc.__archived) {
  201. archived = _p.head_banner_format();
  202. archived = archived.replace("{{HEAD}}", "ARCHIVED");
  203. archived = archived.replace("{{DESCRIPTION}}", "You must restore this document to make it editable.");
  204. return archived;
  205. } else {
  206. return "";
  207. }
  208. },
  209. /*
  210. Check if doc is cancelled
  211. Display cancelled in header if true
  212. */
  213. show_cancelled: function(args) {
  214. if(args.doc && args.doc.docstatus==2) {
  215. cancelled = _p.head_banner_format();
  216. cancelled = cancelled.replace("{{HEAD}}", "CANCELLED");
  217. cancelled = cancelled.replace("{{DESCRIPTION}}", "You must amend this document to make it editable.");
  218. return cancelled;
  219. } else {
  220. return "";
  221. }
  222. },
  223. consolidate_css: function(container, args) {
  224. // Extract <style> content from container
  225. var body_style = '';
  226. var style_list = container.getElementsByTagName('style');
  227. while(style_list && style_list.length>0) {
  228. for(i in style_list) {
  229. if(style_list[i] && style_list[i].innerHTML) {
  230. body_style += style_list[i].innerHTML;
  231. var parent = style_list[i].parentNode;
  232. if(parent) {
  233. parent.removeChild(style_list[i]);
  234. } else {
  235. container.removeChild(style_list[i]);
  236. }
  237. }
  238. }
  239. style_list = container.getElementsByTagName('style');
  240. }
  241. // Concatenate all styles
  242. style_concat = (args.only_body ? '' : _p.def_print_style_body)
  243. + _p.def_print_style_other + args.style + body_style;
  244. return style_concat;
  245. },
  246. // This is used to calculate and substitude values in the HTML
  247. run_embedded_js: function(container, doc) {
  248. var script_list = $(container).find("script");
  249. for(var i=0, j=script_list.length; i<j; i++) {
  250. var element = script_list[i];
  251. var code = element.innerHTML;
  252. var new_html = code ? (eval(code) || "") : "";
  253. if(in_list(["string", "number"], typeof new_html)) {
  254. $(element).replaceWith(this.add_span(new_html + ""));
  255. }
  256. }
  257. // remove scripts once executed
  258. $(container).find("script").remove();
  259. },
  260. add_span: function(html) {
  261. var tags = ["<span", "<p", "<div", "<br", "<table"];
  262. var match = false;
  263. for(var i=0; i<tags.length; i++) {
  264. if(html.match(tags[i])) {
  265. match = true;
  266. break;
  267. }
  268. }
  269. if(!match) {
  270. html = "<span>" + html + "</span>";
  271. }
  272. return html;
  273. },
  274. // Attach letterhead at top of container
  275. show_letterhead: function(container, args) {
  276. if(!args.no_letterhead) {
  277. container.innerHTML = '<div style="max-width: 100%">'
  278. + _p.get_letter_head() + '</div>'
  279. + container.innerHTML;
  280. }
  281. },
  282. render_header_on_break: function(container, args) {
  283. var page_set = container.getElementsByClassName('page-settings');
  284. if(page_set.length) {
  285. for(var i = 0; i < page_set.length; i++) {
  286. var tmp = '';
  287. // if draft/archived, show draft/archived banner
  288. tmp += _p.show_draft(args);
  289. tmp += _p.show_archived(args);
  290. _p.show_letterhead(page_set[i], args);
  291. page_set[i].innerHTML = tmp + page_set[i].innerHTML;
  292. }
  293. }
  294. },
  295. // called by _p.render for final render of print
  296. render_final: function(style, stat, container, args) {
  297. if(!args.only_body) {
  298. var header = '<!DOCTYPE html>\
  299. <html>\
  300. <head>\
  301. <meta charset="utf-8" />\
  302. <title>' + args.title + '</title>\
  303. <style>' + style + '</style>\
  304. </head>\
  305. <body>';
  306. var footer = '\
  307. </body>\
  308. </html>';
  309. } else {
  310. var header = '';
  311. var footer = '';
  312. }
  313. var finished = header
  314. + '<div class="page-settings">'
  315. + stat
  316. + container.innerHTML
  317. + '</div>'
  318. + footer;
  319. // replace relative links by absolute links
  320. var prefix = window.location.href.split("desk")[0]
  321. // find unique matches
  322. var matches = $.unique(finished.match(/src=['"]([^'"]*)['"]/g) || []);
  323. $.each(matches, function(i, v) {
  324. if(v.substr(0,4)=="src=") {
  325. var v = v.substr(5, v.length-6);
  326. if(v.substr(0,4)!="http") {
  327. finished = finished.split(v).join(prefix + lstrip(v, "/"));
  328. }
  329. }
  330. });
  331. return finished;
  332. },
  333. // fetches letter head from current doc or control panel
  334. get_letter_head: function() {
  335. var lh = '';
  336. if(cur_frm.doc.letter_head) {
  337. lh = cstr(frappe.boot.letter_heads[cur_frm.doc.letter_head]);
  338. } else if (frappe.boot.sysdefaults.default_letter_head_content) {
  339. lh = frappe.boot.sysdefaults.default_letter_head_content;
  340. }
  341. return lh;
  342. },
  343. // common print style setting
  344. print_style: "\
  345. .datalabelcell { \
  346. padding: 2px 0px; \
  347. width: 38%; \
  348. vertical-align: top; \
  349. } \
  350. .datainputcell { \
  351. padding: 2px 0px; \
  352. width: 62%; \
  353. text-align: left; \
  354. }\
  355. .sectionHeading { \
  356. font-size: 16px; \
  357. font-weight: bold; \
  358. margin: 8px 0px; \
  359. } \
  360. .columnHeading { \
  361. font-size: 14px; \
  362. font-weight: bold; \
  363. margin: 8px 0px; \
  364. }",
  365. print_std: function(no_letterhead, no_heading) {
  366. // Get doctype, docname, layout for a doctype
  367. var docname = cur_frm.docname;
  368. var doctype = cur_frm.doctype;
  369. var data = frappe.get_children("DocType", doctype, "fields");
  370. var layout = _p.add_layout(doctype);
  371. this.pf_list = [layout];
  372. var me = this;
  373. me.layout = layout;
  374. $.extend(this, {
  375. build_head: function(data, doctype, docname) {
  376. // Heading
  377. var h1_style = {
  378. fontSize: '22px',
  379. marginBottom: '8px'
  380. }
  381. var h1 = $a(me.layout.cur_row.header, 'h1', '', h1_style);
  382. // Get print heading
  383. if (cur_frm.pformat[docname]) {
  384. // first check in cur_frm.pformat
  385. h1.innerHTML = cur_frm.pformat[docname];
  386. } else {
  387. // then check if select print heading exists and has a value
  388. var val = null;
  389. for (var i = 0; i < data.length; i++) {
  390. if (data[i].fieldname === 'select_print_heading') {
  391. val = _f.get_value(doctype, docname, data[i].fieldname);
  392. break;
  393. }
  394. }
  395. // if not, just have doctype has heading
  396. h1.innerHTML = val ? val : __(doctype);
  397. }
  398. var h2_style = {
  399. fontSize: '16px',
  400. color: '#888',
  401. marginBottom: '8px',
  402. paddingBottom: '8px',
  403. borderBottom: (me.layout.with_border ? '0px' :
  404. '1px solid #000')
  405. }
  406. var h2 = $a(me.layout.cur_row.header, 'div', '', h2_style);
  407. h2.innerHTML = docname;
  408. if(cur_frm.state_fieldname && !cur_frm.fields_dict[cur_frm.state_fieldname].df.print_hide) {
  409. $a(h2, 'br');
  410. var span = $a(h2, 'span', '',
  411. {padding: "3px", color: "#fff", backgroundColor: "#777",
  412. display:"inline-block"});
  413. span.innerHTML = cur_frm.doc[cur_frm.state_fieldname];
  414. }
  415. },
  416. build_data: function(data, doctype, docname) {
  417. // Start with a row and a cell in that row
  418. if(data[0] && data[0].fieldtype != "Section Break") {
  419. me.layout.addrow();
  420. if(data[0].fieldtype != "Column Break") {
  421. me.layout.addcell();
  422. }
  423. }
  424. $.extend(this, {
  425. generate_custom_html: function(field, doctype, docname) {
  426. var container = $a(me.layout.cur_cell, 'div');
  427. container.innerHTML = cur_frm.pformat[field.fieldname](locals[doctype][docname]);
  428. },
  429. render_normal: function(field, data, i) {
  430. switch(field.fieldtype) {
  431. case 'Section Break':
  432. me.layout.addrow();
  433. // Add column if no column break after this field
  434. if(data[i+1] && data[i+1].fieldtype !=
  435. 'Column Break') {
  436. me.layout.addcell();
  437. }
  438. break;
  439. case 'Column Break':
  440. me.layout.addcell(field.width, field.label);
  441. break;
  442. case 'Table':
  443. var table = print_table(
  444. doctype, // dt
  445. docname, // dn
  446. field.fieldname,
  447. field.options, // tabletype
  448. null, // cols
  449. null, // head_labels
  450. null, // widths
  451. null); // condition
  452. me.layout = _p.print_std_add_table(table, me.layout, me.pf_list, doctype, no_letterhead);
  453. break;
  454. case 'HTML':
  455. var div = $a(me.layout.cur_cell, 'div');
  456. div.innerHTML = field.options;
  457. break;
  458. case 'Code':
  459. var div = $a(me.layout.cur_cell, 'div');
  460. var val = _f.get_value(doctype, docname,
  461. field.fieldname);
  462. div.innerHTML = '<div>' + __(field.label) +
  463. ': </div><pre style="font-family: Courier, Fixed;">' + (val ? val : '') +
  464. '</pre>';
  465. break;
  466. case 'Text Editor':
  467. var div = $a(me.layout.cur_cell, 'div');
  468. var val = _f.get_value(doctype, docname,
  469. field.fieldname);
  470. div.innerHTML = val ? val : '';
  471. break;
  472. default:
  473. // Add Cell Data
  474. _p.print_std_add_field(doctype, docname, field, me.layout);
  475. break;
  476. }
  477. }
  478. });
  479. // Then build each field
  480. for(var i = 0; i < data.length; i++) {
  481. var fieldname = data[i].fieldname ? data[i].fieldname :
  482. data[i].label;
  483. var field = fieldname ?
  484. frappe.meta.get_docfield(doctype, fieldname, docname) : data[i];
  485. if(!field.print_hide) {
  486. if(cur_frm.pformat[field.fieldname]) {
  487. // If there is a custom method to generate the HTML, then use it
  488. this.generate_custom_html(field, doctype, docname);
  489. } else {
  490. // Do the normal rendering
  491. this.render_normal(field, data, i);
  492. }
  493. }
  494. }
  495. me.layout.close_borders();
  496. },
  497. build_html: function() {
  498. var html = '';
  499. for(var i = 0; i < me.pf_list.length; i++) {
  500. if(me.pf_list[i].wrapper) {
  501. html += me.pf_list[i].wrapper.innerHTML;
  502. } else if(me.pf_list[i].innerHTML) {
  503. html += me.pf_list[i].innerHTML;
  504. } else {
  505. html += me.pf_list[i];
  506. }
  507. }
  508. this.pf_list = [];
  509. return html;
  510. }
  511. });
  512. if(!no_heading) {
  513. this.build_head(data, doctype, docname);
  514. }
  515. this.build_data(data, doctype, docname);
  516. var html = this.build_html();
  517. return html;
  518. },
  519. add_layout: function(doctype) {
  520. var layout = new Layout();
  521. layout.addrow();
  522. if(locals['DocType'][doctype].print_outline == 'Yes') {
  523. layout.with_border = 1
  524. }
  525. return layout;
  526. },
  527. print_std_add_table: function(t, layout, pf_list, dt, no_letterhead) {
  528. if(t.appendChild) {
  529. // If only one table is passed
  530. layout.cur_cell.appendChild(t);
  531. } else {
  532. page_break = '\n\
  533. <div style = "page-break-after: always;" \
  534. class = "page_break"></div><div class="page-settings"></div>';
  535. // If a list of tables is passed
  536. for(var i = 0; i < t.length-1; i++) {
  537. // add to current page
  538. layout.cur_cell.appendChild(t[i]);
  539. layout.close_borders();
  540. pf_list.push(page_break);
  541. // Create new page
  542. layout = _p.add_layout(dt, no_letterhead);
  543. pf_list.push(layout);
  544. layout.addrow();
  545. layout.addcell();
  546. var div = $a(layout.cur_cell, 'div');
  547. div.innerHTML = 'Continued from previous page...';
  548. div.style.padding = '4px';
  549. }
  550. // Append last table
  551. layout.cur_cell.appendChild(t[t.length-1]);
  552. }
  553. return layout;
  554. },
  555. print_std_add_field: function(dt, dn, f, layout) {
  556. var val = _f.get_value(dt, dn, f.fieldname);
  557. if(f.fieldtype!='Button') {
  558. if(val || in_list(['Float', 'Int', 'Currency'], f.fieldtype)) {
  559. // If value or a numeric type then proceed
  560. // Add field table
  561. row = _p.field_tab(layout.cur_cell);
  562. // Add label
  563. row.cells[0].innerHTML = __(f.label ? f.label : f.fieldname);
  564. row.cells[1].innerHTML = frappe.format(val, f, {for_print: true});
  565. // left align currency in normal display
  566. if(f.fieldtype == 'Currency') {
  567. $y(row.cells[1], { textAlign: 'left' });
  568. }
  569. }
  570. }
  571. },
  572. field_tab: function(layout_cell) {
  573. var tab = $a(layout_cell, 'table', '', {width:'100%'});
  574. var row = tab.insertRow(0);
  575. _p.row = row; // Don't know this line's purpose
  576. row.insertCell(0);
  577. row.insertCell(1);
  578. row.cells[0].className = 'datalabelcell';
  579. row.cells[0].style.width = "38%";
  580. row.cells[1].className = 'datainputcell';
  581. return row;
  582. }
  583. });