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.

454 lines
17 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. row_xy_exists: function(x, y)
  268. {
  269. if( x in this.by_x_axis && y in this.by_x_axis[x] ) {
  270. return true;
  271. }
  272. return false;
  273. },
  274. // return the value of a coordinate
  275. get_xy_value: function(x, y)
  276. {
  277. return this.get_field_value(
  278. this.by_x_axis[x][y], this.field_value);
  279. },
  280. // validate a value
  281. validate_xy_value: function(val)
  282. {
  283. try
  284. {
  285. this.parse_xy_value(val);
  286. }
  287. catch(e)
  288. {
  289. return false;
  290. }
  291. return true;
  292. },
  293. // parse a value from user input
  294. parse_xy_value: function(val)
  295. {
  296. return instance.web.parse_value(
  297. val, {'type': this.fields[this.field_value].type});
  298. },
  299. // format a value from the database for display
  300. format_xy_value: function(val)
  301. {
  302. return instance.web.format_value(
  303. val, {'type': this.fields[this.field_value].type});
  304. },
  305. // compute totals
  306. compute_totals: function()
  307. {
  308. var self = this,
  309. grand_total = 0,
  310. totals_x = {},
  311. totals_y = {},
  312. rows = this.by_id,
  313. deferred = jQuery.Deferred();
  314. _.each(rows, function(row)
  315. {
  316. var key_x = self.get_field_value(row, self.field_x_axis),
  317. key_y = self.get_field_value(row, self.field_y_axis);
  318. totals_x[key_x] = (totals_x[key_x] || 0) + self.get_field_value(row, self.field_value);
  319. totals_y[key_y] = (totals_y[key_y] || 0) + self.get_field_value(row, self.field_value);
  320. grand_total += self.get_field_value(row, self.field_value);
  321. });
  322. _.each(totals_y, function(total, y)
  323. {
  324. self.$el.find(
  325. _.str.sprintf('td.row_total[data-y="%s"]', y)).text(
  326. self.format_xy_value(total));
  327. });
  328. _.each(totals_x, function(total, x)
  329. {
  330. self.$el.find(
  331. _.str.sprintf('td.column_total[data-x="%s"]', x)).text(
  332. self.format_xy_value(total));
  333. });
  334. self.$el.find('.grand_total').text(
  335. self.format_xy_value(grand_total))
  336. deferred.resolve({
  337. totals_x: totals_x,
  338. totals_y: totals_y,
  339. grand_total: grand_total,
  340. rows: rows,
  341. });
  342. return deferred;
  343. },
  344. setup_many2one_axes: function()
  345. {
  346. if(this.fields[this.field_x_axis].type == 'many2one' && this.x_axis_clickable)
  347. {
  348. this.$el.find('th[data-x]').addClass('oe_link')
  349. .click(_.partial(
  350. this.proxy(this.many2one_axis_click),
  351. this.field_x_axis, 'x'));
  352. }
  353. if(this.fields[this.field_y_axis].type == 'many2one' && this.y_axis_clickable)
  354. {
  355. this.$el.find('tr[data-y] th').addClass('oe_link')
  356. .click(_.partial(
  357. this.proxy(this.many2one_axis_click),
  358. this.field_y_axis, 'y'));
  359. }
  360. },
  361. many2one_axis_click: function(field, id_attribute, e)
  362. {
  363. this.do_action({
  364. type: 'ir.actions.act_window',
  365. name: this.fields[field].string,
  366. res_model: this.fields[field].relation,
  367. res_id: jQuery(e.currentTarget).data(id_attribute),
  368. views: [[false, 'form']],
  369. target: 'current',
  370. })
  371. },
  372. start: function()
  373. {
  374. var self = this;
  375. this.$el.find('.edit').on(
  376. 'change', self.proxy(this.xy_value_change));
  377. this.compute_totals();
  378. this.setup_many2one_axes();
  379. this.on("change:effective_readonly",
  380. this, this.proxy(this.effective_readonly_change));
  381. this.effective_readonly_change();
  382. return this._super.apply(this, arguments);
  383. },
  384. xy_value_change: function(e)
  385. {
  386. var $this = jQuery(e.currentTarget),
  387. val = $this.val();
  388. if(this.validate_xy_value(val))
  389. {
  390. var data = {}, value = this.parse_xy_value(val);
  391. data[this.field_value] = value;
  392. $this.siblings('.read').text(this.format_xy_value(value));
  393. $this.val(this.format_xy_value(value));
  394. this.dataset.write($this.data('id'), data);
  395. $this.parent().removeClass('oe_form_invalid');
  396. this.compute_totals();
  397. }
  398. else
  399. {
  400. $this.parent().addClass('oe_form_invalid');
  401. }
  402. },
  403. effective_readonly_change: function()
  404. {
  405. this.$el
  406. .find('tbody td.oe_list_field_cell span.oe_form_field .edit')
  407. .toggle(!this.get('effective_readonly'));
  408. this.$el
  409. .find('tbody td.oe_list_field_cell span.oe_form_field .read')
  410. .toggle(this.get('effective_readonly'));
  411. this.$el.find('.edit').first().focus();
  412. },
  413. is_syntax_valid: function()
  414. {
  415. return this.$el.find('.oe_form_invalid').length == 0;
  416. },
  417. // deactivate view related functions
  418. load_views: function() {},
  419. reload_current_view: function() {},
  420. get_active_view: function() {},
  421. });
  422. }