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.

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