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.

546 lines
20 KiB

  1. /* Odoo web_timeline
  2. * Copyright 2015 ACSONE SA/NV
  3. * Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
  4. * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
  5. _.str.toBoolElse = function (str, elseValues, trueValues, falseValues) {
  6. var ret = _.str.toBool(str, trueValues, falseValues);
  7. if (_.isUndefined(ret)) {
  8. return elseValues;
  9. }
  10. return ret;
  11. };
  12. odoo.define('web_timeline.TimelineView', function (require) {
  13. "use strict";
  14. var core = require('web.core');
  15. var form_common = require('web.form_common');
  16. var Model = require('web.DataModel');
  17. var time = require('web.time');
  18. var View = require('web.View');
  19. var widgets = require('web_calendar.widgets');
  20. var _t = core._t;
  21. var _lt = core._lt;
  22. function isNullOrUndef(value) {
  23. return _.isUndefined(value) || _.isNull(value);
  24. }
  25. var TimelineView = View.extend({
  26. template: "TimelineView",
  27. display_name: _lt('Timeline'),
  28. icon: 'fa-clock-o',
  29. quick_create_instance: widgets.QuickCreate,
  30. init: function (parent, dataset, view_id, options) {
  31. this.permissions = {};
  32. this.grouped_by = false;
  33. return this._super.apply(this, arguments);
  34. },
  35. get_perm: function (name) {
  36. var self = this;
  37. var promise = self.permissions[name];
  38. if (self.permissions[name]) {
  39. return $.when(self.permissions[name]);
  40. } else {
  41. return new Model(this.dataset.model)
  42. .call("check_access_rights", [name, false])
  43. .then(function (value) {
  44. self.permissions[name] = value;
  45. return value;
  46. });
  47. }
  48. },
  49. parse_colors: function () {
  50. if (this.fields_view.arch.attrs.colors) {
  51. this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain().compact().map(function (color_pair) {
  52. var pair = color_pair.split(':'), color = pair[0], expr = pair[1];
  53. var temp = py.parse(py.tokenize(expr));
  54. return {
  55. 'color': color,
  56. 'field': temp.expressions[0].value,
  57. 'opt': temp.operators[0],
  58. 'value': temp.expressions[1].value
  59. };
  60. }).value();
  61. }
  62. },
  63. start: function () {
  64. var self = this;
  65. var attrs = this.fields_view.arch.attrs;
  66. var fv = this.fields_view;
  67. this.parse_colors();
  68. this.$timeline = this.$el.find(".oe_timeline_widget");
  69. this.$(".oe_timeline_button_today").click(
  70. this.proxy(this.on_today_clicked));
  71. this.$(".oe_timeline_button_scale_day").click(
  72. this.proxy(this.on_scale_day_clicked));
  73. this.$(".oe_timeline_button_scale_week").click(
  74. this.proxy(this.on_scale_week_clicked));
  75. this.$(".oe_timeline_button_scale_month").click(
  76. this.proxy(this.on_scale_month_clicked));
  77. this.$(".oe_timeline_button_scale_year").click(
  78. this.proxy(this.on_scale_year_clicked));
  79. this.current_window = {
  80. start: new moment(),
  81. end: new moment().add(24, 'hours')
  82. };
  83. this.$el.addClass(attrs['class']);
  84. this.info_fields = [];
  85. if (!attrs.date_start) {
  86. throw new Error(_t("Timeline view has not defined 'date_start' attribute."));
  87. }
  88. this.date_start = attrs.date_start;
  89. this.date_stop = attrs.date_stop;
  90. this.date_delay = attrs.date_delay;
  91. this.no_period = this.date_start == this.date_stop;
  92. this.zoomKey = attrs.zoomKey || '';
  93. this.mode = attrs.mode || attrs.default_window || 'fit';
  94. if (!isNullOrUndef(attrs.quick_create_instance)) {
  95. self.quick_create_instance = 'instance.' + attrs.quick_create_instance;
  96. }
  97. // If this field is set ot true, we don't open the event in form
  98. // view, but in a popup with the view_id passed by this parameter
  99. if (isNullOrUndef(attrs.event_open_popup) || !_.str.toBoolElse(attrs.event_open_popup, true)) {
  100. this.open_popup_action = false;
  101. } else {
  102. this.open_popup_action = attrs.event_open_popup;
  103. }
  104. this.fields = fv.fields;
  105. for (var fld = 0; fld < fv.arch.children.length; fld++) {
  106. this.info_fields.push(fv.arch.children[fld].attrs.name);
  107. }
  108. var fields_get = new Model(this.dataset.model)
  109. .call('fields_get')
  110. .then(function (fields) {
  111. self.fields = fields;
  112. });
  113. this._super.apply(this, self);
  114. return $.when(
  115. self.fields_get,
  116. self.get_perm('unlink'),
  117. self.get_perm('write'),
  118. self.get_perm('create')
  119. ).then(function () {
  120. self.init_timeline();
  121. $(window).trigger('resize');
  122. self.trigger('timeline_view_loaded', fv);
  123. });
  124. },
  125. init_timeline: function () {
  126. var self = this;
  127. var options = {
  128. groupOrder: self.group_order,
  129. editable: {
  130. // add new items by double tapping
  131. add: self.permissions['create'],
  132. // drag items horizontally
  133. updateTime: self.permissions['write'],
  134. // drag items from one group to another
  135. updateGroup: self.permissions['write'],
  136. // delete an item by tapping the delete button top right
  137. remove: self.permissions['unlink']
  138. },
  139. orientation: 'both',
  140. selectable: true,
  141. showCurrentTime: true,
  142. onAdd: self.on_add,
  143. onMove: self.on_move,
  144. onUpdate: self.on_update,
  145. onRemove: self.on_remove,
  146. zoomKey: this.zoomKey
  147. };
  148. if (this.mode) {
  149. var start = false, end = false;
  150. switch (this.mode) {
  151. case 'day':
  152. start = new moment().startOf('day');
  153. end = new moment().endOf('day');
  154. break;
  155. case 'week':
  156. start = new moment().startOf('week');
  157. end = new moment().endOf('week');
  158. break;
  159. case 'month':
  160. start = new moment().startOf('month');
  161. end = new moment().endOf('month');
  162. break;
  163. }
  164. if (end && start) {
  165. options['start'] = start;
  166. options['end'] = end;
  167. }else{
  168. this.mode = 'fit';
  169. }
  170. }
  171. self.timeline = new vis.Timeline(self.$timeline.empty().get(0));
  172. self.timeline.setOptions(options);
  173. if (self.mode && self['on_scale_' + self.mode + '_clicked']) {
  174. self['on_scale_' + self.mode + '_clicked']();
  175. }
  176. self.timeline.on('click', self.on_click);
  177. },
  178. group_order: function (grp1, grp2) {
  179. // display non grouped elements first
  180. if (grp1.id === -1) {
  181. return -1;
  182. }
  183. if (grp2.id === -1) {
  184. return +1;
  185. }
  186. return grp1.content - grp2.content;
  187. },
  188. /* Transform Odoo event object to timeline event object */
  189. event_data_transform: function (evt) {
  190. var self = this;
  191. var date_start = new moment();
  192. var date_stop;
  193. var date_delay = evt[this.date_delay] || false,
  194. all_day = this.all_day ? evt[this.all_day] : false,
  195. res_computed_text = '',
  196. the_title = '',
  197. attendees = [];
  198. if (!all_day) {
  199. date_start = time.auto_str_to_date(evt[this.date_start]);
  200. date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop]) : null;
  201. }
  202. else {
  203. date_start = time.auto_str_to_date(evt[this.date_start].split(' ')[0], 'start');
  204. if (this.no_period) {
  205. date_stop = date_start
  206. } else {
  207. date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop].split(' ')[0], 'stop') : null;
  208. }
  209. }
  210. if (!date_start) {
  211. date_start = new moment();
  212. }
  213. if (!date_stop && date_delay) {
  214. date_stop = moment(date_start).add(date_delay, 'hours').toDate();
  215. }
  216. var group = evt[self.last_group_bys[0]];
  217. if (group) {
  218. group = _.first(group);
  219. } else {
  220. group = -1;
  221. }
  222. _.each(self.colors, function (color) {
  223. if (eval("'" + evt[color.field] + "' " + color.opt + " '" + color.value + "'"))
  224. self.color = color.color;
  225. });
  226. var r = {
  227. 'start': date_start,
  228. 'content': evt.__name != undefined ? evt.__name : evt.display_name,
  229. 'id': evt.id,
  230. 'group': group,
  231. 'evt': evt,
  232. 'style': 'background-color: ' + self.color + ';'
  233. };
  234. // Check if the event is instantaneous, if so, display it with a point on the timeline (no 'end')
  235. if (date_stop && !moment(date_start).isSame(date_stop)) {
  236. r.end = date_stop;
  237. }
  238. self.color = undefined;
  239. return r;
  240. },
  241. do_search: function (domains, contexts, group_bys) {
  242. var self = this;
  243. self.last_domains = domains;
  244. self.last_contexts = contexts;
  245. // select the group by
  246. var n_group_bys = [];
  247. if (this.fields_view.arch.attrs.default_group_by) {
  248. n_group_bys = this.fields_view.arch.attrs.default_group_by.split(',');
  249. }
  250. if (group_bys.length) {
  251. n_group_bys = group_bys;
  252. }
  253. self.last_group_bys = n_group_bys;
  254. // gather the fields to get
  255. var fields = _.compact(_.map(["date_start", "date_delay", "date_stop", "progress"], function (key) {
  256. return self.fields_view.arch.attrs[key] || '';
  257. }));
  258. fields = _.uniq(fields.concat(_.pluck(this.colors, "field").concat(n_group_bys)));
  259. return $.when(this.has_been_loaded).then(function () {
  260. return self.dataset.read_slice(fields, {
  261. domain: domains,
  262. context: contexts
  263. }).then(function (data) {
  264. return self.on_data_loaded(data, n_group_bys);
  265. });
  266. });
  267. },
  268. reload: function () {
  269. var self = this;
  270. if (this.last_domains !== undefined) {
  271. self.current_window = self.timeline.getWindow();
  272. return this.do_search(this.last_domains, this.last_contexts, this.last_group_bys);
  273. }
  274. },
  275. on_data_loaded: function (events, group_bys) {
  276. var self = this;
  277. var ids = _.pluck(events, "id");
  278. return this.dataset.name_get(ids).then(function (names) {
  279. var nevents = _.map(events, function (event) {
  280. return _.extend({
  281. __name: _.detect(names, function (name) {
  282. return name[0] == event.id;
  283. })[1]
  284. }, event);
  285. });
  286. return self.on_data_loaded_2(nevents, group_bys);
  287. });
  288. },
  289. on_data_loaded_2: function (events, group_bys) {
  290. var self = this;
  291. var data = [];
  292. var groups = [];
  293. this.grouped_by = group_bys;
  294. _.each(events, function (event) {
  295. if (event[self.date_start]) {
  296. data.push(self.event_data_transform(event));
  297. }
  298. });
  299. // get the groups
  300. var split_groups = function (events, group_bys) {
  301. if (group_bys.length === 0)
  302. return events;
  303. var groups = [];
  304. groups.push({id: -1, content: _t('-')})
  305. _.each(events, function (event) {
  306. var group_name = event[_.first(group_bys)];
  307. if (group_name) {
  308. var group = _.find(groups, function (group) {
  309. return _.isEqual(group.id, group_name[0]);
  310. });
  311. if (group === undefined) {
  312. group = {id: group_name[0], content: group_name[1]};
  313. groups.push(group);
  314. }
  315. }
  316. });
  317. return groups;
  318. }
  319. var groups = split_groups(events, group_bys);
  320. this.timeline.setGroups(groups);
  321. this.timeline.setItems(data);
  322. if (!this.mode || this.mode == 'fit'){
  323. this.timeline.fit();
  324. }
  325. },
  326. do_show: function () {
  327. this.do_push_state({});
  328. return this._super();
  329. },
  330. is_action_enabled: function (action) {
  331. if (action === 'create' && !this.options.creatable) {
  332. return false;
  333. }
  334. return this._super(action);
  335. },
  336. create_completed: function (id) {
  337. var self = this;
  338. this.dataset.ids = this.dataset.ids.concat([id]);
  339. this.dataset.trigger("dataset_changed", id);
  340. this.dataset.read_ids([id], this.fields).done(function (records) {
  341. var new_event = self.event_data_transform(records[0]);
  342. var items = self.timeline.itemsData;
  343. items.add(new_event);
  344. self.timeline.setItems(items);
  345. });
  346. },
  347. on_add: function (item, callback) {
  348. var self = this;
  349. var context = this.dataset.get_context();
  350. // Initialize default values for creation
  351. var default_context = {};
  352. default_context['default_'.concat(this.date_start)] = item.start;
  353. if (this.date_delay) {
  354. default_context['default_'.concat(this.date_delay)] = 1;
  355. }
  356. if (this.date_stop) {
  357. default_context['default_'.concat(this.date_stop)] = moment(item.start).add(1, 'hours').toDate();
  358. }
  359. if (item.group > 0) {
  360. default_context['default_'.concat(this.last_group_bys[0])] = item.group;
  361. }
  362. context.add(default_context);
  363. // Show popup
  364. var dialog = new form_common.FormViewDialog(this, {
  365. res_model: this.dataset.model,
  366. res_id: null,
  367. context: context,
  368. view_id: +this.open_popup_action
  369. }).open();
  370. dialog.on('create_completed', this, this.create_completed);
  371. return false;
  372. },
  373. write_completed: function (id) {
  374. this.dataset.trigger("dataset_changed", id);
  375. this.current_window = this.timeline.getWindow();
  376. this.reload();
  377. this.timeline.setWindow(this.current_window);
  378. },
  379. on_update: function (item, callback) {
  380. var self = this;
  381. var id = item.evt.id;
  382. var title = item.evt.__name;
  383. if (!this.open_popup_action) {
  384. var index = this.dataset.get_id_index(id);
  385. this.dataset.index = index;
  386. if (this.write_right) {
  387. this.do_switch_view('form', null, {mode: "edit"});
  388. } else {
  389. this.do_switch_view('form', null, {mode: "view"});
  390. }
  391. }
  392. else {
  393. var dialog = new form_common.FormViewDialog(this, {
  394. res_model: this.dataset.model,
  395. res_id: parseInt(id).toString() == id ? parseInt(id) : id,
  396. context: this.dataset.get_context(),
  397. title: title,
  398. view_id: +this.open_popup_action
  399. }).open();
  400. dialog.on('write_completed', this, this.write_completed);
  401. }
  402. return false;
  403. },
  404. on_move: function (item, callback) {
  405. var self = this;
  406. var event_start = item.start;
  407. var event_end = item.end;
  408. var group = false;
  409. if (item.group != -1) {
  410. group = item.group;
  411. }
  412. var data = {};
  413. // In case of a move event, the date_delay stay the same, only date_start and stop must be updated
  414. data[this.date_start] = time.auto_date_to_str(event_start, self.fields[this.date_start].type);
  415. if (this.date_stop) {
  416. // In case of instantaneous event, item.end is not defined
  417. if (event_end) {
  418. data[this.date_stop] = time.auto_date_to_str(event_end, self.fields[this.date_stop].type);
  419. } else {
  420. data[this.date_stop] = data[this.date_start]
  421. }
  422. }
  423. if (this.date_delay && event_end) {
  424. var diff_seconds = Math.round((event_end.getTime() - event_start.getTime()) / 1000);
  425. data[this.date_delay] = diff_seconds / 3600;
  426. }
  427. if (self.grouped_by) {
  428. data[self.grouped_by[0]] = group;
  429. }
  430. var id = item.evt.id;
  431. this.dataset.write(id, data);
  432. },
  433. on_remove: function (item, callback) {
  434. var self = this;
  435. function do_it() {
  436. return $.when(self.dataset.unlink([item.evt.id])).then(function () {
  437. callback(item);
  438. });
  439. }
  440. if (this.options.confirm_on_delete) {
  441. if (confirm(_t("Are you sure you want to delete this record ?"))) {
  442. return do_it();
  443. }
  444. } else
  445. return do_it();
  446. },
  447. on_click: function (e) {
  448. // handle a click on a group header
  449. if (e.what == 'group-label') {
  450. return this.on_group_click(e);
  451. }
  452. },
  453. on_group_click: function (e) {
  454. if (e.group == -1) {
  455. return;
  456. }
  457. return this.do_action({
  458. type: 'ir.actions.act_window',
  459. res_model: this.fields[this.last_group_bys[0]].relation,
  460. res_id: e.group,
  461. target: 'new',
  462. views: [[false, 'form']]
  463. });
  464. },
  465. scale_current_window: function (factor) {
  466. if (this.timeline) {
  467. this.current_window = this.timeline.getWindow();
  468. this.current_window.end = moment(this.current_window.start).add(factor, 'hours');
  469. this.timeline.setWindow(this.current_window);
  470. }
  471. },
  472. on_today_clicked: function () {
  473. this.current_window = {
  474. start: new moment(),
  475. end: new moment().add(24, 'hours')
  476. };
  477. if (this.timeline) {
  478. this.timeline.setWindow(this.current_window);
  479. }
  480. },
  481. on_scale_day_clicked: function () {
  482. this.scale_current_window(24);
  483. },
  484. on_scale_week_clicked: function () {
  485. this.scale_current_window(24 * 7);
  486. },
  487. on_scale_month_clicked: function () {
  488. this.scale_current_window(24 * 30);
  489. },
  490. on_scale_year_clicked: function () {
  491. this.scale_current_window(24 * 365);
  492. }
  493. });
  494. core.view_registry.add('timeline', TimelineView);
  495. return TimelineView;
  496. });