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.

445 lines
16 KiB

  1. /* Copyright 2015 Holger Brunn <hbrunn@therp.nl>
  2. * Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
  3. * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
  4. openerp.web_widget_x2many_2d_matrix = function(instance)
  5. {
  6. instance.web.form.widgets.add(
  7. 'x2many_2d_matrix',
  8. 'instance.web_widget_x2many_2d_matrix.FieldX2Many2dMatrix');
  9. instance.web_widget_x2many_2d_matrix.FieldX2Many2dMatrix = instance.web.form.FieldOne2Many.extend({
  10. template: 'FieldX2Many2dMatrix',
  11. widget_class: 'oe_form_field_x2many_2d_matrix',
  12. // those will be filled with rows from the dataset
  13. by_x_axis: {},
  14. by_y_axis: {},
  15. by_x_axis_sorted: [],
  16. by_y_axis_sorted: [],
  17. by_id: {},
  18. // configuration values
  19. field_x_axis: 'x',
  20. field_label_x_axis: 'x',
  21. field_y_axis: 'y',
  22. field_label_y_axis: 'y',
  23. field_value: 'value',
  24. // information about our datatype
  25. is_numeric: false,
  26. show_row_totals: true,
  27. show_column_totals: true,
  28. // this will be filled with the model's fields_get
  29. fields: {},
  30. // Store fields used to fill HTML attributes
  31. fields_att: {},
  32. x_axis_clickable: true,
  33. y_axis_clickable: true,
  34. // read parameters
  35. init: function(field_manager, node)
  36. {
  37. this.field_x_axis = node.attrs.field_x_axis || this.field_x_axis;
  38. this.field_y_axis = node.attrs.field_y_axis || this.field_y_axis;
  39. this.x_axis_clickable = node.attrs.x_axis_clickable || this.x_axis_clickable;
  40. this.y_axis_clickable = node.attrs.y_axis_clickable || this.y_axis_clickable;
  41. this.field_label_x_axis = node.attrs.field_label_x_axis || this.field_x_axis;
  42. this.field_label_y_axis = node.attrs.field_label_y_axis || this.field_y_axis;
  43. this.field_value = node.attrs.field_value || this.field_value;
  44. for (var property in node.attrs) {
  45. if (property.indexOf("field_att_") === 0) {
  46. this.fields_att[property.substring(10)] = node.attrs[property];
  47. }
  48. }
  49. this.field_editability = node.attrs.field_editability || this.field_editability;
  50. this.show_row_totals = node.attrs.show_row_totals || this.show_row_totals;
  51. this.show_column_totals = node.attrs.show_column_totals || this.show_column_totals;
  52. return this._super.apply(this, arguments);
  53. },
  54. // return a field's value, id in case it's a one2many field
  55. get_field_value: function(row, field, many2one_as_name)
  56. {
  57. if(this.fields[field].type == 'many2one' && _.isArray(row[field]))
  58. {
  59. if(many2one_as_name)
  60. {
  61. return row[field][1];
  62. }
  63. else
  64. {
  65. return row[field][0];
  66. }
  67. }
  68. return row[field];
  69. },
  70. // setup our datastructure for simple access in the template
  71. set_value: function()
  72. {
  73. var self = this,
  74. result = this._super.apply(this, arguments);
  75. self.by_x_axis_sorted = []
  76. self.by_y_axis_sorted = []
  77. self.by_x_axis = {};
  78. self.by_y_axis = {};
  79. self.by_id = {};
  80. return jQuery.when(result).then(function()
  81. {
  82. return self.dataset._model.call('fields_get').then(function(fields)
  83. {
  84. self.fields = fields;
  85. self.is_numeric = fields[self.field_value].type == 'float';
  86. self.show_row_totals &= self.is_numeric;
  87. self.show_column_totals &= self.is_numeric;
  88. self.x_axis_clickable &= self.is_numeric;
  89. self.y_axis_clickable &= self.is_numeric;
  90. })
  91. // if there are cached writes on the parent dataset, read below
  92. // only returns the written data, which is not enough to properly
  93. // set up our data structure. Read those ids here and patch the
  94. // cache
  95. .then(function()
  96. {
  97. var ids_written = _.map(
  98. self.dataset.to_write, function(x) { return x.id });
  99. if(!ids_written.length)
  100. {
  101. return;
  102. }
  103. return (new instance.web.Query(self.dataset._model))
  104. .filter([['id', 'in', ids_written]])
  105. .all()
  106. .then(function(rows)
  107. {
  108. _.each(rows, function(row)
  109. {
  110. var cache = _.find(
  111. self.dataset.cache,
  112. function(x) { return x.id == row.id }
  113. );
  114. _.extend(cache.values, row, _.clone(cache.values));
  115. })
  116. })
  117. })
  118. .then(function()
  119. {
  120. return self.dataset.read_ids(self.dataset.ids).then(function(rows)
  121. {
  122. var read_many2one = {},
  123. many2one_fields = [
  124. self.field_x_axis, self.field_y_axis,
  125. self.field_label_x_axis, self.field_label_y_axis
  126. ];
  127. // prepare to read many2one names if necessary (we can get (id, name) or just id as value)
  128. _.each(many2one_fields, function(field)
  129. {
  130. if(self.fields[field].type == 'many2one')
  131. {
  132. read_many2one[field] = {};
  133. }
  134. });
  135. // setup data structure
  136. _.each(rows, function(row)
  137. {
  138. self.add_xy_row(row);
  139. _.each(read_many2one, function(rows, field)
  140. {
  141. if(!_.isArray(row[field]))
  142. {
  143. rows[row[field]] = rows[row[field]] || []
  144. rows[row[field]].push(row);
  145. }
  146. });
  147. });
  148. // read many2one fields if necessary
  149. var deferrends = [];
  150. _.each(read_many2one, function(rows, field)
  151. {
  152. if(_.isEmpty(rows))
  153. {
  154. return;
  155. }
  156. var model = new instance.web.Model(self.fields[field].relation);
  157. deferrends.push(model.call(
  158. 'name_get',
  159. [_.map(_.keys(rows), function(key) {return parseInt(key)})])
  160. .then(function(names)
  161. {
  162. _.each(names, function(name)
  163. {
  164. _.each(rows[name[0]], function(row)
  165. {
  166. row[field] = name;
  167. });
  168. });
  169. }));
  170. })
  171. if(self.is_started && !self.no_rerender)
  172. {
  173. self.renderElement();
  174. self.compute_totals();
  175. self.setup_many2one_axes();
  176. self.$el.find('.edit').on(
  177. 'change', self.proxy(self.xy_value_change));
  178. self.effective_readonly_change();
  179. }
  180. return jQuery.when.apply(jQuery, deferrends);
  181. });
  182. });
  183. });
  184. },
  185. // do whatever needed to setup internal data structure
  186. add_xy_row: function(row)
  187. {
  188. var x = this.get_field_value(row, this.field_x_axis),
  189. y = this.get_field_value(row, this.field_y_axis);
  190. // row is a *copy* of a row in dataset.cache, fetch
  191. // a reference to this row in order to have the
  192. // internal data structure point to the same data
  193. // the dataset manipulates
  194. _.every(this.dataset.cache, function(cached_row)
  195. {
  196. if(cached_row.id == row.id)
  197. {
  198. row = cached_row.values;
  199. // new rows don't have that
  200. row.id = cached_row.id;
  201. return false;
  202. }
  203. return true;
  204. });
  205. if(this.by_x_axis_sorted.indexOf(x) == -1)
  206. this.by_x_axis_sorted.push(x)
  207. if(this.by_y_axis_sorted.indexOf(y) == -1)
  208. this.by_y_axis_sorted.push(y)
  209. this.by_x_axis[x] = this.by_x_axis[x] || {};
  210. this.by_y_axis[y] = this.by_y_axis[y] || {};
  211. this.by_x_axis[x][y] = row;
  212. this.by_y_axis[y][x] = row;
  213. this.by_id[row.id] = row;
  214. },
  215. // get x axis values in the correct order
  216. get_x_axis_values: function()
  217. {
  218. return this.by_x_axis_sorted
  219. },
  220. // get y axis values in the correct order
  221. get_y_axis_values: function()
  222. {
  223. return this.by_y_axis_sorted
  224. },
  225. // get the label for a value on the x axis
  226. get_x_axis_label: function(x)
  227. {
  228. return this.get_field_value(
  229. _.first(_.values(this.by_x_axis[x])),
  230. this.field_label_x_axis, true);
  231. },
  232. // get the label for a value on the y axis
  233. get_y_axis_label: function(y)
  234. {
  235. return this.get_field_value(
  236. _.first(_.values(this.by_y_axis[y])),
  237. this.field_label_y_axis, true);
  238. },
  239. // return the class(es) the inputs should have
  240. get_xy_value_class: function()
  241. {
  242. var classes = 'oe_form_field oe_form_required';
  243. if(this.is_numeric)
  244. {
  245. classes += ' oe_form_field_float';
  246. }
  247. return classes;
  248. },
  249. // return row id of a coordinate
  250. get_xy_id: function(x, y)
  251. {
  252. return this.by_x_axis[x][y]['id'];
  253. },
  254. get_xy_att: function(x, y)
  255. {
  256. var vals = {};
  257. for (var att in this.fields_att) {
  258. var val = this.get_field_value(
  259. this.by_x_axis[x][y], this.fields_att[att]);
  260. // Discard empty values
  261. if (val) {
  262. vals[att] = val;
  263. }
  264. }
  265. return vals;
  266. },
  267. // return the value of a coordinate
  268. get_xy_value: function(x, y)
  269. {
  270. return this.get_field_value(
  271. this.by_x_axis[x][y], this.field_value);
  272. },
  273. // validate a value
  274. validate_xy_value: function(val)
  275. {
  276. try
  277. {
  278. this.parse_xy_value(val);
  279. }
  280. catch(e)
  281. {
  282. return false;
  283. }
  284. return true;
  285. },
  286. // parse a value from user input
  287. parse_xy_value: function(val)
  288. {
  289. return instance.web.parse_value(
  290. val, {'type': this.fields[this.field_value].type});
  291. },
  292. // format a value from the database for display
  293. format_xy_value: function(val)
  294. {
  295. return instance.web.format_value(
  296. val, {'type': this.fields[this.field_value].type});
  297. },
  298. // compute totals
  299. compute_totals: function()
  300. {
  301. var self = this,
  302. grand_total = 0,
  303. totals_x = {},
  304. totals_y = {},
  305. rows = this.by_id,
  306. deferred = jQuery.Deferred();
  307. _.each(rows, function(row)
  308. {
  309. var key_x = self.get_field_value(row, self.field_x_axis),
  310. key_y = self.get_field_value(row, self.field_y_axis);
  311. totals_x[key_x] = (totals_x[key_x] || 0) + self.get_field_value(row, self.field_value);
  312. totals_y[key_y] = (totals_y[key_y] || 0) + self.get_field_value(row, self.field_value);
  313. grand_total += self.get_field_value(row, self.field_value);
  314. });
  315. _.each(totals_y, function(total, y)
  316. {
  317. self.$el.find(
  318. _.str.sprintf('td.row_total[data-y="%s"]', y)).text(
  319. self.format_xy_value(total));
  320. });
  321. _.each(totals_x, function(total, x)
  322. {
  323. self.$el.find(
  324. _.str.sprintf('td.column_total[data-x="%s"]', x)).text(
  325. self.format_xy_value(total));
  326. });
  327. self.$el.find('.grand_total').text(
  328. self.format_xy_value(grand_total))
  329. deferred.resolve({
  330. totals_x: totals_x,
  331. totals_y: totals_y,
  332. grand_total: grand_total,
  333. rows: rows,
  334. });
  335. return deferred;
  336. },
  337. setup_many2one_axes: function()
  338. {
  339. if(this.fields[this.field_x_axis].type == 'many2one' && this.x_axis_clickable)
  340. {
  341. this.$el.find('th[data-x]').addClass('oe_link')
  342. .click(_.partial(
  343. this.proxy(this.many2one_axis_click),
  344. this.field_x_axis, 'x'));
  345. }
  346. if(this.fields[this.field_y_axis].type == 'many2one' && this.y_axis_clickable)
  347. {
  348. this.$el.find('tr[data-y] th').addClass('oe_link')
  349. .click(_.partial(
  350. this.proxy(this.many2one_axis_click),
  351. this.field_y_axis, 'y'));
  352. }
  353. },
  354. many2one_axis_click: function(field, id_attribute, e)
  355. {
  356. this.do_action({
  357. type: 'ir.actions.act_window',
  358. name: this.fields[field].string,
  359. res_model: this.fields[field].relation,
  360. res_id: jQuery(e.currentTarget).data(id_attribute),
  361. views: [[false, 'form']],
  362. target: 'current',
  363. })
  364. },
  365. start: function()
  366. {
  367. var self = this;
  368. this.$el.find('.edit').on(
  369. 'change', self.proxy(this.xy_value_change));
  370. this.compute_totals();
  371. this.setup_many2one_axes();
  372. this.on("change:effective_readonly",
  373. this, this.proxy(this.effective_readonly_change));
  374. this.effective_readonly_change();
  375. return this._super.apply(this, arguments);
  376. },
  377. xy_value_change: function(e)
  378. {
  379. var $this = jQuery(e.currentTarget),
  380. val = $this.val();
  381. if(this.validate_xy_value(val))
  382. {
  383. var data = {}, value = this.parse_xy_value(val);
  384. data[this.field_value] = value;
  385. $this.siblings('.read').text(this.format_xy_value(value));
  386. $this.val(this.format_xy_value(value));
  387. this.dataset.write($this.data('id'), data);
  388. $this.parent().removeClass('oe_form_invalid');
  389. this.compute_totals();
  390. }
  391. else
  392. {
  393. $this.parent().addClass('oe_form_invalid');
  394. }
  395. },
  396. effective_readonly_change: function()
  397. {
  398. this.$el
  399. .find('tbody td.oe_list_field_cell span.oe_form_field .edit')
  400. .toggle(!this.get('effective_readonly'));
  401. this.$el
  402. .find('tbody td.oe_list_field_cell span.oe_form_field .read')
  403. .toggle(this.get('effective_readonly'));
  404. this.$el.find('.edit').first().focus();
  405. },
  406. is_syntax_valid: function()
  407. {
  408. return this.$el.find('.oe_form_invalid').length == 0;
  409. },
  410. // deactivate view related functions
  411. load_views: function() {},
  412. reload_current_view: function() {},
  413. get_active_view: function() {},
  414. });
  415. }