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.
 
 
 
 
 
 

642 rivejä
18 KiB

  1. // Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
  2. // MIT License. See license.txt
  3. wn.views.ReportFactory = wn.views.Factory.extend({
  4. make: function(route) {
  5. new wn.views.ReportViewPage(route[1], route[2]);
  6. }
  7. });
  8. wn.views.ReportViewPage = Class.extend({
  9. init: function(doctype, docname) {
  10. if(!wn.model.can_get_report(doctype)) {
  11. wn.show_not_permitted(wn.get_route_str());
  12. return;
  13. };
  14. wn.require("assets/js/slickgrid.min.js");
  15. this.doctype = doctype;
  16. this.docname = docname;
  17. this.page_name = wn.get_route_str();
  18. this.make_page();
  19. var me = this;
  20. wn.model.with_doctype(this.doctype, function() {
  21. me.make_report_view();
  22. if(me.docname) {
  23. wn.model.with_doc('Report', me.docname, function(r) {
  24. me.page.reportview.set_columns_and_filters(
  25. JSON.parse(wn.model.get("Report", me.docname)[0].json));
  26. me.page.reportview.set_route_filters();
  27. me.page.reportview.run();
  28. });
  29. } else {
  30. me.page.reportview.set_route_filters();
  31. me.page.reportview.run();
  32. }
  33. });
  34. },
  35. make_page: function() {
  36. var me = this;
  37. this.page = wn.container.add_page(this.page_name);
  38. wn.ui.make_app_page({parent:this.page,
  39. single_column:true});
  40. wn.container.change_to(this.page_name);
  41. $(this.page).on('show', function(){
  42. if(me.page.reportview.set_route_filters())
  43. me.page.reportview.run();
  44. })
  45. },
  46. make_report_view: function() {
  47. var module = locals.DocType[this.doctype].module;
  48. this.page.appframe.set_title(wn._(this.doctype));
  49. this.page.appframe.add_module_icon(module, this.doctype)
  50. this.page.appframe.set_views_for(this.doctype, "report");
  51. this.page.reportview = new wn.views.ReportView({
  52. doctype: this.doctype,
  53. docname: this.docname,
  54. page: this.page,
  55. wrapper: $(this.page).find(".layout-main")
  56. });
  57. }
  58. });
  59. wn.views.ReportView = wn.ui.Listing.extend({
  60. init: function(opts) {
  61. var me = this;
  62. $(this.page).find('.layout-main').html(wn._('Loading Report')+'...');
  63. $(this.page).find('.layout-main').empty();
  64. $.extend(this, opts);
  65. this.can_delete = wn.model.can_delete(me.doctype);
  66. this.tab_name = '`tab'+this.doctype+'`';
  67. this.setup();
  68. },
  69. set_init_columns: function() {
  70. // pre-select mandatory columns
  71. var columns = [['name'], ['owner']];
  72. $.each(wn.meta.docfield_list[this.doctype], function(i, df) {
  73. if(df.in_filter && df.fieldname!='naming_series' && df.fieldtype!='Table') {
  74. columns.push([df.fieldname]);
  75. }
  76. });
  77. this.columns = columns;
  78. },
  79. setup: function() {
  80. var me = this;
  81. this.page_title = wn._('Report')+ ': ' + wn._(this.docname ? (this.doctype + ' - ' + this.docname) : this.doctype);
  82. this.page.appframe.set_title(this.page_title)
  83. this.make({
  84. appframe: this.page.appframe,
  85. method: 'webnotes.widgets.reportview.get',
  86. get_args: this.get_args,
  87. parent: $(this.page).find('.layout-main'),
  88. start: 0,
  89. page_length: 20,
  90. show_filters: true,
  91. new_doctype: this.doctype,
  92. allow_delete: true,
  93. });
  94. this.make_delete();
  95. this.make_column_picker();
  96. this.make_sorter();
  97. this.make_export();
  98. this.set_init_columns();
  99. this.make_save();
  100. this.make_user_restrictions();
  101. this.set_tag_and_status_filter();
  102. },
  103. set_init_columns: function() {
  104. // pre-select mandatory columns
  105. var columns = wn.defaults.get_default("_list_settings:" + this.doctype);
  106. if(!columns) {
  107. var columns = [['name', this.doctype],];
  108. $.each(wn.meta.docfield_list[this.doctype], function(i, df) {
  109. if(df.in_filter && df.fieldname!='naming_series'
  110. && !in_list(wn.model.no_value_type, df.fieldname)) {
  111. columns.push([df.fieldname, df.parent]);
  112. }
  113. });
  114. }
  115. this.columns = columns;
  116. },
  117. // preset columns and filters from saved info
  118. set_columns_and_filters: function(opts) {
  119. var me = this;
  120. if(opts.columns) this.columns = opts.columns;
  121. if(opts.filters) $.each(opts.filters, function(i, f) {
  122. // f = [doctype, fieldname, condition, value]
  123. var df = wn.meta.get_docfield(f[0], f[1]);
  124. if (df && df.fieldtype == "Check") {
  125. var value = f[3] ? "Yes" : "No";
  126. } else {
  127. var value = f[3];
  128. }
  129. me.filter_list.add_filter(f[0], f[1], f[2], value);
  130. });
  131. // first sort
  132. if(opts.sort_by) this.sort_by_select.val(opts.sort_by);
  133. if(opts.sort_order) this.sort_order_select.val(opts.sort_order);
  134. // second sort
  135. if(opts.sort_by_next) this.sort_by_next_select.val(opts.sort_by_next);
  136. if(opts.sort_order_next) this.sort_order_next_select.val(opts.sort_order_next);
  137. },
  138. set_route_filters: function() {
  139. var me = this;
  140. if(wn.route_options) {
  141. me.filter_list.clear_filters();
  142. $.each(wn.route_options, function(key, value) {
  143. me.filter_list.add_filter(me.doctype, key, "=", value);
  144. });
  145. wn.route_options = null;
  146. return true;
  147. }
  148. },
  149. // build args for query
  150. get_args: function() {
  151. var me = this;
  152. return {
  153. doctype: this.doctype,
  154. fields: $.map(this.columns, function(v) { return me.get_full_column_name(v) }),
  155. order_by: this.get_order_by(),
  156. filters: this.filter_list.get_filters(),
  157. docstatus: ['0','1','2'],
  158. with_childnames: 1
  159. }
  160. },
  161. get_order_by: function() {
  162. // first
  163. var order_by = this.get_selected_table_and_column(this.sort_by_select)
  164. + ' ' + this.sort_order_select.val();
  165. // second
  166. if(this.sort_by_next_select.val()) {
  167. order_by += ', ' + this.get_selected_table_and_column(this.sort_by_next_select)
  168. + ' ' + this.sort_order_next_select.val();
  169. }
  170. return order_by;
  171. },
  172. get_selected_table_and_column: function(select) {
  173. return select.selected_doctype ?
  174. this.get_full_column_name([select.selected_fieldname, select.selected_doctype]) : "";
  175. },
  176. // get table_name.column_name
  177. get_full_column_name: function(v) {
  178. if(!v) return;
  179. return (v[1] ? ('`tab' + v[1] + '`') : this.tab_name) + '.' + v[0];
  180. },
  181. // build columns for slickgrid
  182. build_columns: function() {
  183. var me = this;
  184. return $.map(this.columns, function(c) {
  185. var docfield = wn.meta.docfield_map[c[1] || me.doctype][c[0]];
  186. if(!docfield) {
  187. var docfield = wn.model.get_std_field(c[0]);
  188. if(c[0]=="name") {
  189. docfield.options = me.doctype;
  190. docfield.parent = me.doctype;
  191. }
  192. }
  193. coldef = {
  194. id: c[0],
  195. field: c[0],
  196. docfield: docfield,
  197. name: wn._(docfield ? docfield.label : toTitle(c[0])),
  198. width: (docfield ? cint(docfield.width) : 120) || 120,
  199. formatter: function(row, cell, value, columnDef, dataContext) {
  200. var docfield = columnDef.docfield;
  201. if(docfield.fieldname==="_user_tags") docfield.fieldtype = "Tag";
  202. if(docfield.fieldname==="_comments") docfield.fieldtype = "Comment";
  203. return wn.format(value, docfield, null, dataContext);
  204. }
  205. }
  206. return coldef;
  207. });
  208. },
  209. // render data
  210. render_list: function() {
  211. var me = this;
  212. var columns = this.get_columns();
  213. // add sr in data
  214. $.each(this.data, function(i, v) {
  215. // add index
  216. v._idx = i+1;
  217. v.id = v._idx;
  218. });
  219. var options = {
  220. enableCellNavigation: true,
  221. enableColumnReorder: false,
  222. };
  223. if(this.slickgrid_options) {
  224. $.extend(options, this.slickgrid_options);
  225. }
  226. this.col_defs = columns;
  227. this.dataView = new Slick.Data.DataView();
  228. this.set_data(this.data);
  229. var grid_wrapper = this.$w.find('.result-list');
  230. // set height if not auto
  231. if(!options.autoHeight)
  232. grid_wrapper.css('height', '500px');
  233. this.grid = new Slick.Grid(grid_wrapper
  234. .css('border', '1px solid #ccc')
  235. .css('border-top', '0px')
  236. .get(0), this.dataView,
  237. columns, options);
  238. this.grid.setSelectionModel(new Slick.CellSelectionModel());
  239. this.grid.registerPlugin(new Slick.CellExternalCopyManager({
  240. dataItemColumnValueExtractor: function(item, columnDef, value) {
  241. return item[columnDef.field];
  242. }
  243. }));
  244. wn.slickgrid_tools.add_property_setter_on_resize(this.grid);
  245. if(this.start!=0 && !options.autoHeight) {
  246. this.grid.scrollRowIntoView(this.data.length-1);
  247. }
  248. this.grid.onDblClick.subscribe(function(e, args) {
  249. var row = me.dataView.getItem(args.row);
  250. var cell = me.grid.getColumns()[args.cell];
  251. me.edit_cell(row, cell.docfield);
  252. });
  253. this.dataView.onRowsChanged.subscribe(function (e, args) {
  254. me.grid.invalidateRows(args.rows);
  255. me.grid.render();
  256. });
  257. this.grid.onHeaderClick.subscribe(function(e, args) {
  258. if(e.target.className === "slick-resizable-handle")
  259. return;
  260. var df = args.column.docfield,
  261. sort_by = df.parent + "." + df.fieldname;
  262. if(sort_by===me.sort_by_select.val()) {
  263. me.sort_order_select.val(me.sort_order_select.val()==="asc" ? "desc" : "asc");
  264. } else {
  265. me.sort_by_select.val(df.parent + "." + df.fieldname);
  266. me.sort_order_select.val("asc");
  267. }
  268. me.run();
  269. });
  270. },
  271. edit_cell: function(row, docfield) {
  272. if(wn.model.std_fields_list.indexOf(docfield.fieldname)!==-1) {
  273. wn.throw(wn._("Cannot edit standard fields"));
  274. } else if(wn.boot.profile.can_write.indexOf(this.doctype)===-1) {
  275. wn.throw(wn._("No permission to edit"));
  276. }
  277. var me = this;
  278. var d = new wn.ui.Dialog({
  279. title: wn._("Edit") + " " + wn._(docfield.label),
  280. fields: [docfield, {"fieldtype": "Button", "label": "Update"}],
  281. });
  282. d.get_input(docfield.fieldname).val(row[docfield.fieldname]);
  283. d.get_input("update").on("click", function() {
  284. wn.call({
  285. method: "webnotes.client.set_value",
  286. args: {
  287. doctype: docfield.parent,
  288. name: row[docfield.parent===me.doctype ? "name" : docfield.parent+":name"],
  289. fieldname: docfield.fieldname,
  290. value: d.get_value(docfield.fieldname)
  291. },
  292. callback: function(r) {
  293. if(!r.exc) {
  294. d.hide();
  295. var doclist = r.message;
  296. $.each(me.dataView.getItems(), function(i, item) {
  297. if (item.name === doclist[0].name) {
  298. var new_item = $.extend({}, item, doclist[0]);
  299. $.each(doclist, function(i, doc) {
  300. if(item[doc.doctype + ":name"]===doc.name) {
  301. $.each(doc, function(k, v) {
  302. if(wn.model.std_fields_list.indexOf(k)===-1) {
  303. new_item[k] = v;
  304. }
  305. })
  306. }
  307. });
  308. me.dataView.updateItem(item.id, new_item);
  309. }
  310. });
  311. }
  312. }
  313. });
  314. });
  315. d.show();
  316. },
  317. set_data: function() {
  318. this.dataView.beginUpdate();
  319. this.dataView.setItems(this.data);
  320. this.dataView.endUpdate();
  321. },
  322. get_columns: function() {
  323. var std_columns = [{id:'_idx', field:'_idx', name: 'Sr.', width: 40, maxWidth: 40}];
  324. if(this.can_delete) {
  325. std_columns = std_columns.concat([{
  326. id:'_check', field:'_check', name: "", width: 30, maxWidth: 30,
  327. formatter: function(row, cell, value, columnDef, dataContext) {
  328. return repl("<input type='checkbox' \
  329. data-row='%(row)s' %(checked)s>", {
  330. row: row,
  331. checked: (dataContext._checked ? "checked=\"checked\"" : "")
  332. });
  333. }
  334. }]);
  335. }
  336. return std_columns.concat(this.build_columns());
  337. },
  338. // setup column picker
  339. make_column_picker: function() {
  340. var me = this;
  341. this.column_picker = new wn.ui.ColumnPicker(this);
  342. this.page.appframe.add_button(wn._('Pick Columns'), function() {
  343. me.column_picker.show(me.columns);
  344. }, 'icon-th-list');
  345. },
  346. set_tag_and_status_filter: function() {
  347. var me = this;
  348. this.$w.find('.result-list').on("click", ".label-info", function() {
  349. if($(this).attr("data-label")) {
  350. me.set_filter("_user_tags", $(this).attr("data-label"));
  351. }
  352. });
  353. this.$w.find('.result-list').on("click", "[data-workflow-state]", function() {
  354. if($(this).attr("data-workflow-state")) {
  355. me.set_filter(me.state_fieldname,
  356. $(this).attr("data-workflow-state"));
  357. }
  358. });
  359. },
  360. // setup sorter
  361. make_sorter: function() {
  362. var me = this;
  363. this.sort_dialog = new wn.ui.Dialog({title:'Sorting Preferences'});
  364. $(this.sort_dialog.body).html('<p class="help">Sort By</p>\
  365. <div class="sort-column"></div>\
  366. <div><select class="sort-order form-control" style="margin-top: 10px; width: 60%;">\
  367. <option value="asc">'+wn._('Ascending')+'</option>\
  368. <option value="desc">'+wn._('Descending')+'</option>\
  369. </select></div>\
  370. <hr><p class="help">'+wn._('Then By (optional)')+'</p>\
  371. <div class="sort-column-1"></div>\
  372. <div><select class="sort-order-1 form-control" style="margin-top: 10px; width: 60%;">\
  373. <option value="asc">'+wn._('Ascending')+'</option>\
  374. <option value="desc">'+wn._('Descending')+'</option>\
  375. </select></div><hr>\
  376. <div><button class="btn btn-info">'+wn._('Update')+'</div>');
  377. // first
  378. this.sort_by_select = new wn.ui.FieldSelect({
  379. parent: $(this.sort_dialog.body).find('.sort-column'),
  380. doctype: this.doctype
  381. });
  382. this.sort_by_select.$select.css('width', '60%');
  383. this.sort_order_select = $(this.sort_dialog.body).find('.sort-order');
  384. // second
  385. this.sort_by_next_select = new wn.ui.FieldSelect({
  386. parent: $(this.sort_dialog.body).find('.sort-column-1'),
  387. doctype: this.doctype,
  388. with_blank: true
  389. });
  390. this.sort_by_next_select.$select.css('width', '60%');
  391. this.sort_order_next_select = $(this.sort_dialog.body).find('.sort-order-1');
  392. // initial values
  393. this.sort_by_select.set_value(this.doctype, 'modified');
  394. this.sort_order_select.val('desc');
  395. this.sort_by_next_select.clear();
  396. this.sort_order_next_select.val('desc');
  397. // button actions
  398. this.page.appframe.add_button(wn._('Sort By'), function() {
  399. me.sort_dialog.show();
  400. }, 'icon-sort-by-alphabet');
  401. $(this.sort_dialog.body).find('.btn-info').click(function() {
  402. me.sort_dialog.hide();
  403. me.run();
  404. });
  405. },
  406. // setup export
  407. make_export: function() {
  408. var me = this;
  409. if(!wn.model.can_export(this.doctype)) {
  410. return;
  411. }
  412. var export_btn = this.page.appframe.add_button(wn._('Export'), function() {
  413. var args = me.get_args();
  414. args.cmd = 'webnotes.widgets.reportview.export_query'
  415. open_url_post(wn.request.url, args);
  416. }, 'icon-download-alt');
  417. },
  418. // save
  419. make_save: function() {
  420. var me = this;
  421. if(wn.user.is_report_manager()) {
  422. this.page.appframe.add_button(wn._('Save'), function() {
  423. // name
  424. if(me.docname) {
  425. var name = me.docname
  426. } else {
  427. var name = prompt(wn._('Select Report Name'));
  428. if(!name) {
  429. return;
  430. }
  431. }
  432. // callback
  433. return wn.call({
  434. method: 'webnotes.widgets.reportview.save_report',
  435. args: {
  436. name: name,
  437. doctype: me.doctype,
  438. json: JSON.stringify({
  439. filters: me.filter_list.get_filters(),
  440. columns: me.columns,
  441. sort_by: me.sort_by_select.val(),
  442. sort_order: me.sort_order_select.val(),
  443. sort_by_next: me.sort_by_next_select.val(),
  444. sort_order_next: me.sort_order_next_select.val()
  445. })
  446. },
  447. callback: function(r) {
  448. if(r.exc) {
  449. msgprint(wn._("Report was not saved (there were errors)"));
  450. return;
  451. }
  452. if(r.message != me.docname)
  453. wn.set_route('Report', me.doctype, r.message);
  454. }
  455. });
  456. }, 'icon-save');
  457. }
  458. },
  459. make_delete: function() {
  460. var me = this;
  461. if(this.can_delete) {
  462. $(this.page).on("click", "input[type='checkbox'][data-row]", function() {
  463. me.data[$(this).attr("data-row")]._checked
  464. = this.checked ? true : false;
  465. });
  466. this.page.appframe.add_button(wn._("Delete"), function() {
  467. var delete_list = []
  468. $.each(me.data, function(i, d) {
  469. if(d._checked) {
  470. if(d.name)
  471. delete_list.push(d.name);
  472. }
  473. });
  474. if(!delete_list.length)
  475. return;
  476. if(wn.confirm(wn._("This is PERMANENT action and you cannot undo. Continue?"),
  477. function() {
  478. return wn.call({
  479. method: 'webnotes.widgets.reportview.delete_items',
  480. args: {
  481. items: delete_list,
  482. doctype: me.doctype
  483. },
  484. callback: function() {
  485. me.refresh();
  486. }
  487. });
  488. }));
  489. }, 'icon-remove');
  490. }
  491. },
  492. make_user_restrictions: function() {
  493. var me = this;
  494. if(this.docname && wn.model.can_restrict("Report")) {
  495. this.page.appframe.add_button(wn._("User Permission Restrictions"), function() {
  496. wn.route_options = {
  497. property: "Report",
  498. restriction: me.docname
  499. };
  500. wn.set_route("user-properties");
  501. }, "icon-shield");
  502. }
  503. },
  504. });
  505. wn.ui.ColumnPicker = Class.extend({
  506. init: function(list) {
  507. this.list = list;
  508. this.doctype = list.doctype;
  509. },
  510. clear: function() {
  511. this.columns = [];
  512. $(this.dialog.body).html('<div class="help">'+wn._("Drag to sort columns")+'</div>\
  513. <div class="column-list"></div>\
  514. <div><button class="btn btn-default btn-add"><i class="icon-plus"></i>\
  515. '+wn._("Add Column")+'</button></div>\
  516. <hr>\
  517. <div><button class="btn btn-info">'+wn._("Update")+'</div>');
  518. },
  519. show: function(columns) {
  520. var me = this;
  521. if(!this.dialog) {
  522. this.dialog = new wn.ui.Dialog({
  523. title: wn._("Pick Columns"),
  524. width: '400'
  525. });
  526. }
  527. this.clear();
  528. // show existing
  529. $.each(columns, function(i, c) {
  530. me.add_column(c);
  531. });
  532. $(this.dialog.body).find('.column-list').sortable();
  533. // add column
  534. $(this.dialog.body).find('.btn-add').click(function() {
  535. me.add_column(['name']);
  536. });
  537. // update
  538. $(this.dialog.body).find('.btn-info').click(function() {
  539. me.dialog.hide();
  540. // selected columns as list of [column_name, table_name]
  541. var columns = $.map(me.columns, function(v) {
  542. return v ? [[v.selected_fieldname, v.selected_doctype]] : null;
  543. });
  544. wn.defaults.set_default("_list_settings:" + me.doctype, columns);
  545. me.list.columns = columns;
  546. me.list.run();
  547. });
  548. this.dialog.show();
  549. },
  550. add_column: function(c) {
  551. if(!c) return;
  552. var w = $('<div style="padding: 5px; background-color: #eee; \
  553. width: 90%; margin-bottom: 10px; border-radius: 3px; cursor: move;">\
  554. <img src="assets/webnotes/images/ui/drag-handle.png" style="margin-right: 10px;">\
  555. <a class="close" style="margin-top: 5px;">&times</a>\
  556. </div>')
  557. .appendTo($(this.dialog.body).find('.column-list'));
  558. var fieldselect = new wn.ui.FieldSelect({parent:w, doctype:this.doctype}),
  559. me = this;
  560. fieldselect.$select.css({"display": "inline"});
  561. fieldselect.$select.css({width: '70%', 'margin-top':'5px'})
  562. fieldselect.val((c[1] || this.doctype) + "." + c[0]);
  563. w.find('.close').data("fieldselect", fieldselect).click(function() {
  564. console.log(me.columns.indexOf($(this).data('fieldselect')));
  565. delete me.columns[me.columns.indexOf($(this).data('fieldselect'))];
  566. $(this).parent().remove();
  567. });
  568. this.columns.push(fieldselect);
  569. }
  570. });