Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

563 linhas
16 KiB

  1. // Copyright (c) 2012 Web Notes Technologies Pvt Ltd (http://erpnext.com)
  2. //
  3. // MIT License (MIT)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a
  6. // copy of this software and associated documentation files (the "Software"),
  7. // to deal in the Software without restriction, including without limitation
  8. // the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. // and/or sell copies of the Software, and to permit persons to whom the
  10. // Software is furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  16. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  17. // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  18. // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  19. // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
  20. // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21. //
  22. wn.provide('wn.views.doclistview');
  23. wn.provide('wn.doclistviews');
  24. wn.views.doclistview.show = function(doctype) {
  25. var page_name = wn.get_route_str();
  26. if(wn.pages[page_name]) {
  27. wn.container.change_to(wn.pages[page_name]);
  28. } else {
  29. var route = wn.get_route();
  30. if(route[1]) {
  31. wn.model.with_doctype(route[1], function(r) {
  32. if(r && r['403']) {
  33. return;
  34. }
  35. new wn.views.DocListView(route[1]);
  36. });
  37. }
  38. }
  39. }
  40. wn.views.DocListView = wn.ui.Listing.extend({
  41. init: function(doctype) {
  42. this.doctype = doctype;
  43. this.label = get_doctype_label(doctype);
  44. this.label = (this.label.toLowerCase().substr(-4) == 'list') ?
  45. this.label : (this.label + ' List');
  46. this.make_page();
  47. this.setup();
  48. },
  49. make_page: function() {
  50. var me = this;
  51. var page_name = wn.get_route_str();
  52. var page = wn.container.add_page(page_name);
  53. wn.container.change_to(page_name);
  54. this.$page = $(page);
  55. this.$page.html('<div class="layout-wrapper layout-wrapper-background">\
  56. <div class="appframe-area"></div>\
  57. <div class="layout-main-section">\
  58. <div class="wnlist-area"><div class="help">Loading...</div></div>\
  59. </div>\
  60. <div class="layout-side-section">\
  61. <div class="show-docstatus hide" style="margin-bottom: 19px">\
  62. <h4>Show</h4>\
  63. <div><input data-docstatus="0" type="checkbox" checked="checked" /> Drafts</div>\
  64. <div><input data-docstatus="1" type="checkbox" checked="checked" /> Submitted</div>\
  65. <div><input data-docstatus="2" type="checkbox" /> Cancelled</div>\
  66. </div>\
  67. </div>\
  68. <div style="clear: both"></div>\
  69. </div>');
  70. this.appframe = new wn.ui.AppFrame(this.$page.find('.appframe-area'));
  71. wn.views.breadcrumbs(this.appframe, locals.DocType[this.doctype].module, this.doctype);
  72. },
  73. setup: function() {
  74. var me = this;
  75. me.can_delete = wn.model.can_delete(me.doctype);
  76. me.meta = locals.DocType[me.doctype];
  77. me.$page.find('.wnlist-area').empty(),
  78. me.setup_docstatus_filter();
  79. me.setup_listview();
  80. me.init_list();
  81. me.init_stats();
  82. me.make_report_button();
  83. me.add_delete_option();
  84. me.make_help();
  85. },
  86. make_report_button: function() {
  87. var me = this;
  88. if(wn.boot.profile.can_get_report.indexOf(this.doctype)!=-1) {
  89. this.appframe.add_button('Build Report', function() {
  90. wn.set_route('Report2', me.doctype);
  91. }, 'icon-th')
  92. }
  93. },
  94. make_help: function() {
  95. // Help
  96. if(this.meta.description) {
  97. this.appframe.add_help_button(wn.markdown('## ' + this.meta.name + '\n\n'
  98. + this.meta.description));
  99. }
  100. },
  101. setup_docstatus_filter: function() {
  102. var me = this;
  103. this.can_submit = $.map(locals.DocPerm, function(d) {
  104. if(d.parent==me.meta.name && d.submit) return 1
  105. else return null;
  106. }).length;
  107. if(this.can_submit) {
  108. this.$page.find('.show-docstatus').removeClass('hide');
  109. this.$page.find('.show-docstatus input').click(function() {
  110. me.run();
  111. })
  112. }
  113. },
  114. setup_listview: function() {
  115. if(this.meta.__listjs) {
  116. eval(this.meta.__listjs);
  117. this.listview = new wn.doclistviews[this.doctype](this);
  118. } else {
  119. this.listview = new wn.views.ListView(this);
  120. }
  121. this.listview.parent = this;
  122. this.wrapper = this.$page.find('.wnlist-area');
  123. this.page_length = 20;
  124. this.allow_delete = true;
  125. },
  126. init_list: function(auto_run) {
  127. var me = this;
  128. // init list
  129. this.make({
  130. method: 'webnotes.widgets.doclistview.get',
  131. get_args: this.get_args,
  132. parent: this.wrapper,
  133. start: 0,
  134. page_length: this.page_length,
  135. show_filters: true,
  136. show_grid: true,
  137. new_doctype: this.doctype,
  138. allow_delete: this.allow_delete,
  139. no_result_message: this.make_no_result(),
  140. columns: this.listview.fields,
  141. custom_new_doc: me.listview.make_new_doc || undefined,
  142. });
  143. // make_new_doc can be overridden so that default values can be prefilled
  144. // for example - communication list in customer
  145. $(this.wrapper).find('button[list_view_doc="'+me.doctype+'"]').click(function(){
  146. (me.listview.make_new_doc || me.make_new_doc)(me.doctype);
  147. });
  148. if((auto_run !== false) && (auto_run !== 0)) this.run();
  149. },
  150. make_no_result: function() {
  151. var no_result_message = repl('<div class="well">\
  152. <p>No %(doctype_label)s found</p>\
  153. <hr>\
  154. <p><button class="btn btn-info btn-small" list_view_doc="%(doctype)s">\
  155. Make a new %(doctype_label)s</button>\
  156. </p></div>', {
  157. doctype_label: get_doctype_label(this.doctype),
  158. doctype: this.doctype
  159. });
  160. return no_result_message;
  161. },
  162. render_row: function(row, data) {
  163. $(row).css({
  164. "margin-left": "-15px",
  165. "margin-right": "-15px"
  166. });
  167. data.doctype = this.doctype;
  168. this.listview.render(row, data, this);
  169. },
  170. get_query_fields: function() {
  171. return this.listview.fields;
  172. },
  173. get_args: function() {
  174. return {
  175. doctype: this.doctype,
  176. fields: this.get_query_fields(),
  177. filters: this.filter_list.get_filters(),
  178. docstatus: this.can_submit ? $.map(this.$page.find('.show-docstatus :checked'),
  179. function(inp) { return $(inp).attr('data-docstatus') }) : [],
  180. order_by: this.listview.order_by || undefined,
  181. group_by: this.listview.group_by || undefined,
  182. }
  183. },
  184. add_delete_option: function() {
  185. var me = this;
  186. if(this.can_delete) {
  187. this.add_button('Delete', function() { me.delete_items(); }, 'icon-remove');
  188. $('<div style="padding: 9px; margin-left: -15px;"><input type="checkbox" name="select-all" />\
  189. Select all</div>').insertBefore(this.$page.find('.result-list'));
  190. this.$page.find('[name="select-all"]').click(function() {
  191. me.$page.find('.list-delete').attr('checked', $(this).attr('checked') || false);
  192. })
  193. }
  194. },
  195. delete_items: function() {
  196. var me = this;
  197. var dl = $.map(me.$page.find('.list-delete:checked'), function(e) {
  198. return $(e).data('name');
  199. });
  200. if(!dl.length)
  201. return;
  202. if(!confirm('This is PERMANENT action and you cannot undo. Continue?')) {
  203. return;
  204. }
  205. me.set_working(true);
  206. wn.call({
  207. method: 'webnotes.widgets.doclistview.delete_items',
  208. args: {
  209. items: dl,
  210. doctype: me.doctype
  211. },
  212. callback: function() {
  213. me.set_working(false);
  214. me.refresh();
  215. }
  216. })
  217. },
  218. init_stats: function() {
  219. var me = this
  220. wn.call({
  221. method: 'webnotes.widgets.doclistview.get_stats',
  222. args: {
  223. stats: me.listview.stats,
  224. doctype: me.doctype
  225. },
  226. callback: function(r) {
  227. // This gives a predictable stats order
  228. $.each(me.listview.stats, function(i, v) {
  229. me.render_stat(v, r.message[v]);
  230. });
  231. // reload button at the end
  232. if(me.listview.stats.length) {
  233. $('<button class="btn btn-small"><i class="refresh"></i> Refresh</button>')
  234. .click(function() {
  235. me.reload_stats();
  236. }).appendTo($('<div class="stat-wrapper">')
  237. .appendTo(me.$page.find('.layout-side-section')))
  238. }
  239. }
  240. });
  241. },
  242. render_stat: function(field, stat) {
  243. var me = this;
  244. if(!stat || !stat.length) {
  245. if(field=='_user_tags') {
  246. this.$page.find('.layout-side-section')
  247. .append('<div class="stat-wrapper"><h4>Tags</h4>\
  248. <div class="help small"><i>No records tagged.</i><br><br> \
  249. To add a tag, open the document and click on \
  250. "Add Tag" on the sidebar</div></div>');
  251. }
  252. return;
  253. }
  254. var label = wn.meta.docfield_map[this.doctype][field] ?
  255. wn.meta.docfield_map[this.doctype][field].label : field;
  256. if(label=='_user_tags') label = 'Tags';
  257. // grid
  258. var $w = $('<div class="stat-wrapper">\
  259. <h4>'+ label +'</h4>\
  260. <div class="stat-grid">\
  261. </div>\
  262. </div>');
  263. // sort items
  264. stat = stat.sort(function(a, b) { return b[1] - a[1] });
  265. var sum = 0;
  266. $.each(stat, function(i,v) { sum = sum + v[1]; })
  267. // render items
  268. $.each(stat, function(i, v) {
  269. me.render_stat_item(i, v, sum, field).appendTo($w.find('.stat-grid'));
  270. });
  271. $w.appendTo(this.$page.find('.layout-side-section'));
  272. },
  273. render_stat_item: function(i, v, max, field) {
  274. var me = this;
  275. var args = {}
  276. args.label = v[0];
  277. args.width = flt(v[1]) / max * 100;
  278. args.count = v[1];
  279. args.field = field;
  280. $item = $(repl('<div class="stat-item">\
  281. <div class="stat-bar" style="width: %(width)s%"></div>\
  282. <div class="stat-label">\
  283. <a href="#" data-label="%(label)s" data-field="%(field)s">\
  284. %(label)s</a> \
  285. (%(count)s)</div>\
  286. </div>', args));
  287. this.setup_stat_item_click($item);
  288. return $item;
  289. },
  290. reload_stats: function() {
  291. this.$page.find('.layout-side-section .stat-wrapper').remove();
  292. this.init_stats();
  293. },
  294. setup_stat_item_click: function($item) {
  295. var me = this;
  296. $item.find('a').click(function() {
  297. var fieldname = $(this).attr('data-field');
  298. var label = $(this).attr('data-label');
  299. me.set_filter(fieldname, label);
  300. return false;
  301. });
  302. },
  303. set_filter: function(fieldname, label) {
  304. var filter = this.filter_list.get_filter(fieldname);
  305. if(filter) {
  306. var v = filter.field.get_value();
  307. if(v.indexOf(label)!=-1) {
  308. // already set
  309. return false;
  310. } else {
  311. // second filter set for this field
  312. if(fieldname=='_user_tags') {
  313. // and for tags
  314. this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label);
  315. } else {
  316. // or for rest using "in"
  317. filter.set_values(this.doctype, fieldname, 'in', v + ', ' + label);
  318. }
  319. }
  320. } else {
  321. // no filter for this item,
  322. // setup one
  323. if(fieldname=='_user_tags') {
  324. this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label);
  325. } else {
  326. this.filter_list.add_filter(this.doctype, fieldname, '=', label);
  327. }
  328. }
  329. this.run();
  330. }
  331. });
  332. wn.views.ListView = Class.extend({
  333. init: function(doclistview) {
  334. this.doclistview = doclistview;
  335. this.doctype = doclistview.doctype;
  336. var t = "`tab"+this.doctype+"`.";
  337. this.fields = [t + 'name', t + 'owner', t + 'docstatus',
  338. t + '_user_tags', t + 'modified'];
  339. this.stats = ['_user_tags'];
  340. this.show_hide_check_column();
  341. },
  342. columns: [
  343. {width: '3%', content:'check'},
  344. {width: '4%', content:'avatar'},
  345. {width: '3%', content:'docstatus', css: {"text-align": "center"}},
  346. {width: '35%', content:'name'},
  347. {width: '40%', content:'tags', css: {'color':'#aaa'}},
  348. {width: '15%', content:'modified', css: {'text-align': 'right', 'color':'#222'}}
  349. ],
  350. render_column: function(data, parent, opts) {
  351. var me = this;
  352. // style
  353. if(opts.css) {
  354. $.each(opts.css, function(k, v) { $(parent).css(k, v)});
  355. }
  356. // multiple content
  357. if(opts.content.indexOf && opts.content.indexOf('+')!=-1) {
  358. $.map(opts.content.split('+'), function(v) {
  359. me.render_column(data, parent, {content:v});
  360. });
  361. return;
  362. }
  363. // content
  364. if(typeof opts.content=='function') {
  365. opts.content(parent, data, me);
  366. }
  367. else if(opts.content=='name') {
  368. $(parent).append(repl('<a href="#!Form/%(doctype)s/%(name)s">%(name)s</a>', data));
  369. }
  370. else if(opts.content=='avatar') {
  371. $(parent).append(repl('<span class="avatar-small"><img src="%(avatar)s" \
  372. title="%(fullname)s"/></span>',
  373. data));
  374. }
  375. else if(opts.content=='check') {
  376. $(parent).append('<input class="list-delete" type="checkbox">');
  377. $(parent).find('input').data('name', data.name);
  378. }
  379. else if(opts.content=='docstatus') {
  380. $(parent).append(repl('<span class="docstatus"><i class="%(docstatus_icon)s" \
  381. title="%(docstatus_title)s"></i></span>', data));
  382. }
  383. else if(opts.content=='tags') {
  384. this.add_user_tags(parent, data);
  385. }
  386. else if(opts.content=='modified') {
  387. $(parent).append(data.when);
  388. }
  389. else if(opts.type=='bar-graph') {
  390. this.render_bar_graph(parent, data, opts.content, opts.label);
  391. }
  392. else if(opts.type=='link' && opts.doctype) {
  393. $(parent).append(repl('<a href="#!Form/'+opts.doctype+'/'
  394. +data[opts.content]+'">'+data[opts.content]+'</a>', data));
  395. }
  396. else if(opts.template) {
  397. $(parent).append(repl(opts.template, data));
  398. }
  399. else if(data[opts.content]) {
  400. if(opts.type=="date") {
  401. data[opts.content] = wn.datetime.str_to_user(data[opts.content])
  402. }
  403. $(parent).append(repl('<span title="%(title)s"> %(content)s</span>', {
  404. "title": opts.title || opts.content, "content": data[opts.content]}));
  405. }
  406. },
  407. render: function(row, data) {
  408. var me = this;
  409. this.prepare_data(data);
  410. rowhtml = '';
  411. // make table
  412. $.each(this.columns, function(i, v) {
  413. rowhtml += repl('<td style="width: %(width)s"></td>', v);
  414. });
  415. var tr = $(row).html('<table><tbody><tr>' + rowhtml + '</tr></tbody></table>').find('tr').get(0);
  416. // render cells
  417. $.each(this.columns, function(i, v) {
  418. me.render_column(data, tr.cells[i], v);
  419. });
  420. },
  421. prepare_data: function(data) {
  422. data.fullname = wn.user_info(data.owner).fullname;
  423. data.avatar = wn.user_info(data.owner).image;
  424. if(data.modified)
  425. this.prepare_when(data, data.modified);
  426. // docstatus
  427. if(data.docstatus==0 || data.docstatus==null) {
  428. data.docstatus_icon = 'icon-pencil';
  429. data.docstatus_title = 'Editable';
  430. } else if(data.docstatus==1) {
  431. data.docstatus_icon = 'icon-lock';
  432. data.docstatus_title = 'Submitted';
  433. } else if(data.docstatus==2) {
  434. data.docstatus_icon = 'icon-remove';
  435. data.docstatus_title = 'Cancelled';
  436. }
  437. // nulls as strings
  438. for(key in data) {
  439. if(data[key]==null) {
  440. data[key]='';
  441. }
  442. }
  443. },
  444. prepare_when: function(data, date_str) {
  445. if (!date_str) date_str = data.modified;
  446. // when
  447. data.when = (dateutil.str_to_user(date_str)).split(' ')[0];
  448. var diff = dateutil.get_diff(dateutil.get_today(), date_str.split(' ')[0]);
  449. if(diff==0) {
  450. data.when = dateutil.comment_when(date_str);
  451. }
  452. if(diff == 1) {
  453. data.when = 'Yesterday'
  454. }
  455. if(diff == 2) {
  456. data.when = '2 days ago'
  457. }
  458. },
  459. add_user_tags: function(parent, data) {
  460. var me = this;
  461. if(data._user_tags) {
  462. if($(parent).html().length > 0) {
  463. $(parent).append('<br />');
  464. }
  465. $.each(data._user_tags.split(','), function(i, t) {
  466. if(t) {
  467. $('<span class="label label-info" style="cursor: pointer; line-height: 200%">'
  468. + strip(t) + '</span>')
  469. .click(function() {
  470. me.doclistview.set_filter('_user_tags', $(this).text())
  471. })
  472. .appendTo(parent);
  473. }
  474. });
  475. }
  476. },
  477. show_hide_check_column: function() {
  478. if(!this.doclistview.can_delete) {
  479. this.columns = $.map(this.columns, function(v, i) { if(v.content!='check') return v });
  480. }
  481. },
  482. render_bar_graph: function(parent, data, field, label) {
  483. var args = {
  484. percent: data[field],
  485. fully_delivered: (data[field] > 99 ? 'bar-complete' : ''),
  486. label: label
  487. }
  488. $(parent).append(repl('<span class="bar-outer" style="width: 30px; float: right" \
  489. title="%(percent)s% %(label)s">\
  490. <span class="bar-inner %(fully_delivered)s" \
  491. style="width: %(percent)s%;"></span>\
  492. </span>', args));
  493. },
  494. render_icon: function(parent, icon_class, label) {
  495. var icon_html = "<i class='%(icon_class)s' title='%(label)s'></i>";
  496. $(parent).append(repl(icon_html, {icon_class: icon_class, label: label || ''}));
  497. }
  498. });
  499. wn.provide('wn.views.RecordListView');
  500. wn.views.RecordListView = wn.views.DocListView.extend({
  501. init: function(doctype, wrapper, ListView) {
  502. this.doctype = doctype;
  503. this.wrapper = wrapper;
  504. this.listview = new ListView(this);
  505. this.listview.parent = this;
  506. this.setup();
  507. },
  508. setup: function() {
  509. var me = this;
  510. me.page_length = 10;
  511. $(me.wrapper).empty();
  512. me.init_list();
  513. },
  514. get_args: function() {
  515. var args = this._super();
  516. $.each((this.default_filters || []), function(i, f) {
  517. args.filters.push(f);
  518. });
  519. args.docstatus = args.docstatus.concat((this.default_docstatus || []));
  520. return args;
  521. },
  522. });