[MERGE] from trunk
bzr revid: chm@openerp.com-20130320130931-0mk35kjf5e3g8fnd
This commit is contained in:
commit
5ad1ecb48f
|
@ -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",
|
||||
|
|
|
@ -13,7 +13,10 @@ import os
|
|||
import re
|
||||
import simplejson
|
||||
import time
|
||||
import urllib
|
||||
import urllib2
|
||||
import urlparse
|
||||
import xmlrpclib
|
||||
import zlib
|
||||
from xml.etree import ElementTree
|
||||
from cStringIO import StringIO
|
||||
|
@ -90,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
|
||||
|
@ -290,20 +327,19 @@ 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:
|
||||
path += '?mods=' + mods
|
||||
path += '?' + urllib.urlencode({'mods': mods})
|
||||
elif db:
|
||||
path += '?db=' + db
|
||||
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
|
||||
|
@ -533,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))
|
||||
|
||||
|
@ -683,7 +723,7 @@ class WebClient(openerpweb.Controller):
|
|||
|
||||
@openerpweb.jsonrequest
|
||||
def version_info(self, req):
|
||||
return openerp.service.web_services.RPC_VERSION_1
|
||||
return openerp.service.common.exp_version()
|
||||
|
||||
class Proxy(openerpweb.Controller):
|
||||
_cp_path = '/web/proxy'
|
||||
|
@ -868,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:
|
||||
|
@ -1310,7 +1350,7 @@ class Binary(openerpweb.Controller):
|
|||
'id': attachment_id
|
||||
}
|
||||
except Exception:
|
||||
args = {'error':e.faultCode }
|
||||
args = {'error': "Something horrible happened"}
|
||||
return out % (simplejson.dumps(callback), simplejson.dumps(args))
|
||||
|
||||
@openerpweb.httprequest
|
||||
|
|
|
@ -32,12 +32,12 @@ NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
|
|||
</form>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
""", default_filters=['h'])
|
||||
NOTFOUND = Template(u"""
|
||||
<p>Unable to find the module [${module}], please check that the module
|
||||
name is correct and the module is on OpenERP's path.</p>
|
||||
<a href="/web/tests"><< Back to tests</a>
|
||||
""")
|
||||
""", default_filters=['h'])
|
||||
TESTING = Template(u"""<!DOCTYPE html>
|
||||
<html style="height: 100%">
|
||||
<%def name="to_path(module, p)">/${module}/${p}</%def>
|
||||
|
@ -51,9 +51,9 @@ TESTING = Template(u"""<!DOCTYPE html>
|
|||
<script src="/web/static/lib/qunit/qunit.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var oe_db_info = ${db_info};
|
||||
var oe_db_info = ${db_info | n};
|
||||
// List of modules, each module is preceded by its dependencies
|
||||
var oe_all_dependencies = ${dependencies};
|
||||
var oe_all_dependencies = ${dependencies | n};
|
||||
QUnit.config.testTimeout = 5 * 60 * 1000;
|
||||
</script>
|
||||
</head>
|
||||
|
@ -83,7 +83,7 @@ TESTING = Template(u"""<!DOCTYPE html>
|
|||
% endif
|
||||
% endfor
|
||||
</html>
|
||||
""")
|
||||
""", default_filters=['h'])
|
||||
|
||||
class TestRunnerController(http.Controller):
|
||||
_cp_path = '/web/tests'
|
||||
|
|
|
@ -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
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -286,7 +294,8 @@ class HttpRequest(WebRequest):
|
|||
_logger.debug("%s --> %s.%s %r", self.httprequest.method, method.im_class.__name__, method.__name__, akw)
|
||||
try:
|
||||
r = method(self, **self.params)
|
||||
except Exception:
|
||||
except Exception, e:
|
||||
_logger.exception("An exception occured during an http request")
|
||||
se = serialize_exception(e)
|
||||
error = {
|
||||
'code': 200,
|
||||
|
|
|
@ -38,6 +38,7 @@ class Model(object):
|
|||
self.proxy = self.session.proxy('object')
|
||||
|
||||
def __getattr__(self, method):
|
||||
self.session.assert_valid()
|
||||
def proxy(*args, **kw):
|
||||
result = self.proxy.execute_kw(self.session._db, self.session._uid, self.session._password, self.model, method, args, kw)
|
||||
# reorder read
|
||||
|
@ -109,8 +110,8 @@ class OpenERPSession(object):
|
|||
if self._uid and not force:
|
||||
return
|
||||
# TODO use authenticate instead of login
|
||||
uid = self.proxy("common").login(self._db, self._login, self._password)
|
||||
if not uid:
|
||||
self._uid = self.proxy("common").login(self._db, self._login, self._password)
|
||||
if not self._uid:
|
||||
raise AuthenticationError("Authentication failure")
|
||||
|
||||
def ensure_valid(self):
|
||||
|
@ -121,7 +122,6 @@ class OpenERPSession(object):
|
|||
self._uid = None
|
||||
|
||||
def execute(self, model, func, *l, **d):
|
||||
self.assert_valid()
|
||||
model = self.model(model)
|
||||
r = getattr(model, func)(*l, **d)
|
||||
return r
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
*/
|
||||
|
|
|
@ -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));
|
|
@ -1,4 +1,4 @@
|
|||
@charset "UTF-8";
|
||||
@charset "utf-8";
|
||||
@font-face {
|
||||
font-family: "mnmliconsRegular";
|
||||
src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot");
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1263,7 +1269,7 @@
|
|||
color: white;
|
||||
padding: 2px 4px;
|
||||
margin: 1px 6px 0 0;
|
||||
border: 1px solid lightgrey;
|
||||
border: 1px solid lightGray;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
|
@ -1295,7 +1301,7 @@
|
|||
transform: scale(1.1);
|
||||
}
|
||||
.openerp .oe_secondary_submenu .oe_active {
|
||||
border-top: 1px solid lightgrey;
|
||||
border-top: 1px solid lightGray;
|
||||
border-bottom: 1px solid #dedede;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
-moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2);
|
||||
|
@ -1415,7 +1421,13 @@
|
|||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.openerp .oe_view_manager {
|
||||
display: table;
|
||||
height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
.openerp .oe_view_manager .oe_view_manager_body {
|
||||
display: table-row;
|
||||
height: inherit;
|
||||
}
|
||||
.openerp .oe_view_manager .oe_view_manager_view_kanban {
|
||||
|
@ -1800,7 +1812,7 @@
|
|||
}
|
||||
.openerp .oe_searchview .oe_searchview_drawer {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
z-index: 2;
|
||||
margin-top: 4px;
|
||||
top: 100%;
|
||||
right: -1px;
|
||||
|
@ -2225,6 +2237,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;
|
||||
}
|
||||
|
@ -2254,7 +2286,7 @@
|
|||
}
|
||||
.openerp .oe_form .oe_form_label_help[for] span, .openerp .oe_form .oe_form_label[for] span {
|
||||
font-size: 80%;
|
||||
color: darkgreen;
|
||||
color: darkGreen;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
top: -4px;
|
||||
|
@ -2305,6 +2337,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,
|
||||
|
@ -2326,6 +2365,7 @@
|
|||
width: 100%;
|
||||
display: inline-block;
|
||||
padding: 2px 2px 2px 0px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.openerp .oe_form .oe_form_field input {
|
||||
margin: 0px;
|
||||
|
@ -2371,7 +2411,6 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
.openerp .oe_form .oe_form_field_boolean {
|
||||
padding-top: 4px;
|
||||
width: auto;
|
||||
}
|
||||
.openerp .oe_form .oe_datepicker_container {
|
||||
|
@ -3093,8 +3132,25 @@
|
|||
color: #333333;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.openerp .oe_view_manager .oe_view_manager_switch li {
|
||||
line-height: 21px;
|
||||
}
|
||||
.openerp .oe_searchview .oe_searchview_search {
|
||||
top: -1px;
|
||||
}
|
||||
.openerp .oe_form_field_many2one .oe_m2o_cm_button {
|
||||
line-height: 18px;
|
||||
}
|
||||
.openerp .oe_secondary_submenu {
|
||||
line-height: 14px;
|
||||
}
|
||||
.openerp .oe_webclient .oe_star_on, .openerp .oe_webclient .oe_star_off {
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.kitten-mode-activated {
|
||||
background-image: url(http://placekitten.com/g/1365/769);
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
@ -3147,6 +3203,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;
|
||||
}
|
||||
|
@ -3165,6 +3229,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;
|
||||
|
|
|
@ -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
|
||||
|
@ -1137,7 +1141,11 @@ $sheet-padding: 16px
|
|||
// }}}
|
||||
// ViewManager common {{{
|
||||
.oe_view_manager
|
||||
display: table
|
||||
height: inherit
|
||||
width: 100%
|
||||
.oe_view_manager_body
|
||||
display: table-row
|
||||
height: inherit
|
||||
.oe_view_manager_view_kanban
|
||||
height: inherit
|
||||
|
@ -1432,7 +1440,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%
|
||||
|
@ -1761,7 +1769,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
|
||||
|
@ -1833,6 +1855,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,
|
||||
|
@ -1850,6 +1878,7 @@ $sheet-padding: 16px
|
|||
width: 100%
|
||||
display: inline-block
|
||||
padding: 2px 2px 2px 0px
|
||||
vertical-align: top
|
||||
input
|
||||
margin: 0px
|
||||
input[type="text"], input[type="password"], input[type="file"], select
|
||||
|
@ -1879,7 +1908,6 @@ $sheet-padding: 16px
|
|||
.oe_form_field_datetime
|
||||
white-space: nowrap
|
||||
.oe_form_field_boolean
|
||||
padding-top: 4px
|
||||
width: auto
|
||||
.oe_datepicker_container
|
||||
display: none
|
||||
|
@ -2441,9 +2469,22 @@ $sheet-padding: 16px
|
|||
float: right
|
||||
color: #333
|
||||
// }}}
|
||||
@-moz-document url-prefix()
|
||||
.openerp
|
||||
.oe_view_manager .oe_view_manager_switch li
|
||||
line-height: 21px
|
||||
.oe_searchview .oe_searchview_search
|
||||
top: -1px
|
||||
.oe_form_field_many2one .oe_m2o_cm_button
|
||||
line-height: 18px
|
||||
.oe_secondary_submenu
|
||||
line-height: 14px
|
||||
.oe_webclient
|
||||
.oe_star_on, .oe_star_off
|
||||
top: 0px
|
||||
|
||||
// Kitten Mode {{{
|
||||
.kitten-mode-activated
|
||||
background-image: url(http://placekitten.com/g/1365/769)
|
||||
background-size: cover
|
||||
background-attachment: fixed
|
||||
>*
|
||||
|
@ -2485,8 +2526,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
|
||||
|
@ -2502,6 +2551,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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
After Width: | Height: | Size: 783 B |
|
@ -327,6 +327,34 @@ instance.web.ExceptionHandler = {
|
|||
*/
|
||||
instance.web.crash_manager_registry = new instance.web.Registry();
|
||||
|
||||
/**
|
||||
* Handle redirection warnings, which behave more or less like a regular
|
||||
* warning, with an additional redirection button.
|
||||
*/
|
||||
instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
|
||||
init: function(parent, error) {
|
||||
this._super(parent);
|
||||
this.error = error;
|
||||
},
|
||||
display: function() {
|
||||
error = this.error;
|
||||
error.data.message = error.data.arguments[0];
|
||||
|
||||
instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
|
||||
title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
|
||||
buttons: [
|
||||
{text: _t("Ok"), click: function() { $(this).dialog("close"); }},
|
||||
{text: error.data.arguments[2], click: function() {
|
||||
window.location.href='#action='+error.data.arguments[1];
|
||||
$(this).dialog("close");
|
||||
}}
|
||||
]
|
||||
});
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
|
||||
|
||||
instance.web.Loading = instance.web.Widget.extend({
|
||||
template: _t("Loading"),
|
||||
init: function(parent) {
|
||||
|
@ -502,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,
|
||||
|
@ -613,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);
|
||||
|
@ -624,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;
|
||||
|
@ -643,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;
|
||||
}
|
||||
|
@ -654,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");
|
||||
|
@ -704,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');
|
||||
|
@ -772,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;
|
||||
|
@ -1056,7 +1123,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
|
|||
if (!self.session.uid)
|
||||
return;
|
||||
var func = new instance.web.Model("res.users").get_func("read");
|
||||
return func(self.session.uid, ["name", "company_id"]).then(function(res) {
|
||||
return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
|
||||
var topbar_name = res.name;
|
||||
if(instance.session.debug)
|
||||
topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
|
||||
|
@ -1072,6 +1139,9 @@ instance.web.UserMenu = instance.web.Widget.extend({
|
|||
};
|
||||
this.update_promise = this.update_promise.then(fct, fct);
|
||||
},
|
||||
on_menu_help: function() {
|
||||
window.open('http://help.openerp.com', '_blank');
|
||||
},
|
||||
on_menu_logout: function() {
|
||||
this.trigger('user_logout');
|
||||
},
|
||||
|
@ -1185,6 +1255,7 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
return $.when(this._super()).then(function() {
|
||||
if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
|
||||
$("body").addClass("kitten-mode-activated");
|
||||
$("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
|
||||
if ($.blockUI) {
|
||||
$.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">';
|
||||
}
|
||||
|
@ -1271,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
},
|
||||
logo_edit: function(ev) {
|
||||
var self = this;
|
||||
new 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";
|
||||
|
@ -1292,7 +1363,7 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
},
|
||||
check_timezone: function() {
|
||||
var self = this;
|
||||
return new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']]).then(function(result) {
|
||||
return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
|
||||
var user_offset = result[0]['tz_offset'];
|
||||
var offset = -(new Date().getTimezoneOffset());
|
||||
// _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
|
||||
|
@ -1368,8 +1439,9 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
},
|
||||
on_hashchange: function(event) {
|
||||
var self = this;
|
||||
var state = event.getState(true);
|
||||
if (!_.isEqual(this._current_state, state)) {
|
||||
var stringstate = event.getState(false);
|
||||
if (!_.isEqual(this._current_state, stringstate)) {
|
||||
var state = event.getState(true);
|
||||
if(!state.action && state.menu_id) {
|
||||
self.menu.has_been_loaded.done(function() {
|
||||
self.menu.do_reload().done(function() {
|
||||
|
@ -1381,13 +1453,13 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
this.action_manager.do_load_state(state, !!this._current_state);
|
||||
}
|
||||
}
|
||||
this._current_state = state;
|
||||
this._current_state = stringstate;
|
||||
},
|
||||
do_push_state: function(state) {
|
||||
this.set_title(state.title);
|
||||
delete state.title;
|
||||
var url = '#' + $.param(state);
|
||||
this._current_state = _.clone(state);
|
||||
this._current_state = $.deparam($.param(state), false); // stringify all values
|
||||
$.bbq.pushState(url);
|
||||
this.trigger('state_pushed', state);
|
||||
},
|
||||
|
@ -1397,9 +1469,10 @@ instance.web.WebClient = instance.web.Client.extend({
|
|||
.then(function (result) {
|
||||
return self.action_mutex.exec(function() {
|
||||
if (options.needaction) {
|
||||
result.context = new instance.web.CompoundContext(
|
||||
result.context,
|
||||
{search_default_message_unread: true});
|
||||
result.context = new instance.web.CompoundContext(result.context, {
|
||||
search_default_message_unread: true,
|
||||
search_disable_custom_filters: true,
|
||||
});
|
||||
}
|
||||
var completed = $.Deferred();
|
||||
$.when(self.action_manager.do_action(result, {
|
||||
|
@ -1466,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() {
|
||||
|
|
|
@ -228,6 +228,42 @@ instance.web.ParentedMixin = {
|
|||
isDestroyed : function() {
|
||||
return this.__parentedDestroyed;
|
||||
},
|
||||
/**
|
||||
Utility method to only execute asynchronous actions if the current
|
||||
object has not been 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();
|
||||
var self = this;
|
||||
promise.done(function() {
|
||||
if (! self.isDestroyed()) {
|
||||
if (! reject)
|
||||
def.resolve.apply(def, arguments);
|
||||
else
|
||||
def.reject();
|
||||
}
|
||||
}).fail(function() {
|
||||
if (! self.isDestroyed()) {
|
||||
if (! reject)
|
||||
def.reject.apply(def, arguments);
|
||||
else
|
||||
def.reject();
|
||||
}
|
||||
});
|
||||
return def.promise();
|
||||
},
|
||||
/**
|
||||
* Inform the object it should destroy itself, releasing any
|
||||
* resource it could have reserved.
|
||||
|
@ -495,16 +531,7 @@ instance.web.Controller = instance.web.Class.extend(instance.web.PropertiesMixin
|
|||
return false;
|
||||
},
|
||||
rpc: function(url, data, options) {
|
||||
var def = $.Deferred();
|
||||
var self = this;
|
||||
instance.session.rpc(url, data, options).done(function() {
|
||||
if (!self.isDestroyed())
|
||||
def.resolve.apply(def, arguments);
|
||||
}).fail(function() {
|
||||
if (!self.isDestroyed())
|
||||
def.reject.apply(def, arguments);
|
||||
});
|
||||
return def.promise();
|
||||
return this.alive(instance.session.rpc(url, data, options));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -674,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();
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
@ -690,7 +699,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
return filter.user_id && filter.is_default;
|
||||
});
|
||||
if (personal_filter) {
|
||||
this.custom_filters.enable_filter(personal_filter, true);
|
||||
this.custom_filters.toggle_filter(personal_filter, true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -698,7 +707,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
|||
return !filter.user_id && filter.is_default;
|
||||
});
|
||||
if (global_filter) {
|
||||
this.custom_filters.enable_filter(global_filter, true);
|
||||
this.custom_filters.toggle_filter(global_filter, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -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,18 +1503,21 @@ 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) {
|
||||
var self = this;
|
||||
// TODO: context
|
||||
// FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
|
||||
var context = instance.web.pyeval.eval(
|
||||
'contexts', [this.view.dataset.get_context()]);
|
||||
return this.model.call('name_search', [], {
|
||||
name: needle,
|
||||
args: instance.web.pyeval.eval(
|
||||
'domains', this.attrs.domain ? [this.attrs.domain] : [], context),
|
||||
limit: 8,
|
||||
context: {}
|
||||
context: context
|
||||
}).then(function (results) {
|
||||
if (_.isEmpty(results)) { return null; }
|
||||
return [{label: self.attrs.string}].concat(
|
||||
|
@ -1541,6 +1587,9 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
|||
})
|
||||
.on('reset', this.proxy('clear_selection'));
|
||||
this.$el.on('submit', 'form', this.proxy('save_current'));
|
||||
this.$el.on('click', 'input[type=checkbox]', function() {
|
||||
$(this).siblings('input[type=checkbox]').prop('checked', false);
|
||||
});
|
||||
this.$el.on('click', 'h4', function () {
|
||||
self.$el.toggleClass('oe_opened');
|
||||
});
|
||||
|
@ -1591,6 +1640,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
|||
get_groupby: function () { return [filter.context]; },
|
||||
get_domain: function () { return filter.domain; }
|
||||
},
|
||||
_id: filter['id'],
|
||||
is_custom_filter: true,
|
||||
values: [{label: filter.name, value: null}]
|
||||
};
|
||||
|
@ -1632,10 +1682,18 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
|||
}
|
||||
|
||||
$filter.unbind('click').click(function () {
|
||||
self.enable_filter(filter);
|
||||
self.toggle_filter(filter);
|
||||
});
|
||||
},
|
||||
enable_filter: function (filter, preventSearch) {
|
||||
toggle_filter: function (filter, preventSearch) {
|
||||
var current = this.view.query.find(function (facet) {
|
||||
return facet.get('_id') === filter.id;
|
||||
});
|
||||
if (current) {
|
||||
this.view.query.remove(current);
|
||||
this.$filters[this.key_for(filter)].removeClass('oe_selected');
|
||||
return;
|
||||
}
|
||||
this.view.query.reset([this.facet_for(filter)], {
|
||||
preventSearch: preventSearch || false});
|
||||
this.$filters[this.key_for(filter)].addClass('oe_selected');
|
||||
|
@ -1658,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,
|
||||
|
@ -1687,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
|
||||
|
@ -1800,6 +1871,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
|
|||
template: 'SearchView.extended_search.proposition',
|
||||
events: {
|
||||
'change .searchview_extended_prop_field': 'changed',
|
||||
'change .searchview_extended_prop_op': 'operator_changed',
|
||||
'click .searchview_extended_delete_prop': function (e) {
|
||||
e.stopPropagation();
|
||||
this.getParent().remove_proposition(this);
|
||||
|
@ -1831,6 +1903,17 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
|
|||
this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
|
||||
}
|
||||
},
|
||||
operator_changed: function (e) {
|
||||
var $value = this.$('.searchview_extended_prop_value');
|
||||
switch ($(e.target).val()) {
|
||||
case '∃':
|
||||
case '∄':
|
||||
$value.hide();
|
||||
break;
|
||||
default:
|
||||
$value.show();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Selects the provided field object
|
||||
*
|
||||
|
@ -1859,7 +1942,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
|
|||
.text(String(operator.text))
|
||||
.appendTo(self.$('.searchview_extended_prop_op'));
|
||||
});
|
||||
var $value_loc = this.$('.searchview_extended_prop_value').empty();
|
||||
var $value_loc = this.$('.searchview_extended_prop_value').show().empty();
|
||||
this.value.appendTo($value_loc);
|
||||
|
||||
},
|
||||
|
@ -1867,19 +1950,12 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @
|
|||
if ( this.attrs.selected == null)
|
||||
return null;
|
||||
var field = this.attrs.selected;
|
||||
var op = this.$('.searchview_extended_prop_op')[0];
|
||||
var operator = op.options[op.selectedIndex];
|
||||
var op_select = this.$('.searchview_extended_prop_op')[0];
|
||||
var operator = op_select.options[op_select.selectedIndex];
|
||||
|
||||
return {
|
||||
label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), {
|
||||
field: field.string,
|
||||
// According to spec, HTMLOptionElement#label should return
|
||||
// HTMLOptionElement#text when not defined/empty, but it does
|
||||
// not in older Webkit (between Safari 5.1.5 and Chrome 17) and
|
||||
// Gecko (pre Firefox 7) browsers, so we need a manual fallback
|
||||
// for those
|
||||
operator: operator.label || operator.text,
|
||||
value: this.value}),
|
||||
value: [field.name, operator.value, this.value.get_value()]
|
||||
label: this.value.get_label(field, operator),
|
||||
value: this.value.get_domain(field, operator),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -1889,6 +1965,37 @@ instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend
|
|||
this._super(parent);
|
||||
this.field = field;
|
||||
},
|
||||
get_label: function (field, operator) {
|
||||
var format;
|
||||
switch (operator.value) {
|
||||
case '∃': case '∄': format = _t('%(field)s %(operator)s'); break;
|
||||
default: format = _t('%(field)s %(operator)s "%(value)s"'); break;
|
||||
}
|
||||
return this.format_label(format, field, operator);
|
||||
},
|
||||
format_label: function (format, field, operator) {
|
||||
return _.str.sprintf(format, {
|
||||
field: field.string,
|
||||
// According to spec, HTMLOptionElement#label should return
|
||||
// HTMLOptionElement#text when not defined/empty, but it does
|
||||
// not in older Webkit (between Safari 5.1.5 and Chrome 17) and
|
||||
// Gecko (pre Firefox 7) browsers, so we need a manual fallback
|
||||
// for those
|
||||
operator: operator.label || operator.text,
|
||||
value: this
|
||||
});
|
||||
},
|
||||
get_domain: function (field, operator) {
|
||||
switch (operator.value) {
|
||||
case '∃': return this.make_domain(field.name, '!=', false);
|
||||
case '∄': return this.make_domain(field.name, '=', false);
|
||||
default: return this.make_domain(
|
||||
field.name, operator.value, this.get_value());
|
||||
}
|
||||
},
|
||||
make_domain: function (field, operator, value) {
|
||||
return [field, operator, value];
|
||||
},
|
||||
/**
|
||||
* Returns a human-readable version of the value, in case the "logical"
|
||||
* and the "semantic" values of a field differ (as for selection fields,
|
||||
|
@ -1908,7 +2015,9 @@ instance.web.search.ExtendedSearchProposition.Char = instance.web.search.Extende
|
|||
{value: "ilike", text: _lt("contains")},
|
||||
{value: "not ilike", text: _lt("doesn't contain")},
|
||||
{value: "=", text: _lt("is equal to")},
|
||||
{value: "!=", text: _lt("is not equal to")}
|
||||
{value: "!=", text: _lt("is not equal to")},
|
||||
{value: "∃", text: _lt("is set")},
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
get_value: function() {
|
||||
return this.$el.val();
|
||||
|
@ -1922,7 +2031,9 @@ instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.Ext
|
|||
{value: ">", text: _lt("greater than")},
|
||||
{value: "<", text: _lt("less than")},
|
||||
{value: ">=", text: _lt("greater or equal than")},
|
||||
{value: "<=", text: _lt("less or equal than")}
|
||||
{value: "<=", text: _lt("less or equal than")},
|
||||
{value: "∃", text: _lt("is set")},
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
/**
|
||||
* Date widgets live in view_form which is not yet loaded when this is
|
||||
|
@ -1956,7 +2067,9 @@ instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.Exte
|
|||
{value: ">", text: _lt("greater than")},
|
||||
{value: "<", text: _lt("less than")},
|
||||
{value: ">=", text: _lt("greater or equal than")},
|
||||
{value: "<=", text: _lt("less or equal than")}
|
||||
{value: "<=", text: _lt("less or equal than")},
|
||||
{value: "∃", text: _lt("is set")},
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
toString: function () {
|
||||
return this.$el.val();
|
||||
|
@ -1981,7 +2094,9 @@ instance.web.search.ExtendedSearchProposition.Float = instance.web.search.Extend
|
|||
{value: ">", text: _lt("greater than")},
|
||||
{value: "<", text: _lt("less than")},
|
||||
{value: ">=", text: _lt("greater or equal than")},
|
||||
{value: "<=", text: _lt("less or equal than")}
|
||||
{value: "<=", text: _lt("less or equal than")},
|
||||
{value: "∃", text: _lt("is set")},
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
toString: function () {
|
||||
return this.$el.val();
|
||||
|
@ -1999,7 +2114,9 @@ instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.Ex
|
|||
template: 'SearchView.extended_search.proposition.selection',
|
||||
operators: [
|
||||
{value: "=", text: _lt("is")},
|
||||
{value: "!=", text: _lt("is not")}
|
||||
{value: "!=", text: _lt("is not")},
|
||||
{value: "∃", text: _lt("is set")},
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
toString: function () {
|
||||
var select = this.$el[0];
|
||||
|
@ -2016,7 +2133,10 @@ instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.Exte
|
|||
{value: "=", text: _lt("is true")},
|
||||
{value: "!=", text: _lt("is false")}
|
||||
],
|
||||
toString: function () { return ''; },
|
||||
get_label: function (field, operator) {
|
||||
return this.format_label(
|
||||
_t('%(field)s %(operator)s'), field, operator);
|
||||
},
|
||||
get_value: function() {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -247,13 +247,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
|
||||
do_load_state: function(state, warm) {
|
||||
if (state.id && this.datarecord.id != state.id) {
|
||||
if (!this.dataset.get_id_index(state.id)) {
|
||||
if (this.dataset.get_id_index(state.id) === null) {
|
||||
this.dataset.ids.push(state.id);
|
||||
}
|
||||
this.dataset.select_id(state.id);
|
||||
if (warm) {
|
||||
this.do_show();
|
||||
}
|
||||
this.do_show({ reload: warm });
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
@ -511,9 +509,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
var on_change = widget.node.attrs.on_change;
|
||||
if (on_change) {
|
||||
var change_spec = self.parse_on_change(on_change, widget);
|
||||
var id = [self.datarecord.id == null ? [] : [self.datarecord.id]];
|
||||
def = new instance.web.Model(self.dataset.model).call(
|
||||
change_spec.method, id.concat(change_spec.args));
|
||||
var ids = [];
|
||||
if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
|
||||
// In case of a o2m virtual id, we should pass an empty ids list
|
||||
ids.push(self.datarecord.id);
|
||||
}
|
||||
def = self.alive(new instance.web.Model(self.dataset.model).call(
|
||||
change_spec.method, [ids].concat(change_spec.args)));
|
||||
} else {
|
||||
def = $.when({});
|
||||
}
|
||||
|
@ -531,9 +533,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
var condition = fieldname + '=' + value_;
|
||||
|
||||
if (value_) {
|
||||
return new instance.web.Model('ir.values').call(
|
||||
return self.alive(new instance.web.Model('ir.values').call(
|
||||
'get_defaults', [self.model, condition]
|
||||
).then(function (results) {
|
||||
)).then(function (results) {
|
||||
if (!results.length) {
|
||||
return response;
|
||||
}
|
||||
|
@ -808,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];
|
||||
|
@ -817,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) {
|
||||
|
@ -834,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)) {
|
||||
|
@ -842,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);
|
||||
}
|
||||
|
@ -1191,6 +1198,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
|
|||
$('button', doc).each(function() {
|
||||
$(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
|
||||
});
|
||||
// IE's html parser is also a css parser. How convenient...
|
||||
$('board', doc).each(function() {
|
||||
$(this).attr('layout', $(this).attr('style'));
|
||||
});
|
||||
return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
|
||||
},
|
||||
render_to: function($target) {
|
||||
|
@ -1881,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();
|
||||
|
@ -1910,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 {
|
||||
|
@ -2212,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',
|
||||
|
@ -2327,11 +2353,12 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
|
|||
this._super();
|
||||
} else {
|
||||
var tmp = this.get('value');
|
||||
var s = /(\w+):(.+)/.exec(tmp);
|
||||
var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
|
||||
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() {
|
||||
|
@ -2395,6 +2422,11 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
|
|||
showButtonPanel: true,
|
||||
firstDay: Date.CultureInfo.firstDayOfWeek
|
||||
});
|
||||
// Some clicks in the datepicker dialog are not stopped by the
|
||||
// datepicker and "bubble through", unexpectedly triggering the bus's
|
||||
// click event. Prevent that.
|
||||
this.picker('widget').click(function (e) { e.stopPropagation(); });
|
||||
|
||||
this.$el.find('img.oe_datepicker_trigger').click(function() {
|
||||
if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
|
||||
self.$input.focus();
|
||||
|
@ -2534,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() {
|
||||
|
@ -2587,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
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2625,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() {
|
||||
|
@ -2957,7 +2996,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
|
|||
case $.ui.keyCode.DOWN:
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
init: function(field_manager, node) {
|
||||
this._super(field_manager, node);
|
||||
|
@ -2988,6 +3027,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
|
||||
},
|
||||
|
@ -3047,9 +3106,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
|
|||
this.$input.keydown(input_changed);
|
||||
this.$input.change(input_changed);
|
||||
this.$drop_down.click(function() {
|
||||
self.$input.focus();
|
||||
if (self.$input.autocomplete("widget").is(":visible")) {
|
||||
self.$input.autocomplete("close");
|
||||
self.$input.focus();
|
||||
self.$input.autocomplete("close");
|
||||
} else {
|
||||
if (self.get("value") && ! self.floating) {
|
||||
self.$input.autocomplete("search", "");
|
||||
|
@ -3058,6 +3117,15 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Autocomplete close on dialog content scroll
|
||||
var close_autocomplete = _.debounce(function() {
|
||||
if (self.$input.autocomplete("widget").is(":visible")) {
|
||||
self.$input.autocomplete("close");
|
||||
}
|
||||
}, 50);
|
||||
this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
|
||||
|
||||
self.ed_def = $.Deferred();
|
||||
self.uned_def = $.Deferred();
|
||||
var ed_delay = 200;
|
||||
|
@ -3209,7 +3277,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;
|
||||
});
|
||||
|
@ -3240,7 +3309,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() {
|
||||
|
@ -3659,8 +3728,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);
|
||||
});
|
||||
|
@ -3719,8 +3788,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
|
||||
|
@ -3750,11 +3823,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);
|
||||
|
@ -3967,6 +4040,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',
|
||||
|
@ -3985,6 +4059,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();
|
||||
}
|
||||
},
|
||||
|
@ -4035,10 +4110,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();
|
||||
|
@ -4094,6 +4172,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
|
|||
focus: function () {
|
||||
this.$text[0].focus();
|
||||
},
|
||||
set_dimensions: function (height, width) {
|
||||
this._super(height, width);
|
||||
this.$("textarea").css({
|
||||
width: width,
|
||||
minHeight: height
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -4238,6 +4323,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, {
|
||||
|
@ -4450,6 +4536,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
|
|||
* options:
|
||||
* -readonly: only applicable when not in creation mode, default to false
|
||||
* - alternative_form_view
|
||||
* - view_id
|
||||
* - write_function
|
||||
* - read_function
|
||||
* - create_function
|
||||
|
@ -4471,9 +4558,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);
|
||||
});
|
||||
|
@ -4516,7 +4603,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
|
|||
_.extend(options, {
|
||||
$buttons: this.$buttonpane,
|
||||
});
|
||||
this.view_form = new instance.web.FormView(this, this.dataset, false, options);
|
||||
this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
|
||||
if (this.options.alternative_form_view) {
|
||||
this.view_form.set_embedded_view(this.options.alternative_form_view);
|
||||
}
|
||||
|
@ -4545,6 +4632,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
|
|||
});
|
||||
var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
|
||||
$cbutton.click(function() {
|
||||
self.view_form.trigger('on_button_cancel');
|
||||
self.check_exit();
|
||||
});
|
||||
self.view_form.do_show();
|
||||
|
@ -5014,7 +5102,7 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
|
|||
});
|
||||
|
||||
/**
|
||||
* Widget for (one2many field) to upload one or more file in same time and display in list.
|
||||
* Widget for (many2many field) to upload one or more file in same time and display in list.
|
||||
* The user can delete his files.
|
||||
* Options on attribute ; "blockui" {Boolean} block the UI or not
|
||||
* during the file is uploading
|
||||
|
@ -5028,6 +5116,8 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
|
|||
if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
|
||||
throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
|
||||
}
|
||||
this.data = {};
|
||||
this.set_value([]);
|
||||
this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
|
||||
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
|
||||
$(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
|
||||
|
@ -5037,73 +5127,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
|
|||
this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
|
||||
},
|
||||
set_value: function(value_) {
|
||||
var value_ = value_ || [];
|
||||
var self = this;
|
||||
var ids = [];
|
||||
_.each(value_, function(command) {
|
||||
if (isNaN(command) && command.id == undefined) {
|
||||
switch (command[0]) {
|
||||
case commands.CREATE:
|
||||
ids = ids.concat(command[2]);
|
||||
return;
|
||||
case commands.REPLACE_WITH:
|
||||
ids = ids.concat(command[2]);
|
||||
return;
|
||||
case commands.UPDATE:
|
||||
ids = ids.concat(command[2]);
|
||||
return;
|
||||
case commands.LINK_TO:
|
||||
ids = ids.concat(command[1]);
|
||||
return;
|
||||
case commands.DELETE:
|
||||
ids = _.filter(ids, function (id) { return id != command[1];});
|
||||
return;
|
||||
case commands.DELETE_ALL:
|
||||
ids = [];
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
ids.push(command);
|
||||
}
|
||||
});
|
||||
this._super( ids );
|
||||
value_ = value_ || [];
|
||||
if (value_.length >= 1 && value_[0] instanceof Array) {
|
||||
value_ = value_[0][2];
|
||||
}
|
||||
this._super(value_);
|
||||
},
|
||||
get_value: function() {
|
||||
return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
|
||||
var tmp = [commands.replace_with(this.get("value"))];
|
||||
return tmp;
|
||||
},
|
||||
get_file_url: function (attachment) {
|
||||
return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
|
||||
},
|
||||
read_name_values : function () {
|
||||
var self = this;
|
||||
// select the list of id for a get_name
|
||||
var values = [];
|
||||
_.each(this.get('value'), function (val) {
|
||||
if (typeof val != 'object') {
|
||||
values.push(val);
|
||||
}
|
||||
});
|
||||
// don't reset know values
|
||||
var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } );
|
||||
// send request for get_name
|
||||
if (values.length) {
|
||||
return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
|
||||
if (_value.length) {
|
||||
return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).done(function (datas) {
|
||||
_.each(datas, function (data) {
|
||||
data.no_unlink = true;
|
||||
data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
|
||||
|
||||
_.each(self.get('value'), function (val, key) {
|
||||
if(val == data.id) {
|
||||
self.get('value')[key] = data;
|
||||
}
|
||||
});
|
||||
self.data[data.id] = data;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return $.when(this.get('value'));
|
||||
return $.when();
|
||||
}
|
||||
},
|
||||
render_value: function () {
|
||||
var self = this;
|
||||
this.read_name_values().then(function (datas) {
|
||||
this.read_name_values().then(function () {
|
||||
|
||||
var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
|
||||
render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
|
||||
|
@ -5121,45 +5177,36 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
|
|||
var self = this;
|
||||
var $target = $(event.target);
|
||||
if ($target.val() !== '') {
|
||||
|
||||
var filename = $target.val().replace(/.*[\\\/]/,'');
|
||||
|
||||
// if the files is currently uploded, don't send again
|
||||
if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
|
||||
// don't uplode more of one file in same time
|
||||
if (self.data[0] && self.data[0].upload ) {
|
||||
return false;
|
||||
}
|
||||
for (var id in this.get('value')) {
|
||||
// if the files exits, delete the file before upload (if it's a new file)
|
||||
if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
|
||||
self.ds_file.unlink([id]);
|
||||
}
|
||||
}
|
||||
|
||||
// block UI or not
|
||||
if(this.node.attrs.blockui>0) {
|
||||
instance.web.blockUI();
|
||||
}
|
||||
|
||||
// if the files exits for this answer, delete the file before upload
|
||||
var files = _.filter(this.get('value'), function (file) {
|
||||
if((file.filename || file.name) == filename) {
|
||||
self.ds_file.unlink([file.id]);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO : unactivate send on wizard and form
|
||||
|
||||
// submit file
|
||||
this.$('form.oe_form_binary_form').submit();
|
||||
this.$(".oe_fileupload").hide();
|
||||
|
||||
// add file on result
|
||||
files.push({
|
||||
// add file on data result
|
||||
this.data[0] = {
|
||||
'id': 0,
|
||||
'name': filename,
|
||||
'filename': filename,
|
||||
'url': '',
|
||||
'upload': true
|
||||
});
|
||||
|
||||
this.set({'value': files});
|
||||
};
|
||||
}
|
||||
},
|
||||
on_file_loaded: function (event, result) {
|
||||
|
@ -5170,39 +5217,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie
|
|||
instance.web.unblockUI();
|
||||
}
|
||||
|
||||
// TODO : activate send on wizard and form
|
||||
|
||||
if (result.error || !result.id ) {
|
||||
this.do_warn( _t('Uploading error'), result.error);
|
||||
files = _.filter(files, function (val) { return !val.upload; });
|
||||
delete this.data[0];
|
||||
} else {
|
||||
for(var i in files){
|
||||
if(files[i].filename == result.filename && files[i].upload) {
|
||||
files[i] = {
|
||||
'id': result.id,
|
||||
'name': result.name,
|
||||
'filename': result.filename,
|
||||
'url': this.get_file_url(result)
|
||||
};
|
||||
}
|
||||
if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
|
||||
delete this.data[0];
|
||||
this.data[result.id] = {
|
||||
'id': result.id,
|
||||
'name': result.name,
|
||||
'filename': result.filename,
|
||||
'url': this.get_file_url(result)
|
||||
};
|
||||
} else {
|
||||
this.data[result.id] = {
|
||||
'id': result.id,
|
||||
'name': result.name,
|
||||
'filename': result.filename,
|
||||
'url': this.get_file_url(result)
|
||||
};
|
||||
}
|
||||
var values = _.clone(this.get('value'));
|
||||
values.push(result.id);
|
||||
this.set({'value': values});
|
||||
}
|
||||
|
||||
this.set({'value': files});
|
||||
this.render_value()
|
||||
},
|
||||
on_file_delete: function (event) {
|
||||
event.stopPropagation();
|
||||
var file_id=$(event.target).data("id");
|
||||
if (file_id) {
|
||||
var files=[];
|
||||
for(var i in this.get('value')){
|
||||
if(file_id != this.get('value')[i].id){
|
||||
files.push(this.get('value')[i]);
|
||||
}
|
||||
else if(!this.get('value')[i].no_unlink) {
|
||||
this.ds_file.unlink([file_id]);
|
||||
}
|
||||
var files = _.filter(this.get('value'), function (id) {return id != file_id;});
|
||||
if(!this.data[file_id].no_unlink) {
|
||||
this.ds_file.unlink([file_id]);
|
||||
}
|
||||
this.set({'value': files});
|
||||
}
|
||||
|
@ -5216,8 +5263,20 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
|
|||
this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
|
||||
this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
|
||||
this.set({value: false});
|
||||
this.selection = [];
|
||||
this.set("selection", []);
|
||||
this.selection_dm = new instance.web.DropMisordered();
|
||||
},
|
||||
start: function() {
|
||||
this.field_manager.on("view_content_has_changed", this, this.calc_domain);
|
||||
this.calc_domain();
|
||||
this.on("change:value", this, this.get_selection);
|
||||
this.on("change:evaluated_selection_domain", this, 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);
|
||||
}
|
||||
|
@ -5234,15 +5293,25 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
|
|||
},
|
||||
render_value: function() {
|
||||
var self = this;
|
||||
self.get_selection().done(function() {
|
||||
var content = QWeb.render("FieldStatus.content", {widget: self});
|
||||
self.$el.html(content);
|
||||
var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
|
||||
var color = colors[self.get('value')];
|
||||
if (color) {
|
||||
self.$("oe_active").css("color", color);
|
||||
}
|
||||
});
|
||||
var content = QWeb.render("FieldStatus.content", {widget: self});
|
||||
self.$el.html(content);
|
||||
var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
|
||||
var color = colors[self.get('value')];
|
||||
if (color) {
|
||||
self.$("oe_active").css("color", color);
|
||||
}
|
||||
},
|
||||
calc_domain: function() {
|
||||
var d = instance.web.pyeval.eval('domain', this.build_domain());
|
||||
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);
|
||||
}
|
||||
},
|
||||
/** Get the selection and render it
|
||||
* selection: [[identifier, value_to_display], ...]
|
||||
|
@ -5251,32 +5320,37 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
|
|||
*/
|
||||
get_selection: function() {
|
||||
var self = this;
|
||||
self.selection = [];
|
||||
if (this.field.type == "many2one") {
|
||||
var domain = [];
|
||||
if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) {
|
||||
var d = instance.web.pyeval.eval('domain', self.build_domain());
|
||||
domain = ['|', ['id', '=', self.get('value')]].concat(d);
|
||||
}
|
||||
var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain);
|
||||
return ds.read_slice(['name'], {}).then(function (records) {
|
||||
for(var i = 0; i < records.length; i++) {
|
||||
self.selection.push([records[i].id, records[i].name]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For field type selection filter values according to
|
||||
// statusbar_visible attribute of the field. For example:
|
||||
// statusbar_visible="draft,open".
|
||||
var selection = this.field.selection;
|
||||
for(var i=0; i < selection.length; i++) {
|
||||
var key = selection[i][0];
|
||||
if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
|
||||
this.selection.push(selection[i]);
|
||||
var selection = [];
|
||||
|
||||
var calculation = _.bind(function() {
|
||||
if (this.field.type == "many2one") {
|
||||
var domain = [];
|
||||
var ds = new instance.web.DataSetSearch(this, this.field.relation,
|
||||
self.build_context(), this.get("evaluated_selection_domain"));
|
||||
return ds.read_slice(['name'], {}).then(function (records) {
|
||||
for(var i = 0; i < records.length; i++) {
|
||||
selection.push([records[i].id, records[i].name]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For field type selection filter values according to
|
||||
// statusbar_visible attribute of the field. For example:
|
||||
// statusbar_visible="draft,open".
|
||||
var select = this.field.selection;
|
||||
for(var i=0; i < select.length; i++) {
|
||||
var key = select[i][0];
|
||||
if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
|
||||
selection.push(select[i]);
|
||||
}
|
||||
}
|
||||
return $.when();
|
||||
}
|
||||
return $.when();
|
||||
}
|
||||
}, this);
|
||||
this.selection_dm.add(calculation()).then(function () {
|
||||
if (! _.isEqual(selection, self.get("selection"))) {
|
||||
self.set("selection", selection);
|
||||
}
|
||||
});
|
||||
},
|
||||
on_click_stage: function (ev) {
|
||||
var self = this;
|
||||
|
@ -5320,16 +5394,16 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
|
|||
this.set({"currency_info": null});
|
||||
return;
|
||||
}
|
||||
return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"])
|
||||
.filter([["id", "=", self.get("currency")]]).first()).then(function(res) {
|
||||
return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
|
||||
.filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
|
||||
self.set({"currency_info": res});
|
||||
});
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
@ -1186,7 +1186,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
|||
}
|
||||
});
|
||||
instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{
|
||||
passtrough_events: 'action deleted row_link',
|
||||
passthrough_events: 'action deleted row_link',
|
||||
/**
|
||||
* Grouped display for the ListView. Handles basic DOM events and interacts
|
||||
* with the :js:class:`~DataGroup` bound to it.
|
||||
|
@ -1406,7 +1406,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
|
|||
// can have selections spanning multiple links
|
||||
var selection = self.get_selection();
|
||||
$this.trigger(e, [selection.ids, selection.records]);
|
||||
}).bind(this.passtrough_events, function (e) {
|
||||
}).bind(this.passthrough_events, function (e) {
|
||||
// additional positional parameters are provided to trigger as an
|
||||
// Array, following the event type or event object, but are
|
||||
// provided to the .bind event handler as *args.
|
||||
|
@ -2212,7 +2212,7 @@ instance.web.list.Binary = instance.web.list.Column.extend({
|
|||
if (value && value.substr(0, 10).indexOf(' ') == -1) {
|
||||
download_url = "data:application/octet-stream;base64," + value;
|
||||
} else {
|
||||
download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
|
||||
download_url = instance.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
|
||||
if (this.filename) {
|
||||
download_url += '&filename_field=' + this.filename;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
@ -132,6 +133,15 @@ openerp.web.list_editable = function (instance) {
|
|||
var self = this;
|
||||
// tree/@editable takes priority on everything else if present.
|
||||
var result = this._super(data, grouped);
|
||||
|
||||
// In case current editor was started previously, also has to run
|
||||
// when toggling from editable to non-editable in case form widgets
|
||||
// have setup global behaviors expecting themselves to exist
|
||||
// somehow.
|
||||
this.editor.destroy();
|
||||
// Editor is not restartable due to formview not being restartable
|
||||
this.editor = this.make_editor();
|
||||
|
||||
if (this.editable()) {
|
||||
this.$el.addClass('oe_list_editable');
|
||||
// FIXME: any hook available to ensure this is only done once?
|
||||
|
@ -143,10 +153,6 @@ openerp.web.list_editable = function (instance) {
|
|||
e.preventDefault();
|
||||
self.cancel_edition();
|
||||
});
|
||||
this.editor.destroy();
|
||||
// Editor is not restartable due to formview not being
|
||||
// restartable
|
||||
this.editor = this.make_editor();
|
||||
var editor_ready = this.editor.prependTo(this.$el)
|
||||
.done(this.proxy('setup_events'));
|
||||
|
||||
|
@ -795,7 +801,7 @@ openerp.web.list_editable = function (instance) {
|
|||
});
|
||||
|
||||
instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
|
||||
passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved",
|
||||
passthrough_events: instance.web.ListView.Groups.prototype.passthrough_events + " edit saved",
|
||||
get_row_for: function (record) {
|
||||
return _(this.children).chain()
|
||||
.invoke('get_row_for', record)
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
@ -190,6 +193,18 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
|||
});
|
||||
state = _.extend(params || {}, state);
|
||||
}
|
||||
if (this.inner_action.context) {
|
||||
var active_id = this.inner_action.context.active_id;
|
||||
if (active_id) {
|
||||
state["active_id"] = active_id;
|
||||
}
|
||||
var active_ids = this.inner_action.context.active_ids;
|
||||
if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
|
||||
// We don't push active_ids if it's a single element array containing the active_id
|
||||
// This makes the url shorter in most cases.
|
||||
state["active_ids"] = this.inner_action.context.active_ids.join(',');
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!this.dialog) {
|
||||
this.getParent().do_push_state(state);
|
||||
|
@ -212,8 +227,22 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
|||
} else {
|
||||
var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
|
||||
if (run_action) {
|
||||
var add_context = {};
|
||||
if (state.active_id) {
|
||||
add_context.active_id = state.active_id;
|
||||
}
|
||||
if (state.active_ids) {
|
||||
// The jQuery BBQ plugin does some parsing on values that are valid integers.
|
||||
// It means that if there's only one item, it will do parseInt() on it,
|
||||
// otherwise it will keep the comma seperated list as string.
|
||||
add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
|
||||
return parseInt(id, 10) || id;
|
||||
});
|
||||
} else if (state.active_id) {
|
||||
add_context.active_ids = [state.active_id];
|
||||
}
|
||||
this.null_action();
|
||||
action_loaded = this.do_action(state.action);
|
||||
action_loaded = this.do_action(state.action, { additional_context: add_context });
|
||||
$.when(action_loaded || null).done(function() {
|
||||
instance.webclient.menu.has_been_loaded.done(function() {
|
||||
if (self.inner_action && self.inner_action.id) {
|
||||
|
@ -249,12 +278,27 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
|||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Execute an OpenERP action
|
||||
*
|
||||
* @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
|
||||
* @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.
|
||||
* @return {jQuery.Deferred} Action loaded
|
||||
*/
|
||||
do_action: function(action, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
clear_breadcrumbs: false,
|
||||
on_reverse_breadcrumb: function() {},
|
||||
hide_breadcrumb: false,
|
||||
on_close: function() {},
|
||||
action_menu_id: null,
|
||||
additional_context: {},
|
||||
});
|
||||
if (action === false) {
|
||||
action = { type: 'ir.actions.act_window_close' };
|
||||
|
@ -269,9 +313,13 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
|||
}
|
||||
|
||||
// Ensure context & domain are evaluated and can be manipulated/used
|
||||
if (action.context) {
|
||||
action.context = instance.web.pyeval.eval(
|
||||
'context', action.context);
|
||||
var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
|
||||
action.context = instance.web.pyeval.eval('context', ncontext);
|
||||
if (action.context.active_id || action.context.active_ids) {
|
||||
// Here we assume that when an `active_id` or `active_ids` is used
|
||||
// in the context, we are in a `related` action, so we disable the
|
||||
// searchview's default custom filters.
|
||||
action.context.search_disable_custom_filters = true;
|
||||
}
|
||||
if (action.domain) {
|
||||
action.domain = instance.web.pyeval.eval(
|
||||
|
@ -360,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) {
|
||||
|
@ -384,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({});
|
||||
|
@ -539,7 +593,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
|
|||
_.each(_.keys(self.views), function(view_name) {
|
||||
var controller = self.views[view_name].controller;
|
||||
if (controller) {
|
||||
var container = self.$el.find(".oe_view_manager_view_" + view_name + ":first");
|
||||
var container = self.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_name);
|
||||
if (view_name === view_type) {
|
||||
container.show();
|
||||
controller.do_show(view_options || {});
|
||||
|
@ -582,7 +636,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
|
|||
controller.on('switch_mode', self, this.switch_mode);
|
||||
controller.on('previous_view', self, this.prev_view);
|
||||
|
||||
var container = this.$el.find(".oe_view_manager_view_" + view_type);
|
||||
var container = this.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_type);
|
||||
var view_promise = controller.appendTo(container);
|
||||
this.views[view_type].controller = controller;
|
||||
this.views[view_type].deferred.resolve(view_type);
|
||||
|
@ -597,10 +651,24 @@ instance.web.ViewManager = instance.web.Widget.extend({
|
|||
self.trigger("controller_inited",view_type,controller);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @returns {Number|Boolean} the view id of the given type, false if not found
|
||||
*/
|
||||
get_view_id: function(view_type) {
|
||||
return this.views[view_type] && this.views[view_type].view_id || false;
|
||||
},
|
||||
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) {
|
||||
|
@ -612,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) {
|
||||
|
@ -646,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
|
||||
|
@ -953,10 +1021,11 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
|
|||
});
|
||||
},
|
||||
do_create_view: function(view_type) {
|
||||
var r = this._super.apply(this, arguments);
|
||||
var view = this.views[view_type].controller;
|
||||
view.set({ 'title': this.action.name });
|
||||
return r;
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function() {
|
||||
var view = self.views[view_type].controller;
|
||||
view.set({ 'title': self.action.name });
|
||||
});
|
||||
},
|
||||
get_action_manager: function() {
|
||||
var cur = this;
|
||||
|
@ -1213,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) {
|
||||
|
@ -1270,11 +1340,6 @@ instance.web.View = instance.web.Widget.extend({
|
|||
active_ids: [record_id],
|
||||
active_model: dataset.model
|
||||
});
|
||||
if (("" + action.context).match(/\bactive_id\b/)) {
|
||||
// Special case: when the context is evaluted using
|
||||
// the active_id, we want to disable the custom filters.
|
||||
ncontext.add({ search_disable_custom_filters: true });
|
||||
}
|
||||
}
|
||||
ncontext.add(action.context || {});
|
||||
action.context = ncontext;
|
||||
|
@ -1401,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) {
|
||||
|
@ -1432,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);
|
||||
});
|
||||
};
|
||||
|
@ -1502,13 +1567,27 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
|
|||
}
|
||||
};
|
||||
instance.web.xml_to_str = function(node) {
|
||||
var str = "";
|
||||
if (window.XMLSerializer) {
|
||||
return (new XMLSerializer()).serializeToString(node);
|
||||
str = (new XMLSerializer()).serializeToString(node);
|
||||
} else if (window.ActiveXObject) {
|
||||
return node.xml;
|
||||
str = node.xml;
|
||||
} else {
|
||||
throw new Error(_t("Could not serialize XML"));
|
||||
}
|
||||
// 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) {
|
||||
if (void_elements.indexOf(tag) < 0) {
|
||||
return "<" + tag + attrs + "></" + tag + ">";
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<td>
|
||||
<p>
|
||||
<t t-js="d">
|
||||
var message = d.message ? d.message : d.error.data.message;
|
||||
var message = d.message ? d.message : d.error.data.fault_code;
|
||||
d.html_error = context.engine.tools.html_escape(message)
|
||||
.replace(/\n/g, '<br/>');
|
||||
</t>
|
||||
|
@ -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-Z][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;">
|
||||
|
@ -403,8 +421,9 @@
|
|||
<img class="oe_topbar_avatar" t-att-data-default-src="_s + '/web/static/src/img/user_menu_avatar.png'"/>
|
||||
<span class="oe_topbar_name"/>
|
||||
<ul class="oe_dropdown_menu">
|
||||
<li><a href="#" data-menu="about">About OpenERP</a></li>
|
||||
<li><a href="#" data-menu="settings">Preferences</a></li>
|
||||
<li><a href="#" data-menu="about">About OpenERP</a></li>
|
||||
<li><a href="#" data-menu="help">Help</a></li>
|
||||
<li><a href="#" data-menu="logout">Log out</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
|
@ -1031,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">
|
||||
|
@ -1060,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">
|
||||
|
@ -1095,7 +1120,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"
|
||||
|
@ -1104,7 +1129,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>
|
||||
|
@ -1265,8 +1290,9 @@
|
|||
</t>
|
||||
<t t-name="FieldBinaryFileUploader.files">
|
||||
<div class="oe_attachments">
|
||||
<t t-if="widget.get('value')">
|
||||
<t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
|
||||
<t t-if="!widget.get('effective_readonly')">
|
||||
<t t-foreach="widget.get('value')" t-as="id">
|
||||
<t t-set="file" t-value="widget.data[id]"/>
|
||||
<div class="oe_attachment">
|
||||
<span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
|
||||
<span class="oe_fileuploader_in_process">...Upload in progress...</span>
|
||||
|
@ -1280,7 +1306,10 @@
|
|||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
|
||||
</t>
|
||||
<t t-if="widget.get('effective_readonly')">
|
||||
<t t-foreach="widget.get('value')" t-as="id">
|
||||
<t t-set="file" t-value="widget.data[id]"/>
|
||||
<div>
|
||||
<a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
|
||||
<t t-raw="file.name || file.filename"/>
|
||||
|
@ -1296,10 +1325,10 @@
|
|||
<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">/web/binary/upload_attachment</t>
|
||||
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
|
||||
<input type="hidden" name="model" t-att-value="widget.view.model"/>
|
||||
<input type="hidden" name="id" value="0"/>
|
||||
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
|
||||
|
@ -1495,7 +1524,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>
|
||||
|
@ -1583,15 +1612,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>
|
||||
|
@ -1822,7 +1842,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">
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
|
@ -318,6 +318,28 @@ openerp.testing.section('defaults', {
|
|||
"facet value should match provided default's selection");
|
||||
});
|
||||
});
|
||||
test("M2O default: value array", {asserts: 2}, function (instance, $s, mock) {
|
||||
var view = {inputs: []}, id = 5;
|
||||
var f = new instance.web.search.ManyToOneField(
|
||||
{attrs: {name: 'dummy', string: 'Dummy'}},
|
||||
{relation: 'dummy.model.name'},
|
||||
view);
|
||||
mock('dummy.model.name:name_get', function (args) {
|
||||
equal(args[0], id);
|
||||
return [[id, "DumDumDum"]];
|
||||
});
|
||||
return f.facet_for_defaults({dummy: [id]})
|
||||
.done(function (facet) {
|
||||
var model = facet;
|
||||
if (!(model instanceof instance.web.search.Facet)) {
|
||||
model = new instance.web.search.Facet(facet);
|
||||
}
|
||||
deepEqual(
|
||||
model.values.toJSON(),
|
||||
[{label: "DumDumDum", value: id}],
|
||||
"should support default as a singleton");
|
||||
});
|
||||
});
|
||||
test("M2O default: value", {asserts: 1}, function (instance, $s, mock) {
|
||||
var view = {inputs: []}, id = 4;
|
||||
var f = new instance.web.search.ManyToOneField(
|
||||
|
@ -330,8 +352,17 @@ openerp.testing.section('defaults', {
|
|||
ok(!facet, "an invalid m2o default should yield a non-facet");
|
||||
});
|
||||
});
|
||||
test("M2O default: values", {rpc: false}, function (instance) {
|
||||
var view = {inputs: []};
|
||||
var f = new instance.web.search.ManyToOneField(
|
||||
{attrs: {name: 'dummy', string: 'Dummy'}},
|
||||
{relation: 'dummy.model.name'},
|
||||
view);
|
||||
raises(function () { f.facet_for_defaults({dummy: [6, 7]}) },
|
||||
"should not accept multiple default values");
|
||||
})
|
||||
});
|
||||
openerp.testing.section('completions', {
|
||||
openerp.testing.section('search.completions', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -526,7 +557,7 @@ openerp.testing.section('completions', {
|
|||
return [[42, "choice 1"], [43, "choice @"]];
|
||||
});
|
||||
|
||||
var view = {inputs: []};
|
||||
var view = {inputs: [], dataset: {get_context: function () {}}};
|
||||
var f = new instance.web.search.ManyToOneField(
|
||||
{attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view);
|
||||
return f.complete("bob")
|
||||
|
@ -555,7 +586,7 @@ openerp.testing.section('completions', {
|
|||
strictEqual(kwargs.name, 'bob');
|
||||
return [];
|
||||
});
|
||||
var view = {inputs: []};
|
||||
var view = {inputs: [], dataset: {get_context: function () {}}};
|
||||
var f = new instance.web.search.ManyToOneField(
|
||||
{attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view);
|
||||
return f.complete("bob")
|
||||
|
@ -563,8 +594,28 @@ openerp.testing.section('completions', {
|
|||
ok(!c, "no match should yield no completion");
|
||||
});
|
||||
});
|
||||
test("M2O filtered", {asserts: 2}, function (instance, $s, mock) {
|
||||
mock('dummy.model:name_search', function (args, kwargs) {
|
||||
deepEqual(args, [], "should have no positional arguments");
|
||||
deepEqual(kwargs, {
|
||||
name: 'bob',
|
||||
limit: 8,
|
||||
args: [['foo', '=', 'bar']],
|
||||
context: {flag: 1},
|
||||
}, "should use filtering domain");
|
||||
return [[42, "Match"]];
|
||||
});
|
||||
var view = {
|
||||
inputs: [],
|
||||
dataset: {get_context: function () { return {flag: 1}; }}
|
||||
};
|
||||
var f = new instance.web.search.ManyToOneField(
|
||||
{attrs: {string: 'Dummy', domain: '[["foo", "=", "bar"]]'}},
|
||||
{relation: 'dummy.model'}, view);
|
||||
return f.complete("bob");
|
||||
});
|
||||
});
|
||||
openerp.testing.section('search-serialization', {
|
||||
openerp.testing.section('search.serialization', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -804,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})
|
||||
|
@ -829,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");
|
||||
});
|
||||
|
@ -871,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
|
||||
|
@ -894,7 +945,7 @@ openerp.testing.section('removal', {
|
|||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('drawer', {
|
||||
openerp.testing.section('search.drawer', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true
|
||||
|
@ -910,7 +961,7 @@ openerp.testing.section('drawer', {
|
|||
});
|
||||
});
|
||||
});
|
||||
openerp.testing.section('filters', {
|
||||
openerp.testing.section('search.filters', {
|
||||
dependencies: ['web.search'],
|
||||
rpc: 'mock',
|
||||
templates: true,
|
||||
|
@ -995,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
|
||||
|
@ -1038,8 +1186,68 @@ openerp.testing.section('saved_filters', {
|
|||
"should not be checked anymore");
|
||||
});
|
||||
});
|
||||
test('toggling', {asserts: 2}, function (instance, $fix, mock) {
|
||||
var view = makeSearchView(instance);
|
||||
mock('ir.filters:get_filters', function () {
|
||||
return [{name: 'filter name', user_id: 42, id: 1}];
|
||||
});
|
||||
|
||||
return view.appendTo($fix)
|
||||
.done(function () {
|
||||
var $row = $fix.find('.oe_searchview_custom li:first').click();
|
||||
equal(view.query.length, 1, "should have one facet");
|
||||
$row.click();
|
||||
equal(view.query.length, 0, "should have removed facet");
|
||||
});
|
||||
});
|
||||
test('replacement', {asserts: 4}, function (instance, $fix, mock) {
|
||||
var view = makeSearchView(instance);
|
||||
mock('ir.filters:get_filters', function () {
|
||||
return [
|
||||
{name: 'f', user_id: 42, id: 1, context: {'private': 1}},
|
||||
{name: 'f', user_id: false, id: 2, context: {'private': 0}}
|
||||
];
|
||||
});
|
||||
return view.appendTo($fix)
|
||||
.done(function () {
|
||||
$fix.find('.oe_searchview_custom li:eq(0)').click();
|
||||
equal(view.query.length, 1, "should have one facet");
|
||||
deepEqual(
|
||||
view.query.at(0).get('field').get_context(),
|
||||
{'private': 1},
|
||||
"should have selected first filter");
|
||||
$fix.find('.oe_searchview_custom li:eq(1)').click();
|
||||
equal(view.query.length, 1, "should have one facet");
|
||||
deepEqual(
|
||||
view.query.at(0).get('field').get_context(),
|
||||
{'private': 0},
|
||||
"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
|
||||
|
@ -1120,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="{"invisible": 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="{"invisible": 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="{"invisible": 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="{"invisible": 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
.openerp .oe_calendar .dhx_cal_select_menu .dhx_menu_icon.icon_edit {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_calendar .dhx_cal_navline {
|
||||
.openerp .oe_calendar .dhx_cal_navline, .openerp .oe_calendar .dhx_cal_header {
|
||||
z-index: auto;
|
||||
}
|
||||
.openerp .oe_calendar.oe_cal_month .dhx_cal_data table tr td:last-child div.dhx_month_body {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
// Dhtmlx Scheduler css overrides
|
||||
.dhx_cal_select_menu .dhx_menu_icon.icon_edit
|
||||
display: none
|
||||
.dhx_cal_navline
|
||||
.dhx_cal_navline, .dhx_cal_header
|
||||
// dhtmlx sets the index to 3 (in glossy theme)
|
||||
// I didn't found the reason yet but it overlaps the
|
||||
// dropdown menus and I want to avoid a z-index war.
|
||||
|
|
|
@ -414,6 +414,20 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
|
|||
self.slow_create(event_id, event_obj);
|
||||
});
|
||||
},
|
||||
get_form_popup_infos: function() {
|
||||
var parent = this.getParent();
|
||||
var infos = {
|
||||
view_id: false,
|
||||
title: this.name,
|
||||
};
|
||||
if (parent instanceof instance.web.ViewManager) {
|
||||
infos.view_id = parent.get_view_id('form');
|
||||
if (parent instanceof instance.web.ViewManagerAction && parent.action && parent.action.name) {
|
||||
infos.title = parent.action.name;
|
||||
}
|
||||
}
|
||||
return infos;
|
||||
},
|
||||
slow_create: function(event_id, event_obj) {
|
||||
var self = this;
|
||||
if (this.current_mode() === 'month') {
|
||||
|
@ -431,9 +445,11 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
|
|||
});
|
||||
var something_saved = false;
|
||||
var pop = new instance.web.form.FormOpenPopup(this);
|
||||
var pop_infos = this.get_form_popup_infos();
|
||||
pop.show_element(this.dataset.model, null, this.dataset.get_context(defaults), {
|
||||
title: _t("Create: ") + ' ' + this.name,
|
||||
title: _.str.sprintf(_t("Create: %s"), pop_infos.title),
|
||||
disable_multiple_selection: true,
|
||||
view_id: pop_infos.view_id,
|
||||
});
|
||||
pop.on('closed', self, function() {
|
||||
if (!something_saved) {
|
||||
|
@ -465,9 +481,11 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
|
|||
});
|
||||
} else {
|
||||
var pop = new instance.web.form.FormOpenPopup(this);
|
||||
var pop_infos = this.get_form_popup_infos();
|
||||
var id_from_dataset = this.dataset.ids[index]; // dhtmlx scheduler loses id's type
|
||||
pop.show_element(this.dataset.model, id_from_dataset, this.dataset.get_context(), {
|
||||
title: _t("Edit: ") + this.name
|
||||
title: _.str.sprintf(_t("Edit: %s"), pop_infos.title),
|
||||
view_id: pop_infos.view_id,
|
||||
});
|
||||
pop.on('write_completed', self, function(){
|
||||
self.reload_event(id_from_dataset);
|
||||
|
|
|
@ -386,6 +386,7 @@ function GanttChart()
|
|||
|
||||
this.hourInPixelsWork = this.dayInPixels / this.hoursInDay;
|
||||
this.hourInPixels = this.dayInPixels / 24;
|
||||
this.minWidthResize = this.hourInPixels;
|
||||
this.countDays = 0;
|
||||
this.startDate = null;
|
||||
this.initialPos = 0;
|
||||
|
@ -4812,7 +4813,7 @@ GanttTask.prototype.getResizeInfo = function()
|
|||
childParentPosX = posChildTaskItem;
|
||||
}
|
||||
|
||||
this.minWidthResize = this.Chart.dayInPixels;
|
||||
this.minWidthResize = this.Chart.minWidthResize;
|
||||
|
||||
if (this.childTask.length > 0)
|
||||
{
|
||||
|
|
|
@ -24,8 +24,8 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
var self = this;
|
||||
this.fields_view = fields_view_get;
|
||||
this.$el.addClass(this.fields_view.arch.attrs['class']);
|
||||
return new instance.web.Model(this.dataset.model)
|
||||
.call('fields_get').then(function (fields) {
|
||||
return self.alive(new instance.web.Model(this.dataset.model)
|
||||
.call('fields_get')).then(function (fields) {
|
||||
self.fields = fields;
|
||||
self.has_been_loaded.resolve();
|
||||
});
|
||||
|
@ -44,7 +44,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
n_group_bys = group_bys;
|
||||
}
|
||||
// gather the fields to get
|
||||
var fields = _.compact(_.map(["date_start", "date_delay", "date_stop"], function(key) {
|
||||
var fields = _.compact(_.map(["date_start", "date_delay", "date_stop", "progress"], function(key) {
|
||||
return self.fields_view.arch.attrs[key] || '';
|
||||
}));
|
||||
fields = _.uniq(fields.concat(n_group_bys));
|
||||
|
@ -75,7 +75,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
on_data_loaded_2: function(tasks, group_bys) {
|
||||
var self = this;
|
||||
$(".oe_gantt", this.$el).html("");
|
||||
|
||||
|
||||
//prevent more that 1 group by
|
||||
if (group_bys.length > 0) {
|
||||
group_bys = [group_bys[0]];
|
||||
|
@ -114,6 +114,11 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
var task_ids = {};
|
||||
// creation of the chart
|
||||
var generate_task_info = function(task, plevel) {
|
||||
if (_.isNumber(task[self.fields_view.arch.attrs.progress])) {
|
||||
var percent = task[self.fields_view.arch.attrs.progress] || 0;
|
||||
} else {
|
||||
var percent = 100;
|
||||
}
|
||||
var level = plevel || 0;
|
||||
if (task.__is_group) {
|
||||
var task_infos = _.compact(_.map(task.tasks, function(sub_task) {
|
||||
|
@ -128,7 +133,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
return memo === undefined || date > memo ? date : memo;
|
||||
}, undefined);
|
||||
var duration = (task_stop.getTime() - task_start.getTime()) / (1000 * 60 * 60);
|
||||
var group_name = instance.web.format_value(task.name, self.fields[group_bys[level]]);
|
||||
var group_name = task.name ? instance.web.format_value(task.name, self.fields[group_bys[level]]) : "-";
|
||||
if (level == 0) {
|
||||
var group = new GanttProjectInfo(_.uniqueId("gantt_project_"), group_name, task_start);
|
||||
_.each(task_infos, function(el) {
|
||||
|
@ -136,7 +141,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
});
|
||||
return group;
|
||||
} else {
|
||||
var group = new GanttTaskInfo(_.uniqueId("gantt_project_task_"), group_name, task_start, duration || 1, 100);
|
||||
var group = new GanttTaskInfo(_.uniqueId("gantt_project_task_"), group_name, task_start, duration || 1, percent);
|
||||
_.each(task_infos, function(el) {
|
||||
group.addChildTask(el.task_info);
|
||||
});
|
||||
|
@ -151,7 +156,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
if (self.fields_view.arch.attrs.date_stop) {
|
||||
task_stop = instance.web.auto_str_to_date(task[self.fields_view.arch.attrs.date_stop]);
|
||||
if (!task_stop)
|
||||
return;
|
||||
task_stop = task_start;
|
||||
} else { // we assume date_duration is defined
|
||||
var tmp = instance.web.format_value(task[self.fields_view.arch.attrs.date_delay],
|
||||
self.fields[self.fields_view.arch.attrs.date_delay]);
|
||||
|
@ -161,7 +166,7 @@ instance.web_gantt.GanttView = instance.web.View.extend({
|
|||
}
|
||||
var duration = (task_stop.getTime() - task_start.getTime()) / (1000 * 60 * 60);
|
||||
var id = _.uniqueId("gantt_task_");
|
||||
var task_info = new GanttTaskInfo(id, task_name, task_start, ((duration / 24) * 8) || 1, 100);
|
||||
var task_info = new GanttTaskInfo(id, task_name, task_start, ((duration / 24) * 8) || 1, percent);
|
||||
task_info.internal_task = task;
|
||||
task_ids[id] = task_info;
|
||||
return {task_info: task_info, task_start: task_start, task_stop: task_stop};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.openerp a.dropdown-menu-icon {
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
color: #4c4c4c;
|
||||
right: 8px;
|
||||
|
@ -23,7 +23,7 @@
|
|||
padding: 8px;
|
||||
border: 1px solid #afafb6;
|
||||
background: white;
|
||||
z-index: 900;
|
||||
z-index: 1;
|
||||
min-width: 160px;
|
||||
overflow-x: hidden;
|
||||
-moz-border-radius: 3px;
|
||||
|
|
|
@ -246,7 +246,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
|
|||
var result = [];
|
||||
var ticks = {};
|
||||
|
||||
return obj.call("fields_view_get", [view_id, 'graph']).then(function(tmp) {
|
||||
return this.alive(obj.call("fields_view_get", [view_id, 'graph', context]).then(function(tmp) {
|
||||
view_get = tmp;
|
||||
fields = view_get['fields'];
|
||||
var toload = _.select(group_by, function(x) { return fields[x] === undefined });
|
||||
|
@ -368,7 +368,7 @@ instance.web_graph.GraphView = instance.web.View.extend({
|
|||
'ticks': _.map(ticks, function(el, key) { return [el, key] })
|
||||
};
|
||||
return res;
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
// Render the graph and update menu styles
|
||||
|
|
|
@ -7,6 +7,15 @@
|
|||
background: url(/web/static/src/img/form_sheetbg.png);
|
||||
width: 100%;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_length {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_kanban_view .oe_kanban_group_length .oe_tag {
|
||||
position: relative;
|
||||
top: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.openerp .oe_kanban_view .ui-sortable-placeholder {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
visibility: visible !important;
|
||||
|
|
|
@ -146,11 +146,11 @@
|
|||
text-overflow: ellipsis
|
||||
.oe_fold_column
|
||||
.oe_kanban_group_length
|
||||
position: absolute
|
||||
top: -1px
|
||||
right: -14px
|
||||
float: right
|
||||
display: block
|
||||
position: absolute
|
||||
top: -1px
|
||||
right: -14px
|
||||
float: right
|
||||
display: block
|
||||
&.oe_kanban_grouped
|
||||
.oe_kanban_column, .oe_kanban_group_header
|
||||
width: 185px
|
||||
|
@ -217,7 +217,7 @@
|
|||
z-index: 2
|
||||
.oe_kanban_header .oe_dropdown_toggle
|
||||
top: -2px
|
||||
height: 14px;
|
||||
height: 14px
|
||||
.oe_kanban_card, .oe_dropdown_toggle
|
||||
cursor: pointer
|
||||
display: inline-block
|
||||
|
|
|
@ -134,7 +134,9 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
|
|||
switch (node.tag) {
|
||||
case 'field':
|
||||
if (this.fields_view.fields[node.attrs.name].type === 'many2many') {
|
||||
this.many2manys.push(node.attrs.name);
|
||||
if (_.indexOf(this.many2manys, node.attrs.name) < 0) {
|
||||
this.many2manys.push(node.attrs.name);
|
||||
}
|
||||
node.tag = 'div';
|
||||
node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
|
||||
} else {
|
||||
|
@ -226,14 +228,15 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
|
|||
this.search_domain = domain;
|
||||
this.search_context = context;
|
||||
this.search_group_by = group_by;
|
||||
$.when(this.has_been_loaded).done(function() {
|
||||
return $.when(this.has_been_loaded).then(function() {
|
||||
self.group_by = group_by.length ? group_by[0] : self.fields_view.arch.attrs.default_group_by;
|
||||
self.group_by_field = self.fields_view.fields[self.group_by] || {};
|
||||
self.grouped_by_m2o = (self.group_by_field.type === 'many2one');
|
||||
self.$buttons.find('.oe_alternative').toggle(self.grouped_by_m2o);
|
||||
self.$el.toggleClass('oe_kanban_grouped_by_m2o', self.grouped_by_m2o);
|
||||
var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(self.group_by);
|
||||
$.when(grouping).done(function(groups) {
|
||||
var grouping_fields = self.group_by ? [self.group_by].concat(_.keys(self.aggregates)) : undefined;
|
||||
var grouping = new instance.web.Model(self.dataset.model, context, domain).query().group_by(grouping_fields);
|
||||
return self.alive($.when(grouping)).done(function(groups) {
|
||||
self.remove_no_result();
|
||||
if (groups) {
|
||||
self.do_process_groups(groups);
|
||||
|
@ -655,6 +658,7 @@ instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
|
|||
self.view.dataset.ids = ids.concat(self.dataset.ids);
|
||||
self.do_add_records(records);
|
||||
self.compute_cards_auto_height();
|
||||
self.view.postprocess_m2m_tags();
|
||||
return records;
|
||||
});
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue