Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

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