[REM] web,project: remove legacy data export

This commit remove the legacy implementation of the data export
dialog, which wasn't used since the removal of the legacy list
view. This also allowed us to finally remove the data.js file,
which contained the "legacy legacy" implementation of the model
(DataSetStatic, DataSetSearch...). This was deprecated since v11.

Part of task 3168640

Part-of: odoo/odoo#111809
This commit is contained in:
Aaron Bohy
2023-02-08 13:27:58 +01:00
parent 49297bc7bb
commit ee3e28987e
7 changed files with 1 additions and 1646 deletions
-2
View File
@@ -153,7 +153,6 @@
'web/static/src/legacy/backend_utils.js',
'web/static/src/legacy/utils.js',
'web/static/src/legacy/web_client.js',
'web/static/src/legacy/js/_deprecated/data.js',
'web/static/src/legacy/js/chrome/*',
'web/static/src/legacy/js/components/*',
'web/static/src/legacy/js/control_panel/*',
@@ -177,7 +176,6 @@
'web/static/src/legacy/js/services/session.js',
'web/static/src/legacy/js/tools/tools.js',
'web/static/src/legacy/js/views/**/*',
'web/static/src/legacy/js/widgets/data_export.js',
'web/static/src/legacy/js/widgets/date_picker.js',
'web/static/src/legacy/js/widgets/domain_selector_dialog.js',
'web/static/src/legacy/js/widgets/domain_selector.js',
-2
View File
@@ -216,7 +216,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/scss/ace.scss',
'web/static/src/legacy/scss/fields.scss',
'web/static/src/legacy/scss/views.scss',
'web/static/src/legacy/scss/data_export.scss',
'base/static/src/scss/onboarding.scss',
'web/static/src/legacy/scss/attachment_preview.scss',
'web/static/src/legacy/scss/base_document_layout.scss',
@@ -256,7 +255,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/js/services/session.js',
'web/static/src/legacy/js/tools/tools.js',
'web/static/src/legacy/js/views/**/*',
'web/static/src/legacy/js/widgets/data_export.js',
'web/static/src/legacy/js/widgets/date_picker.js',
'web/static/src/legacy/js/widgets/domain_selector_dialog.js',
'web/static/src/legacy/js/widgets/domain_selector.js',
@@ -1,852 +0,0 @@
odoo.define('web.data', function (require) {
"use strict";
var Class = require('web.Class');
var Context = require('web.Context');
var concurrency = require('web.concurrency');
var mixins = require('web.mixins');
var session = require('web.session');
var translation = require('web.translation');
var pyUtils = require('web.py_utils');
var _t = translation._t;
/**
* Serializes the sort criterion array of a dataset into a form which can be
* consumed by OpenERP's RPC APIs.
*
* @param {Array} criterion array of fields, from first to last criteria, prefixed with '-' for reverse sorting
* @returns {String} SQL-like sorting string (``ORDER BY``) clause
*/
function serialize_sort(criterion) {
return _.map(criterion,
function (criteria) {
if (criteria[0] === '-') {
return criteria.slice(1) + ' DESC';
}
return criteria + ' ASC';
}).join(', ');
}
/**
* Reverse of the serialize_sort function: convert an array of SQL-like sort
* descriptors into a list of fields prefixed with '-' if necessary.
*/
function deserialize_sort(criterion) {
return _.map(criterion, function (criteria) {
var split = _.without(criteria.split(' '), '');
return (split[1] && split[1].toLowerCase() === 'desc' ? '-' : '') + split[0];
});
}
var Query = Class.extend({
init: function (model, fields) {
this._model = model;
this._fields = fields;
this._filter = [];
this._context = {};
this._lazy = true;
this._limit = false;
this._offset = 0;
this._order_by = [];
},
clone: function (to_set) {
to_set = to_set || {};
var q = new Query(this._model, this._fields);
q._context = this._context;
q._filter = this._filter;
q._lazy = this._lazy;
q._limit = this._limit;
q._offset = this._offset;
q._order_by = this._order_by;
for(var key in to_set) {
if (!to_set.hasOwnProperty(key)) { continue; }
switch(key) {
case 'filter':
q._filter = (q._filter || []).concat(to_set.filter || []);
break;
case 'context':
q._context = new Context(
q._context, to_set.context);
break;
case 'lazy':
case 'limit':
case 'offset':
case 'order_by':
q['_' + key] = to_set[key];
}
}
return q;
},
_execute: function (options) {
var self = this;
options = options || {};
return session.rpc('/web/dataset/search_read', {
model: this._model.name,
fields: this._fields || false,
domain: pyUtils.eval('domains',
[this._model.domain(this._filter)]),
context: pyUtils.eval('contexts',
[this._model.context(this._context)]),
offset: this._offset,
limit: this._limit,
sort: serialize_sort(this._order_by)
}, options).then(function (results) {
self._count = results.length;
return results.records;
}, null);
},
/**
* Fetches the first record matching the query, or null
*
* @param {Object} [options] additional options for the rpc() method
* @returns {Promise<Object|null>}
*/
first: function (options) {
var self = this;
return this.clone({limit: 1})._execute(options).then(function (records) {
delete self._count;
if (records.length) { return records[0]; }
return null;
});
},
/**
* Fetches all records matching the query
*
* @param {Object} [options] additional options for the rpc() method
* @returns {Promise<Array<>>}
*/
all: function (options) {
return this._execute(options);
},
/**
* Fetches the number of records matching the query in the database
*
* @returns {Promise<Number>}
*/
count: function () {
if (this._count !== undefined) { return Promise.resolve(this._count); }
return this._model.call(
'search_count', [this._filter], {
context: this._model.context(this._context)});
},
/**
* Performs a groups read according to the provided grouping criterion
*
* @param {String|Array<String>} grouping
* @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
*/
group_by: function (grouping) {
var ctx = pyUtils.eval(
'context', this._model.context(this._context));
// undefined passed in explicitly (!)
if (_.isUndefined(grouping)) {
grouping = [];
}
if (!(grouping instanceof Array)) {
grouping = _.toArray(arguments);
}
if (_.isEmpty(grouping) && !ctx.group_by_no_leaf) {
return null;
}
var raw_fields = _.map(grouping.concat(this._fields || []), function (field) {
return field.split(':')[0];
});
var self = this;
return this._model.call('read_group', {
groupby: grouping,
fields: _.uniq(raw_fields),
domain: this._model.domain(this._filter),
context: ctx,
offset: this._offset,
lazy: this._lazy,
limit: this._limit,
orderby: serialize_sort(this._order_by) || false
}).then(function (results) {
return _(results).map(function (result) {
// FIX: querygroup initialization
result.__context = result.__context || {};
result.__context.group_by = result.__context.group_by || [];
_.defaults(result.__context, ctx);
var grouping_fields = self._lazy ? [grouping[0]] : grouping;
return new QueryGroup(
self._model.name, grouping_fields, result);
});
});
},
/**
* Creates a new query with the union of the current query's context and
* the new context.
*
* @param context context data to add to the query
* @returns {openerp.web.Query}
*/
context: function (context) {
if (!context) { return this; }
return this.clone({context: context});
},
/**
* Creates a new query with the union of the current query's filter and
* the new domain.
*
* @param domain domain data to AND with the current query filter
* @returns {openerp.web.Query}
*/
filter: function (domain) {
if (!domain) { return this; }
return this.clone({filter: domain});
},
/**
* Creates a new query with the provided parameter lazy replacing the current
* query's own.
*
* @param {Boolean} lazy indicates if the read_group should return only the
* first level of groupby records, or should return the records grouped by
* all levels at once (so, it makes only 1 db request).
* @returns {openerp.web.Query}
*/
lazy: function (lazy) {
return this.clone({lazy: lazy});
},
/**
* Creates a new query with the provided limit replacing the current
* query's own limit
*
* @param {Number} limit maximum number of records the query should retrieve
* @returns {openerp.web.Query}
*/
limit: function (limit) {
return this.clone({limit: limit});
},
/**
* Creates a new query with the provided offset replacing the current
* query's own offset
*
* @param {Number} offset number of records the query should skip before starting its retrieval
* @returns {openerp.web.Query}
*/
offset: function (offset) {
return this.clone({offset: offset});
},
/**
* Creates a new query with the provided ordering parameters replacing
* those of the current query
*
* @param {String...} fields ordering clauses
* @returns {openerp.web.Query}
*/
order_by: function (fields) {
if (fields === undefined) { return this; }
if (!(fields instanceof Array)) {
fields = _.toArray(arguments);
}
if (_.isEmpty(fields)) { return this; }
return this.clone({order_by: fields});
}
});
var QueryGroup = Class.extend({
init: function (model, grouping_fields, read_group_group) {
// In cases where group_by_no_leaf and no group_by, the result of
// read_group has aggregate fields but no __context or __domain.
// Create default (empty) values for those so that things don't break
var fixed_group = _.extend(
{__context: {group_by: []}, __domain: []},
read_group_group);
var count_key = (grouping_fields[0] && grouping_fields[0].split(':')[0]) + '_count';
var aggregates = {};
for (var key in fixed_group) {
if (fixed_group.hasOwnProperty(key)) {
if (!(key.indexOf('__') === 0
|| _.contains(grouping_fields, key)
|| (key === count_key))) {
aggregates[key] = fixed_group[key] || 0;
}
}
}
this.model = new Model(
model, fixed_group.__context, fixed_group.__domain);
var group_size = fixed_group[count_key] || fixed_group.__count || 0;
var leaf_group = fixed_group.__context.group_by.length === 0;
var value = (grouping_fields.length === 1)
? fixed_group[grouping_fields[0]]
: _.map(grouping_fields, function (field) { return fixed_group[field]; });
var grouped_on = (grouping_fields.length === 1)
? grouping_fields[0]
: grouping_fields;
this.attributes = {
folded: !!(fixed_group.__fold),
grouped_on: grouped_on,
// if terminal group (or no group) and group_by_no_leaf => use group.__count
length: group_size,
value: value,
// A group is open-able if it's not a leaf in group_by_no_leaf mode
has_children: !(leaf_group && fixed_group.__context.group_by_no_leaf),
aggregates: aggregates
};
},
get: function (key) {
return this.attributes[key];
},
subgroups: function () {
return this.model.query().group_by(this.model.context().group_by);
},
query: function () {
return this.model.query.apply(this.model, arguments);
}
});
var DataSet = Class.extend(mixins.PropertiesMixin, {
/**
* Collection of OpenERP records, used to share records and the current selection between views.
*
* @constructs instance.web.DataSet
*
* @param {String} model the OpenERP model this dataset will manage
*/
init: function (parent, model, context) {
mixins.PropertiesMixin.init.call(this);
this.setParent(parent);
this.model = model;
this.context = context || {};
this.index = null;
this._sort = [];
this._model = new Model(model, context);
this.orderer = new concurrency.DropMisordered();
},
previous: function () {
this.index -= 1;
if (!this.ids.length) {
this.index = null;
} else if (this.index < 0) {
this.index = this.ids.length - 1;
}
return this;
},
next: function () {
this.index += 1;
if (!this.ids.length) {
this.index = null;
} else if (this.index >= this.ids.length) {
this.index = 0;
}
return this;
},
select_id: function (id) {
var idx = this.get_id_index(id);
if (idx === null) {
return false;
} else {
this.index = idx;
return true;
}
},
get_id_index: function (id) {
for (var i=0, ii=this.ids.length; i<ii; i++) {
// Here we use type coercion because of the mess potentially caused by
// OpenERP ids fetched from the DOM as string. (eg: dhtmlxcalendar)
// OpenERP ids can be non-numeric too ! (eg: recursive events in calendar)
if (id == this.ids[i]) {
return i;
}
}
return null;
},
/**
* Read records.
*
* @param {Array} ids identifiers of the records to read
* @param {Array} [fields] fields to read and return, by default all fields are returned
* @param {Object} [options]
* @returns {Promise}
*/
read_ids: function (ids, fields, options) {
if (_.isEmpty(ids)) {
return Promise.resolve([]);
}
options = options || {};
var method = 'read';
var ids_arg = ids;
var context = this.get_context(options.context);
if (options.check_access_rule === true){
method = 'search_read';
ids_arg = [['id', 'in', ids]];
context = new Context(context, {active_test: false});
}
return this._model.call(method,
[ids_arg, fields || false],
{context: context})
.then(function (records) {
if (records.length <= 1) { return records; }
var indexes = {};
for (var i = 0; i < ids.length; i++) {
indexes[ids[i]] = i;
}
records.sort(function (a, b) {
return indexes[a.id] - indexes[b.id];
});
return records;
});
},
/**
* Read a slice of the records represented by this DataSet, based on its
* domain and context.
*
* @param {Array} [fields] fields to read and return, by default all fields are returned
* @params {Object} [options]
* @param {Number} [options.offset=0] The index from which selected records should be returned
* @param {Number} [options.limit=null] The maximum number of records to return
* @returns {Promise}
*/
read_slice: function (fields, options) {
var self = this;
options = options || {};
var query = this._model.query(fields)
.limit(options.limit || false)
.offset(options.offset || 0)
.all();
var prom = this.orderer.add(query);
prom.then(function (records) {
self.ids = _(records).pluck('id');
});
return prom;
},
/**
* Reads the current dataset record (from its index)
*
* @params {Array} [fields] fields to read and return, by default all fields are returned
* @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
* @returns {Promise}
*/
read_index: function (fields, options) {
options = options || {};
return this.read_ids([this.ids[this.index]], fields, options).then(function (records) {
if (_.isEmpty(records)) { return Promise.reject(); }
return records[0];
});
},
/**
* Reads default values for the current model
*
* @param {Array} [fields] fields to get default values for, by default all defaults are read
* @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
* @returns {Promise}
*/
default_get: function (fields, options) {
options = options || {};
return this._model.call('default_get',
[fields], {context: this.get_context(options.context)});
},
/**
* Creates a new record in db
*
* @param {Object} data field values to set on the new record
* @param {Object} options Dictionary that can contain the following keys:
* - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {Promise}
*/
create: function (data, options) {
var self = this;
var prom = this._model.call('create', [data], {
context: this.get_context()
});
prom.then(function () {
self.trigger('dataset_changed', data, options);
});
return prom;
},
/**
* Saves the provided data in an existing db record
*
* @param {Number|String} id identifier for the record to alter
* @param {Object} data field values to write into the record
* @param {Object} options Dictionary that can contain the following keys:
* - context: The context to use in the server-side call.
* - readonly_fields: Values from readonly fields that were updated by
* on_changes. Only used by the BufferedDataSet to make the o2m work correctly.
* @returns {Promise}
*/
write: function (id, data, options) {
options = options || {};
var self = this;
var prom = this._model.call('write', [[id], data], {
context: this.get_context(options.context)
});
prom.then(function () {
self.trigger('dataset_changed', id, data, options);
});
return prom;
},
/**
* Deletes an existing record from the database
*
* @param {Number|String} ids identifier of the record to delete
*/
unlink: function (ids) {
var self = this;
var prom = this._model.call('unlink', [ids], {
context: this.get_context()
});
prom.then(function () {
self.trigger('dataset_changed', ids);
});
return prom;
},
/**
* Calls an arbitrary RPC method
*
* @param {String} method name of the method (on the current model) to call
* @param {Array} [args] arguments to pass to the method
* @param {Function} callback
* @param {Function} error_callback
* @returns {Promise}
*/
call: function (method, args) {
return this._model.call(method, args);
},
/**
* Calls a button method, usually returning some sort of action
*
* @param {String} method
* @param {Array} [args]
* @returns {Promise}
*/
call_button: function (method, args) {
return this._model.call_button(method, args);
},
/**
* Fetches the "readable name" for records, based on intrinsic rules
*
* @param {Array} ids
* @returns {Promise}
*/
name_get: function (ids) {
return this._model.call('name_get', [ids], {context: this.get_context()});
},
/**
*
* @param {String} name name to perform a search for/on
* @param {Array} [domain=[]] filters for the objects returned, OpenERP domain
* @param {String} [operator='ilike'] matching operator to use with the provided name value
* @param {Number} [limit=0] maximum number of matches to return
* @param {Function} callback function to call with name_search result
* @returns {Promise}
*/
name_search: function (name, domain, operator, limit) {
return this._model.call('name_search', {
name: name || '',
args: domain || false,
operator: operator || 'ilike',
context: this._model.context(),
limit: limit || 0
});
},
/**
* @param name
*/
name_create: function (name, context) {
return this._model.call('name_create', [name], {context: this.get_context(context)});
},
get_context: function (request_context) {
return this._model.context(request_context);
},
/**
* Reads or changes sort criteria on the dataset.
*
* If not provided with any argument, serializes the sort criteria to
* an SQL-like form usable by OpenERP's ORM.
*
* If given a field, will set that field as first sorting criteria or,
* if the field is already the first sorting criteria, will reverse it.
*
* @param {String} [field] field to sort on, reverses it (toggle from ASC to DESC) if already the main sort criteria
* @param {Boolean} [force_reverse=false] forces inserting the field as DESC
* @returns {String|undefined}
*/
sort: function (field, force_reverse) {
if (!field) {
return serialize_sort(this._sort);
}
var reverse = force_reverse || (this._sort[0] === field);
this._sort.splice.apply(
this._sort, [0, this._sort.length].concat(
_.without(this._sort, field, '-' + field)));
this._sort.unshift((reverse ? '-' : '') + field);
return undefined;
},
/**
* Set the sort criteria on the dataset.
*
* @param {Array} fields_list: list of fields order descriptors, as used by
* Odoo's ORM (such as 'name desc', 'product_id', 'order_date asc')
*/
set_sort: function (fields_list) {
this._sort = deserialize_sort(fields_list);
},
size: function () {
return this.ids.length;
},
alter_ids: function (n_ids) {
this.ids = n_ids;
},
remove_ids: function (ids) {
this.alter_ids(_(this.ids).difference(ids));
},
add_ids: function (ids, at) {
var args = [at, 0].concat(_.difference(ids, this.ids));
this.ids.splice.apply(this.ids, args);
},
/**
* Resequence records.
*
* @param {Array} ids identifiers of the records to resequence
* @returns {Promise}
*/
resequence: function (ids, options) {
options = options || {};
return session.rpc('/web/dataset/resequence', {
model: this.model,
ids: ids,
context: pyUtils.eval(
'context', this.get_context(options.context)),
}).then(function (results) {
return results;
});
},
});
var DataSetStatic = DataSet.extend({
init: function (parent, model, context, ids) {
this._super(parent, model, context);
// all local records
this.ids = ids || [];
},
read_slice: function (fields, options) {
options = options || {};
fields = fields || {};
var offset = options.offset || 0,
limit = options.limit || false;
var end_pos = limit && limit !== -1 ? offset + limit : this.ids.length;
return this.read_ids(this.ids.slice(offset, end_pos), fields, options);
},
set_ids: function (ids) {
this.ids = ids;
if (ids.length === 0) {
this.index = null;
} else if (this.index >= ids.length - 1) {
this.index = ids.length - 1;
}
},
unlink: function (ids) {
this.set_ids(_.without.apply(null, [this.ids].concat(ids)));
this.trigger('unlink', ids);
return Promise.resolve({result: true});
},
});
var DataSetSearch = DataSet.extend({
/**
* @constructs instance.web.DataSetSearch
* @extends instance.web.DataSet
*
* @param {Object} parent
* @param {String} model
* @param {Object} context
* @param {Array} domain
*/
init: function (parent, model, context, domain) {
this._super(parent, model, context);
this.domain = domain || [];
this._length = null;
this.ids = [];
this._model = new Model(model, context, domain);
},
/**
* Read a slice of the records represented by this DataSet, based on its
* domain and context.
*
* @params {Object} options
* @param {Array} [options.fields] fields to read and return, by default all fields are returned
* @param {Object} [options.context] context data to add to the request payload, on top of the DataSet's own context
* @param {Array} [options.domain] domain data to add to the request payload, ANDed with the dataset's domain
* @param {Number} [options.offset=0] The index from which selected records should be returned
* @param {Number} [options.limit=null] The maximum number of records to return
* @returns {Promise}
*/
read_slice: function (fields, options) {
options = options || {};
var self = this;
var q = this._model.query(fields || false)
.filter(options.domain)
.context(options.context)
.offset(options.offset || 0)
.limit(options.limit || false);
q = q.order_by.apply(q, this._sort);
var prom = this.orderer.add(q.all());
prom.then(function (records) {
// FIXME: not sure about that one, *could* have discarded count
q.count().then(function (count) { self._length = count; });
self.ids = _(records).pluck('id');
});
return prom;
},
get_domain: function (other_domain) {
return this._model.domain(other_domain);
},
alter_ids: function (ids) {
this._super(ids);
if (this.index !== null && this.index >= this.ids.length) {
this.index = this.ids.length > 0 ? this.ids.length - 1 : 0;
}
},
remove_ids: function (ids) {
var before = this.ids.length;
this._super(ids);
if (this._length) {
this._length -= (before - this.ids.length);
}
},
add_ids: function (ids, at) {
var before = this.ids.length;
this._super(ids, at);
if(this._length){
this._length += (this.ids.length - before);
}
},
unlink: function (ids, callback, error_callback) {
var self = this;
var prom = this._super(ids);
prom.then(function () {
self.remove_ids( ids);
self.trigger("dataset_changed", ids, callback, error_callback);
});
return prom;
},
size: function () {
if (this._length !== null) {
return this._length;
}
return this._super();
}
});
var data = {
Query: Query,
DataSet: DataSet,
DataSetStatic: DataSetStatic,
DataSetSearch: DataSetSearch,
/** @type String */
noDisplayContent: "<em class=\"text-warning\">" + _t("Unnamed") + "</em>",
};
var Model = Class.extend({
/**
new openerp.web.Model(name[, context[, domain]])
@constructs instance.web.Model
@extends instance.web.Class
@param {String} name name of the OpenERP model this object is bound to
@param {Object} [context]
@param {Array} [domain]
*/
init: function(name, context, domain) {
this.name = name;
this._context = context || {};
this._domain = domain || [];
},
/**
* @deprecated does not allow to specify kwargs, directly use call() instead
*/
get_func: function (method_name) {
var self = this;
return function () {
return self.call(method_name, _.toArray(arguments));
};
},
/**
* Fetches a Query instance bound to this model, for searching
*
* @param {Array<String>} [fields] fields to ultimately fetch during the search
* @returns {instance.web.Query}
*/
query: function (fields) {
return new data.Query(this, fields);
},
/**
* Fetches the model's domain, combined with the provided domain if any
*
* @param {Array} [domain] to combine with the model's internal domain
* @returns {Array} The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
*/
domain: function (domain) {
if (!domain) { return this._domain; }
return this._domain.concat(domain);
},
/**
* Fetches the combination of the user's context and the domain context,
* combined with the provided context if any
*
* @param {Object} [context] to combine with the model's internal context
* @returns {web.Context} The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
*/
context: function (context) {
return new Context(session.user_context, this._context, context || {});
},
/**
* Call a method (over RPC) on the bound OpenERP model.
*
* @param {String} method name of the method to call
* @param {Array} [args] positipyEvalonal arguments
* @param {Object} [kwargs] keyword arguments
* @param {Object} [options] additional options for the rpc() method
* @returns {Promise<>} call result
*/
call: function (method, args, kwargs, options) {
args = args || [];
kwargs = kwargs || {};
if (!_.isArray(args)) {
// call(method, kwargs)
kwargs = args;
args = [];
}
pyUtils.ensure_evaluated(args, kwargs);
var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method;
return session.rpc(call_kw, {
model: this.name,
method: method,
args: args,
kwargs: kwargs
}, options);
},
call_button: function (method, args) {
pyUtils.ensure_evaluated(args, {});
// context should be the last argument
var context = (args || []).length > 0 ? args.pop() : {};
return session.rpc('/web/dataset/call_button', {
model: this.name,
method: method,
args: args || [],
kwargs: {context: context},
});
},
});
return data;
});
@@ -16,7 +16,6 @@ odoo.define('web.relational_fields', function (require) {
var AbstractField = require('web.AbstractField');
var concurrency = require('web.concurrency');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
const { FormViewDialog } = require("@web/views/view_dialogs/form_view_dialog");
@@ -668,7 +667,7 @@ var FieldMany2One = AbstractField.extend({
result[1] = displayName;
return {
id,
label: escape(displayName) || data.noDisplayContent,
label: escape(displayName) || `<em class="text-warning">${escape(_t("Unnamed"))}</em>`,
value: displayName,
name: displayName,
};
@@ -1,694 +0,0 @@
odoo.define('web.DataExport', function (require) {
"use strict";
var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
var data = require('web.data');
var framework = require('web.framework');
var pyUtils = require('web.py_utils');
var QWeb = core.qweb;
var _t = core._t;
var DataExport = Dialog.extend({
template: 'ExportDialog',
events: {
'change .o_exported_lists_select': '_onChangeExportList',
'change .o_import_compat input': '_onChangeCompatibleInput',
'click .o_add_field': '_onClickAddField',
'click .o_delete_exported_list': '_onClickDeleteExportListBtn',
'click .o_expand': '_onClickExpand',
'click .o_remove_field': '_onClickRemoveField',
'click .o_save_list .o_save_list_btn': '_onClickSaveListBtn',
'click .o_save_list .o_cancel_list_btn': '_resetTemplateField',
'click .o_export_tree_item': '_onClickTreeItem',
'dblclick .o_export_tree_item:not(.haschild)': '_onDblclickTreeItem',
'keydown .o_export_tree_item': '_onKeydownTreeItem',
'keydown .o_save_list_name': '_onKeydownSaveList',
'input .o_export_search_input': '_onSearchInput',
},
/**
* @constructor
* @param {Widget} parent
* @param {Object} record
* @param {string[]} defaultExportFields
*/
init: function (parent, record, defaultExportFields, groupedBy, activeDomain, idsToExport) {
var options = {
title: _t("Export Data"),
buttons: [
{text: _t("Export"), click: this._onExportData, classes: 'btn-primary'},
{text: _t("Close"), close: true},
],
fullscreen: config.device.isMobile,
};
this._super(parent, options);
this.records = {};
this.record = record;
this.defaultExportFields = defaultExportFields;
this.groupby = groupedBy;
this.exports = new data.DataSetSearch(this, 'ir.exports', this.record.getContext());
this.rowIndex = 0;
this.rowIndexLevel = 0;
this.isCompatibleMode = false;
this.domain = activeDomain || this.record.domain;
this.idsToExport = activeDomain ? false: idsToExport;
},
/**
* @override
*/
start: function () {
var self = this;
var proms = [this._super.apply(this, arguments)];
// The default for the ".modal_content" element is "max-height: 100%;"
// but we want it to always expand to "height: 100%;" for this modal.
// This can be achieved thanks to CSS modification without touching
// the ".modal-content" rules... but not with Internet explorer (11).
this.$modal.find('.modal-content').css('height', '100%');
this.$fieldsList = this.$('.o_fields_list');
proms.push(this._rpc({route: '/web/export/formats'}).then(doSetupExportFormats));
proms.push(this._onChangeCompatibleInput().then(function () {
_.each(self.defaultExportFields, function (field) {
var record = self.records[field];
self._addField(record.id, record.string);
});
}));
proms.push(this._showExportsList());
// Bind sortable events after Dialog is open
this.opened().then(function () {
self.$('.o_fields_list').sortable({
axis: 'y',
cursor: 'grabbing',
handle: '.o_short_field',
forcePlaceholderSize: true,
placeholder: 'o-field-placeholder bg-light border border-primary',
update: self.proxy('_resetTemplateField'),
});
});
return Promise.all(proms);
function doSetupExportFormats(formats) {
var $fmts = self.$('.o_export_format');
_.each(formats, function (format) {
var $radio = $('<input/>', {type: 'radio', value: format.tag, name: 'o_export_format_name', class: 'form-check-input', id: 'o_radio' + format.label});
var $label = $('<label/>', {html: format.label, class: 'form-check-label', for: 'o_radio' + format.label});
if (format.error) {
$radio.prop('disabled', true);
$label.html(_.str.sprintf("%s — %s", format.label, format.error));
}
$fmts.append($("<div class='radio form-check-inline ps-4'></div>").append($radio, $label));
});
self.$exportFormatInputs = $fmts.find('input');
self.$exportFormatInputs.filter(':enabled').first().prop('checked', true);
}
},
/**
* Export all data with default values (fields, domain)
*/
export() {
let exportedFields = this.defaultExportFields.map(field => ({
name: field,
label: this.record.fields[field].string,
store: this.record.fields[field].store,
type: this.record.fields[field].type,
}));
this._exportData(exportedFields, 'xlsx', false);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Add the field in the export list
*
* @private
* @param {string} fieldID
* @param {string} label
*/
_addField: function (fieldID, label) {
var $fieldList = this.$('.o_fields_list');
if (!$fieldList.find(".o_export_field[data-field_id='" + fieldID + "']").length) {
$fieldList.append(
$('<li>', {'class': 'o_export_field', 'data-field_id': fieldID}).append(
$('<span>', {'class': "fa fa-sort o_short_field mx-1"}),
label.trim(),
$('<span>', {'class': 'o_remove_field cursor-pointer fa fa-trash m-1 float-end ', 'title': _t("Remove field")})
)
);
}
},
/**
* Submit the user data and export the file
*
* @private
*/
_exportData(exportedFields, exportFormat, idsToExport) {
if (_.isEmpty(exportedFields)) {
Dialog.alert(this, _t("Please select fields to export..."));
return;
}
if (this.isCompatibleMode) {
exportedFields.unshift({ name: 'id', label: _t('External ID') });
}
framework.blockUI();
this.getSession().get_file({
url: '/web/export/' + exportFormat,
data: {
data: JSON.stringify({
model: this.record.model,
fields: exportedFields,
ids: idsToExport,
domain: this.domain,
groupby: this.groupby,
context: pyUtils.eval('contexts', [this.record.getContext()]),
import_compat: this.isCompatibleMode,
})
},
complete: framework.unblockUI,
error: (error) => this.call('crash_manager', 'rpc_error', error),
});
},
/**
* @private
* @returns {string[]} exportFields
*/
_getFields: function () {
var exportFields = this.$('.o_export_field').map(function () {
return $(this).data('field_id');
}).get();
if (exportFields.length === 0) {
Dialog.alert(this, _t("Please select fields to save export list..."));
}
return exportFields;
},
/**
* Fetch the field info for the relational field. This method will be
* invoked when the user expands the relational field from keyboard/mouse.
*
* @private
* @param {Object} record
*/
_onExpandAction: function (record) {
var self = this;
if (!record.children) {
return;
}
var model = record.params.model;
var prefix = record.params.prefix;
var name = record.params.name;
var excludeFields = [];
if (record.relation_field) {
excludeFields.push(record.relation_field);
}
if (!record.loaded) {
this._rpc({
route: '/web/export/get_fields',
params: {
model: model,
prefix: prefix,
parent_name: name,
import_compat: this.isCompatibleMode,
parent_field_type: record.field_type,
parent_field: record.params.parent_field,
exclude: excludeFields,
},
}).then(function (results) {
record.loaded = true;
self._onShowData(results, record.id);
});
} else {
this._showContent(record.id);
}
},
/**
* After the fetching the fields info for the relational field, this method
* will render a list of a field for expanded relational field.
*
* @private
* @param {Object[]} records
* @param {string} expansion
*/
_onShowData: function (records, expansion) {
var self = this;
if (expansion) {
this.$('.o_export_tree_item[data-id="' + expansion + '"]')
.addClass('show')
.find('.o_expand_parent')
.toggleClass('fa-chevron-right fa-chevron-down')
.next()
.after(QWeb.render('Export.TreeItems', {fields: records, debug: config.isDebug()}));
} else {
this.$('.o_left_field_panel').empty().append(
$('<div/>').addClass('o_field_tree_structure my-2')
.append(QWeb.render('Export.TreeItems', {fields: records, debug: config.isDebug()}))
);
}
_.extend(this.records, _.object(_.pluck(records, 'id'), records));
this.$records = this.$('.o_export_tree_item');
this.$records.each(function (i, el) {
var $el = $(el);
$el.find('.o_tree_column').first().toggleClass('o_required fw-bolder', !!self.records[$el.data('id')].required);
});
},
/**
* @private
*/
_addNewTemplate: function () {
this.$('.o_exported_lists').addClass('d-none');
this.$(".o_save_list")
.show()
.find(".o_save_list_name")
.val("")
.focus();
},
/**
* @private
*/
_resetTemplateField: function () {
this.$('.o_exported_lists_select').val("");
this.$('.o_delete_exported_list').addClass('d-none');
this.$('.o_exported_lists').removeClass('d-none');
this.$(".o_save_list")
.hide()
.find(".o_save_list_name").val("");
},
/**
* If relational fields info is already fetched then this method is
* used to display fields.
*
* @private
* @param {string} fieldID
*/
_showContent: function (fieldID) {
var $item = this.$('.o_export_tree_item[data-id="' + fieldID + '"]');
$item.toggleClass('show');
var isOpen = $item.hasClass('show');
$item.children('.o_expand_parent').toggleClass('fa-chevron-down', !!isOpen).toggleClass('fa-chevron-right', !isOpen);
var $childField = $item.find('.o_export_tree_item');
var childLength = (fieldID.split('/')).length + 1;
for (var i = 0 ; i < $childField.length ; i++) {
var $child = $childField.eq(i);
if (!isOpen) {
$child.hide();
} else if (childLength === $childField.eq(i).data('id').split('/').length) {
if ($child.hasClass('show')) {
$child.removeClass('show');
$child.children('.o_expand_parent').removeClass('fa-chevron-down').addClass('fa-chevron-right');
}
$child.show();
}
}
},
/**
* Fetches the saved export list for the current model
*
* @private
* @returns {Deferred}
*/
_showExportsList: function () {
var self = this;
if (this.$('.o_exported_lists_select').is(':hidden')) {
this.$('.o_exported_lists').show();
return Promise.resolve();
}
return this._rpc({
model: 'ir.exports',
method: 'search_read',
fields: ['name'],
domain: [['resource', '=', this.record.model]]
}).then(function (exportList) {
self.$('.o_exported_lists').append(QWeb.render('Export.SavedList', {
existing_exports: exportList,
}));
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* This method will fill fields to export when user change exported field list
*
* @private
*/
_onChangeExportList: function () {
var self = this;
var exportID = this.$('.o_exported_lists_select option:selected').val();
this.$('.o_delete_exported_list').toggleClass('d-none', !exportID);
if (exportID && exportID !== 'new_template') {
this.$('.o_fields_list').empty();
this._rpc({
route: '/web/export/namelist',
params: {
model: this.record.model,
export_id: parseInt(exportID, 10),
},
}).then(function (fieldList) {
_.each(fieldList, function (field) {
self._addField(field.name, field.label);
});
});
} else if (exportID === 'new_template') {
self._addNewTemplate();
}
},
/**
* @private
* @returns {Deferred}
*/
_onChangeCompatibleInput: function () {
var self = this;
this.isCompatibleMode = this.$('.o_import_compat input').is(':checked');
this.$('.o_field_tree_structure').remove();
this._resetTemplateField();
return this._rpc({
route: '/web/export/get_fields',
params: {
model: this.record.model,
import_compat: this.isCompatibleMode,
},
}).then(function (records) {
var compatibleFields = _.map(records, function (record) { return record.id; });
self._onShowData(records);
self.$('.o_fields_list').empty();
const forceDefaultExportFields = records.filter(r => r.default_export).map(r => r.id);
_.chain(self.$fieldsList.find('.o_export_field'))
.map(function (field) { return $(field).data('field_id'); })
.union(forceDefaultExportFields)
.union(self.defaultExportFields)
.intersection(compatibleFields)
.uniq()
.each(function (field) {
var record = _.find(records, function (rec) {
return rec.id === field;
});
self._addField(record.id, record.string);
});
self.$('#o-export-search-filter').val('');
});
},
/**
* Add a field to export list
*
* @private
* @param {Event} ev
*/
_onClickAddField: function(ev) {
ev.stopPropagation();
var $field = $(ev.currentTarget);
this._resetTemplateField();
this._addField($field.closest('.o_export_tree_item').data('id'), $field.closest('.o_tree_column').text());
},
/**
* Delete selected export list item from the saved export list
*
* @private
*/
_onClickDeleteExportListBtn: function () {
var self = this;
var selectExp = this.$('.o_exported_lists_select option:selected');
var options = {
confirm_callback: function () {
if (selectExp.val()) {
self.exports.unlink([parseInt(selectExp.val(), 10)]);
selectExp.remove();
if (self.$('.o_exported_lists_select option').length <= 1) {
self.$('.o_exported_lists').hide();
}
}
}
};
Dialog.confirm(this, _t("Do you really want to delete this export template?"), options);
},
/**
* @private
* @param {Event} ev
*/
_onClickExpand: function (ev) {
this._onExpandAction(this.records[$(ev.target).closest('.o_export_tree_item').data('id')]);
},
/**
* Remove selected field from export field list
*
* @private
* @param {Event} ev
*/
_onClickRemoveField: function (ev) {
$(ev.currentTarget).closest('.o_export_field').remove();
this._resetTemplateField();
},
/**
* This method will create a record in 'ir.exports' model with list of
* selected fields.
*
* @private
*/
_onClickSaveListBtn: function () {
var self = this;
var $saveList = this.$('.o_save_list');
var value = $saveList.find('input').val();
if (!value) {
Dialog.alert(this, _t("Please enter save field list name"));
return;
}
var fields = this._getFields();
if (fields.length === 0) {
return;
}
$saveList.hide();
this.exports.create({
name: value,
resource: this.record.model,
export_fields: _.map(fields, function (field) {
return [0, 0, { name: field }];
}),
}).then(function (exportListID) {
if (!exportListID) {
return;
}
var $select = self.$('.o_exported_lists_select');
if ($select.length === 0 || $select.is(':hidden')) {
self._showExportsList();
}
$select.append(new Option(value, exportListID));
self.$('.o_exported_lists').removeClass('d-none');
$select.val(exportListID);
});
},
/**
* @private
* @param ev
*/
_onClickTreeItem: function (ev) {
ev.stopPropagation();
var $elem = $(ev.currentTarget);
var rowIndex = $elem.prevAll('.o_export_tree_item').length;
var rowIndexLevel = $elem.parents('.o_export_tree_item').length;
if (ev.shiftKey && rowIndexLevel === this.rowIndexLevel) {
var minIndex = Math.min(rowIndex, this.rowIndex);
var maxIndex = Math.max(rowIndex, this.rowIndex);
this.$records.filter(function () { return ($elem.parent()[0] === $(this).parent()[0]); })
.slice(minIndex, maxIndex + 1)
.addClass('o_selected')
.filter(':not(:last)')
.each(processChildren);
}
this.rowIndex = rowIndex;
this.rowIndexLevel = rowIndexLevel;
if (ev.ctrlKey) {
$elem.toggleClass('o_selected').focus();
} else if (ev.shiftKey) {
$elem.addClass('o_selected').focus();
} else {
this.$('.o_selected').removeClass('o_selected');
$elem.addClass('o_selected').focus();
}
function processChildren() {
var $child = $(this);
if ($child.hasClass('show')) {
$child.children('.o_export_tree_item')
.addClass('o_selected')
.each(processChildren);
}
}
},
/**
* Submit the user data and export the file
*
* @private
*/
_onExportData() {
let exportedFields = this.$('.o_export_field').map((i, field) => ({
name: $(field).data('field_id'),
label: field.textContent,
}
)).get();
let exportFormat = this.$exportFormatInputs.filter(':checked').val();
this._exportData(exportedFields, exportFormat, this.idsToExport);
},
/**
* Add a field to export field list on double click
*
* @private
* @param {Event} ev
*/
_onDblclickTreeItem: function (ev) {
var self = this;
this._resetTemplateField();
function addElement(el) {
self._addField(el.getAttribute('data-id'), el.querySelector('.o_tree_column').textContent);
}
var target = ev.currentTarget;
target.classList.remove('o_selected');
// Add parent fields to export
[].reverse.call($(target).parents('.o_export_tree_item')).each(function () {
addElement(this);
});
// add field itself
addElement(target);
},
/**
* @private
* @param ev
*/
_onKeydownSaveList: function (ev) {
if (ev.keyCode === $.ui.keyCode.ENTER) {
this._onClickSaveListBtn();
}
},
/**
* Handles the keyboard navigation for the fields
*
* @private
* @param ev
*/
_onKeydownTreeItem: function (ev) {
ev.stopPropagation();
var $el = $(ev.currentTarget);
var record = this.records[$el.data('id')];
switch (ev.keyCode || ev.which) {
case $.ui.keyCode.LEFT:
if ($el.hasClass('show')) {
this._onExpandAction(record);
}
break;
case $.ui.keyCode.RIGHT:
if (!$el.hasClass('show')) {
this._onExpandAction(record);
}
break;
case $.ui.keyCode.UP:
var $prev = $el.prev('.o_export_tree_item');
if ($prev.length === 1) {
while ($prev.hasClass('show')) {
$prev = $prev.children('.o_export_tree_item').last();
}
} else {
$prev = $el.parent('.o_export_tree_item');
if ($prev.length === 0) {
break;
}
}
$el.removeClass('o_selected').blur();
$prev.addClass("o_selected").focus();
break;
case $.ui.keyCode.DOWN:
var $next;
if ($el.hasClass('show')) {
$next = $el.children('.o_export_tree_item').first();
} else {
$next = $el.next('.o_export_tree_item');
if ($next.length === 0) {
$next = $el.parent('.o_export_tree_item').next('.o_export_tree_item');
if ($next.length === 0) {
break;
}
}
}
$el.removeClass('o_selected').blur();
$next.addClass('o_selected').focus();
break;
}
},
/**
* Search fields from a field list.
*
* @private
*/
_onSearchInput: function (ev) {
var searchText = $(ev.currentTarget).val().trim().toUpperCase();
if (!searchText) {
this.$('.o_no_match').remove();
this.$(".o_export_tree_item").show();
this.$(".o_export_tree_item.haschild:not(.show) .o_export_tree_item").hide();
return;
}
var matchItems = this.$(".o_tree_column").filter(function () {
var title = this.getAttribute('title');
return this.innerText.toUpperCase().indexOf(searchText) >= 0
|| title && title.toUpperCase().indexOf(searchText) >= 0;
}).parent();
this.$(".o_export_tree_item").hide();
if (matchItems.length) {
this.$('.o_no_match').remove();
_.each(matchItems, function (col) {
var $col = $(col);
$col.show();
$col.parents('.haschild.show').show();
if (!$col.parent().hasClass('show') && !$col.parent().hasClass('o_field_tree_structure')) {
$col.hide();
}
});
} else if (!this.$('.o_no_match').length) {
this.$(".o_field_tree_structure").append($("<h3/>", {
class: 'text-center text-muted mt-5 o_no_match',
text: _t("No match found.")
}));
}
},
});
return DataExport;
});
@@ -1,18 +0,0 @@
.o_export .o_selected > .o_tree_column {
background-color: $o-brand-odoo;
color: white;
}
.o_short_field {
@include o-grab-cursor;
}
@include media-breakpoint-down(md) {
.ui-sortable-handle {
display: none;
}
.o_left_panel, .o_right_panel {
height: 50% !important;
}
}
-76
View File
@@ -931,82 +931,6 @@
</div>
</t>
<div t-name="ExportDialog" class="o_export">
<div class="row o-export-panel h-100">
<div class="o_left_panel col-12 col-md-6 h-100 d-flex flex-column flex-nowrap">
<div class="form-check o_import_compat">
<input class="form-check-input" id="o-update-data" type="checkbox"/>
<label class="form-check-label" for="o-update-data">
I want to update data (import-compatible export)
</label>
</div>
<div class="mt-3">
<h4>Available fields</h4>
</div>
<div class="input-group mb-3">
<input type="search" class="form-control o_export_search_input" id="o-export-search-filter" placeholder="Search" />
</div>
<div class="o_left_field_panel flex-grow-1 px-2 overflow-auto border "/>
</div>
<div class="o_right_panel col-12 col-md-6 h-100 d-flex flex-column flex-nowrap mt-3 mt-md-0">
<div class="o_export_format">
<strong>Export Format:</strong>
</div>
<div class="mt-3">
<h4>Fields to export</h4>
</div>
<div class="o_save_list">
<t t-call="Export.SaveList"/>
</div>
<div class="o_exported_lists"/>
<div class="o_right_field_panel flex-grow-1 px-2 overflow-auto border ">
<ul class="o_fields_list list-unstyled my-2"/>
</div>
</div>
</div>
</div>
<t t-name="Export.TreeItems">
<t t-foreach="fields" t-as="field" t-key="field_index">
<t t-set="has_child" t-value="field.children &amp;&amp; (field.id).split('/').length != 3" />
<div t-att-data-id="field.id" t-attf-class="o_export_tree_item #{has_child ? 'haschild o_expand' : ''} cursor-pointer position-relative ps-3 " tabindex="-1" role="treeitem"> <!-- tabindex make the div focusable -->
<span t-if="has_child" class="o_expand_parent d-inline-block position-absolute top-0 start-0 mt-1 ml-n3 small fa fa-chevron-right" role="img" aria-label="Show sub-fields" title="Show sub-fields"/>
<div t-attf-class="o_tree_column text-wrap">
<t t-esc="field.string"/>
<t t-if="debug and field.id">
(<t t-esc="field.id"/>)
</t>
<span title="Select field" class="fa fa-plus float-end m-1 o_add_field"/>
</div>
</div>
</t>
</t>
<t t-name="Export.SaveList">
<div class="input-group mb-3">
<label class="pt-2 fw-bold">Save as : </label>
<input class="form-control ms-4 o_save_list_name"
placeholder="New template"/>
<button type="button" class="btn btn-secondary ms-1 o_save_list_btn"><i class="fa fa-floppy-o"/></button>
<button type="button" class="btn btn-secondary ms-1 o_cancel_list_btn"><i class="fa fa-times"/></button>
</div>
</t>
<t t-name="Export.SavedList">
<div class="input-group mb-3">
<label class="pt-2 mb-0 fw-bold">Template: </label>
<select class="form-select ms-4 o_exported_lists_select">
<option></option>
<option class="fst-italic" value="new_template">New template</option>
<t t-foreach="existing_exports" t-as="export" t-key="export_index">
<option t-att-value="export.id">
<t t-esc="export.name"/>
</option>
</t>
</select>
<button type="button" class="btn btn-secondary d-none ms-1 o_delete_exported_list">
<i class="fa fa-trash"/>
</button>
</div>
</t>
<t t-name="Throbber">
<div>
<div class="oe_blockui_spin" style="height: 50px">