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
18 KiB

  1. odoo.define('web_timeline.TimelineRenderer', function (require) {
  2. "use strict";
  3. var AbstractRenderer = require('web.AbstractRenderer');
  4. var core = require('web.core');
  5. var time = require('web.time');
  6. var utils = require('web.utils');
  7. var session = require('web.session');
  8. var QWeb = require('web.QWeb');
  9. var field_utils = require('web.field_utils');
  10. var TimelineCanvas = require('web_timeline.TimelineCanvas');
  11. var _t = core._t;
  12. var TimelineRenderer = AbstractRenderer.extend({
  13. template: "TimelineView",
  14. events: _.extend({}, AbstractRenderer.prototype.events, {
  15. 'click .oe_timeline_button_today': '_onTodayClicked',
  16. 'click .oe_timeline_button_scale_day': '_onScaleDayClicked',
  17. 'click .oe_timeline_button_scale_week': '_onScaleWeekClicked',
  18. 'click .oe_timeline_button_scale_month': '_onScaleMonthClicked',
  19. 'click .oe_timeline_button_scale_year': '_onScaleYearClicked',
  20. }),
  21. /**
  22. * @constructor
  23. */
  24. init: function (parent, state, params) {
  25. this._super.apply(this, arguments);
  26. this.modelName = params.model;
  27. this.mode = params.mode;
  28. this.options = params.options;
  29. this.permissions = params.permissions;
  30. this.timeline = params.timeline;
  31. this.min_height = params.min_height;
  32. this.date_start = params.date_start;
  33. this.date_stop = params.date_stop;
  34. this.date_delay = params.date_delay;
  35. this.colors = params.colors;
  36. this.fieldNames = params.fieldNames;
  37. this.dependency_arrow = params.dependency_arrow;
  38. this.view = params.view;
  39. this.modelClass = this.view.model;
  40. },
  41. /**
  42. * @override
  43. */
  44. start: function () {
  45. var self = this;
  46. var attrs = this.arch.attrs;
  47. this.current_window = {
  48. start: new moment(),
  49. end: new moment().add(24, 'hours')
  50. };
  51. this.$el.addClass(attrs.class);
  52. this.$timeline = this.$el.find(".oe_timeline_widget");
  53. if (!this.date_start) {
  54. throw new Error(_t("Timeline view has not defined 'date_start' attribute."));
  55. }
  56. this._super.apply(this, self);
  57. },
  58. /**
  59. * Triggered when the timeline is attached to the DOM.
  60. */
  61. on_attach_callback: function() {
  62. var height = this.$el.parent().height() - this.$el.find('.oe_timeline_buttons').height();
  63. if (height > this.min_height) {
  64. this.timeline.setOptions({
  65. height: height
  66. });
  67. }
  68. },
  69. /**
  70. * @override
  71. */
  72. _render: function () {
  73. var self = this;
  74. return $.when().then(function () {
  75. // Prevent Double Rendering on Updates
  76. if (!self.timeline) {
  77. self.init_timeline();
  78. $(window).trigger('resize');
  79. }
  80. });
  81. },
  82. /**
  83. * Set the timeline window to today (day).
  84. *
  85. * @private
  86. */
  87. _onTodayClicked: function () {
  88. this.current_window = {
  89. start: new moment(),
  90. end: new moment().add(24, 'hours')
  91. };
  92. if (this.timeline) {
  93. this.timeline.setWindow(this.current_window);
  94. }
  95. },
  96. /**
  97. * Scale the timeline window to a day.
  98. *
  99. * @private
  100. */
  101. _onScaleDayClicked: function () {
  102. this._scaleCurrentWindow(24);
  103. },
  104. /**
  105. * Scale the timeline window to a week.
  106. *
  107. * @private
  108. */
  109. _onScaleWeekClicked: function () {
  110. this._scaleCurrentWindow(24 * 7);
  111. },
  112. /**
  113. * Scale the timeline window to a month.
  114. *
  115. * @private
  116. */
  117. _onScaleMonthClicked: function () {
  118. this._scaleCurrentWindow(24 * 30);
  119. },
  120. /**
  121. * Scale the timeline window to a year.
  122. *
  123. * @private
  124. */
  125. _onScaleYearClicked: function () {
  126. this._scaleCurrentWindow(24 * 365);
  127. },
  128. /**
  129. * Scales the timeline window based on the current window.
  130. *
  131. * @param {Integer} factor The timespan (in hours) the window must be scaled to.
  132. * @private
  133. */
  134. _scaleCurrentWindow: function (factor) {
  135. if (this.timeline) {
  136. this.current_window = this.timeline.getWindow();
  137. this.current_window.end = moment(this.current_window.start).add(factor, 'hours');
  138. this.timeline.setWindow(this.current_window);
  139. }
  140. },
  141. /**
  142. * Computes the initial visible window.
  143. *
  144. * @private
  145. */
  146. _computeMode: function () {
  147. if (this.mode) {
  148. var start = false, end = false;
  149. switch (this.mode) {
  150. case 'day':
  151. start = new moment().startOf('day');
  152. end = new moment().endOf('day');
  153. break;
  154. case 'week':
  155. start = new moment().startOf('week');
  156. end = new moment().endOf('week');
  157. break;
  158. case 'month':
  159. start = new moment().startOf('month');
  160. end = new moment().endOf('month');
  161. break;
  162. }
  163. if (end && start) {
  164. this.options.start = start;
  165. this.options.end = end;
  166. } else {
  167. this.mode = 'fit';
  168. }
  169. }
  170. },
  171. /**
  172. * Initializes the timeline (http://visjs.org/docs/timeline/).
  173. *
  174. * @private
  175. */
  176. init_timeline: function () {
  177. var self = this;
  178. this._computeMode();
  179. this.options.editable = {
  180. // add new items by double tapping
  181. add: this.modelClass.data.rights.create,
  182. // drag items horizontally
  183. updateTime: this.modelClass.data.rights.write,
  184. // drag items from one group to another
  185. updateGroup: this.modelClass.data.rights.write,
  186. // delete an item by tapping the delete button top right
  187. remove: this.modelClass.data.rights.unlink,
  188. };
  189. $.extend(this.options, {
  190. onAdd: self.on_add,
  191. onMove: self.on_move,
  192. onUpdate: self.on_update,
  193. onRemove: self.on_remove,
  194. horizontalScroll: false,
  195. verticalScroll:true,
  196. zoomKey: 'ctrlKey',
  197. });
  198. this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
  199. if (this.arch.children.length) {
  200. var tmpl = utils.json_node_to_xml(
  201. _.filter(this.arch.children, function(item) {
  202. return item.tag === 'templates';
  203. })[0]
  204. );
  205. this.qweb.add_template(tmpl);
  206. }
  207. this.timeline = new vis.Timeline(self.$timeline.empty().get(0));
  208. $(this.timeline.dom.leftContainer).scroll(function() {
  209. var hei = ($('.vis-foreground .vis-group:first-child')[0].style.height).split("px");
  210. var itemset_height = ($('.vis-itemset')[0].style.height).split("px");
  211. var new_height = parseInt(itemset_height[0]) - (parseInt(hei[0]) - 100)
  212. $('.vis-itemset').css('height', new_height)
  213. });
  214. this.timeline.setOptions(this.options);
  215. if (self.mode && self['on_scale_' + self.mode + '_clicked']) {
  216. self['on_scale_' + self.mode + '_clicked']();
  217. }
  218. this.timeline.on('click', self.on_group_click);
  219. var group_bys = this.arch.attrs.default_group_by.split(',');
  220. this.last_group_bys = group_bys;
  221. this.last_domains = this.modelClass.data.domain;
  222. this.on_data_loaded(this.modelClass.data.data, group_bys);
  223. this.$centerContainer = $(this.timeline.dom.centerContainer);
  224. this.canvas = new TimelineCanvas(this);
  225. this.canvas.appendTo(this.$centerContainer);
  226. this.timeline.on('changed', function() {
  227. self.draw_canvas();
  228. self.canvas.$el.attr(
  229. 'style',
  230. self.$el.find('.vis-content').attr('style') + self.$el.find('.vis-itemset').attr('style')
  231. );
  232. });
  233. },
  234. /**
  235. * Clears and draws the canvas items.
  236. *
  237. * @private
  238. */
  239. draw_canvas: function () {
  240. this.canvas.clear();
  241. if (this.dependency_arrow) {
  242. this.draw_dependencies();
  243. }
  244. },
  245. /**
  246. * Draw item dependencies on canvas.
  247. *
  248. * @private
  249. */
  250. draw_dependencies: function () {
  251. var self = this;
  252. var items = this.timeline.itemSet.items;
  253. _.each(items, function(item) {
  254. if (!item.data.evt) {
  255. return;
  256. }
  257. _.each(item.data.evt[self.dependency_arrow], function(id) {
  258. if (id in items) {
  259. self.draw_dependency(item, items[id]);
  260. }
  261. });
  262. });
  263. },
  264. /**
  265. * Draws a dependency arrow between 2 timeline items.
  266. *
  267. * @param {Object} from Start timeline item
  268. * @param {Object} to Destination timeline item
  269. * @param {Object} options
  270. * @param {Object} options.line_color Color of the line
  271. * @param {Object} options.line_width The width of the line
  272. * @private
  273. */
  274. draw_dependency: function (from, to, options) {
  275. if (!from.displayed || !to.displayed) {
  276. return;
  277. }
  278. var defaults = _.defaults({}, options, {
  279. line_color: 'black',
  280. line_width: 1
  281. });
  282. this.canvas.draw_arrow(from.dom.box, to.dom.box, defaults.line_color, defaults.line_width);
  283. },
  284. /**
  285. * Load display_name of records.
  286. *
  287. * @private
  288. * @returns {jQuery.Deferred}
  289. */
  290. on_data_loaded: function (events, group_bys, adjust_window) {
  291. var self = this;
  292. var ids = _.pluck(events, "id");
  293. return this._rpc({
  294. model: this.modelName,
  295. method: 'name_get',
  296. args: [
  297. ids,
  298. ],
  299. context: this.getSession().user_context,
  300. }).then(function(names) {
  301. var nevents = _.map(events, function (event) {
  302. return _.extend({
  303. __name: _.detect(names, function (name) {
  304. return name[0] === event.id;
  305. })[1]
  306. }, event);
  307. });
  308. return self.on_data_loaded_2(nevents, group_bys, adjust_window);
  309. });
  310. },
  311. /**
  312. * Set groups and events.
  313. *
  314. * @private
  315. */
  316. on_data_loaded_2: function (events, group_bys, adjust_window) {
  317. var self = this;
  318. var data = [];
  319. var groups = [];
  320. this.grouped_by = group_bys;
  321. _.each(events, function (event) {
  322. if (event[self.date_start]) {
  323. data.push(self.event_data_transform(event));
  324. }
  325. });
  326. groups = this.split_groups(events, group_bys);
  327. this.timeline.setGroups(groups);
  328. this.timeline.setItems(data);
  329. var mode = !this.mode || this.mode === 'fit';
  330. var adjust = _.isUndefined(adjust_window) || adjust_window;
  331. if (mode && adjust) {
  332. this.timeline.fit();
  333. }
  334. },
  335. /**
  336. * Get the groups.
  337. *
  338. * @private
  339. * @returns {Array}
  340. */
  341. split_groups: function (events, group_bys) {
  342. if (group_bys.length === 0) {
  343. return events;
  344. }
  345. var groups = [];
  346. groups.push({id: -1, content: _t('-')});
  347. _.each(events, function (event) {
  348. var group_name = event[_.first(group_bys)];
  349. if (group_name) {
  350. if (group_name instanceof Array) {
  351. var group = _.find(groups, function (existing_group) {
  352. return _.isEqual(existing_group.id, group_name[0]);
  353. });
  354. if (_.isUndefined(group)) {
  355. group = {
  356. id: group_name[0],
  357. content: group_name[1]
  358. };
  359. groups.push(group);
  360. }
  361. }
  362. }
  363. });
  364. return groups;
  365. },
  366. /**
  367. * Transform Odoo event object to timeline event object.
  368. *
  369. * @private
  370. * @returns {Object}
  371. */
  372. event_data_transform: function (evt) {
  373. var self = this;
  374. var date_start = new moment();
  375. var date_stop = null;
  376. var date_delay = evt[this.date_delay] || false,
  377. all_day = this.all_day ? evt[this.all_day] : false;
  378. if (all_day) {
  379. date_start = time.auto_str_to_date(evt[this.date_start].split(' ')[0], 'start');
  380. if (this.no_period) {
  381. date_stop = date_start;
  382. } else {
  383. date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop].split(' ')[0], 'stop') : null;
  384. }
  385. } else {
  386. date_start = time.auto_str_to_date(evt[this.date_start]);
  387. date_stop = this.date_stop ? time.auto_str_to_date(evt[this.date_stop]) : null;
  388. }
  389. if (!date_stop && date_delay) {
  390. date_stop = moment(date_start).add(date_delay, 'hours').toDate();
  391. }
  392. var group = evt[self.last_group_bys[0]];
  393. if (group && group instanceof Array) {
  394. group = _.first(group);
  395. } else {
  396. group = -1;
  397. }
  398. _.each(self.colors, function (color) {
  399. if (eval("'" + evt[color.field] + "' " + color.opt + " '" + color.value + "'")) {
  400. self.color = color.color;
  401. }
  402. });
  403. var content = _.isUndefined(evt.__name) ? evt.display_name : evt.__name;
  404. if (this.arch.children.length) {
  405. content = this.render_timeline_item(evt);
  406. }
  407. var r = {
  408. 'start': date_start,
  409. 'content': content,
  410. 'id': evt.id,
  411. 'group': group,
  412. 'evt': evt,
  413. 'style': 'background-color: ' + self.color + ';'
  414. };
  415. // Check if the event is instantaneous, if so, display it with a point on the timeline (no 'end')
  416. if (date_stop && !moment(date_start).isSame(date_stop)) {
  417. r.end = date_stop;
  418. }
  419. self.color = null;
  420. return r;
  421. },
  422. /**
  423. * Render timeline item template.
  424. *
  425. * @param {Object} evt Record
  426. * @private
  427. * @returns {String} Rendered template
  428. */
  429. render_timeline_item: function (evt) {
  430. if(this.qweb.has_template('timeline-item')) {
  431. return this.qweb.render('timeline-item', {
  432. 'record': evt,
  433. 'field_utils': field_utils
  434. });
  435. }
  436. console.error(
  437. _t('Template "timeline-item" not present in timeline view definition.')
  438. );
  439. },
  440. /**
  441. * Handle a click on a group header.
  442. *
  443. * @private
  444. */
  445. on_group_click: function (e) {
  446. if (e.what === 'group-label' && e.group !== -1) {
  447. this._trigger(e, function() {
  448. // Do nothing
  449. }, 'onGroupClick');
  450. }
  451. },
  452. /**
  453. * Trigger onUpdate.
  454. *
  455. * @private
  456. */
  457. on_update: function (item, callback) {
  458. this._trigger(item, callback, 'onUpdate');
  459. },
  460. /**
  461. * Trigger onMove.
  462. *
  463. * @private
  464. */
  465. on_move: function (item, callback) {
  466. this._trigger(item, callback, 'onMove');
  467. },
  468. /**
  469. * Trigger onRemove.
  470. *
  471. * @private
  472. */
  473. on_remove: function (item, callback) {
  474. this._trigger(item, callback, 'onRemove');
  475. },
  476. /**
  477. * Trigger onAdd.
  478. *
  479. * @private
  480. */
  481. on_add: function (item, callback) {
  482. this._trigger(item, callback, 'onAdd');
  483. },
  484. /**
  485. * trigger_up encapsulation adds by default the rights, and the renderer.
  486. *
  487. * @private
  488. */
  489. _trigger: function (item, callback, trigger) {
  490. this.trigger_up(trigger, {
  491. 'item': item,
  492. 'callback': callback,
  493. 'rights': this.modelClass.data.rights,
  494. 'renderer': this,
  495. });
  496. },
  497. });
  498. return TimelineRenderer;
  499. });