[MERGE] from trunk

bzr revid: chm@openerp.com-20130326090427-g78bigwi30rzbthd
This commit is contained in:
Christophe Matthieu 2013-03-26 10:04:27 +01:00
commit 7e895d90cb
337 changed files with 26260 additions and 22306 deletions

View File

@ -26,6 +26,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/spinjs/spin.js",
"static/lib/jquery.autosize/jquery.autosize.js",
"static/lib/jquery.blockUI/jquery.blockUI.js",
"static/lib/jquery.placeholder/jquery.placeholder.js",
"static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js",
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
"static/lib/jquery.ui.notify/js/jquery.notify.js",

View File

@ -15,6 +15,8 @@ import simplejson
import time
import urllib
import urllib2
import urlparse
import xmlrpclib
import zlib
from xml.etree import ElementTree
from cStringIO import StringIO
@ -91,16 +93,50 @@ def db_list(req):
dbs = [i for i in dbs if re.match(r, i)]
return dbs
def db_monodb(req):
# if only one db exists, return it else return False
def db_monodb_redirect(req):
db = False
redirect = False
# 1 try the db in the url
db_url = req.params.get('db')
if db_url:
return (db_url, False)
try:
dbs = db_list(req)
if len(dbs) == 1:
return dbs[0]
except Exception:
# ignore access denied
pass
return False
dbs = []
# 2 use the database from the cookie if it's listable and still listed
cookie_db = req.httprequest.cookies.get('last_used_database')
if cookie_db in dbs:
db = cookie_db
# 3 use the first db
if dbs and not db:
db = dbs[0]
# redirect to the chosen db if multiple are available
if db and len(dbs) > 1:
query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True))
query.update({ 'db': db })
redirect = req.httprequest.path + '?' + urllib.urlencode(query)
return (db, redirect)
def db_monodb(req):
# if only one db exists, return it else return False
return db_monodb_redirect(req)[0]
def redirect_with_hash(req, url, code=303):
if req.httprequest.user_agent.browser == 'msie':
try:
version = float(req.httprequest.user_agent.version)
if version < 10:
return "<html><head><script>window.location = '%s#' + location.hash;</script></head></html>" % url
except Exception:
pass
return werkzeug.utils.redirect(url, code)
def module_topological_sort(modules):
""" Return a list of module names sorted so that their dependencies of the
@ -291,6 +327,10 @@ def manifest_glob(req, extension, addons=None, db=None):
return r
def manifest_list(req, extension, mods=None, db=None):
""" list ressources to load specifying either:
mods: a comma separated string listing modules
db: a database name (return all installed modules in that database)
"""
if not req.debug:
path = '/web/webclient/' + extension
if mods is not None:
@ -299,12 +339,7 @@ def manifest_list(req, extension, mods=None, db=None):
path += '?' + urllib.urlencode({'db': db})
return [path]
files = manifest_glob(req, extension, addons=mods, db=db)
i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
if i_am_diabetic:
return [wp for _fp, wp in files]
else:
return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
return [wp for _fp, wp in files]
def get_last_modified(files):
""" Returns the modification time of the most recently modified
@ -534,6 +569,10 @@ class Home(openerpweb.Controller):
@openerpweb.httprequest
def index(self, req, s_action=None, db=None, **kw):
db, redir = db_monodb_redirect(req)
if redir:
return redirect_with_hash(req, redir)
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
@ -869,7 +908,7 @@ class Session(openerpweb.Controller):
"""
saved_actions = req.httpsession.get('saved_actions')
if not saved_actions:
saved_actions = {"next":0, "actions":{}}
saved_actions = {"next":1, "actions":{}}
req.httpsession['saved_actions'] = saved_actions
# we don't allow more than 10 stored actions
if len(saved_actions["actions"]) >= 10:

View File

@ -107,6 +107,12 @@ formatted differently). If an input *may* fetch multiple completion
items, it *should* prefix those with a section title using its own
name. This has no technical consequence but is clearer for users.
.. note::
If a field is :js:func:`invisible
<openerp.web.search.Input.visible>`, its completion function will
*not* be called.
Providing drawer/supplementary UI
+++++++++++++++++++++++++++++++++
@ -145,6 +151,11 @@ started only once (per view).
dynamically collects, lays out and renders filters? =>
exercises drawer thingies
.. note::
An :js:func:`invisible <openerp.web.search.Input.visible>` input
will not be inserted into the drawer.
Converting from facet objects
+++++++++++++++++++++++++++++

View File

@ -92,6 +92,14 @@ class WebRequest(object):
if not self.session:
self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session
# set db/uid trackers - they're cleaned up at the WSGI
# dispatching phase in openerp.service.wsgi_server.application
if self.session._db:
threading.current_thread().dbname = self.session._db
if self.session._uid:
threading.current_thread().uid = self.session._uid
self.context = self.params.pop('context', {})
self.debug = self.params.pop('debug', False) is not False
# Determine self.lang

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2602
addons/web/i18n/lv.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@ Date.CultureInfo = {
firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"],
/* Month Name Strings */
monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
abbreviatedMonthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"],
monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
abbreviatedMonthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"],
/* AM/PM Designators */
amDesignator: "ص",
@ -82,18 +82,18 @@ Date.CultureInfo = {
* As well, please review the list of "Future Strings" section below.
*/
regexPatterns: {
jan: /^كانون الثاني/i,
feb: /^شباط/i,
mar: /^آذار/i,
apr: /^نيسان/i,
may: /^أيار/i,
jun: /^حزيران/i,
jul: /^تموز/i,
aug: /^آب/i,
sep: /^أيلول/i,
oct: /^تشرين الأول/i,
nov: /^تشرين الثاني/i,
dec: /^كانون الأول/i,
jan: /^يناير/i,
feb: /^فبراير/i,
mar: /^مارس/i,
apr: /^أبريل/i,
may: /^مايو/i,
jun: /^يونيو/i,
jul: /^يوليو/i,
aug: /^أغسطس/i,
sep: /^سبتمبر/i,
oct: /^أكتوبر/i,
nov: /^نوفمبر/i,
dec: /^ديسمبر/i,
sun: /^الاحد/i,
mon: /^ا(1)?/i,
@ -192,4 +192,4 @@ Date.CultureInfo = {
* end end
* long long
* short short
*/
*/

View File

@ -0,0 +1,157 @@
/*! http://mths.be/placeholder v2.0.7 by @mathias */
;(function(window, document, $) {
var isInputSupported = 'placeholder' in document.createElement('input'),
isTextareaSupported = 'placeholder' in document.createElement('textarea'),
prototype = $.fn,
valHooks = $.valHooks,
hooks,
placeholder;
if (isInputSupported && isTextareaSupported) {
placeholder = prototype.placeholder = function() {
return this;
};
placeholder.input = placeholder.textarea = true;
} else {
placeholder = prototype.placeholder = function() {
var $this = this;
$this
.filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]')
.not('.placeholder')
.bind({
'focus.placeholder': clearPlaceholder,
'blur.placeholder': setPlaceholder
})
.data('placeholder-enabled', true)
.trigger('blur.placeholder');
return $this;
};
placeholder.input = isInputSupported;
placeholder.textarea = isTextareaSupported;
hooks = {
'get': function(element) {
var $element = $(element);
return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value;
},
'set': function(element, value) {
var $element = $(element);
if (!$element.data('placeholder-enabled')) {
return element.value = value;
}
if (value == '') {
element.value = value;
// Issue #56: Setting the placeholder causes problems if the element continues to have focus.
if (element != document.activeElement) {
// We can't use `triggerHandler` here because of dummy text/password inputs :(
setPlaceholder.call(element);
}
} else if ($element.hasClass('placeholder')) {
clearPlaceholder.call(element, true, value) || (element.value = value);
} else {
element.value = value;
}
// `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363
return $element;
}
};
isInputSupported || (valHooks.input = hooks);
isTextareaSupported || (valHooks.textarea = hooks);
$(function() {
// Look for forms
$(document).delegate('form', 'submit.placeholder', function() {
// Clear the placeholder values so they don't get submitted
var $inputs = $('.placeholder', this).each(clearPlaceholder);
setTimeout(function() {
$inputs.each(setPlaceholder);
}, 10);
});
});
// Clear placeholder values upon page reload
$(window).bind('beforeunload.placeholder', function() {
$('.placeholder').each(function() {
this.value = '';
});
});
}
function args(elem) {
// Return an object of element attributes
var newAttrs = {},
rinlinejQuery = /^jQuery\d+$/;
$.each(elem.attributes, function(i, attr) {
if (attr.specified && !rinlinejQuery.test(attr.name)) {
newAttrs[attr.name] = attr.value;
}
});
return newAttrs;
}
function clearPlaceholder(event, value) {
var input = this,
$input = $(input);
if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) {
if ($input.data('placeholder-password')) {
$input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id'));
// If `clearPlaceholder` was called from `$.valHooks.input.set`
if (event === true) {
return $input[0].value = value;
}
$input.focus();
} else {
input.value = '';
$input.removeClass('placeholder');
input == document.activeElement && input.select();
}
}
}
function setPlaceholder() {
var $replacement,
input = this,
$input = $(input),
$origInput = $input,
id = this.id;
if (input.value == '') {
if (input.type == 'password') {
if (!$input.data('placeholder-textinput')) {
try {
$replacement = $input.clone().attr({ 'type': 'text' });
} catch(e) {
$replacement = $('<input>').attr($.extend(args(this), { 'type': 'text' }));
}
$replacement
.removeAttr('name')
.data({
'placeholder-password': true,
'placeholder-id': id
})
.bind('focus.placeholder', clearPlaceholder);
$input
.data({
'placeholder-textinput': $replacement,
'placeholder-id': id
})
.before($replacement);
}
$input = $input.removeAttr('id').hide().prev().attr('id', id).show();
// Note: `$input[0] != input` now!
}
$input.addClass('placeholder');
$input[0].value = $input.attr('placeholder');
} else {
$input.removeClass('placeholder');
}
}
}(this, document, jQuery));

View File

@ -355,6 +355,12 @@
float: right;
margin-left: 8px;
}
.openerp .oe_text_center {
text-align: center;
}
.openerp .oe_text_left {
text-align: left;
}
.openerp .oe_text_right {
text-align: right;
}
@ -1806,7 +1812,7 @@
}
.openerp .oe_searchview .oe_searchview_drawer {
position: absolute;
z-index: 100;
z-index: 2;
margin-top: 4px;
top: 100%;
right: -1px;
@ -2228,6 +2234,26 @@
.openerp .oe_form .oe_subtotal_footer label.oe_form_label_help {
font-weight: normal;
}
.openerp .oe_form .oe_form_box_info {
background: #ffee99;
border-bottom: 1px solid #ccbb66;
padding: 4px;
}
.openerp .oe_form .oe_form_box_info > p {
margin: auto;
}
.openerp .oe_form .oe_form_box_warning {
background: #bd362f;
border-bottom: 1px solid #990000;
padding: 4px;
}
.openerp .oe_form .oe_form_box_warning * {
color: white;
text-shadow: none;
}
.openerp .oe_form .oe_form_box_warning > p {
margin: auto;
}
.openerp .oe_form .oe_form_button {
margin: 2px;
}
@ -2330,6 +2356,13 @@
.openerp .oe_form .oe_form_field_text {
width: 100%;
}
.openerp .oe_form .oe_form_field_text .oe_form_text_content {
text-overflow: ellipsis;
display: inline-block;
white-space: pre-wrap;
overflow-x: hidden;
width: 100%;
}
.openerp .oe_form .oe_form_field_char input,
.openerp .oe_form .oe_form_field_url input,
.openerp .oe_form .oe_form_field_email input,
@ -3187,6 +3220,14 @@ div.ui-widget-overlay {
border-radius: 3px;
}
.openerp .db_option_table td {
padding-bottom: 10px !important;
}
.openerp_ie .placeholder {
color: #afafb6 !important;
font-style: italic !important;
}
.openerp_ie .oe_form_field_boolean input {
background: white;
}
@ -3205,6 +3246,9 @@ div.ui-widget-overlay {
padding-top: 0;
padding-bottom: 0;
}
.openerp_ie .oe_view_manager_view_kanban {
display: table-cell;
}
.openerp_ie .oe_view_manager_buttons button.oe_write_full {
padding-top: 0;
padding-bottom: 0;
@ -3353,7 +3397,6 @@ div.ui-widget-overlay {
overflow: hidden !important;
}
}
.blockUI.blockOverlay {
background-color: black;
opacity: 0.6;

View File

@ -370,6 +370,10 @@ $sheet-padding: 16px
.oe_right
float: right
margin-left: 8px
.oe_text_center
text-align: center
.oe_text_left
text-align: left
.oe_text_right
text-align: right
.oe_clear
@ -1453,7 +1457,7 @@ $sheet-padding: 16px
.oe_searchview_drawer
position: absolute
z-index: 100
z-index: 2
// detach drawer from field slightly
margin-top: 4px
top: 100%
@ -1777,7 +1781,21 @@ $sheet-padding: 16px
padding: 2px 11px 2px 0px !important
label.oe_form_label_help
font-weight: normal
.oe_form_box_info
background: #fe9
border-bottom: 1px solid #cb6
padding: 4px
> p
margin: auto
.oe_form_box_warning
background: #bd362f
border-bottom: 1px solid #900
padding: 4px
*
color: white
text-shadow: none
> p
margin: auto
// }}}
// FormView.group {{{
.oe_form
@ -1849,6 +1867,12 @@ $sheet-padding: 16px
.oe_form
.oe_form_field_text
width: 100%
.oe_form_text_content
text-overflow: ellipsis
display: inline-block
white-space: pre-wrap
overflow-x: hidden
width: 100%
.oe_form_field_char input,
.oe_form_field_url input,
.oe_form_field_email input,
@ -2512,8 +2536,16 @@ div.ui-widget-overlay
@include radius(3px)
// }}}
.openerp
.db_option_table
td
padding-bottom: 10px !important
// Internet Explorer 9+ specifics {{{
.openerp_ie
.placeholder
color: $tag-border !important
font-style: italic !important
.oe_form_field_boolean input
background: #fff
.db_option_table .oe_form_field_selection
@ -2529,6 +2561,8 @@ div.ui-widget-overlay
button.oe_highlight
padding-top: 0
padding-bottom: 0
.oe_view_manager_view_kanban
display: table-cell
.oe_view_manager_buttons
button.oe_write_full
padding-top: 0

View File

@ -530,7 +530,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
'login': 'admin',
'password': form_obj['create_admin_pwd'],
'login_successful': function() {
self.do_action("reload");
var action = {
type: "ir.actions.client",
tag: 'reload',
params: {
url_search : {
db: form_obj['db_name'],
},
}
};
self.do_action(action);
},
},
_push_me: false,
@ -641,6 +650,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage
instance.web.Login = instance.web.Widget.extend({
template: "Login",
remember_credentials: true,
events: {
'change input[name=db],select[name=db]': function(ev) {
this.set('database_selector', $(ev.currentTarget).val());
},
},
init: function(parent, action) {
this._super(parent);
@ -652,18 +666,18 @@ instance.web.Login = instance.web.Widget.extend({
if (_.isEmpty(this.params)) {
this.params = $.bbq.getState(true);
}
if (action && action.params && action.params.db) {
this.params.db = action.params.db;
} else if ($.deparam.querystring().db) {
this.params.db = $.deparam.querystring().db;
}
if (this.params.db) {
this.selected_db = this.params.db;
}
if (this.params.login_successful) {
this.on('login_successful', this, this.params.login_successful);
}
if (this.has_local_storage && this.remember_credentials) {
this.selected_db = localStorage.getItem('last_db_login_success');
this.selected_login = localStorage.getItem('last_login_login_success');
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
this.selected_password = localStorage.getItem('last_password_login_success');
}
}
},
start: function() {
var self = this;
@ -671,10 +685,10 @@ instance.web.Login = instance.web.Widget.extend({
self.$el.find('.oe_login_manage_db').click(function() {
self.do_action("database_manager");
});
self.on('change:database_selector', this, function() {
this.database_selected(this.get('database_selector'));
});
var d = $.when();
if ($.deparam.querystring().db) {
self.params.db = $.deparam.querystring().db;
}
if ($.param.fragment().token) {
self.params.token = $.param.fragment().token;
}
@ -682,16 +696,44 @@ instance.web.Login = instance.web.Widget.extend({
if (self.params.db && self.params.login && self.params.password) {
d = self.do_login(self.params.db, self.params.login, self.params.password);
} else {
if (self.params.db) {
self.on_db_loaded([self.params.db])
} else {
d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed);
}
d = self.rpc("/web/database/get_list", {})
.done(self.on_db_loaded)
.fail(self.on_db_failed)
.always(function() {
if (self.selected_db && self.has_local_storage && self.remember_credentials) {
self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
if (self.session.debug) {
self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
}
}
});
}
return d;
},
remember_last_used_database: function(db) {
// This cookie will be used server side in order to avoid db reloading on first visit
var ttl = 24 * 60 * 60 * 365;
document.cookie = [
'last_used_database=' + db,
'path=/',
'max-age=' + ttl,
'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString()
].join(';');
},
database_selected: function(db) {
var params = $.deparam.querystring();
params.db = db;
this.remember_last_used_database(db);
this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
this.$('[name=login], [name=password]').prop('readonly', true);
instance.web.redirect('/?' + $.param(params));
},
on_db_loaded: function (result) {
var self = this;
this.db_list = result;
if (!this.selected_db) {
this.selected_db = result[0];
}
this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
if(this.db_list.length === 0) {
this.do_action("database_manager");
@ -732,17 +774,11 @@ instance.web.Login = instance.web.Widget.extend({
self.hide_error();
self.$(".oe_login_pane").fadeOut("slow");
return this.session.session_authenticate(db, login, password).then(function() {
if (self.has_local_storage) {
if(self.remember_credentials) {
localStorage.setItem('last_db_login_success', db);
localStorage.setItem('last_login_login_success', login);
if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
localStorage.setItem('last_password_login_success', password);
}
} else {
localStorage.setItem('last_db_login_success', '');
localStorage.setItem('last_login_login_success', '');
localStorage.setItem('last_password_login_success', '');
self.remember_last_used_database(db);
if (self.has_local_storage && self.remember_credentials) {
localStorage.setItem(db + '|last_login', login);
if (self.session.debug) {
localStorage.setItem(db + '|last_password', password);
}
}
self.trigger('login_successful');
@ -800,6 +836,9 @@ instance.web.Reload = function(parent, action) {
var sobj = $.deparam(l.search.substr(1));
sobj.ts = new Date().getTime();
if (params.url_search) {
sobj = _.extend(sobj, params.url_search);
}
var search = '?' + $.param(sobj);
var hash = l.hash;
@ -1303,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({
},
logo_edit: function(ev) {
var self = this;
new self.alive(instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
result.res_id = res['company_id'][0];
result.target = "new";
@ -1500,8 +1539,9 @@ instance.web.EmbeddedClient = instance.web.Client.extend({
});
},
do_action: function(action) {
return this.action_manager.do_action(action);
do_action: function(/*...*/) {
var am = this.action_manager;
return am.do_action.apply(am, arguments);
},
authenticate: function() {

View File

@ -232,13 +232,17 @@ instance.web.ParentedMixin = {
Utility method to only execute asynchronous actions if the current
object has not been destroyed.
@param {Promise} promise The promise representing the asynchronous action.
@param {bool} reject Defaults to false. If true, the returned promise will be
rejected with no arguments if the current object is destroyed. If false,
the returned promise will never be resolved nor rejected.
@returns {Promise} A promise that will mirror the given promise if everything goes
fine but will either be rejected with no arguments or never resolved if the
current object is destroyed.
@param {$.Deferred} promise The promise representing the asynchronous
action.
@param {bool} [reject=false] If true, the returned promise will be
rejected with no arguments if the current
object is destroyed. If false, the
returned promise will never be resolved
or rejected.
@returns {$.Deferred} A promise that will mirror the given promise if
everything goes fine but will either be rejected
with no arguments or never resolved if the
current object is destroyed.
*/
alive: function(promise, reject) {
var def = $.Deferred();
@ -697,10 +701,10 @@ instance.web.Widget = instance.web.Controller.extend({
* Method called after rendering. Mostly used to bind actions, perform asynchronous
* calls, etc...
*
* By convention, the method should return a promise to inform the caller when
* this widget has been initialized.
* By convention, this method should return an object that can be passed to $.when()
* to inform the caller when this widget has been initialized.
*
* @returns {jQuery.Deferred}
* @returns {jQuery.Deferred or any}
*/
start: function() {
return $.when();

View File

@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
});
},
session_is_valid: function() {
var db = $.deparam.querystring().db;
if (db && this.db !== db) {
return false;
}
return !!this.uid;
},
/**

View File

@ -481,9 +481,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
* 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 {$.Deferred}
*/
create: function(data) {
create: function(data, options) {
return this._model.call('create', [data], {context: this.get_context()});
},
/**
@ -491,8 +494,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin,
*
* @param {Number|String} id identifier for the record to alter
* @param {Object} data field values to write into the record
* @param {Function} callback function called with operation result
* @param {Function} error_callback function called in case of write error
* @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 {$.Deferred}
*/
write: function (id, data, options) {
@ -732,10 +737,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
self.last_default_get = res;
});
},
create: function(data) {
var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
defaults: this.last_default_get};
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)}));
create: function(data, options) {
var cached = {
id:_.uniqueId(this.virtual_id_prefix),
values: _.extend({}, data, (options || {}).readonly_fields || {}),
defaults: this.last_default_get
};
this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)}));
this.cache.push(cached);
return $.Deferred().resolve(cached.id).promise();
},
@ -762,7 +770,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
cached = {id: id, values: {}};
this.cache.push(cached);
}
$.extend(cached.values, record.values);
$.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {}));
if (dirty)
this.trigger("dataset_changed", id, data, options);
return $.Deferred().resolve(true).promise();
@ -860,8 +868,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
}
return completion.promise();
},
call_button: function (method, args) {
var id = args[0][0], index;
/**
* Invalidates caching of a record in the dataset to ensure the next read
* of that record will hit the server.
*
* Of use when an action is going to remote-alter a record which will then
* need to be reloaded, e.g. action button.
*
* @param {Object} id record to remove from the BDS's cache
*/
evict_record: function (id) {
for(var i=0, len=this.cache.length; i<len; ++i) {
var record = this.cache[i];
// if record we call the button upon is in the cache
@ -871,8 +887,15 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
break;
}
}
},
call_button: function (method, args) {
this.evict_record(args[0][0]);
return this._super(method, args);
},
exec_workflow: function (id, signal) {
this.evict_record(id);
return this._super(id, signal);
},
alter_ids: function(n_ids) {
this._super(n_ids);
this.trigger("dataset_changed", n_ids);
@ -903,9 +926,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({
return this._super.apply(this, arguments);
}
},
create: function(data) {
create: function(data, options) {
if (this.create_function) {
return this.create_function(data, this._super);
return this.create_function(data, options, this._super);
} else {
return this._super.apply(this, arguments);
}
@ -946,7 +969,10 @@ instance.web.CompoundContext = instance.web.Class.extend({
},
get_eval_context: function () {
return this.__eval_context;
}
},
eval: function() {
return instance.web.pyeval.eval('context', this, undefined, {no_user_context: true});
},
});
instance.web.CompoundDomain = instance.web.Class.extend({
@ -969,7 +995,10 @@ instance.web.CompoundDomain = instance.web.Class.extend({
},
get_eval_context: function() {
return this.__eval_context;
}
},
eval: function() {
return instance.web.pyeval.eval('domain', this);
},
});
instance.web.DropMisordered = instance.web.Class.extend({

View File

@ -502,12 +502,15 @@ openerp.web.pyeval = function (instance) {
return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
});
var args = _.map(('year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday').split(' '), function (arg) {
return [arg, null]
});
args.unshift('*');
var relativedelta = py.type('relativedelta', null, {
__init__: function () {
this.ops = py.PY_parseArgs(arguments,
'* year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday');
this.ops = py.PY_parseArgs(arguments, args);
},
__add__: function (other) {
if (!py.PY_isInstance(other, datetime.date)) {
@ -600,7 +603,7 @@ openerp.web.pyeval = function (instance) {
});
var eval_contexts = function (contexts, evaluation_context) {
evaluation_context = evaluation_context || {};
evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
return _(contexts).reduce(function (result_context, ctx) {
// __eval_context evaluations can lead to some of `contexts`'s
// values being null, skip them as well as empty contexts
@ -625,9 +628,10 @@ openerp.web.pyeval = function (instance) {
// siblings
_.extend(evaluation_context, evaluated);
return _.extend(result_context, evaluated);
}, _.extend({}, instance.session.user_context));
}, {});
};
var eval_domains = function (domains, evaluation_context) {
evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
var result_domain = [];
_(domains).each(function (domain) {
if (_.isString(domain)) {
@ -654,6 +658,7 @@ openerp.web.pyeval = function (instance) {
return result_domain;
};
var eval_groupbys = function (contexts, evaluation_context) {
evaluation_context = _.extend(instance.web.pyeval.context(), evaluation_context || {});
var result_group = [];
_(contexts).each(function (ctx) {
if (_.isString(ctx)) {
@ -704,14 +709,15 @@ openerp.web.pyeval = function (instance) {
* @param {Array} object domains or contexts to evaluate
* @param {Object} [context] evaluation context
*/
instance.web.pyeval.eval = function (type, object, context) {
instance.web.pyeval.eval = function (type, object, context, options) {
options = options || {};
context = _.extend(instance.web.pyeval.context(), context || {});
context['context'] = py.dict.fromJSON(context);
//noinspection FallthroughInSwitchStatementJS
switch(type) {
case 'context': object = [object];
case 'contexts': return eval_contexts(object, context);
case 'contexts': return eval_contexts((options.no_user_context ? [] : [instance.session.user_context]).concat(object), context);
case 'domain': object = [object];
case 'domains': return eval_domains(object, context);
case 'groupbys': return eval_groupbys(object, context);

View File

@ -324,7 +324,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
e.preventDefault();
break;
}
}
},
'autocompleteopen': function () {
this.$el.autocomplete('widget').css('z-index', 3);
},
},
/**
* @constructs instance.web.SearchView
@ -352,7 +355,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
this.has_defaults = !_.isEmpty(this.defaults);
this.inputs = [];
this.controls = {};
this.controls = [];
this.headless = this.options.hidden && !this.has_defaults;
@ -379,6 +382,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
model: this.dataset._model,
view_id: this.view_id,
view_type: 'search',
context: this.dataset.get_context(),
});
$.when(load_view).then(function (r) {
@ -492,6 +496,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*/
complete_global_search: function (req, resp) {
$.when.apply(null, _(this.inputs).chain()
.filter(function (input) { return input.visible(); })
.invoke('complete', req.term)
.value()).then(function () {
resp(_(_(arguments).compact()).flatten(true));
@ -580,18 +585,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*
* @param {Array} items a list of nodes to convert to widgets
* @param {Object} fields a mapping of field names to (ORM) field attributes
* @param {String} [group_name] name of the group to put the new controls in
* @param {Object} [group] group to put the new controls in
*/
make_widgets: function (items, fields, group_name) {
group_name = group_name || null;
if (!(group_name in this.controls)) {
this.controls[group_name] = [];
make_widgets: function (items, fields, group) {
if (!group) {
group = new instance.web.search.Group(
this, 'q', {attrs: {string: _t("Filters")}});
}
var self = this, group = this.controls[group_name];
var self = this;
var filters = [];
_.each(items, function (item) {
if (filters.length && item.tag !== 'filter') {
group.push(new instance.web.search.FilterGroup(filters, this));
group.push(new instance.web.search.FilterGroup(filters, group));
filters = [];
}
@ -599,15 +604,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
case 'separator': case 'newline':
break;
case 'filter':
filters.push(new instance.web.search.Filter(item, this));
filters.push(new instance.web.search.Filter(item, group));
break;
case 'group':
self.make_widgets(item.children, fields, item.attrs.string);
self.make_widgets(item.children, fields,
new instance.web.search.Group(group, 'w', item));
break;
case 'field':
group.push(this.make_field(item, fields[item['attrs'].name]));
var field = this.make_field(
item, fields[item['attrs'].name], group);
group.push(field);
// filters
self.make_widgets(item.children, fields, group_name);
self.make_widgets(item.children, fields, group);
break;
}
}, this);
@ -622,12 +630,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
*
* @param {Object} item fields_view_get node for the field
* @param {Object} field fields_get result for the field
* @param {Object} [parent]
* @returns instance.web.search.Field
*/
make_field: function (item, field) {
make_field: function (item, field, parent) {
var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
if(obj) {
return new (obj) (item, field, this);
return new (obj) (item, field, parent || this);
} else {
console.group('Unknown field type ' + field.type);
console.error('View node', item);
@ -862,13 +871,18 @@ instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web
* @constructs instance.web.search.Widget
* @extends instance.web.Widget
*
* @param view the ancestor view of this widget
* @param parent parent of this widget
*/
init: function (view) {
this._super(view);
this.view = view;
init: function (parent) {
this._super(parent);
var ancestor = parent;
do {
this.view = ancestor;
} while (!(ancestor instanceof instance.web.SearchView)
&& (ancestor = (ancestor.getParent && ancestor.getParent())));
}
});
instance.web.search.add_expand_listener = function($root) {
$root.find('a.searchview_group_string').click(function (e) {
$root.toggleClass('folded expanded');
@ -877,13 +891,24 @@ instance.web.search.add_expand_listener = function($root) {
});
};
instance.web.search.Group = instance.web.search.Widget.extend({
template: 'SearchView.group',
init: function (view_section, view, fields) {
this._super(view);
this.attrs = view_section.attrs;
this.lines = view.make_widgets(
view_section.children, fields);
}
init: function (parent, icon, node) {
this._super(parent);
var attrs = node.attrs;
this.modifiers = attrs.modifiers =
attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
this.attrs = attrs;
this.icon = icon;
this.name = attrs.string;
this.children = [];
this.view.controls.push(this);
},
push: function (input) {
this.children.push(input);
},
visible: function () {
return !this.modifiers.invisible;
},
});
instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
@ -892,12 +917,12 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
* @constructs instance.web.search.Input
* @extends instance.web.search.Widget
*
* @param view
* @param parent
*/
init: function (view) {
this._super(view);
init: function (parent) {
this._super(parent);
this.load_attrs({});
this.view.inputs.push(this);
this.style = undefined;
},
/**
* Fetch auto-completion values for the widget.
@ -945,15 +970,30 @@ instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instan
"get_domain not implemented for widget " + this.attrs.type);
},
load_attrs: function (attrs) {
if (attrs.modifiers) {
attrs.modifiers = JSON.parse(attrs.modifiers);
attrs.invisible = attrs.modifiers.invisible || false;
if (attrs.invisible) {
this.style = 'display: none;'
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
this.attrs = attrs;
},
/**
* Returns whether the input is "visible". The default behavior is to
* query the ``modifiers.invisible`` flag on the input's description or
* view node.
*
* @returns {Boolean}
*/
visible: function () {
if (this.attrs.modifiers.invisible) {
return false;
}
var parent = this;
while ((parent = parent.getParent()) &&
( (parent instanceof instance.web.search.Group)
|| (parent instanceof instance.web.search.Input))) {
if (!parent.visible()) {
return false;
}
}
this.attrs = attrs;
}
return true;
},
});
instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
template: 'SearchView.filters',
@ -967,17 +1007,19 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
* @extends instance.web.search.Input
*
* @param {Array<instance.web.search.Filter>} filters elements of the group
* @param {instance.web.SearchView} view view in which the filters are contained
* @param {instance.web.SearchView} parent parent in which the filters are contained
*/
init: function (filters, view) {
init: function (filters, parent) {
// If all filters are group_by and we're not initializing a GroupbyGroup,
// create a GroupbyGroup instead of the current FilterGroup
if (!(this instanceof instance.web.search.GroupbyGroup) &&
_(filters).all(function (f) {
return f.attrs.context && f.attrs.context.group_by; })) {
return new instance.web.search.GroupbyGroup(filters, view);
if (!f.attrs.context) { return false; }
var c = instance.web.pyeval.eval('context', f.attrs.context);
return !_.isEmpty(c.group_by);})) {
return new instance.web.search.GroupbyGroup(filters, parent);
}
this._super(view);
this._super(parent);
this.filters = filters;
this.view.query.on('add remove change reset', this.proxy('search_change'));
},
@ -1096,6 +1138,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
var self = this;
item = item.toLowerCase();
var facet_values = _(this.filters).chain()
.filter(function (filter) { return filter.visible(); })
.filter(function (filter) {
var at = {
string: filter.attrs.string || '',
@ -1122,8 +1165,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in
instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
icon: 'w',
completion_label: _lt("Group by: %s"),
init: function (filters, view) {
this._super(filters, view);
init: function (filters, parent) {
this._super(filters, parent);
// Not flanders: facet unicity is handled through the
// (category, field) pair of facet attributes. This is all well and
// good for regular filter groups where a group matches a facet, but for
@ -1131,8 +1174,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
// view which proxies to the first GroupbyGroup, so it can be used
// for every GroupbyGroup and still provides the various methods needed
// by the search view. Use weirdo name to avoid risks of conflicts
if (!this.getParent()._s_groupby) {
this.getParent()._s_groupby = {
if (!this.view._s_groupby) {
this.view._s_groupby = {
help: "See GroupbyGroup#init",
get_context: this.proxy('get_context'),
get_domain: this.proxy('get_domain'),
@ -1141,14 +1184,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
}
},
match_facet: function (facet) {
return facet.get('field') === this.getParent()._s_groupby;
return facet.get('field') === this.view._s_groupby;
},
make_facet: function (values) {
return {
category: _t("GroupBy"),
icon: this.icon,
values: values,
field: this.getParent()._s_groupby
field: this.view._s_groupby
};
}
});
@ -1166,10 +1209,10 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc
* @extends instance.web.search.Input
*
* @param node
* @param view
* @param parent
*/
init: function (node, view) {
this._super(view);
init: function (node, parent) {
this._super(parent);
this.load_attrs(node.attrs);
},
facet_for: function () { return $.when(null); },
@ -1185,10 +1228,10 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
*
* @param view_section
* @param field
* @param view
* @param parent
*/
init: function (view_section, field, view) {
this._super(view);
init: function (view_section, field, parent) {
this._super(parent);
this.load_attrs(_.extend({}, field, view_section.attrs));
},
facet_for: function (value) {
@ -1228,7 +1271,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc
*
* @param {String} name the field's name
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
* @param {Number|String} value parsed value for the field
* @param {Number|String} facet parsed value for the field
* @returns {Array<Array>} domain to include in the resulting search
*/
make_domain: function (name, operator, facet) {
@ -1460,8 +1503,8 @@ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @le
});
instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
default_operator: {},
init: function (view_section, field, view) {
this._super(view_section, field, view);
init: function (view_section, field, parent) {
this._super(view_section, field, parent);
this.model = new instance.web.Model(this.attrs.relation);
},
complete: function (needle) {
@ -1673,6 +1716,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
if (!_.isEmpty(results.group_by)) {
results.context.group_by = results.group_by;
}
// Don't save user_context keys in the custom filter, otherwise end
// up with e.g. wrong uid or lang stored *and used in subsequent
// reqs*
var ctx = results.context;
_(_.keys(instance.session.user_context)).each(function (key) {
delete ctx[key];
});
var filter = {
name: $name.val(),
user_id: private_filter ? instance.session.uid : false,
@ -1702,22 +1752,28 @@ instance.web.search.Filters = instance.web.search.Input.extend({
var running_count = 0;
// get total filters count
var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
var filters_count = _(this.view.controls).chain()
var visible_filters = _(this.view.controls).chain().reject(function (group) {
return _(_(group.children).filter(is_group)).isEmpty()
|| group.modifiers.invisible;
});
var filters_count = visible_filters
.pluck('children')
.flatten()
.filter(is_group)
.map(function (i) { return i.filters.length; })
.sum()
.value();
var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) {
var filters = _(inputs).filter(is_group);
return {
name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group,
filters: filters,
length: _(filters).chain().map(function (i) {
return i.filters.length; }).sum().value()
};
});
var col1 = [], col2 = visible_filters.map(function (group) {
var filters = _(group.children).filter(is_group);
return {
name: _.str.sprintf("<span class='oe_i'>%s</span> %s",
group.icon, group.name),
filters: filters,
length: _(filters).chain().map(function (i) {
return i.filters.length; }).sum().value()
};
}).value();
while (col2.length) {
// col1 + group should be smaller than col2 + group

View File

@ -810,7 +810,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
try {
var form_invalid = false,
values = {},
first_invalid_field = null;
first_invalid_field = null,
readonly_values = {};
for (var f in self.fields) {
if (!self.fields.hasOwnProperty(f)) { continue; }
f = self.fields[f];
@ -819,11 +820,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
if (!first_invalid_field) {
first_invalid_field = f;
}
} else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) {
} else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
// Special case 'id' field, do not save this field
// on 'create' : save all non readonly fields
// on 'edit' : save non readonly modified fields
values[f.name] = f.get_value();
if (!f.get("readonly")) {
values[f.name] = f.get_value();
} else {
readonly_values[f.name] = f.get_value();
}
}
}
if (form_invalid) {
@ -836,7 +841,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
var save_deferral;
if (!self.datarecord.id) {
// Creation save
save_deferral = self.dataset.create(values).then(function(r) {
save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
return self.record_created(r, prepend_on_create);
}, null);
} else if (_.isEmpty(values)) {
@ -844,7 +849,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
save_deferral = $.Deferred().resolve({}).promise();
} else {
// Write save
save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) {
save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
return self.record_saved(r);
}, null);
}
@ -1887,10 +1892,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
}
this.view.on('view_content_has_changed', this, this.check_disable);
},
start: function() {
this._super.apply(this, arguments);
this.view.on('view_content_has_changed', this, this.check_disable);
this.check_disable();
this.$el.click(this.on_click);
if (this.node.attrs.help || instance.session.debug) {
this.do_attach_tooltip();
@ -1916,18 +1922,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
modal: true,
buttons: [
{text: _t("Cancel"), click: function() {
def.resolve();
$(this).dialog("close");
}
},
{text: _t("Ok"), click: function() {
self.on_confirmed().done(function() {
def.resolve();
var self2 = this;
self.on_confirmed().always(function() {
$(self2).dialog("close");
});
$(this).dialog("close");
}
}
]
],
beforeClose: function() {
def.resolve();
},
});
return def.promise();
} else {
@ -2218,6 +2226,18 @@ instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.Reini
},
});
/**
Some hack to make placeholders work in ie9.
*/
if ($.browser.msie && $.browser.version === "9.0") {
document.addEventListener("DOMNodeInserted",function(event){
var nodename = event.target.nodeName.toLowerCase();
if ( nodename === "input" || nodename == "textarea" ) {
$(event.target).placeholder();
}
});
}
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
template: 'FieldChar',
widget_class: 'oe_form_field_char',
@ -2337,7 +2357,8 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
if (!s) {
tmp = "http://" + this.get('value');
}
this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : '');
var text = this.get('value') ? this.node.attrs.text || tmp : '';
this.$el.find('a').attr('href', tmp).text(text);
}
},
on_button_clicked: function() {
@ -2382,7 +2403,6 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
type_of_date: "datetime",
events: {
'change .oe_datepicker_master': 'change_datetime',
'dragstart img.oe_datepicker_trigger': function () { return false; },
},
init: function(parent) {
this._super(parent);
@ -2546,42 +2566,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
},
'change textarea': 'store_dom_value',
},
init: function (field_manager, node) {
this._super(field_manager, node);
},
initialize_content: function() {
var self = this;
this.$textarea = this.$el.find('textarea');
this.auto_sized = false;
this.default_height = this.$textarea.css('height');
if (this.get("effective_readonly")) {
this.$textarea.attr('disabled', 'disabled');
if (! this.get("effective_readonly")) {
this.$textarea = this.$el.find('textarea');
this.auto_sized = false;
this.default_height = this.$textarea.css('height');
if (this.get("effective_readonly")) {
this.$textarea.attr('disabled', 'disabled');
}
this.setupFocus(this.$textarea);
} else {
this.$textarea = undefined;
}
this.setupFocus(this.$textarea);
},
commit_value: function () {
this.store_dom_value();
if (! this.get("effective_readonly") && this.$textarea) {
this.store_dom_value();
}
return this._super();
},
store_dom_value: function () {
if (!this.get('effective_readonly') && this.$('textarea').length) {
this.internal_set_value(
instance.web.parse_value(
this.$textarea.val(),
this));
}
this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
},
render_value: function() {
var show_value = instance.web.format_value(this.get('value'), this, '');
if (show_value === '') {
this.$textarea.css('height', parseInt(this.default_height)+"px");
}
this.$textarea.val(show_value);
if (! this.auto_sized) {
this.auto_sized = true;
this.$textarea.autosize();
if (! this.get("effective_readonly")) {
var show_value = instance.web.format_value(this.get('value'), this, '');
if (show_value === '') {
this.$textarea.css('height', parseInt(this.default_height)+"px");
}
this.$textarea.val(show_value);
if (! this.auto_sized) {
this.auto_sized = true;
this.$textarea.autosize();
} else {
this.$textarea.trigger("autosize");
}
} else {
this.$textarea.trigger("autosize");
var txt = this.get("value") || '';
this.$(".oe_form_text_content").text(txt);
}
},
is_syntax_valid: function() {
@ -2599,14 +2622,18 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
return this.get('value') === '' || this._super();
},
focus: function($el) {
this.$textarea[0].focus();
if (!this.get("effective_readonly") && this.$textarea) {
this.$textarea[0].focus();
}
},
set_dimensions: function (height, width) {
this._super(height, width);
this.$textarea.css({
width: width,
minHeight: height
});
if (!this.get("effective_readonly") && this.$textarea) {
this.$textarea.css({
width: width,
minHeight: height
});
}
},
});
@ -2637,7 +2664,7 @@ instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instanc
"| removeformat | bullets numbering | outdent " +
"indent | link unlink | source",
bodyStyle: // style to assign to document body contained within the editor
"margin:4px; color:#4c4c4c; font-size:13px; font-family:\"Lucida Grande\",Helvetica,Verdana,Arial,sans-serif; cursor:text"
"margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
});
this.$cleditor = this.$textarea.cleditor()[0];
this.$cleditor.change(function() {
@ -3004,8 +3031,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
e.stopPropagation();
}
},
'dragstart .oe_m2o_drop_down_button img': function () { return false; },
'dragstart .oe_m2o_cm_button': function () { return false; }
},
init: function(field_manager, node) {
this._super(field_manager, node);
@ -3036,6 +3061,26 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
if (!this.get("effective_readonly"))
this.render_editable();
},
destroy_content: function () {
if (this.$drop_down) {
this.$drop_down.off('click');
delete this.$drop_down;
}
if (this.$input) {
this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
'focus focusout change keydown');
delete this.$input;
}
if (this.$follow_button) {
this.$follow_button.off('blur focus click');
delete this.$follow_button;
}
},
destroy: function () {
this.destroy_content();
return this._super();
},
init_error_displayer: function() {
// nothing
},
@ -3266,7 +3311,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
res_model: self.field.relation,
res_id: self.get("value"),
views: [[false, 'form']],
target: 'current'
target: 'current',
context: self.build_context().eval(),
});
return false;
});
@ -3297,7 +3343,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
},
focus: function () {
if (!this.get('effective_readonly')) {
this.$input[0].focus();
this.$input && this.$input[0].focus();
}
},
_quick_create: function() {
@ -3716,8 +3762,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
var pop = new instance.web.form.FormOpenPopup(this);
pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
title: _t("Open: ") + self.o2m.string,
create_function: function(data) {
return self.o2m.dataset.create(data).done(function(r) {
create_function: function(data, options) {
return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r);
});
@ -3776,8 +3822,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
this.o2m.trigger_on_change();
},
is_valid: function () {
var form = this.editor.form;
var editor = this.editor;
var form = editor.form;
// If no edition is pending, the listview can not be invalid (?)
if (!editor.record) {
return true
}
// If the form has not been modified, the view can only be valid
// NB: is_dirty will also be set on defaults/onchanges/whatever?
// oe_form_dirty seems to only be set on actual user actions
@ -3807,11 +3857,11 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
title: _t("Create: ") + self.o2m.string,
initial_view: "form",
alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
create_function: function(data, callback, error_callback) {
return self.o2m.dataset.create(data).done(function(r) {
create_function: function(data, options) {
return self.o2m.dataset.create(data, options).done(function(r) {
self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
self.o2m.dataset.trigger("dataset_changed", r);
}).done(callback).fail(error_callback);
});
},
read_function: function() {
return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
@ -4024,6 +4074,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (this.get("effective_readonly"))
return;
var self = this;
var ignore_blur = false;
self.$text = this.$("textarea");
self.$text.textext({
plugins : 'tags arrow autocomplete',
@ -4042,6 +4093,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
if (data.id) {
self.add_id(data.id);
} else {
ignore_blur = true;
data.action();
}
},
@ -4092,10 +4144,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self.$text
.focusin(function () {
self.trigger('focused');
ignore_blur = false;
})
.focusout(function() {
self.$text.trigger("setInputData", "");
self.trigger('blurred');
if (!ignore_blur) {
self.trigger('blurred');
}
}).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown();
@ -4302,6 +4357,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in
});
}
},
is_action_enabled: function () { return true; },
});
instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
@ -4536,9 +4592,9 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
this.created_elements = [];
this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
this.dataset.read_function = this.options.read_function;
this.dataset.create_function = function(data, sup) {
this.dataset.create_function = function(data, options, sup) {
var fct = self.options.create_function || sup;
return fct.call(this, data).done(function(r) {
return fct.call(this, data, options).done(function(r) {
self.trigger('create_completed saved', r);
self.created_elements.push(r);
});
@ -5250,11 +5306,11 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
this.calc_domain();
this.on("change:value", this, this.get_selection);
this.on("change:evaluated_selection_domain", this, this.get_selection);
this.get_selection();
this.on("change:selection", this, function() {
this.selection = this.get("selection");
this.render_value();
});
this.get_selection();
if (this.options.clickable) {
this.$el.on('click','li',this.on_click_stage);
}
@ -5281,7 +5337,12 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
},
calc_domain: function() {
var d = instance.web.pyeval.eval('domain', this.build_domain());
domain = ['|', ['id', '=', this.get('value')]].concat(d);
var domain = []; //if there is no domain defined, fetch all the records
if (d.length) {
domain = ['|',['id', '=', this.get('value')]].concat(d);
}
if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
this.set("evaluated_selection_domain", domain);
}
@ -5373,10 +5434,10 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
});
},
parse_value: function(val, def) {
return instance.web.parse_value(val, {type: "float"}, def);
return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
},
format_value: function(val, def) {
return instance.web.format_value(val, {type: "float"}, def);
return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
},
});

View File

@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
.appendTo($this.empty())
.click(function (e) {e.stopPropagation();})
.append('<option value="80">80</option>' +
'<option value="100">100</option>' +
'<option value="200">200</option>' +
'<option value="500">500</option>' +
'<option value="2000">2000</option>' +
'<option value="NaN">' + _t("Unlimited") + '</option>')
.change(function () {
var val = parseInt($select.val(), 10);

View File

@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) {
});
},
editable: function () {
return !this.options.disable_editable_mode
return !this.grouped
&& !this.options.disable_editable_mode
&& (this.fields_view.arch.attrs.editable
|| this._context_editable
|| this.options.editable);

View File

@ -149,6 +149,9 @@ instance.web.ActionManager = instance.web.Widget.extend({
for (var i = 0; i < this.breadcrumbs.length; i += 1) {
var item = this.breadcrumbs[i];
var tit = item.get_title();
if (item.hide_breadcrumb) {
continue;
}
if (!_.isArray(tit)) {
tit = [tit];
}
@ -282,6 +285,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
* @param {Object} [options]
* @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
* @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
* @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
* @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
* @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
* @param {Object} [options.additional_context] Additional context to be merged with the action's context.
@ -291,6 +295,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
options = _.defaults(options || {}, {
clear_breadcrumbs: false,
on_reverse_breadcrumb: function() {},
hide_breadcrumb: false,
on_close: function() {},
action_menu_id: null,
additional_context: {},
@ -403,7 +408,12 @@ instance.web.ActionManager = instance.web.Widget.extend({
widget: function () { return new instance.web.ViewManagerAction(self, action); },
action: action,
klass: 'oe_act_window',
post_process: function (widget) { widget.add_breadcrumb(options.on_reverse_breadcrumb); }
post_process: function (widget) {
widget.add_breadcrumb({
on_reverse_breadcrumb: options.on_reverse_breadcrumb,
hide_breadcrumb: options.hide_breadcrumb,
});
},
}, options);
},
ir_actions_client: function (action, options) {
@ -427,6 +437,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
widget: widget,
title: action.name,
on_reverse_breadcrumb: options.on_reverse_breadcrumb,
hide_breadcrumb: options.hide_breadcrumb,
});
if (action.tag !== 'reload') {
self.do_push_state({});
@ -649,7 +660,15 @@ instance.web.ViewManager = instance.web.Widget.extend({
set_title: function(title) {
this.$el.find('.oe_view_title_text:first').text(title);
},
add_breadcrumb: function(on_reverse_breadcrumb) {
add_breadcrumb: function(options) {
var options = options || {};
// 7.0 backward compatibility
if (typeof options == 'function') {
options = {
on_reverse_breadcrumb: options
};
}
// end of 7.0 backward compatibility
var self = this;
var views = [this.active_view || this.views_src[0].view_type];
this.on('switch_mode', self, function(mode) {
@ -661,7 +680,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
views.push(mode);
}
});
this.getParent().push_breadcrumb({
var item = _.extend({
widget: this,
action: this.action,
show: function(index) {
@ -695,9 +714,9 @@ instance.web.ViewManager = instance.web.Widget.extend({
titles.pop();
}
return titles;
},
on_reverse_breadcrumb: on_reverse_breadcrumb,
});
}
}, options);
this.getParent().push_breadcrumb(item);
},
/**
* Returns to the view preceding the caller view in this manager's
@ -1263,6 +1282,7 @@ instance.web.View = instance.web.Widget.extend({
"view_id": this.view_id,
"view_type": this.view_type,
"toolbar": !!this.options.$sidebar,
"context": this.dataset.get_context(),
});
}
return view_loaded_def.then(function(r) {
@ -1446,11 +1466,11 @@ instance.web.View = instance.web.Widget.extend({
* Performs a fields_view_get and apply postprocessing.
* return a {$.Deferred} resolved with the fvg
*
* @param {Object} [args]
* @param {Object} args
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
* @param {null|Object} args.context context if args.model is a string
* @param {null|Number} args.view_id id of the view to be loaded, default view if null
* @param {null|String} args.view_type type of view to be loaded if view_id is null
* @param {Object} [args.context] context if args.model is a string
* @param {Number} [args.view_id] id of the view to be loaded, default view if null
* @param {String} [args.view_type] type of view to be loaded if view_id is null
* @param {Boolean} [args.toolbar=false] get the toolbar definition
*/
instance.web.fields_view_get = function(args) {
@ -1477,7 +1497,7 @@ instance.web.fields_view_get = function(args) {
if (typeof model === 'string') {
model = new instance.web.Model(args.model, args.context);
}
return args.model.call('fields_view_get', [args.view_id, args.view_type, model.context(), args.toolbar]).then(function(fvg) {
return args.model.call('fields_view_get', [args.view_id, args.view_type, args.context, args.toolbar]).then(function(fvg) {
return postprocess(fvg);
});
};
@ -1555,12 +1575,17 @@ instance.web.xml_to_str = function(node) {
} else {
throw new Error(_t("Could not serialize XML"));
}
// Browsers won't deal with self closing tags except br, hr, input, ...
// http://stackoverflow.com/questions/97522/what-are-all-the-valid-self-closing-elements-in-xhtml-as-implemented-by-the-maj
//
// Browsers won't deal with self closing tags except void elements:
// http://www.w3.org/TR/html-markup/syntax.html
var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
// The following regex is a bit naive but it's ok for the xmlserializer output
str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
return "<" + tag + attrs + "></" + tag + ">";
if (void_elements.indexOf(tag) < 0) {
return "<" + tag + attrs + "></" + tag + ">";
} else {
return match;
}
});
return str;
};

View File

@ -40,11 +40,11 @@
<td>
<p>
<t t-js="d">
var message = d.message ? d.message : d.error.data.message;
d.html_error = context.engine.tools.html_escape(message)
var message = d.error.data.message || d.error.data.fault_code;
d.error.html_error = context.engine.tools.html_escape(message)
.replace(/\n/g, '<br/>');
</t>
<t t-raw="html_error"/>
<t t-raw="error.html_error"/>
</p>
</td>
</tr>
@ -71,9 +71,9 @@
</div>
<ul>
<li>Username</li>
<li><input name="login" type="text" t-att-value="widget.selected_login || ''" autofocus="autofocus"/></li>
<li><input name="login" type="text" value="" autofocus="autofocus"/></li>
<li>Password</li>
<li><input name="password" type="password" t-att-value="widget.selected_password || ''"/></li>
<li><input name="password" type="password" value=""/></li>
<li><button name="submit">Log in</button></li>
</ul>
</form>
@ -106,33 +106,47 @@
<div class="oe_view_manager_header" style="padding: 8px;">
<div class="oe_header_row">
<h2 class="oe_view_title">
<span class="oe_view_title_text oe_breadcrumb_title">Create Database</span>
<span class="oe_view_title_text oe_breadcrumb_title">Create a New Database</span>
</h2>
<button type="submit" class="oe_button oe_highlight db_create">Create</button>
</div>
</div>
</div>
<table align="center" class="db_option_table">
<p class="oe_grey" style="margin: 10px">
Fill in this form to create an OpenERP database. You can
create databases for different companies or for different
goals (testing, production). Once the database is created,
you will be able to install your first application.
</p>
<p class="oe_grey" style="margin: 10px">
By default, the master password is 'admin'. This password
is required to created, delete dump or restore databases.
</p>
<table class="db_option_table" style="margin: 10px">
<tr>
<td><label for="super_admin_pwd">Master password:</label></td>
<td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td>
<td>
<input type="password" name="super_admin_pwd" class="required" value="admin"/>
</td>
</tr>
<tr>
<td><label for="db_name">New database name:</label></td>
<td><input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true"/></td>
<td><label for="db_name">Select a database name:</label></td>
<td>
<input type="text" name="db_name" class="required" matches="^[a-zA-Z0-9][a-zA-Z0-9_-]+$" autofocus="true" placeholder="e.g. mycompany"/>
</td>
</tr>
<tr>
<td><label for="demo_data">Load Demonstration data:</label></td>
<td><label for="demo_data">Load demonstration data:</label></td>
<td class="oe_form_group_cell">
<span class="oe_form_field oe_form_field_boolean">
<span class="oe_form_field oe_form_field_boolean oe_grey" >
<input type="checkbox" name="demo_data" />
Check this box to evaluate OpenERP.
</span>
</td>
</tr>
<tr>
<td><label for="db_lang">Default language:</label></td>
<td class="oe_form_field oe_form_field_selection ">
<select name="db_lang" t-if="widget.lang_list">
<td class="oe_form_field oe_form_field_selection">
<select name="db_lang" t-if="widget.lang_list" class="oe_inline">
<t t-foreach="widget.lang_list" t-as="lang">
<option t-att-value="lang[0]" t-att-selected="lang[0] === 'en_US' ? 'selected' : undefined">
<t t-esc="lang[1]" />
@ -142,13 +156,17 @@
</td>
</tr>
<tr>
<td><label for="create_admin_pwd">Admin password:</label></td>
<td><label for="create_admin_pwd">Choose a password:</label></td>
<td><input type="password" name="create_admin_pwd" class="required" /></td>
</tr>
<tr>
<td><label for="create_confirm_pwd">Confirm password:</label></td>
<td><input type="password" name="create_confirm_pwd" class="required" equalTo="input[name=create_admin_pwd]"/></td>
</tr>
<tr>
<td></td>
<td><button type="submit" class="oe_button oe_highlight db_create">Create Database</button></td>
</tr>
</table>
</form>
<form id="db_duplicate" name="duplicate_db_form" style="display: none;">
@ -1032,17 +1050,22 @@
</t>
<t t-name="FieldText">
<div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style">
<textarea rows="6"
t-att-name="widget.name"
class="field_text"
t-att-tabindex="widget.node.attrs.tabindex"
t-att-autofocus="widget.node.attrs.autofocus"
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
t-att-maxlength="widget.field.size"
></textarea><img class="oe_field_translate oe_input_icon"
t-if="widget.field.translate and !widget.get('effective_readonly')"
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
/>
<t t-if="!widget.get('effective_readonly')">
<textarea rows="6"
t-att-name="widget.name"
class="field_text"
t-att-tabindex="widget.node.attrs.tabindex"
t-att-autofocus="widget.node.attrs.autofocus"
t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''"
t-att-maxlength="widget.field.size"
></textarea><img class="oe_field_translate oe_input_icon"
t-if="widget.field.translate and !widget.get('effective_readonly')"
t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"
/>
</t>
<t t-if="widget.get('effective_readonly')">
<span class="oe_form_text_content"></span>
</t>
</div>
</t>
<t t-name="FieldTextHtml">
@ -1061,8 +1084,9 @@
t-att-name="widget.name"
t-att-placeholder="placeholder"
class="oe_datepicker_master"
/><img class="oe_input_icon oe_datepicker_trigger" t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
title="Select date" width="16" height="16" border="0"/>
/><img class="oe_input_icon oe_datepicker_trigger" draggable="false"
t-att-src='_s + "/web/static/src/img/ui/field_calendar.png"'
title="Select date" width="16" height="16" border="0"/>
</span>
</t>
<t t-name="FieldDate">
@ -1119,7 +1143,7 @@
</t>
<t t-if="!widget.get('effective_readonly')">
<a t-if="! widget.options.no_open" href="#" tabindex="-1"
class="oe_m2o_cm_button oe_e">/</a>
class="oe_m2o_cm_button oe_e" draggable="false">/</a>
<div>
<input type="text"
t-att-id="widget.id_for_label"
@ -1128,7 +1152,7 @@
t-att-placeholder="widget.node.attrs.placeholder"
/>
<span class="oe_m2o_drop_down_button">
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"'/>
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/>
</span>
</div>
</t>
@ -1324,7 +1348,7 @@
<div class="oe_add" t-if="!widget.get('effective_readonly')">
<!-- uploader of file -->
<button class="oe_attach"><span class="oe_e">'</span></button>
<span class='oe_attach_label'>File</span>
<span class='oe_attach_label'><t t-esc="widget.string"/></span>
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
@ -1523,7 +1547,7 @@
<t t-esc="attrs.string"/>
</button>
<ul t-name="SearchView.filters">
<li t-foreach="widget.filters" t-as="filter"
<li t-foreach="widget.filters" t-as="filter" t-if="filter.visible()"
t-att-title="filter.attrs.string ? filter.attrs.help : undefined">
<t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/>
</li>
@ -1611,15 +1635,6 @@
</div>
</div>
</t>
<t t-name="SearchView.group">
<t t-call="SearchView.util.expand">
<t t-set="expand" t-value="attrs.expand"/>
<t t-set="label" t-value="attrs.string"/>
<t t-set="content">
<t t-call="SearchView.render_lines"/>
</t>
</t>
</t>
<div t-name="SearchView.Filters" class="oe_searchview_filters oe_searchview_section">
</div>
@ -1850,7 +1865,7 @@
<tr>
<td t-foreach="records[0]" t-as="column">
<input class="sel_fields" placeholder="--- Don't Import ---"/><span class="oe_m2o_drop_down_button">
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' /></span>
<img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/></span>
</td>
</tr>
<tr t-foreach="records" t-as="record" class="oe_import_grid-row">

View File

@ -259,6 +259,12 @@ openerp.testing.section('eval.types', {
py.eval('a + a', ctx);
}, /^Error: TypeError:/);
});
test('relastivedelta', function (instance) {
strictEqual(
py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')",
instance.web.pyeval.context()),
"2012-02-14 23:59:59");
})
});
openerp.testing.section('eval.edc', {
dependencies: ['web.data'],

View File

@ -1,4 +1,4 @@
openerp.testing.section('query', {
openerp.testing.section('search.query', {
dependencies: ['web.search']
}, function (test) {
test('Adding a facet to the query creates a facet and a value', function (instance) {
@ -180,7 +180,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
});
return view;
};
openerp.testing.section('defaults', {
openerp.testing.section('search.defaults', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
@ -362,7 +362,7 @@ openerp.testing.section('defaults', {
"should not accept multiple default values");
})
});
openerp.testing.section('completions', {
openerp.testing.section('search.completions', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -615,7 +615,7 @@ openerp.testing.section('completions', {
return f.complete("bob");
});
});
openerp.testing.section('search-serialization', {
openerp.testing.section('search.serialization', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -855,11 +855,11 @@ openerp.testing.section('search-serialization', {
test('FilterGroup', {asserts: 6}, function (instance) {
var view = {inputs: [], query: {on: function () {}}};
var filter_a = new instance.web.search.Filter(
{attrs: {name: 'a', context: 'c1', domain: 'd1'}}, view);
{attrs: {name: 'a', context: '{"c1": True}', domain: 'd1'}}, view);
var filter_b = new instance.web.search.Filter(
{attrs: {name: 'b', context: 'c2', domain: 'd2'}}, view);
{attrs: {name: 'b', context: '{"c2": True}', domain: 'd2'}}, view);
var filter_c = new instance.web.search.Filter(
{attrs: {name: 'c', context: 'c3', domain: 'd3'}}, view);
{attrs: {name: 'c', context: '{"c3": True}', domain: 'd3'}}, view);
var group = new instance.web.search.FilterGroup(
[filter_a, filter_b, filter_c], view);
return group.facet_for_defaults({a: true, c: true})
@ -880,7 +880,7 @@ openerp.testing.section('search-serialization', {
equal(context.__ref, 'compound_context',
"context should be compound");
deepEqual(context.__contexts, [
'c1', 'c3'
'{"c1": True}', '{"c3": True}'
], "context should merge all filter contexts");
ok(!context.get_eval_context(), "context should have no evaluation context");
});
@ -922,7 +922,7 @@ openerp.testing.section('search-serialization', {
return $.when(t1, t2);
});
});
openerp.testing.section('removal', {
openerp.testing.section('search.removal', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -945,7 +945,7 @@ openerp.testing.section('removal', {
});
});
});
openerp.testing.section('drawer', {
openerp.testing.section('search.drawer', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -961,7 +961,7 @@ openerp.testing.section('drawer', {
});
});
});
openerp.testing.section('filters', {
openerp.testing.section('search.filters', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
@ -1046,7 +1046,104 @@ openerp.testing.section('filters', {
});
});
});
openerp.testing.section('saved_filters', {
openerp.testing.section('search.groupby', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
}, function (test) {
test('basic', {
asserts: 7,
setup: function (instance, $s, mock) {
mock('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
}
});
}
}, function (instance, $fix) {
var view = makeSearchView(instance);
return view.appendTo($fix)
.done(function () {
// 3 filters, 1 filtergroup group, 1 custom filter, 1 advanced, 1 Filters
equal(view.inputs.length, 7,
'should have 7 inputs total');
var group = _.find(view.inputs, function (f) {
return f instanceof instance.web.search.GroupbyGroup
});
ok(group, "should have a GroupbyGroup input");
strictEqual(group.getParent(), view,
"group's parent should be view");
group.toggle(group.filters[0]);
group.toggle(group.filters[2]);
var results = view.build_search_data();
deepEqual(results.errors, [], "should have no errors");
deepEqual(results.domains, [], "should have no domain");
deepEqual(results.contexts, [
new instance.web.CompoundContext(
"{'group_by': 'foo'}", "{'group_by': 'baz'}")
], "should have compound contexts");
deepEqual(results.groupbys, [
"{'group_by': 'foo'}",
"{'group_by': 'baz'}"
], "should have sequence of contexts")
});
});
test('unified multiple groupby groups', {
asserts: 4,
setup: function (instance, $s, mock) {
mock('dummy.model:fields_view_get', function () {
return {
type: 'search',
fields: {},
arch: [
'<search>',
'<filter string="Foo" context="{\'group_by\': \'foo\'}"/>',
'<separator/>',
'<filter string="Bar" context="{\'group_by\': \'bar\'}"/>',
'<separator/>',
'<filter string="Baz" context="{\'group_by\': \'baz\'}"/>',
'</search>'
].join(''),
}
});
}
}, function (instance, $fix) {
var view = makeSearchView(instance);
return view.appendTo($fix)
.done(function () {
// 3 filters, 3 filtergroups, 1 custom filter, 1 advanced, 1 Filters
equal(view.inputs.length, 9, "should have 9 inputs total");
var groups = _.filter(view.inputs, function (f) {
return f instanceof instance.web.search.GroupbyGroup
});
equal(groups.length, 3, "should have 3 GroupbyGroups");
groups[0].toggle(groups[0].filters[0]);
groups[2].toggle(groups[2].filters[0]);
equal(view.query.length, 1,
"should have unified groupby groups in single facet");
deepEqual(view.build_search_data(), {
errors: [],
domains: [],
contexts: [new instance.web.CompoundContext(
"{'group_by': 'foo'}", "{'group_by': 'baz'}")],
groupbys: [ "{'group_by': 'foo'}", "{'group_by': 'baz'}" ],
}, "should only have contexts & groupbys in search data");
});
});
});
openerp.testing.section('search.filters.saved', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -1127,8 +1224,30 @@ openerp.testing.section('saved_filters', {
"should have selected second filter");
});
});
test('creation', {asserts: 2}, function (instance, $fix, mock) {
// force a user context
instance.session.user_context = {foo: 'bar'};
var view = makeSearchView(instance);
var done = $.Deferred();
mock('ir.filters:get_filters', function () { return []; });
mock('ir.filters:create_or_replace', function (args) {
var filter = args[0];
deepEqual(filter.context, {}, "should have empty context");
deepEqual(filter.domain, [], "should have empty domain");
done.resolve();
});
return view.appendTo($fix)
.then(function () {
$fix.find('.oe_searchview_custom input#oe_searchview_custom_input')
.text("filter name")
.end()
.find('.oe_searchview_custom button').click();
return done.promise();
});
});
});
openerp.testing.section('advanced', {
openerp.testing.section('search.advanced', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true
@ -1209,3 +1328,159 @@ openerp.testing.section('advanced', {
});
// TODO: UI tests?
});
openerp.testing.section('search.invisible', {
dependencies: ['web.search'],
rpc: 'mock',
templates: true,
}, function (test) {
var registerTestField = function (instance, methods) {
instance.web.search.fields.add('test', 'instance.testing.TestWidget');
instance.testing = {
TestWidget: instance.web.search.Field.extend(methods),
};
};
var makeView = function (instance, mock, fields, arch, defaults) {
mock('ir.filters:get_filters', function () { return []; });
mock('test.model:fields_get', function () { return fields; });
mock('test.model:fields_view_get', function () {
return { type: 'search', fields: fields, arch: arch };
});
var ds = new instance.web.DataSet(null, 'test.model');
return new instance.web.SearchView(null, ds, false, defaults);
};
// Invisible fields should not auto-complete
test('invisible-field-no-autocomplete', {asserts: 1}, function (instance, $fix, mock) {
registerTestField(instance, {
complete: function () {
return $.when([{label: this.attrs.string}]);
},
});
var view = makeView(instance, mock, {
field0: {type: 'test', string: 'Field 0'},
field1: {type: 'test', string: 'Field 1'},
}, ['<search>',
'<field name="field0"/>',
'<field name="field1" modifiers="{&quot;invisible&quot;: true}"/>',
'</search>'].join());
return view.appendTo($fix)
.then(function () {
var done = $.Deferred();
view.complete_global_search({term: 'test'}, function (comps) {
done.resolve(comps);
});
return done;
}).then(function (completions) {
deepEqual(completions, [{label: 'Field 0'}],
"should only complete the visible field");
});
});
// Invisible filters should not appear in the drawer
test('invisible-filter-no-drawer', {asserts: 4}, function (instance, $fix, mock) {
var view = makeView(instance, mock, {}, [
'<search>',
'<filter string="filter 0"/>',
'<filter string="filter 1" modifiers="{&quot;invisible&quot;: true}"/>',
'</search>'].join());
return view.appendTo($fix)
.then(function () {
var $fs = $fix.find('.oe_searchview_filters ul');
strictEqual($fs.children().length,
1,
"should only display one filter");
strictEqual(_.str.trim($fs.children().text()),
"filter 0",
"should only display filter 0");
var done = $.Deferred();
view.complete_global_search({term: 'filter'}, function (comps) {
done.resolve();
strictEqual(comps.length, 1, "should only complete visible filter");
strictEqual(comps[0].label, "Filter on: filter 0",
"should complete filter 0");
});
return done;
});
});
// 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) {
registerTestField(instance, {
complete: function () {
return $.when([{label: this.attrs.string}]);
},
});
var view = makeView(instance, mock, {
field0: {type: 'test', string: 'Field 0'},
field1: {type: 'test', string: 'Field 1'},
}, [
'<search>',
'<group string="Visibles">',
'<field name="field0"/>',
'<filter string="Filter 0"/>',
'</group>',
'<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
'<field name="field1"/>',
'<filter string="Filter 1"/>',
'</group>',
'</search>'
].join(''));
return view.appendTo($fix)
.then(function () {
strictEqual($fix.find('.oe_searchview_filters h3').length,
1,
"should only display one group");
strictEqual($fix.find('.oe_searchview_filters h3').text(),
'w Visibles',
"should only display the Visibles group (and its icon char)");
var $fs = $fix.find('.oe_searchview_filters ul');
strictEqual($fs.children().length, 1,
"should only have one filter in the drawer");
strictEqual(_.str.trim($fs.text()), "Filter 0",
"should have filter 0 as sole filter");
var done = $.Deferred();
view.complete_global_search({term: 'filter'}, function (compls) {
done.resolve();
strictEqual(compls.length, 2,
"should have 2 completions");
deepEqual(_.pluck(compls, 'label'),
['Field 0', 'Filter on: Filter 0'],
"should complete on field 0 and filter 0");
});
return done;
});
});
// Default on invisible fields should still work, for fields and filters both
test('invisible-defaults', {asserts: 1}, function (instance, $fix, mock) {
var view = makeView(instance, mock, {
field: {type: 'char', string: "Field"},
field2: {type: 'char', string: "Field 2"},
}, [
'<search>',
'<field name="field2"/>',
'<filter name="filter2" string="Filter"',
' domain="[[\'qwa\', \'=\', 42]]"/>',
'<group string="Invisibles" modifiers="{&quot;invisible&quot;: true}">',
'<field name="field"/>',
'<filter name="filter" string="Filter"',
' domain="[[\'whee\', \'=\', \'42\']]"/>',
'</group>',
'</search>'
].join(''), {field: "foo", filter: true});
return view.appendTo($fix)
.then(function () {
deepEqual(view.build_search_data(), {
errors: [],
groupbys: [],
contexts: [],
domains: [
// Generated from field
[['field', 'ilike', 'foo']],
// generated from filter
"[['whee', '=', '42']]"
],
}, "should yield invisible fields selected by defaults");
});
});
});

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"X-Poedit-Language: Czech\n"
#. module: web_calendar

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -15,8 +15,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
"Language: es\n"
#. module: web_calendar

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:09+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

View File

@ -14,8 +14,8 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2013-02-07 05:08+0000\n"
"X-Generator: Launchpad (build 16477)\n"
"X-Launchpad-Export-Date: 2013-03-19 05:51+0000\n"
"X-Generator: Launchpad (build 16532)\n"
#. module: web_calendar
#. openerp-web

Some files were not shown because too many files have changed in this diff Show More