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.

293 lines
9.9 KiB

  1. /* Copyright 2015 Therp BV <http://therp.nl>
  2. * Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
  3. * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
  4. odoo.define("web_advanced_search", function (require) {
  5. "use strict";
  6. var core = require("web.core");
  7. var Domain = require("web.Domain");
  8. var DomainSelectorDialog = require("web.DomainSelectorDialog");
  9. var field_registry = require("web.field_registry");
  10. var FieldManagerMixin = require("web.FieldManagerMixin");
  11. var FilterMenu = require("web.FilterMenu");
  12. var human_domain = require("web_advanced_search.human_domain");
  13. var SearchView = require("web.SearchView");
  14. var Widget = require("web.Widget");
  15. var Char = core.search_filters_registry.get("char");
  16. SearchView.include({
  17. custom_events: _.extend({}, SearchView.prototype.custom_events, {
  18. "get_dataset": "_on_get_dataset",
  19. }),
  20. /**
  21. * Add or update a `dataset` attribute in event target
  22. *
  23. * The search view dataset includes things such as the model, which
  24. * is required to make some parts of search views smarter.
  25. *
  26. * @param {OdooEvent} event The target will get the dataset.
  27. */
  28. _on_get_dataset: function (event) {
  29. event.target.dataset = this.dataset;
  30. event.stopPropagation();
  31. },
  32. });
  33. /**
  34. * An almost dummy search proposition, to use with domain widget
  35. */
  36. var AdvancedSearchProposition = Widget.extend({
  37. init: function (parent, model, domain) {
  38. this._super(parent);
  39. this.model = model;
  40. this.domain = new Domain(domain);
  41. },
  42. get_filter: function () {
  43. var domain_array = this.domain.toArray();
  44. return {
  45. attrs: {
  46. domain: domain_array,
  47. // TODO Remove when merged
  48. // https://github.com/odoo/odoo/pull/25922
  49. string: human_domain.getHumanDomain(
  50. this,
  51. this.model,
  52. domain_array
  53. ),
  54. },
  55. children: [],
  56. tag: "filter",
  57. };
  58. },
  59. });
  60. // Add advanced search features
  61. FilterMenu.include({
  62. custom_events: _.extend({}, FilterMenu.prototype.custom_events, {
  63. "domain_selected": "advanced_search_commit",
  64. }),
  65. events: _.extend({}, FilterMenu.prototype.events, {
  66. "click .o_add_advanced_search": "advanced_search_open",
  67. }),
  68. init: function () {
  69. this._super.apply(this, arguments);
  70. this.trigger_up("get_dataset");
  71. },
  72. /**
  73. * Open advanced search dialog
  74. *
  75. * @returns {$.Deferred} The opening dialog itself.
  76. */
  77. advanced_search_open: function () {
  78. var domain_selector_dialog = new DomainSelectorDialog(
  79. this,
  80. this.dataset.model,
  81. "[]",
  82. {
  83. debugMode: core.debug,
  84. readonly: false,
  85. }
  86. );
  87. // Add 1st domain node by default
  88. domain_selector_dialog.domainSelector._onAddFirstButtonClick();
  89. return domain_selector_dialog.open();
  90. },
  91. /**
  92. * Apply advanced search on dialog save
  93. *
  94. * @param {OdooEvent} event A `domain_selected` event from the dialog.
  95. */
  96. advanced_search_commit: function (event) {
  97. _.invoke(this.propositions, "destroy");
  98. var proposition = new AdvancedSearchProposition(
  99. this,
  100. this.dataset.model,
  101. event.data.domain
  102. );
  103. this.propositions = [proposition];
  104. this.commit_search();
  105. },
  106. });
  107. /**
  108. * A search field for relational fields.
  109. *
  110. * It implements and extends the `FieldManagerMixin`, and acts as if it
  111. * were a reduced dummy controller. Some actions "mock" the underlying
  112. * model, since sometimes we use a char widget to fill related fields
  113. * (which is not supported by that widget), and fields need an underlying
  114. * model implementation, which can only hold fake data, given a search view
  115. * has no data on it by definition.
  116. */
  117. var Relational = Char.extend(FieldManagerMixin, {
  118. tagName: "div",
  119. className: "x2x_container",
  120. attributes: {},
  121. init: function () {
  122. this._super.apply(this, arguments);
  123. // To make widgets work, we need a model and an empty record
  124. FieldManagerMixin.init.call(this);
  125. this.trigger_up("get_dataset");
  126. // Make equal and not equal appear 1st and 2nd
  127. this.operators = _.sortBy(
  128. this.operators,
  129. function(op) {
  130. switch(op.value) {
  131. case "=":
  132. return -2;
  133. case "!=":
  134. return -1;
  135. default:
  136. return 0;
  137. }
  138. });
  139. // Create dummy record with only the field the user is searching
  140. var params = {
  141. fieldNames: [this.field.name],
  142. modelName: this.dataset.model,
  143. context: this.dataset.context,
  144. // res_id: "virtual_0",
  145. fields: {},
  146. type: "record",
  147. viewType: "default",
  148. fieldsInfo: {
  149. default: {},
  150. },
  151. };
  152. // See https://stackoverflow.com/a/11508530/1468388
  153. params.fields[this.field.name] = _.omit(this.field, "onChange");
  154. params.fieldsInfo.default[this.field.name] = {};
  155. // Emulate `model.load()`, without RPC-calling `default_get()`
  156. this.datapoint_id = this.model._makeDataPoint(params).id;
  157. this.model.applyDefaultValues(
  158. this.datapoint_id,
  159. {},
  160. params.fieldNames
  161. );
  162. // To generate a new fake ID
  163. this._fake_id = -1;
  164. },
  165. start: function () {
  166. var result = this._super.apply(this, arguments);
  167. // Render the initial widget
  168. result.done($.proxy(this, "show_inputs", $("<input value='='/>")));
  169. return result;
  170. },
  171. destroy: function () {
  172. if (this._field_widget) {
  173. this._field_widget.destroy();
  174. }
  175. this.model.destroy();
  176. delete this.record;
  177. return this._super.apply(this, arguments);
  178. },
  179. _get_record: function () {
  180. return this.model.get(this.datapoint_id);
  181. },
  182. show_inputs: function ($operator) {
  183. // Get widget class to be used
  184. switch ($operator.val()) {
  185. case "=":
  186. case "!=":
  187. this._field_widget_name = "many2one";
  188. break;
  189. default:
  190. this._field_widget_name = "char";
  191. }
  192. var _Widget = field_registry.get(this._field_widget_name);
  193. // Destroy previous widget, if any
  194. if (this._field_widget) {
  195. this._field_widget.destroy();
  196. delete this._field_widget;
  197. }
  198. // Create new widget
  199. var options = {
  200. mode: "edit",
  201. attrs: {
  202. options: {
  203. no_create_edit: true,
  204. no_create: true,
  205. no_open: true,
  206. no_quick_create: true,
  207. },
  208. },
  209. };
  210. this._field_widget = new _Widget(
  211. this,
  212. this.field.name,
  213. this._get_record(),
  214. options
  215. );
  216. this._field_widget.appendTo(this.$el);
  217. return this._super.apply(this, arguments);
  218. },
  219. _applyChanges: function (dataPointID, changes, event) {
  220. // Make char updates look like valid x2one updates
  221. if (_.isNaN(changes[this.field.name].id)) {
  222. changes[this.field.name] = {
  223. id: this._fake_id--,
  224. display_name: event.target.lastSetValue,
  225. };
  226. }
  227. return FieldManagerMixin._applyChanges.apply(this, arguments);
  228. },
  229. _confirmChange: function (id, fields, event) {
  230. this.datapoint_id = id;
  231. return this._field_widget.reset(this._get_record(), event);
  232. },
  233. get_value: function () {
  234. try {
  235. switch (this._field_widget_name) {
  236. case "many2one":
  237. return this._field_widget.value.res_id;
  238. default:
  239. return this._field_widget.value.data.display_name;
  240. }
  241. } catch (error) {
  242. if (error.name === "TypeError") {
  243. return false;
  244. }
  245. }
  246. },
  247. toString: function () {
  248. try {
  249. switch (this._field_widget_name) {
  250. case "many2one":
  251. return this._field_widget.value.data.display_name;
  252. }
  253. return this._super.apply(this, arguments);
  254. } catch (error) {
  255. if (error.name === "TypeError") {
  256. return "";
  257. }
  258. }
  259. },
  260. });
  261. // Register search filter widgets
  262. core.search_filters_registry
  263. .add("many2many", Relational)
  264. .add("many2one", Relational)
  265. .add("one2many", Relational);
  266. return {
  267. AdvancedSearchProposition: AdvancedSearchProposition,
  268. Relational: Relational,
  269. };
  270. });