[MERGE] Forward-port of 7.0 bugfixes, up to rev. 3912

revision-id: nicolas.vanhoren@openerp.com-20130430094843-9m1629m517vjtm1o

bzr revid: xmo@openerp.com-20130424102025-w33zyopm96r7q09p
bzr revid: odo@openerp.com-20130430103337-i9wuj8zf3h41h1ay
This commit is contained in:
Olivier Dony 2013-04-30 12:33:37 +02:00
commit ba5710962e
23 changed files with 3684 additions and 4090 deletions

View File

@ -13,6 +13,7 @@ This module provides the core of the OpenERP Web Client.
'auto_install': True,
'post_load': 'wsgi_postload',
'js' : [
"static/src/fixbind.js",
"static/lib/datejs/globalization/en-US.js",
"static/lib/datejs/core.js",
"static/lib/datejs/parser.js",
@ -76,6 +77,7 @@ This module provides the core of the OpenERP Web Client.
"static/test/class.js",
"static/test/registry.js",
"static/test/form.js",
"static/test/data.js",
"static/test/list-utils.js",
"static/test/formats.js",
"static/test/rpc.js",

View File

@ -643,6 +643,18 @@ class WebClient(openerpweb.Controller):
content, checksum = concat_files((f[0] for f in files), reader)
# move up all @import and @charset rules to the top
matches = []
def push(matchobj):
matches.append(matchobj.group(0))
return ''
content = re.sub(re.compile("(@charset.+;$)", re.M), push, content)
content = re.sub(re.compile("(@import.+;$)", re.M), push, content)
matches.append(content)
content = '\n'.join(matches)
return make_conditional(
req, req.make_response(content, [('Content-Type', 'text/css')]),
last_modified, checksum)
@ -1363,19 +1375,30 @@ class Binary(openerpweb.Controller):
elif dbname is None:
dbname = db_monodb(req)
if uid is None:
if not uid:
uid = openerp.SUPERUSER_ID
if not dbname:
image_data = self.placeholder(req, 'logo.png')
else:
registry = openerp.modules.registry.RegistryManager.get(dbname)
with registry.cursor() as cr:
user = registry.get('res.users').browse(cr, uid, uid)
if user.company_id.logo_web:
image_data = user.company_id.logo_web.decode('base64')
else:
image_data = self.placeholder(req, 'nologo.png')
try:
# create an empty registry
registry = openerp.modules.registry.Registry(dbname.lower())
with registry.cursor() as cr:
cr.execute("""SELECT c.logo_web
FROM res_users u
LEFT JOIN res_company c
ON c.id = u.company_id
WHERE u.id = %s
""", (uid,))
row = cr.fetchone()
if row and row[0]:
image_data = str(row[0]).decode('base64')
else:
image_data = self.placeholder(req, 'nologo.png')
except Exception:
image_data = self.placeholder(req, 'logo.png')
headers = [
('Content-Type', 'image/png'),
('Content-Length', len(image_data)),
@ -1420,7 +1443,7 @@ class Action(openerpweb.Controller):
else:
return False
class Export(View):
class Export(openerpweb.Controller):
_cp_path = "/web/export"
@openerpweb.jsonrequest
@ -1561,7 +1584,7 @@ class Export(View):
(prefix + '/' + k, prefix_string + '/' + v)
for k, v in self.fields_info(req, model, export_fields).iteritems())
#noinspection PyPropertyDefinition
class ExportFormat(object):
@property
def content_type(self):
""" Provides the format's content type """
@ -1609,7 +1632,7 @@ class Export(View):
('Content-Type', self.content_type)],
cookies={'fileToken': int(token)})
class CSVExport(Export):
class CSVExport(ExportFormat, http.Controller):
_cp_path = '/web/export/csv'
fmt = {'tag': 'csv', 'label': 'CSV'}
@ -1644,7 +1667,7 @@ class CSVExport(Export):
fp.close()
return data
class ExcelExport(Export):
class ExcelExport(ExportFormat, http.Controller):
_cp_path = '/web/export/xls'
fmt = {
'tag': 'xls',
@ -1683,7 +1706,7 @@ class ExcelExport(Export):
fp.close()
return data
class Reports(View):
class Reports(openerpweb.Controller):
_cp_path = "/web/report"
POLLING_DELAY = 0.25
TYPES_MAPPING = {

View File

@ -19,6 +19,7 @@ import time
import traceback
import urlparse
import uuid
import errno
import babel.core
import simplejson
@ -200,7 +201,7 @@ class JsonRequest(WebRequest):
_logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
response['id'] = self.jsonrequest.get('id')
response["result"] = method(self, **self.params)
except session.AuthenticationError:
except session.AuthenticationError, e:
se = serialize_exception(e)
error = {
'code': 100,
@ -354,17 +355,31 @@ def httprequest(f):
addons_module = {}
addons_manifest = {}
controllers_class = []
controllers_class_path = {}
controllers_object = {}
controllers_object_path = {}
controllers_path = {}
class ControllerType(type):
def __init__(cls, name, bases, attrs):
super(ControllerType, cls).__init__(name, bases, attrs)
controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
controllers_class.append(name_class)
path = attrs.get('_cp_path')
if path not in controllers_class_path:
controllers_class_path[path] = name_class
class Controller(object):
__metaclass__ = ControllerType
def __new__(cls, *args, **kwargs):
subclasses = [c for c in cls.__subclasses__() if c._cp_path == cls._cp_path]
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
return object.__new__(cls)
#----------------------------------------------------------
# Session context manager
#----------------------------------------------------------
@ -476,8 +491,15 @@ def session_path():
except Exception:
username = "unknown"
path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
if not os.path.exists(path):
try:
os.mkdir(path, 0700)
except OSError as exc:
if exc.errno == errno.EEXIST:
# directory exists: ensure it has the correct permissions
# this will fail if the directory is not owned by the current user
os.chmod(path, 0700)
else:
raise
return path
class Root(object):
@ -557,10 +579,11 @@ class Root(object):
addons_manifest[module] = manifest
self.statics['/%s/static' % module] = path_static
for k, v in controllers_class:
if k not in controllers_object:
o = v()
controllers_object[k] = o
for k, v in controllers_class_path.items():
if k not in controllers_object_path and hasattr(v[1], '_cp_path'):
o = v[1]()
controllers_object[v[0]] = o
controllers_object_path[k] = o
if hasattr(o, '_cp_path'):
controllers_path[o._cp_path] = o

File diff suppressed because it is too large Load Diff

View File

@ -123,6 +123,7 @@ $sheet-padding: 16px
font-size: 1px
letter-spacing: -1px
color: transparent
text-shadow: none
font-weight: normal
&:before
font: 21px "mnmliconsRegular"
@ -133,6 +134,7 @@ $sheet-padding: 16px
font-size: 1px
letter-spacing: -1px
color: transparent
text-shadow: none
font-weight: normal
&:before
font: $size "entypoRegular"
@ -1979,9 +1981,9 @@ $sheet-padding: 16px
.oe_form_field_float input
width: 7em
.oe_form_field_date input
width: 7.5em
width: 100px
.oe_form_field_datetime input
width: 11.5em
width: 150px
// }}}
// FormView.fields_binary {{{
/* http://www.quirksmode.org/dom/inputfile.html
@ -2558,6 +2560,8 @@ div.ui-widget-overlay
.placeholder
color: $tag-border !important
font-style: italic !important
.oe_form_binary_file
width: 80px
.oe_form_field_boolean input
background: #fff
.db_option_table .oe_form_field_selection

View File

@ -0,0 +1,28 @@
// Fix old versions of Webkit (such as ones used on iOS < 6 or PhantomJS <= 1.7)
// which does not have Function.prototype.bind function
// Use moz polyfill:
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}

View File

@ -251,9 +251,9 @@ instance.web.CrashManager = instance.web.Class.extend({
if (handler) {
new (handler)(this, error).display();
return;
};
}
if (error.data.name === "openerp.addons.web.session SessionExpiredException") {
this.show_warning({type: "Session Expired", data: { message: "Your OpenERP session expired. Please refresh the current web page." }});
this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
return;
}
if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning"
@ -530,16 +530,11 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
'login': 'admin',
'password': form_obj['create_admin_pwd'],
'login_successful': function() {
var action = {
type: "ir.actions.client",
tag: 'reload',
params: {
url_search : {
db: form_obj['db_name'],
},
}
};
self.do_action(action);
var url = '/?db=' + form_obj['db_name'];
if (self.session.debug) {
url += '&debug';
}
instance.web.redirect(url);
},
},
_push_me: false,
@ -918,9 +913,7 @@ instance.web.Menu = instance.web.Widget.extend({
self.reflow();
// launch the fetch of needaction counters, asynchronous
if (!_.isEmpty(menu_data.all_menu_ids)) {
this.rpc("/web/menu/load_needaction", {menu_ids: menu_data.all_menu_ids}).done(function(r) {
self.on_needaction_loaded(r);
});
this.do_load_needaction(menu_data.all_menu_ids);
}
});
var lazyreflow = _.debounce(this.reflow.bind(this), 200);
@ -946,7 +939,7 @@ instance.web.Menu = instance.web.Widget.extend({
this.data = {data: data};
this.renderElement();
this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
this.$el.on('click', 'a[data-menu]', this.on_menu_click);
this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
// Hide second level submenus
this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
if (self.current_menu) {
@ -955,6 +948,12 @@ instance.web.Menu = instance.web.Widget.extend({
this.trigger('menu_loaded', data);
this.has_been_loaded.resolve();
},
do_load_needaction: function (menu_ids) {
var self = this;
return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
self.on_needaction_loaded(r);
});
},
on_needaction_loaded: function(data) {
var self = this;
this.needaction_data = data;
@ -1086,11 +1085,36 @@ instance.web.Menu = instance.web.Widget.extend({
}
this.open_menu(id);
},
do_reload_needaction: function () {
var self = this;
self.do_load_needaction([self.current_menu]).then(function () {
self.trigger("need_action_reloaded");
});
},
/**
* Jquery event handler for menu click
*
* @param {Event} ev the jquery event
*/
on_top_menu_click: function(ev) {
var self = this;
var id = $(ev.currentTarget).data('menu');
var menu_ids = [id];
var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
function add_menu_ids (menu) {
if (menu.children) {
_.each(menu.children, function (menu) {
menu_ids.push(menu.id);
add_menu_ids(menu);
});
}
};
add_menu_ids(menu);
self.do_load_needaction(menu_ids).then(function () {
self.trigger("need_action_reloaded");
});
this.on_menu_click(ev);
},
on_menu_click: function(ev) {
ev.preventDefault();
var needaction = $(ev.target).is('div.oe_menu_counter');

View File

@ -112,24 +112,27 @@ instance.web.Query = instance.web.Class.extend({
* @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null}
*/
group_by: function (grouping) {
if (grouping === undefined) {
return null;
var ctx = instance.web.pyeval.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)) { return null; }
if (_.isEmpty(grouping) && !ctx['group_by_no_leaf']) {
return null;
}
var self = this;
var ctx = instance.web.pyeval.eval(
'context', this._model.context(this._context));
return this._model.call('read_group', {
groupby: grouping,
fields: _.uniq(grouping.concat(this._fields || [])),
domain: this._model.domain(this._filter),
context: this._model.context(this._context),
context: ctx,
offset: this._offset,
limit: this._limit,
orderby: instance.web.serialize_sort(this._order_by) || false
@ -325,7 +328,7 @@ instance.web.Model = instance.web.Class.extend({
* Fetches the model's domain, combined with the provided domain if any
*
* @param {Array} [domain] to combine with the model's internal domain
* @returns The model's internal domain, or the AND-ed union of the model's internal domain and the provided domain
* @returns {instance.web.CompoundDomain} 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; }
@ -337,7 +340,7 @@ instance.web.Model = instance.web.Class.extend({
* combined with the provided context if any
*
* @param {Object} [context] to combine with the model's internal context
* @returns The union of the user's context and the model's internal context, as well as the provided context if any. In that order.
* @returns {instance.web.CompoundContext} 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 instance.web.CompoundContext(
@ -604,6 +607,9 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
alter_ids: function(n_ids) {
this.ids = n_ids;
},
remove_ids: function (ids) {
this.alter_ids(_(this.ids).difference(ids));
},
/**
* Resequence records.
*
@ -701,22 +707,28 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({
get_domain: function (other_domain) {
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);
}
},
unlink: function(ids, callback, error_callback) {
var self = this;
return this._super(ids).done(function(result) {
self.ids = _(self.ids).difference(ids);
if (self._length) {
self._length -= 1;
}
if (self.index !== null) {
self.index = self.index <= self.ids.length - 1 ?
self.index : (self.ids.length > 0 ? self.ids.length -1 : 0);
}
self.remove_ids( ids);
self.trigger("dataset_changed", ids, callback, error_callback);
});
},
size: function () {
if (this._length !== undefined) {
if (this._length != null) {
return this._length;
}
return this._super();

View File

@ -314,4 +314,34 @@ instance.web.auto_date_to_str = function(value, type) {
}
};
/**
* performs a half up rounding with arbitrary precision, correcting for float loss of precision
* See the corresponding float_round() in server/tools/float_utils.py for more info
* @param {Number} the value to be rounded
* @param {Number} a non zero precision parameter. eg: 0.01 rounds to two digits.
*/
instance.web.round_precision = function(value, precision){
if(!value){
return 0;
}else if(!precision){
throw new Error('round_precision(...): Cannot round value: '+value+' with a precision of zero (or undefined)');
}
var normalized_value = value / precision;
var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
var epsilon = Math.pow(2, epsilon_magnitude - 53);
normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
var rounded_value = Math.round(normalized_value);
return rounded_value * precision;
};
/**
* performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision
* See the corresponding float_round() in server/tools/float_utils.py for more info
* @param {Number} the value to be rounded
* @param {Number} the number of decimals. eg: round_decimals(3.141592,2) -> 3.14
*/
instance.web.round_decimals = function(value, decimals){
return instance.web.round_precision(value, Math.pow(10,-decimals));
};
};

View File

@ -326,7 +326,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
}
},
'autocompleteopen': function () {
this.$el.autocomplete('widget').css('z-index', 3);
this.$el.autocomplete('widget').css('z-index', 1004);
},
},
/**
@ -1039,7 +1039,9 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
facet.values.each(function (v) {
var i = _(self.filters).indexOf(v.get('value'));
if (i === -1) { return; }
$filters.eq(i).addClass('oe_selected');
$filters.filter(function () {
return Number($(this).data('index')) === i;
}).addClass('oe_selected');
});
},
/**
@ -1129,7 +1131,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
});
},
toggle_filter: function (e) {
this.toggle(this.filters[$(e.target).index()]);
this.toggle(this.filters[Number($(e.target).data('index'))]);
},
toggle: function (filter) {
this.view.query.toggle(this.make_facet([this.make_value(filter)]));
@ -1337,20 +1339,22 @@ instance.web.search.CharField = instance.web.search.Field.extend( /** @lends ins
}
});
instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{
value_from: function () {
if (!this.$el.val()) {
return null;
}
var val = this.parse(this.$el.val()),
check = Number(this.$el.val());
if (isNaN(val) || val !== check) {
this.$el.addClass('error');
throw new instance.web.search.Invalid(
this.attrs.name, this.$el.val(), this.error_message);
}
this.$el.removeClass('error');
return val;
}
complete: function (value) {
var val = this.parse(value);
if (isNaN(val)) { return $.when(); }
var label = _.str.sprintf(
_t("Search %(field)s for: %(value)s"), {
field: '<em>' + this.attrs.string + '</em>',
value: '<strong>' + _.str.escapeHTML(value) + '</strong>'});
return $.when([{
label: label,
facet: {
category: this.attrs.string,
field: this,
values: [{label: value, value: val}]
}
}]);
},
});
/**
* @class

View File

@ -176,9 +176,9 @@ openerp.testing = {};
});
QUnit.module(testing.current_module + '.' + name, {_oe: options});
body(testing.case);
body(testing['case']);
};
testing.case = function (name, options, callback) {
testing['case'] = function (name, options, callback) {
if (_.isFunction(options)) {
callback = options;
options = {};

View File

@ -91,6 +91,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
init: function(parent, dataset, view_id, options) {
var self = this;
this._super(parent);
this.ViewManager = parent;
this.set_default_options(options);
this.dataset = dataset;
this.model = dataset.model;
@ -720,6 +721,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return this.save().done(function(result) {
self.trigger("save", result);
self.to_view_mode();
}).then(function(result) {
self.ViewManager.ActionManager.__parentedParent.menu.do_reload_needaction();
});
},
on_button_cancel: function(event) {
@ -765,7 +768,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
this.has_been_loaded.done(function() {
if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
self.dataset.unlink([self.datarecord.id]).done(function() {
self.execute_pager_action('next');
if (self.dataset.size()) {
self.execute_pager_action('next');
} else {
self.do_action('history_back');
}
def.resolve();
});
} else {
@ -802,6 +809,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (save_obj.error)
return $.Deferred().reject();
return $.when.apply($, save_obj.ret);
}).done(function() {
self.$el.removeClass('oe_form_dirty');
});
},
_process_save: function(save_obj) {
@ -1021,7 +1030,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
return value;
}
var fields = _.chain(this.fields)
.map(function (field, name) {
.map(function (field) {
var value = field.get_value();
// ignore fields which are empty, invisible, readonly, o2m
// or m2m
@ -1036,7 +1045,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
}
return {
name: name,
name: field.name,
string: field.string,
value: value,
displayed: display(field, value),
@ -1047,10 +1056,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
.value();
var conditions = _.chain(self.fields)
.filter(function (field) { return field.field.change_default; })
.map(function (field, name) {
.map(function (field) {
var value = field.get_value();
return {
name: name,
name: field.name,
string: field.string,
value: value,
displayed: display(field, value),
@ -1444,6 +1453,9 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
$(this).children().each(function() {
var $td = $(this),
$child = $td.children(':first');
if ($child.attr('cell-class')) {
$td.addClass($child.attr('cell-class'));
}
switch ($child[0].tagName.toLowerCase()) {
case 'separator':
break;
@ -1520,7 +1532,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
if (! page.__ic)
return;
page.__ic.on("change:effective_invisible", null, function() {
if (!page.__ic.get('effective_invisible')) {
if (!page.__ic.get('effective_invisible') && page.autofocus) {
$new_notebook.tabs('select', i);
return;
}
@ -2126,7 +2138,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
value without triggering a re-rendering.
*/
internal_set_value: function(value_) {
var tmp = this.no_render;
var tmp = this.no_rerender;
this.no_rerender = true;
this.set({'value': value_});
this.no_rerender = tmp;
@ -2296,7 +2308,8 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function() {
this.$('input:first')[0].focus();
var input = this.$('input:first')[0];
return input ? input.focus() : false;
},
set_dimensions: function (height, width) {
this._super(height, width);
@ -2393,7 +2406,8 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
this._super.apply(this, [value_]);
},
focus: function () {
this.$('input:first').select();
var $input = this.$('input:first');
return $input.length ? $input.select() : false;
}
});
@ -2413,6 +2427,42 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
this.$input = this.$el.find('input.oe_datepicker_master');
this.$input_picker = this.$el.find('input.oe_datepicker_container');
$.datepicker.setDefaults({
clearText: _t('Clear'),
clearStatus: _t('Erase the current date'),
closeText: _t('Done'),
closeStatus: _t('Close without change'),
prevText: _t('<Prev'),
prevStatus: _t('Show the previous month'),
nextText: _t('Next>'),
nextStatus: _t('Show the next month'),
currentText: _t('Today'),
currentStatus: _t('Show the current month'),
monthNames: Date.CultureInfo.monthNames,
monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
monthStatus: _t('Show a different month'),
yearStatus: _t('Show a different year'),
weekHeader: _t('Wk'),
weekStatus: _t('Week of the year'),
dayNames: Date.CultureInfo.dayNames,
dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
dayNamesMin: Date.CultureInfo.shortestDayNames,
dayStatus: _t('Set DD as first week day'),
dateStatus: _t('Select D, M d'),
firstDay: Date.CultureInfo.firstDayOfWeek,
initStatus: _t('Select a date'),
isRTL: false
});
$.timepicker.setDefaults({
timeOnlyTitle: _t('Choose Time'),
timeText: _t('Time'),
hourText: _t('Hour'),
minuteText: _t('Minute'),
secondText: _t('Second'),
currentText: _t('Now'),
closeText: _t('Done')
});
this.picker({
onClose: this.on_picker_select,
onSelect: this.on_picker_select,
@ -2539,9 +2589,8 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc
return this.get('value') === '' || this._super();
},
focus: function() {
if (this.datewidget && this.datewidget.$input) {
this.datewidget.$input[0].focus();
}
var input = this.datewidget && this.datewidget.$input[0];
return input ? input.focus() : false;
},
set_dimensions: function (height, width) {
this._super(height, width);
@ -2622,9 +2671,8 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function($el) {
if (!this.get("effective_readonly") && this.$textarea) {
this.$textarea[0].focus();
}
var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
return input ? input.focus() : false;
},
set_dimensions: function (height, width) {
this._super(height, width);
@ -2707,7 +2755,8 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
this.$checkbox[0].checked = this.get('value');
},
focus: function() {
this.$checkbox[0].focus();
var input = this.$checkbox && this.$checkbox[0];
return input ? input.focus() : false;
}
});
@ -2793,7 +2842,8 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan
}
},
focus: function() {
this.$('select:first')[0].focus();
var input = this.$('select:first')[0];
return input ? input.focus() : false;
},
set_dimensions: function (height, width) {
this._super(height, width);
@ -3336,7 +3386,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
}
if (! no_recurse) {
var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
dataset.name_get([self.get("value")]).done(function(data) {
this.alive(dataset.name_get([self.get("value")])).done(function(data) {
self.display_value["" + self.get("value")] = data[0][1];
self.render_value(true);
});
@ -3401,9 +3451,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
return ! this.get("value");
},
focus: function () {
if (!this.get('effective_readonly')) {
this.$input && this.$input[0].focus();
}
var input = !this.get('effective_readonly') && this.$input && this.$input[0];
return input ? input.focus() : false;
},
_quick_create: function() {
this.no_ed = true;
@ -4263,7 +4312,8 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
this.set({'value': _.uniq(this.get('value').concat([id]))});
},
focus: function () {
this.$text[0].focus();
var input = this.$text && this.$text[0];
return input ? input.focus() : false;
},
set_dimensions: function (height, width) {
this._super(height, width);

View File

@ -578,7 +578,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
this.no_leaf = !!context['group_by_no_leaf'];
this.grouped = !!group_by;
return this.load_view(context).then(
return this.alive(this.load_view(context)).then(
this.proxy('reload_content'));
},
/**
@ -895,8 +895,9 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
this.record_callbacks = {
'remove': function (event, record) {
var $row = self.$current.children(
'[data-id=' + record.get('id') + ']');
var id = record.get('id');
self.dataset.remove_ids([id])
var $row = self.$current.children('[data-id=' + id + ']');
var index = $row.data('index');
$row.remove();
},

View File

@ -521,6 +521,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
return x;
}
});
this.ActionManager = parent;
this.views = {};
this.flags = flags || {};
this.registry = instance.web.views;
@ -1004,7 +1005,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
switch_mode: function (view_type, no_store, options) {
var self = this;
return $.when(this._super.apply(this, arguments)).done(function () {
return this.alive($.when(this._super.apply(this, arguments))).done(function () {
var controller = self.views[self.active_view].controller;
self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
view: controller,
@ -1252,6 +1253,7 @@ instance.web.View = instance.web.Widget.extend({
view_type: undefined,
init: function(parent, dataset, view_id, options) {
this._super(parent);
this.ViewManager = parent;
this.dataset = dataset;
this.view_id = view_id;
this.set_default_options(options);
@ -1323,7 +1325,6 @@ instance.web.View = instance.web.Widget.extend({
}
};
var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
var handler = function (action) {
if (action && action.constructor == Object) {
var ncontext = new instance.web.CompoundContext(context);
@ -1360,7 +1361,11 @@ instance.web.View = instance.web.Widget.extend({
}
}
args.push(context);
return dataset.call_button(action_data.name, args).then(handler);
return dataset.call_button(action_data.name, args).then(handler).then(function () {
if (self.ViewManager.ActionManager) {
self.ViewManager.ActionManager.__parentedParent.menu.do_reload_needaction();
}
});
} else if (action_data.type=="action") {
return this.rpc('/web/action/load', {
action_id: action_data.name,

View File

@ -650,7 +650,7 @@
<li t-if="section.name == 'files'" class="oe_sidebar_add_attachment">
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action">/web/binary/upload_attachment</t>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" t-att-value="widget.dataset and widget.dataset.model"/>
<input type="hidden" name="id" t-att-value="widget.model_id"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
@ -1552,8 +1552,9 @@
<t t-esc="attrs.string"/>
</button>
<ul t-name="SearchView.filters">
<li t-foreach="widget.filters" t-as="filter" t-if="filter.visible()"
t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
<li t-foreach="widget.filters" t-as="filter" t-if="!filter.visible || filter.visible()"
t-att-title="filter.attrs.string ? filter.attrs.help : undefined"
t-att-data-index="filter_index">
<t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
</li>
</ul>

View File

@ -0,0 +1,76 @@
openerp.testing.section('data.model.group_by', {
rpc: 'mock',
dependencies: ['web.data'],
}, function (test) {
var group_result = [{
bar: 3, bar_count: 5, __context: {}, __domain: [['bar', '=', 3]],
}, {
bar: 5, bar_count: 3, __context: {}, __domain: [['bar', '=', 5]],
}, {
bar: 8, bar_count: 0, __context: {}, __domain: [['bar', '=', 8]],
}];
test('basic', {asserts: 7}, function (instance, $fix, mock) {
var m = new instance.web.Model('foo');
mock('foo:read_group', function (args, kwargs) {
deepEqual(kwargs.fields, ['bar'],
"should read grouping field");
deepEqual(kwargs.groupby, ['bar'],
"should have single grouping field");
return group_result;
});
mock('/web/dataset/search_read', function (args) {
deepEqual(args.params.domain, [['bar', '=', 3]],
"should have domain matching that of group_by result");
return {records: [
{bar: 3, id: 1},
{bar: 3, id: 2},
{bar: 3, id: 4},
{bar: 3, id: 8},
{bar: 3, id: 16}
], length: 5};
});
return m.query().group_by('bar')
.then(function (groups) {
ok(groups, "should have data");
equal(groups.length, 3, "should have three results");
var first = groups[0];
ok(first.attributes.has_children, "should have children");
return first.query().all();
}).done(function (first) {
equal(first.length, 5, "should have 5 records")
});
});
test('noleaf', {asserts: 5}, function (instance, $fix, mock) {
var m = new instance.web.Model('foo', {group_by_no_leaf: true});
mock('foo:read_group', function (args, kwargs) {
deepEqual(kwargs.fields, ['bar'],
"should read grouping field");
deepEqual(kwargs.groupby, ['bar'],
"should have single grouping field");
return group_result;
});
return m.query().group_by('bar')
.then(function (groups) {
ok(groups, "should have data");
equal(groups.length, 3, "should have three results");
ok(!groups[0].attributes.has_children,
"should not have children because no_leaf");
})
});
test('nogroup', {rpc: false}, function (instance, $f, mock) {
var m = new instance.web.Model('foo');
strictEqual(m.query().group_by(), null, "should not group");
});
test('empty.noleaf', {asserts: 1}, function (instance, $f, mock) {
var m = new instance.web.Model('foo', {group_by_no_leaf: true});
mock('foo:read_group', function (args, kwargs) {
return [{__context: [], __domain: []}];
});
return m.query().group_by().done(function (groups) {
strictEqual(groups.length, 1,
"should generate a single fake-ish group");
});
});
});

View File

@ -614,6 +614,59 @@ openerp.testing.section('search.completions', {
{relation: 'dummy.model'}, view);
return f.complete("bob");
});
test('Integer: invalid', {asserts: 1}, function (instance) {
var view = {inputs: []};
var f = new instance.web.search.IntegerField(
{attrs: {string: "Dummy"}}, {}, view);
return f.complete("qux")
.done(function (completions) {
ok(!completions, "non-number => no completion");
});
});
test('Integer: non-zero', {asserts: 5}, function (instance) {
var view = {inputs: []};
var f = new instance.web.search.IntegerField(
{attrs: {string: "Dummy"}}, {}, view);
return f.complete("-2")
.done(function (completions) {
equal(completions.length, 1, "number fields provide 1 completion only");
var facet = new instance.web.search.Facet(completions[0].facet);
equal(facet.get('category'), f.attrs.string);
equal(facet.get('field'), f);
var value = facet.values.at(0);
equal(value.get('label'), "-2");
equal(value.get('value'), -2);
});
});
test('Integer: zero', {asserts: 3}, function (instance) {
var view = {inputs: []};
var f = new instance.web.search.IntegerField(
{attrs: {string: "Dummy"}}, {}, view);
return f.complete("0")
.done(function (completions) {
equal(completions.length, 1, "number fields provide 1 completion only");
var facet = new instance.web.search.Facet(completions[0].facet);
var value = facet.values.at(0);
equal(value.get('label'), "0");
equal(value.get('value'), 0);
});
});
test('Float: non-zero', {asserts: 5}, function (instance) {
var view = {inputs: []};
var f = new instance.web.search.FloatField(
{attrs: {string: "Dummy"}}, {}, view);
return f.complete("42.37")
.done(function (completions) {
equal(completions.length, 1, "float fields provide 1 completion only");
var facet = new instance.web.search.Facet(completions[0].facet);
equal(facet.get('category'), f.attrs.string);
equal(facet.get('field'), f);
var value = facet.values.at(0);
equal(value.get('label'), "42.37");
equal(value.get('value'), 42.37);
});
});
});
openerp.testing.section('search.serialization', {
dependencies: ['web.search'],
@ -1361,7 +1414,7 @@ openerp.testing.section('search.invisible', {
}, ['<search>',
'<field name="field0"/>',
'<field name="field1" modifiers="{&quot;invisible&quot;: true}"/>',
'</search>'].join());
'</search>'].join(''));
return view.appendTo($fix)
.then(function () {
var done = $.Deferred();
@ -1380,7 +1433,7 @@ openerp.testing.section('search.invisible', {
'<search>',
'<filter string="filter 0"/>',
'<filter string="filter 1" modifiers="{&quot;invisible&quot;: true}"/>',
'</search>'].join());
'</search>'].join(''));
return view.appendTo($fix)
.then(function () {
var $fs = $fix.find('.oe_searchview_filters ul');
@ -1400,6 +1453,26 @@ openerp.testing.section('search.invisible', {
return done;
});
});
test('invisible-previous-sibling', {asserts: 3}, function (instance, $fix, mock) {
var view = makeView(instance, mock, {}, [
'<search>',
'<filter string="filter 0" context="{&quot;test&quot;: 0}"/>',
'<filter string="filter 1" modifiers="{&quot;invisible&quot;: true}" context="{&quot;test&quot;: 1}"/>',
'<filter string="filter 2" modifiers="{&quot;invisible&quot;: true}" context="{&quot;test&quot;: 2}"/>',
'<filter string="filter 3" context="{&quot;test&quot;: 3}"/>',
'</search>'].join(''));
return view.appendTo($fix)
.done(function () {
// Select filter 3
$fix.find('.oe_searchview_filters ul li:contains("filter 3")').click();
equal(view.query.length, 1, "should have selected a filter");
var facet = view.query.at(0);
strictEqual(facet.values.at(0).get('label'), "filter 3",
"should have correctly labelled the facet");
deepEqual(view.build_search_data().contexts, [{test: 3}],
"should have built correct context");
});
});
// Invisible filter groups should not appear in the drawer
// Group invisibility should be inherited by children
test('group-invisibility', {asserts: 6}, function (instance, $fix, mock) {

View File

@ -265,6 +265,12 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
//To parse Events we have to convert date Format
var res_events = [],
sidebar_items = {};
var selection_label = {};
if(this.fields[this.color_field].selection) {
_(this.fields[this.color_field].selection).each(function(value){
selection_label[value[0]] = value[1];
});
}
for (var e = 0; e < events.length; e++) {
var evt = events[e];
if (!evt[this.date_start]) {
@ -274,6 +280,9 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
if (this.color_field) {
var filter = evt[this.color_field];
if (filter) {
if(this.fields[this.color_field].selection) {
filter = selection_label[filter];
}
var filter_value = (typeof filter === 'object') ? filter[0] : filter;
if (typeof(fn_filter) === 'function' && !fn_filter(filter_value)) {
continue;
@ -339,9 +348,13 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
},
get_event_data: function(event_obj) {
var data = {
name: event_obj.text
name: event_obj.text || scheduler.locale.labels.new_event
};
data[this.date_start] = instance.web.datetime_to_str(event_obj.start_date);
if (this.fields[this.date_start].type == 'date') {
data[this.date_start] = instance.web.date_to_str(event_obj.start_date)
}else {
data[this.date_start] = instance.web.datetime_to_str(event_obj.start_date)
}
if (this.date_stop) {
data[this.date_stop] = instance.web.datetime_to_str(event_obj.end_date);
}

View File

@ -1,6 +1,6 @@
import openerp
class DiagramView(openerp.addons.web.controllers.main.View):
class DiagramView(openerp.addons.web.http.Controller):
_cp_path = "/web_diagram/diagram"
@openerp.addons.web.http.jsonrequest

File diff suppressed because it is too large Load Diff

View File

@ -80,6 +80,8 @@
position: relative
top: +8px
font-weight: bold
.oe_kanban_header:hover .oe_kanban_group_length
display: none
.ui-sortable-placeholder
border: 1px solid rgba(0,0,0,0.1)
visibility: visible !important

View File

@ -261,10 +261,13 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
var remaining = groups.length - 1,
groups_array = [];
return $.when.apply(null, _.map(groups, function (group, index) {
var def = $.when([]);
var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
new instance.web.CompoundContext(self.dataset.get_context(), group.model.context()), group.model.domain());
return dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit })
.then(function (records) {
if (group.attributes.length >= 1) {
def = dataset.read_slice(self.fields_keys.concat(['__last_update']), { 'limit': self.limit });
}
return def.then(function(records) {
self.nb_records += records.length;
self.dataset.ids.push.apply(self.dataset.ids, dataset.ids);
groups_array[index] = new instance.web_kanban.KanbanGroup(self, records, group, dataset);

View File

@ -15,7 +15,7 @@ instance.web.ViewManagerAction.include({
}
evt.currentTarget.selectedIndex = 0;
}else{
return this._super.apply(this,arguments);
return this._super.apply(this,arguments);
}
}
});
@ -232,10 +232,11 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
return main_object;
},
parse_xml: function(arch, view_id) {
//First element of att_list must be element tagname.
main_object = {
'level': 0,
'id': this.xml_element_id +=1,
'att_list': [],
'att_list': ["view"],
'name': _.str.sprintf("<view view_id = %s>", view_id),
'child_id': []
};
@ -535,15 +536,22 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null);
parent_tr = self.get_object_by_id(parseInt($(parent_tr).attr('id').replace(/[^0-9]+/g, '')), this.one_object['main_object'], [])[0].att_list[0];
_.each([tr, parent_tr],function(element) {
var value = _.has(_CHILDREN, element) ? element : _.str.include(html_tag, element)?"html_tag":false;
var value = _.has(_CHILDREN, element) ? element : _.str.include(html_tag, element)?"html_tag":false;
property_to_check.push(value);
});
field_dataset.call( 'fields_get', []).done(function(result) {
var fields = _.keys(result);
fields.push(" "),fields.sort();
self.on_add_node(property_to_check, fields);
self.on_add_node(property_to_check, fields, self.inject_position(parent_tr,tr));
});
},
inject_position : function(parent_tag,current_tag){
if(parent_tag == "view")
return ['Inside'];
if(current_tag == "field")
return ['After','Before'];
return ['After','Before','Inside'];
},
do_node_edit: function(side) {
var self = this;
var result = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], []);
@ -637,12 +645,12 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
var children = _.filter(xml_arch.childNodes[0].childNodes, function (child) {
return child.nodeType == 1;
});
arch.arch = _.detect(children, function(xml_child) {
var inherited_view = _.detect(children, function(xml_child) {
var temp_obj = self.create_View_Node(xml_child),
insert = _.intersection(_.flatten(temp_obj.att_list),_.uniq(check_list));
if (insert.length == _.uniq(check_list).length ) {return xml_child;}
});
xml_arch = QWeb.load_xml(arch.arch);
xml_arch = QWeb.load_xml(instance.web.xml_to_str(inherited_view));
}
return self.do_save_xml(xml_arch.documentElement, obj[0].child_id[0],obj[0].child_id, move_direct, update_values,arch);
},
@ -941,11 +949,11 @@ instance.web_view_editor.ViewEditor = instance.web.Widget.extend({
});
return def.promise();
},
on_add_node: function(properties, fields){
on_add_node: function(properties, fields, position){
var self = this;
var render_list = [{'name': 'node_type','selection': _.keys(_CHILDREN).sort(), 'value': 'field', 'string': 'Node Type','type': 'selection'},
{'name': 'field_value','selection': fields, 'value': false, 'string': '','type': 'selection'},
{'name': 'position','selection': ['After','Before','Inside'], 'value': false, 'string': 'Position','type': 'selection'}];
{'name': 'position','selection': position, 'value': false, 'string': 'Position','type': 'selection'}];
this.add_widget = [];
this.add_node_dialog = new instance.web.Dialog(this,{
title: _t("Properties"),
@ -1186,7 +1194,7 @@ var _CHILDREN = {
//e.g.:xyz 'td' : ['field']
};
// Generic html_tag list and can be added html tag in future. It's support above _CHILDREN dict's *html_tag* by default.
// For specific child node one has to define tag above and specify children tag in list. Like above xyz example.
// For specific child node one has to define tag above and specify children tag in list. Like above xyz example.
var html_tag = ['div','h1','h2','h3','h4','h5','h6','td','tr'];
var _ICONS = ['','STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD',