diff --git a/addons/base/__init__.py b/addons/base/__init__.py index ee5959455ad..343b53d050d 100644 --- a/addons/base/__init__.py +++ b/addons/base/__init__.py @@ -1 +1,6 @@ +import common import controllers + +# TODO +# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point + diff --git a/addons/base/common/__init__.py b/addons/base/common/__init__.py new file mode 100644 index 00000000000..60699c86f43 --- /dev/null +++ b/addons/base/common/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +# TODO if from openerpserver use backendlocal +# from backendlocal import * +from backendrpc import * +from dispatch import * diff --git a/openerpweb/ast.py b/addons/base/common/ast.py similarity index 100% rename from openerpweb/ast.py rename to addons/base/common/ast.py diff --git a/addons/base/common/backendlocal.py b/addons/base/common/backendlocal.py new file mode 100644 index 00000000000..dac33991c4d --- /dev/null +++ b/addons/base/common/backendlocal.py @@ -0,0 +1,23 @@ +#---------------------------------------------------------- +# OpenERPSession local openerp backend access +#---------------------------------------------------------- +class OpenERPUnboundException(Exception): + pass + +class OpenERPConnector(object): + pass + +class OpenERPAuth(object): + pass + +class OpenERPModel(object): + def __init__(self, session, model): + self._session = session + self._model = model + + def __getattr__(self, name): + return lambda *l:self._session.execute(self._model, name, *l) + +class OpenERPSession(object): + def __init__(self, model_factory=OpenERPModel): + pass diff --git a/addons/base/common/backendrpc.py b/addons/base/common/backendrpc.py new file mode 100644 index 00000000000..d2e2f6615e1 --- /dev/null +++ b/addons/base/common/backendrpc.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +import datetime +import urllib +import dateutil.relativedelta +import functools +import logging +import optparse +import os +import sys +import tempfile +import time +import traceback +import uuid +import xmlrpclib + +import cherrypy +import cherrypy.lib.static +import simplejson + +import nonliterals +#---------------------------------------------------------- +# OpenERPSession RPC openerp backend access +#---------------------------------------------------------- +class OpenERPUnboundException(Exception): + pass + +class OpenERPConnector(object): + pass + +class OpenERPAuth(object): + pass + +class OpenERPModel(object): + def __init__(self, session, model): + self._session = session + self._model = model + + def __getattr__(self, name): + return lambda *l:self._session.execute(self._model, name, *l) + +class OpenERPSession(object): + """ + An OpenERP RPC session, a given user can own multiple such sessions + in a web session. + + .. attribute:: context + + The session context, a ``dict``. Can be reloaded by calling + :meth:`openerpweb.openerpweb.OpenERPSession.get_context` + + .. attribute:: domains_store + + A ``dict`` matching domain keys to evaluable (but non-literal) domains. + + Used to store references to non-literal domains which need to be + round-tripped to the client browser. + """ + def __init__(self, server='127.0.0.1', port=8069, model_factory=OpenERPModel): + self._server = server + self._port = port + self._db = False + self._uid = False + self._login = False + self._password = False + self.model_factory = model_factory + self._locale = 'en_US' + self.context = {} + self.contexts_store = {} + self.domains_store = {} + self._lang = {} + self.remote_timezone = 'utc' + self.client_timezone = False + + def proxy(self, service): + s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service)) + return s + + def bind(self, db, uid, password): + self._db = db + self._uid = uid + self._password = password + + def login(self, db, login, password): + uid = self.proxy('common').login(db, login, password) + self.bind(db, uid, password) + self._login = login + + if uid: self.get_context() + return uid + + def assert_valid(self): + """ + Ensures this session is valid (logged into the openerp server) + """ + if not (self._db and self._uid and self._password): + raise OpenERPUnboundException() + + def execute(self, model, func, *l, **d): + self.assert_valid() + r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d) + return r + + def exec_workflow(self, model, id, signal): + self.assert_valid() + r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id) + return r + + def model(self, model): + """ Get an RPC proxy for the object ``model``, bound to this session. + + :param model: an OpenERP model name + :type model: str + :rtype: :class:`openerpweb.openerpweb.OpenERPModel` + """ + return self.model_factory(self, model) + + def get_context(self): + """ Re-initializes the current user's session context (based on + his preferences) by calling res.users.get_context() with the old + context + + :returns: the new context + """ + assert self._uid, "The user needs to be logged-in to initialize his context" + self.context = self.model('res.users').context_get(self.context) + self.context = self.context or {} + + self.client_timezone = self.context.get("tz", False) + # invalid code, anyway we decided the server will be in UTC + #if self.client_timezone: + # self.remote_timezone = self.execute('common', 'timezone_get') + + self._locale = self.context.get('lang','en_US') + lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)]) + if lang_ids: + self._lang = self.execute('res.lang', 'read',lang_ids[0], []) + return self.context + + @property + def base_eval_context(self): + """ Default evaluation context for the session. + + Used to evaluate contexts and domains. + """ + base = dict( + uid=self._uid, + current_date=datetime.date.today().strftime('%Y-%m-%d'), + time=time, + datetime=datetime, + relativedelta=dateutil.relativedelta.relativedelta + ) + base.update(self.context) + return base + + def evaluation_context(self, context=None): + """ Returns the session's evaluation context, augmented with the + provided context if any. + + :param dict context: to add merge in the session's base eval context + :returns: the augmented context + :rtype: dict + """ + d = dict(self.base_eval_context) + if context: + d.update(context) + d['context'] = d + return d + + def eval_context(self, context_to_eval, context=None): + """ Evaluates the provided context_to_eval in the context (haha) of + the context. Also merges the evaluated context with the session's context. + + :param context_to_eval: a context to evaluate. Must be a dict or a + non-literal context. If it's a dict, will be + returned as-is + :type context_to_eval: openerpweb.nonliterals.Context + :returns: the evaluated context + :rtype: dict + + :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor + a Context + """ + ctx = dict( + self.base_eval_context, + **(context or {})) + + # adding the context of the session to send to the openerp server + ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {}) + ccontext.session = self + return ccontext.evaluate(ctx) + + def eval_domain(self, domain, context=None): + """ Evaluates the provided domain using the provided context + (merged with the session's evaluation context) + + :param domain: an OpenERP domain as a list or as a + :class:`openerpweb.nonliterals.Domain` instance + + In the second case, it will be evaluated and returned. + :type domain: openerpweb.nonliterals.Domain + :param dict context: the context to use in the evaluation, if any. + :returns: the evaluated domain + :rtype: list + + :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain + """ + if isinstance(domain, list): + return domain + + cdomain = nonliterals.CompoundDomain(domain) + cdomain.session = self + return cdomain.evaluate(context or {}) diff --git a/openerpweb/dates.py b/addons/base/common/dates.py similarity index 100% rename from openerpweb/dates.py rename to addons/base/common/dates.py diff --git a/openerpweb/openerpweb.py b/addons/base/common/dispatch.py similarity index 61% rename from openerpweb/openerpweb.py rename to addons/base/common/dispatch.py index b2364692203..7d50785bf56 100644 --- a/openerpweb/openerpweb.py +++ b/addons/base/common/dispatch.py @@ -3,6 +3,7 @@ import datetime import urllib import dateutil.relativedelta import functools +import logging import optparse import os import sys @@ -17,14 +18,18 @@ import cherrypy.lib.static import simplejson import nonliterals -import logging +# TODO if from openerpserver use backendlocal +# from backendlocal import * +from backendrpc import * #----------------------------------------------------------- # Globals #----------------------------------------------------------- -path_root = os.path.dirname(os.path.dirname(os.path.normpath(__file__))) -path_addons = os.path.join(path_root, 'addons') +import __main__ + +path_root = __main__.path_root +path_addons = __main__.path_addons cherrypy_root = None #----------------------------------------------------------- @@ -38,199 +43,6 @@ controllers_class = {} controllers_object = {} controllers_path = {} -#---------------------------------------------------------- -# OpenERP Client Library -#---------------------------------------------------------- -class OpenERPUnboundException(Exception): - pass - -class OpenERPConnector(object): - pass - -class OpenERPAuth(object): - pass - -class OpenERPModel(object): - def __init__(self, session, model): - self._session = session - self._model = model - - def __getattr__(self, name): - return lambda *l:self._session.execute(self._model, name, *l) - -class OpenERPSession(object): - """ - An OpenERP RPC session, a given user can own multiple such sessions - in a web session. - - .. attribute:: context - - The session context, a ``dict``. Can be reloaded by calling - :meth:`openerpweb.openerpweb.OpenERPSession.get_context` - - .. attribute:: domains_store - - A ``dict`` matching domain keys to evaluable (but non-literal) domains. - - Used to store references to non-literal domains which need to be - round-tripped to the client browser. - """ - def __init__(self, server='127.0.0.1', port=8069, model_factory=OpenERPModel): - self._server = server - self._port = port - self._db = False - self._uid = False - self._login = False - self._password = False - self.model_factory = model_factory - self._locale = 'en_US' - self.context = {} - self.contexts_store = {} - self.domains_store = {} - self._lang = {} - self.remote_timezone = 'utc' - self.client_timezone = False - - def proxy(self, service): - s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service)) - return s - - def bind(self, db, uid, password): - self._db = db - self._uid = uid - self._password = password - - def login(self, db, login, password): - uid = self.proxy('common').login(db, login, password) - self.bind(db, uid, password) - self._login = login - - if uid: self.get_context() - return uid - - def assert_valid(self): - """ - Ensures this session is valid (logged into the openerp server) - """ - if not (self._db and self._uid and self._password): - raise OpenERPUnboundException() - - def execute(self, model, func, *l, **d): - self.assert_valid() - r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d) - return r - - def exec_workflow(self, model, id, signal): - self.assert_valid() - r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id) - return r - - def model(self, model): - """ Get an RPC proxy for the object ``model``, bound to this session. - - :param model: an OpenERP model name - :type model: str - :rtype: :class:`openerpweb.openerpweb.OpenERPModel` - """ - return self.model_factory(self, model) - - def get_context(self): - """ Re-initializes the current user's session context (based on - his preferences) by calling res.users.get_context() with the old - context - - :returns: the new context - """ - assert self._uid, "The user needs to be logged-in to initialize his context" - self.context = self.model('res.users').context_get(self.context) - self.context = self.context or {} - - self.client_timezone = self.context.get("tz", False) - # invalid code, anyway we decided the server will be in UTC - #if self.client_timezone: - # self.remote_timezone = self.execute('common', 'timezone_get') - - self._locale = self.context.get('lang','en_US') - lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)]) - if lang_ids: - self._lang = self.execute('res.lang', 'read',lang_ids[0], []) - return self.context - - @property - def base_eval_context(self): - """ Default evaluation context for the session. - - Used to evaluate contexts and domains. - """ - base = dict( - uid=self._uid, - current_date=datetime.date.today().strftime('%Y-%m-%d'), - time=time, - datetime=datetime, - relativedelta=dateutil.relativedelta.relativedelta - ) - base.update(self.context) - return base - - def evaluation_context(self, context=None): - """ Returns the session's evaluation context, augmented with the - provided context if any. - - :param dict context: to add merge in the session's base eval context - :returns: the augmented context - :rtype: dict - """ - d = dict(self.base_eval_context) - if context: - d.update(context) - d['context'] = d - return d - - def eval_context(self, context_to_eval, context=None): - """ Evaluates the provided context_to_eval in the context (haha) of - the context. Also merges the evaluated context with the session's context. - - :param context_to_eval: a context to evaluate. Must be a dict or a - non-literal context. If it's a dict, will be - returned as-is - :type context_to_eval: openerpweb.nonliterals.Context - :returns: the evaluated context - :rtype: dict - - :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor - a Context - """ - ctx = dict( - self.base_eval_context, - **(context or {})) - - # adding the context of the session to send to the openerp server - ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {}) - ccontext.session = self - return ccontext.evaluate(ctx) - - def eval_domain(self, domain, context=None): - """ Evaluates the provided domain using the provided context - (merged with the session's evaluation context) - - :param domain: an OpenERP domain as a list or as a - :class:`openerpweb.nonliterals.Domain` instance - - In the second case, it will be evaluated and returned. - :type domain: openerpweb.nonliterals.Domain - :param dict context: the context to use in the evaluation, if any. - :returns: the evaluated domain - :rtype: list - - :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain - """ - if isinstance(domain, list): - return domain - - cdomain = nonliterals.CompoundDomain(domain) - cdomain.session = self - return cdomain.evaluate(context or {}) - #---------------------------------------------------------- # OpenERP Web RequestHandler #---------------------------------------------------------- @@ -425,12 +237,16 @@ class Root(object): controllers_path[o._cp_path] = o def default(self, *l, **kw): - #print "default",l,kw + print "default",l,kw # handle static files if len(l) > 2 and l[1] == 'static': # sanitize path p = os.path.normpath(os.path.join(*l)) - return cherrypy.lib.static.serve_file(os.path.join(path_addons, p)) + p2 = os.path.join(path_addons, p) + print "p",p + print "p2",p2 + + return cherrypy.lib.static.serve_file(p2) elif len(l) > 1: for i in range(len(l), 1, -1): ps = "/" + "/".join(l[0:i]) diff --git a/openerpweb/nonliterals.py b/addons/base/common/nonliterals.py similarity index 100% rename from openerpweb/nonliterals.py rename to addons/base/common/nonliterals.py diff --git a/openerpweb/tests/__init__.py b/addons/base/common/tests/__init__.py similarity index 100% rename from openerpweb/tests/__init__.py rename to addons/base/common/tests/__init__.py diff --git a/openerpweb/tests/test_model.py b/addons/base/common/tests/test_model.py similarity index 100% rename from openerpweb/tests/test_model.py rename to addons/base/common/tests/test_model.py diff --git a/openerpweb/tests/test_nonliterals.py b/addons/base/common/tests/test_nonliterals.py similarity index 100% rename from openerpweb/tests/test_nonliterals.py rename to addons/base/common/tests/test_nonliterals.py diff --git a/openerpweb/tests/test_session.py b/addons/base/common/tests/test_session.py similarity index 100% rename from openerpweb/tests/test_session.py rename to addons/base/common/tests/test_session.py diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index d5eaafaecee..0d652500a35 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -9,17 +9,22 @@ import re import simplejson import textwrap import xmlrpclib +import time from xml.etree import ElementTree from cStringIO import StringIO import cherrypy -import openerpweb -import openerpweb.ast -import openerpweb.nonliterals +import base.common as openerpweb +import base.common.ast +import base.common.nonliterals +openerpweb.ast = base.common.ast +openerpweb.nonliterals = base.common.nonliterals from babel.messages.pofile import read_po +_REPORT_POLLER_DELAY = 0.05 + # Should move to openerpweb.Xml2Json class Xml2Json: # xml2json-direct @@ -140,7 +145,7 @@ class WebClient(openerpweb.Controller): return content @openerpweb.httprequest - def home(self, req, s_action=None): + def home(self, req, s_action=None, **kw): # script tags jslist = ['/base/webclient/js'] if req.debug: @@ -423,25 +428,28 @@ def eval_context_and_domain(session, context, domain=None): return e_context, e_domain -def load_actions_from_ir_values(req, key, key2, models, meta, context): +def load_actions_from_ir_values(req, key, key2, models, meta): + context = req.session.eval_context(req.context) Values = req.session.model('ir.values') actions = Values.get(key, key2, models, meta, context) - return [(id, name, clean_action(action, req.session, context=context)) + return [(id, name, clean_action(req, action)) for id, name, action in actions] -def clean_action(action, session, context=None): +def clean_action(req, action): action.setdefault('flags', {}) if action['type'] != 'ir.actions.act_window': return action + + context = req.session.eval_context(req.context) + eval_ctx = req.session.evaluation_context(context) + # values come from the server, we can just eval them if isinstance(action.get('context'), basestring): - localvars = session.evaluation_context(context=context) - action['context'] = eval( action['context'], localvars ) or {} + action['context'] = eval( action['context'], eval_ctx ) or {} if isinstance(action.get('domain'), basestring): - localvars = session.evaluation_context( action.get('context', {})) - action['domain'] = eval( action['domain'], localvars ) or [] + action['domain'] = eval( action['domain'], eval_ctx ) or [] return fix_view_modes(action) @@ -559,8 +567,7 @@ class Menu(openerpweb.Controller): @openerpweb.jsonrequest def action(self, req, menu_id): actions = load_actions_from_ir_values(req,'action', 'tree_but_open', - [('ir.ui.menu', menu_id)], False, - req.session.eval_context(req.context)) + [('ir.ui.menu', menu_id)], False) return {"action": actions} class DataSet(openerpweb.Controller): @@ -572,15 +579,15 @@ class DataSet(openerpweb.Controller): req.session.eval_context(req.context))} @openerpweb.jsonrequest - def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None): - return self.do_search_read(request, model, fields, offset, limit, domain, sort) - def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None + def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None): + return self.do_search_read(req, model, fields, offset, limit, domain, sort) + def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None , sort=None): """ Performs a search() followed by a read() (if needed) using the provided search criteria - :param request: a JSON-RPC request object - :type request: openerpweb.JsonRequest + :param req: a JSON-RPC request object + :type req: openerpweb.JsonRequest :param str model: the name of the model to search on :param fields: a list of the fields to return in the result records :type fields: [str] @@ -593,10 +600,10 @@ class DataSet(openerpweb.Controller): matching fields selection set) :rtype: list """ - Model = request.session.model(model) + Model = req.session.model(model) context, domain = eval_context_and_domain( - request.session, request.context, domain) + req.session, req.context, domain) ids = Model.search(domain, 0, False, sort or False, context) # need to fill the dataset with all ids for the (domain, context) pair, @@ -618,22 +625,22 @@ class DataSet(openerpweb.Controller): @openerpweb.jsonrequest - def read(self, request, model, ids, fields=False): - return self.do_search_read(request, model, ids, fields) + def read(self, req, model, ids, fields=False): + return self.do_search_read(req, model, ids, fields) @openerpweb.jsonrequest - def get(self, request, model, ids, fields=False): - return self.do_get(request, model, ids, fields) + def get(self, req, model, ids, fields=False): + return self.do_get(req, model, ids, fields) - def do_get(self, request, model, ids, fields=False): + def do_get(self, req, model, ids, fields=False): """ Fetches and returns the records of the model ``model`` whose ids are in ``ids``. The results are in the same order as the inputs, but elements may be missing (if there is no record left for the id) - :param request: the JSON-RPC2 request object - :type request: openerpweb.JsonRequest + :param req: the JSON-RPC2 request object + :type req: openerpweb.JsonRequest :param model: the model to read from :type model: str :param ids: a list of identifiers @@ -644,8 +651,8 @@ class DataSet(openerpweb.Controller): :returns: a list of records, in the same order as the list of ids :rtype: list """ - Model = request.session.model(model) - records = Model.read(ids, fields, request.session.eval_context(request.context)) + Model = req.session.model(model) + records = Model.read(ids, fields, req.session.eval_context(req.context)) record_map = dict((record['id'], record) for record in records) @@ -673,9 +680,9 @@ class DataSet(openerpweb.Controller): return {'result': r} @openerpweb.jsonrequest - def unlink(self, request, model, ids=()): - Model = request.session.model(model) - return Model.unlink(ids, request.session.eval_context(request.context)) + def unlink(self, req, model, ids=()): + Model = req.session.model(model) + return Model.unlink(ids, req.session.eval_context(req.context)) def call_common(self, req, model, method, args, domain_id=None, context_id=None): domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else [] @@ -696,7 +703,7 @@ class DataSet(openerpweb.Controller): def call_button(self, req, model, method, args, domain_id=None, context_id=None): action = self.call_common(req, model, method, args, domain_id, context_id) if isinstance(action, dict) and action.get('type') != '': - return {'result': clean_action(action, req.session)} + return {'result': clean_action(req, action)} return {'result': False} @openerpweb.jsonrequest @@ -718,9 +725,9 @@ class DataSet(openerpweb.Controller): class DataGroup(openerpweb.Controller): _cp_path = "/base/group" @openerpweb.jsonrequest - def read(self, request, model, fields, group_by_fields, domain=None, sort=None): - Model = request.session.model(model) - context, domain = eval_context_and_domain(request.session, request.context, domain) + def read(self, req, model, fields, group_by_fields, domain=None, sort=None): + Model = req.session.model(model) + context, domain = eval_context_and_domain(req.session, req.context, domain) return Model.read_group( domain or [], fields, group_by_fields, 0, False, @@ -729,13 +736,13 @@ class DataGroup(openerpweb.Controller): class View(openerpweb.Controller): _cp_path = "/base/view" - def fields_view_get(self, request, model, view_id, view_type, + def fields_view_get(self, req, model, view_id, view_type, transform=True, toolbar=False, submenu=False): - Model = request.session.model(model) - context = request.session.eval_context(request.context) + Model = req.session.model(model) + context = req.session.eval_context(req.context) fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu) # todo fme?: check that we should pass the evaluated context here - self.process_view(request.session, fvg, context, transform) + self.process_view(req.session, fvg, context, transform) return fvg def process_view(self, session, fvg, context, transform): @@ -768,20 +775,20 @@ class View(openerpweb.Controller): field["context"] = self.parse_context(field["context"], session) @openerpweb.jsonrequest - def add_custom(self, request, view_id, arch): - CustomView = request.session.model('ir.ui.view.custom') + def add_custom(self, req, view_id, arch): + CustomView = req.session.model('ir.ui.view.custom') CustomView.create({ - 'user_id': request.session._uid, + 'user_id': req.session._uid, 'ref_id': view_id, 'arch': arch - }, request.session.eval_context(request.context)) + }, req.session.eval_context(req.context)) return {'result': True} @openerpweb.jsonrequest - def undo_custom(self, request, view_id, reset=False): - CustomView = request.session.model('ir.ui.view.custom') - context = request.session.eval_context(request.context) - vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)], + def undo_custom(self, req, view_id, reset=False): + CustomView = req.session.model('ir.ui.view.custom') + context = req.session.eval_context(req.context) + vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)], 0, False, False, context) if vcustom: if reset: @@ -1040,13 +1047,13 @@ class Action(openerpweb.Controller): action = req.session.model(action_type[0]['type']).read([action_id], False, context) if action: - value = clean_action(action[0], req.session) + value = clean_action(req, action[0]) return {'result': value} @openerpweb.jsonrequest def run(self, req, action_id): - return clean_action(req.session.model('ir.actions.server').run( - [action_id], req.session.eval_context(req.context)), req.session) + return clean_action(req, req.session.model('ir.actions.server').run( + [action_id], req.session.eval_context(req.context))) class TreeView(View): _cp_path = "/base/treeview" @@ -1059,7 +1066,7 @@ class TreeView(View): def action(self, req, model, id): return load_actions_from_ir_values( req,'action', 'tree_but_open',[(model, id)], - False, req.session.eval_context(req.context)) + False) def export_csv(fields, result): fp = StringIO() @@ -1273,3 +1280,25 @@ class Export(View): return export_xls(field, result) else: return export_csv(field, result) + +class Export(View): + _cp_path = "/base/report" + + @openerpweb.jsonrequest + def get_report(self, req, action): + report_srv = req.session.proxy("report") + context = req.session.eval_context(openerpweb.nonliterals.CompoundContext(req.context, \ + action["context"])) + + args = [req.session._db, req.session._uid, req.session._password, action["report_name"], context["active_ids"], {"id": context["active_id"], "model": context["active_model"], "report_type": action["report_type"]}, context] + report_id = report_srv.report(*args) + report = None + while True: + args2 = [req.session._db, req.session._uid, req.session._password, report_id] + report = report_srv.report_get(*args2) + if report["state"]: + break + time.sleep(_REPORT_POLLER_DELAY) + + #TODO: ok now we've got the report, and so what? + return False diff --git a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js index cb456eed14c..2356f3b606b 100644 --- a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js +++ b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js @@ -32,7 +32,7 @@ if(!menu) { menu=$('
').hide().css({ position:'absolute', - zIndex:'500' + zIndex:'2000' }).appendTo('body').bind('click', function(e) { e.stopPropagation() }) diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index cd420ea8c0d..4133a9c8623 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -597,13 +597,13 @@ label.error { background-color: #ccc; } .openerp .searchview_group.folded .searchview_group_string { - background: url("../img/ui/group-folded.png") no-repeat scroll 0 50%; + background: url("/base/static/src/img/ui/group-folded.png") no-repeat scroll 0 50%; } .openerp .searchview_group.folded .searchview_group_content { display: none; } .openerp .searchview_group.expanded .searchview_group_string { - background: url("../img/ui/group-expanded.png") no-repeat scroll 0 50%; + background: url("/base/static/src/img/ui/group-expanded.png") no-repeat scroll 0 50%; } .openerp .searchview_group.expanded .searchview_group_content { display: block; @@ -635,13 +635,13 @@ label.error { } .openerp .oe_search-view-custom-filter-btn span { - background: url(../img/icons/gtk-add.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-add.png) repeat-y; padding-left: 18px; } .openerp .searchview_extended_add_proposition span { font-size: 0.9em; - background: url(../img/icons/gtk-add.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-add.png) repeat-y; padding-left: 18px; } @@ -657,7 +657,7 @@ label.error { .openerp .searchview_extended_delete_group span, .openerp .searchview_extended_delete_prop span { font-size: 0.9em; - background: url(../img/icons/gtk-close.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-close.png) repeat-y; padding-left: 18px; } /* List */ @@ -870,6 +870,9 @@ label.error { .openerp .oe_forms input[type="text"], .openerp .oe_forms input[type="password"], .openerp .oe_forms select, .openerp .oe_forms .button { height: 22px; } +.openerp .oe_forms input.field_datetime { + min-width: 11em; +} .openerp .oe_forms .button { color: #4c4c4c; white-space: nowrap; @@ -892,6 +895,9 @@ label.error { position: relative; top: 4px; } +.openerp .oe_trad_field.touched { + border: 1px solid green !important; +} /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image @@ -1085,7 +1091,7 @@ label.error { cursor: pointer; border: 1px solid #D2CFCF; display: block; - background: url(../img/toggle-a-bg.png); + background: url(/base/static/src/img/toggle-a-bg.png); width: 21px; height: 21px; z-index: 10; @@ -1100,7 +1106,7 @@ label.error { } .openerp li.oe_sidebar_print { padding-left: 20px; - background: 1px 3px url(../img/icons/gtk-print.png) no-repeat; + background: 1px 3px url(/base/static/src/img/icons/gtk-print.png) no-repeat; } .openerp.kitten-mode-activated .main_table { diff --git a/addons/base/static/src/js/boot.js b/addons/base/static/src/js/boot.js index 0dd8dcdc980..8fe62d2e5a4 100644 --- a/addons/base/static/src/js/boot.js +++ b/addons/base/static/src/js/boot.js @@ -50,7 +50,9 @@ openerp.base = function(instance) { openerp.base.core(instance); - openerp.base.dates(instance); + if (openerp.base.dates) { + openerp.base.dates(instance); + } openerp.base.formats(instance); openerp.base.chrome(instance); openerp.base.data(instance); diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index 954e2024965..720bc38d54a 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -479,8 +479,6 @@ openerp.base.Login = openerp.base.Widget.extend({ this.selected_login = localStorage.getItem('last_login_login_success'); } if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) { - this.selected_db = this.selected_db || "trunk"; - this.selected_login = this.selected_login || "admin"; this.selected_password = this.selected_password || "a"; } }, @@ -564,22 +562,25 @@ openerp.base.Login = openerp.base.Widget.extend({ }); openerp.base.Header = openerp.base.Widget.extend({ - init: function(parent, element_id) { - this._super(parent, element_id); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - this.qs = '?debug' - } else { - this.qs = '' - } + template: "Header", + identifier_prefix: 'oe-app-header-', + init: function(parent) { + this._super(parent); + this.qs = "?" + jQuery.param.querystring(); + this.$content = $(); }, start: function() { - return this.do_update(); + this._super(); }, do_update: function () { - this.$element.html(QWeb.render("Header", this)); + this.$content = $(QWeb.render("Header-content", {widget: this})); + this.$content.appendTo(this.$element); this.$element.find(".logout").click(this.on_logout); return this.shortcut_load(); }, + do_reset: function() { + this.$content.remove(); + }, shortcut_load :function(){ var self = this, sc = self.session.shortcuts, @@ -590,22 +591,27 @@ openerp.base.Header = openerp.base.Widget.extend({ sc.binding = {}; $(sc.binding).bind({ 'add': function (e, attrs) { - var $shortcut = $('
  • ', { + shortcuts_ds.create(attrs, function (out) { + $('
  • ', { + 'data-shortcut-id':out.result, 'data-id': attrs.res_id }).text(attrs.name) - .appendTo(self.$element.find('.oe-shortcuts ul')); - shortcuts_ds.create(attrs, function (out) { - $shortcut.data('shortcut-id', out.result); + .appendTo(self.$element.find('.oe-shortcuts ul')); + attrs.id = out.result; + sc.push(attrs); }); }, 'remove-current': function () { var menu_id = self.session.active_id; var $shortcut = self.$element - .find('.oe-shortcuts li[data-id=' + menu_id + ']'); + .find('.oe-shortcuts li[data-id=' + menu_id + ']'); var shortcut_id = $shortcut.data('shortcut-id'); $shortcut.remove(); shortcuts_ds.unlink([shortcut_id]); - } + var sc_new = _.reject(sc, function(shortcut){ return shortcut_id === shortcut.id}); + sc.splice(0, sc.length); + sc.push.apply(sc, sc_new); + } }); } return this.rpc('/base/session/sc_list', {}, function(shortcuts) { @@ -615,6 +621,7 @@ openerp.base.Header = openerp.base.Widget.extend({ self.$element.find('.oe-shortcuts') .html(QWeb.render('Shortcuts', {'shortcuts': shortcuts})) .undelegate('li', 'click') + .delegate('li', 'click', function(e) { e.stopPropagation(); var id = $(this).data('id'); @@ -743,21 +750,23 @@ openerp.base.WebClient = openerp.base.Widget.extend({ // Do you autorize this ? will be replaced by notify() in controller openerp.base.Widget.prototype.notification = new openerp.base.Notification(this, "oe_notification"); - this.header = new openerp.base.Header(this, "oe_header"); + + this.header = new openerp.base.Header(this); this.login = new openerp.base.Login(this, "oe_login"); this.header.on_logout.add(this.login.on_logout); + this.header.on_action.add(this.on_menu_action); this.session.on_session_invalid.add(this.login.do_ask_login); this.session.on_session_valid.add_last(this.header.do_update); + this.session.on_session_invalid.add_last(this.header.do_reset); this.session.on_session_valid.add_last(this.on_logged); this.menu = new openerp.base.Menu(this, "oe_menu", "oe_secondary_menu"); this.menu.on_action.add(this.on_menu_action); - this.header.on_action.add(this.on_menu_action); }, start: function() { + this.header.appendTo($("#oe_header")); this.session.start(); - this.header.start(); this.login.start(); this.menu.start(); this.notification.notify("OpenERP Client", "The openerp client has been initialized."); diff --git a/addons/base/static/src/js/core.js b/addons/base/static/src/js/core.js index e872ced1f0d..4cd46504a69 100644 --- a/addons/base/static/src/js/core.js +++ b/addons/base/static/src/js/core.js @@ -567,8 +567,8 @@ openerp.base.Session = openerp.base.CallbackEnabled.extend( /** @lends openerp.b self.do_load_js(file_list.concat(files)); }); } else { - self.do_load_css(file_list.concat(["/base/webclient/css?mods="+modules])); - self.do_load_js(["/base/webclient/js?mods="+modules]); + self.do_load_css(["/base/webclient/css?mods="+modules]); + self.do_load_js(file_list.concat(["/base/webclient/js?mods="+modules])); } openerp._modules_loaded = true; }); diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 04c23224806..e95adaf8870 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -355,12 +355,13 @@ openerp.base.DataSet = openerp.base.Widget.extend( /** @lends openerp.base.Data * @param {Function} error_callback function called in case of write error * @returns {$.Deferred} */ - write: function (id, data, callback, error_callback) { + write: function (id, data, options, callback, error_callback) { + var options = options || {}; return this.rpc('/base/dataset/save', { model: this.model, id: id, data: data, - context: this.get_context() + context: this.get_context(options.context) }, callback, error_callback); }, /** @@ -396,18 +397,18 @@ openerp.base.DataSet = openerp.base.Widget.extend( /** @lends openerp.base.Data * * @param {String} method * @param {Array} [args] - * @param {Number} [domain_id] index of a domain to evaluate in the args array - * @param {Number} [context_id] index of a context to evaluate in the args array + * @param {Number} [domain_index] index of a domain to evaluate in the args array + * @param {Number} [context_index] index of a context to evaluate in the args array * @param {Function} callback * @param {Function }error_callback * @returns {$.Deferred} */ - call_and_eval: function (method, args, domain_id, context_id, callback, error_callback) { + call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) { return this.rpc('/base/dataset/call', { model: this.model, method: method, - domain_id: domain_id || null, - context_id: context_id || null, + domain_id: domain_index || null, + context_id: context_index || null, args: args || [] }, callback, error_callback); }, diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 7a4d3c92d89..e2a99e9b8c5 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -157,6 +157,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi this.do_update_pager(record.id == null); if (this.sidebar) { this.sidebar.attachments.do_update(); + this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id); } if (this.default_focus_field && !this.embedded_view) { this.default_focus_field.focus(); @@ -342,7 +343,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi self.on_created(r, success, prepend_on_create); }); } else { - return this.dataset.write(this.datarecord.id, values, function(r) { + return this.dataset.write(this.datarecord.id, values, {}, function(r) { self.on_saved(r, success); }); } @@ -441,6 +442,10 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormVi values[key] = val; }); return values; + }, + get_selected_ids: function() { + var id = this.dataset.ids[this.dataset.index]; + return id ? [id] : []; } }); @@ -875,6 +880,9 @@ openerp.base.form.Field = openerp.base.form.Widget.extend({ }, update_dom: function() { this._super.apply(this, arguments); + if (this.field.translate) { + this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id); + } if (!this.disable_utility_classes) { this.$element.toggleClass('disabled', this.readonly); this.$element.toggleClass('required', this.required); @@ -949,7 +957,7 @@ openerp.base.form.FieldChar = openerp.base.form.Field.extend({ }, set_value: function(value) { this._super.apply(this, arguments); - var show_value = (value != null && value !== false) ? value : ''; + var show_value = openerp.base.format_value(value, this, ''); this.$element.find('input').val(show_value); }, update_dom: function() { @@ -957,16 +965,16 @@ openerp.base.form.FieldChar = openerp.base.form.Field.extend({ this.$element.find('input').attr('disabled', this.readonly); }, set_value_from_ui: function() { - this.value = this.$element.find('input').val(); + this.value = openerp.base.parse_value(this.$element.find('input').val(), this); this._super(); }, validate: function() { this.invalid = false; - var value = this.$element.find('input').val(); - if (value === "") { - this.invalid = this.required; - } else if (this.validation_regex) { - this.invalid = !this.validation_regex.test(value); + try { + var value = openerp.base.parse_value(this.$element.find('input').val(), this, ''); + this.invalid = this.required && value === ''; + } catch(e) { + this.invalid = true; } }, focus: function() { @@ -978,7 +986,6 @@ openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldEmail"; - this.validation_regex = /@/; }, start: function() { this._super.apply(this, arguments); @@ -993,8 +1000,7 @@ openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({ }, set_value: function(value) { this._super.apply(this, arguments); - var show_value = (value != null && value !== false) ? value : ''; - this.$element.find('a').attr('href', 'mailto:' + show_value); + this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val()); } }); @@ -1017,44 +1023,13 @@ openerp.base.form.FieldUrl = openerp.base.form.FieldChar.extend({ }); openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({ - init: function(view, node) { - this._super(view, node); - this.validation_regex = /^-?\d+(\.\d+)?$/; - }, set_value: function(value) { - this._super.apply(this, [value]); if (value === false || value === undefined) { // As in GTK client, floats default to 0 value = 0; this.dirty = true; } - var show_value = value.toFixed(2); - this.$element.find('input').val(show_value); - }, - set_value_from_ui: function() { - this.value = Number(this.$element.find('input').val().replace(/,/g, '.')); - this._super(); - } -}); - -openerp.base.form.FieldInteger = openerp.base.form.FieldFloat.extend({ - init: function(view, node) { - this._super(view, node); - this.validation_regex = /^-?\d+$/; - }, - set_value: function(value) { this._super.apply(this, [value]); - if (value === false || value === undefined) { - // TODO fme: check if GTK client default integers to 0 (like it does with floats) - value = 0; - this.dirty = true; - } - var show_value = parseInt(value, 10); - this.$element.find('input').val(show_value); - }, - set_value_from_ui: function() { - this.value = Number(this.$element.find('input').val()); - this._super(); } }); @@ -1063,7 +1038,6 @@ openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ this._super(view, node); this.template = "FieldDate"; this.jqueryui_object = 'datetimepicker'; - this.validation_regex = /^\d+-\d+-\d+( \d+:\d+(:\d+)?)?$/; }, start: function() { this._super.apply(this, arguments); @@ -1103,8 +1077,6 @@ openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({ var value = this.$element.find('input').val(); if (value === "") { this.invalid = this.required; - } else if (this.validation_regex) { - this.invalid = !this.validation_regex.test(value); } else { this.invalid = !this.$element.find('input')[this.jqueryui_object]('getDate'); } @@ -1120,39 +1092,15 @@ openerp.base.form.FieldDate = openerp.base.form.FieldDatetime.extend({ init: function(view, node) { this._super(view, node); this.jqueryui_object = 'datepicker'; - this.validation_regex = /^\d+-\d+-\d+$/; }, parse: openerp.base.str_to_date, format: openerp.base.date_to_str }); -openerp.base.form.FieldFloatTime = openerp.base.form.FieldChar.extend({ - init: function(view, node) { - this._super(view, node); - this.validation_regex = /^\d+:\d+$/; - }, - set_value: function(value) { - this._super.apply(this, [value]); - if (value === false || value === undefined) { - // As in GTK client, floats default to 0 - value = 0; - this.dirty = true; - } - var show_value = _.sprintf("%02d:%02d", Math.floor(value), Math.round((value % 1) * 60)); - this.$element.find('input').val(show_value); - }, - set_value_from_ui: function() { - var time = this.$element.find('input').val().split(':'); - this.set_value(parseInt(time[0], 10) + parseInt(time[1], 10) / 60); - this._super(); - } -}); - openerp.base.form.FieldText = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); this.template = "FieldText"; - this.validation_regex = null; }, start: function() { this._super.apply(this, arguments); @@ -1160,7 +1108,7 @@ openerp.base.form.FieldText = openerp.base.form.Field.extend({ }, set_value: function(value) { this._super.apply(this, arguments); - var show_value = (value != null && value !== false) ? value : ''; + var show_value = openerp.base.format_value(value, this, ''); this.$element.find('textarea').val(show_value); }, update_dom: function() { @@ -1168,16 +1116,16 @@ openerp.base.form.FieldText = openerp.base.form.Field.extend({ this.$element.find('textarea').attr('disabled', this.readonly); }, set_value_from_ui: function() { - this.value = this.$element.find('textarea').val(); + this.value = openerp.base.parse_value(this.$element.find('textarea').val(), this); this._super(); }, validate: function() { this.invalid = false; - var value = this.$element.find('textarea').val(); - if (value === "") { - this.invalid = this.required; - } else if (this.validation_regex) { - this.invalid = !this.validation_regex.test(value); + try { + var value = openerp.base.parse_value(this.$element.find('textarea').val(), this, ''); + this.invalid = this.required && value === ''; + } catch(e) { + this.invalid = true; } }, focus: function() { @@ -1366,36 +1314,56 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ this.$input = this.$element.find("input"); this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); this.$menu_btn = this.$element.find(".oe-m2o-cm-button"); - + // context menu - var bindings = {}; - bindings[this.cm_id + "_search"] = function() { - self._search_create_popup("search"); - }; - bindings[this.cm_id + "_create"] = function() { - self._search_create_popup("form"); - }; - bindings[this.cm_id + "_open"] = function() { - if (!self.value) { - return; - } - var pop = new openerp.base.form.FormOpenPopup(self.view); - pop.show_element(self.field.relation, self.value[0],self.build_context(), {}); - pop.on_write_completed.add_last(function() { - self.set_value(self.value[0]); + var init_context_menu_def = $.Deferred().then(function(e) { + var rdataset = new openerp.base.DataSetStatic(self, "ir.values", self.build_context()); + rdataset.call("get", ['action', 'client_action_relate', + [[self.field.relation, false]], false, rdataset.get_context()], false, 0) + .then(function(result) { + self.related_entries = result; + + var $cmenu = $("#" + self.cm_id); + $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self})); + var bindings = {}; + bindings[self.cm_id + "_search"] = function() { + self._search_create_popup("search"); + }; + bindings[self.cm_id + "_create"] = function() { + self._search_create_popup("form"); + }; + bindings[self.cm_id + "_open"] = function() { + if (!self.value) { + return; + } + var pop = new openerp.base.form.FormOpenPopup(self.view); + pop.show_element(self.field.relation, self.value[0],self.build_context(), {}); + pop.on_write_completed.add_last(function() { + self.set_value(self.value[0]); + }); + }; + _.each(_.range(self.related_entries.length), function(i) { + bindings[self.cm_id + "_related_" + i] = function() { + self.open_related(self.related_entries[i]); + }; + }); + var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true, + bindings: bindings, itemStyle: {"color": ""}, + onContextMenu: function() { + if(self.value) { + $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm"); + } else { + $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm"); + } + return true; + }, menuStyle: {width: "200px"} + }); + setTimeout(function() {self.$menu_btn.trigger(e);}, 0); }); - }; - var cmenu = this.$menu_btn.contextMenu(this.cm_id, {'leftClickToo': true, - bindings: bindings, itemStyle: {"color": ""}, - onContextMenu: function() { - if(self.value) { - $("#" + self.cm_id + "_open").removeClass("oe-m2o-disabled-cm"); - } else { - $("#" + self.cm_id + "_open").addClass("oe-m2o-disabled-cm"); - } - return true; - } }); + var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()}; + this.$menu_btn.bind('contextmenu', ctx_callback); + this.$menu_btn.click(ctx_callback); // some behavior for input this.$input.keyup(function() { @@ -1555,12 +1523,14 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, set_value: function(value) { value = value || null; + this.invalid = false; var self = this; - var _super = this._super; this.tmp_value = value; + self.update_dom(); + self.on_value_changed(); var real_set_value = function(rval) { self.tmp_value = undefined; - _super.apply(self, rval); + self.value = rval; self.original_value = undefined; self._change_int_ext_value(rval); }; @@ -1586,9 +1556,27 @@ openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({ }, validate: function() { this.invalid = false; - if (this.value === null) { + var val = this.tmp_value !== undefined ? this.tmp_value : this.value; + if (val === null) { this.invalid = this.required; } + }, + open_related: function(related) { + var self = this; + if (!self.value) + return; + var additional_context = { + active_id: self.value[0], + active_ids: [self.value[0]], + active_model: self.field.relation + }; + self.rpc("/base/action/load", { + action_id: related[2].id, + context: additional_context + }, function(result) { + result.result.context = _.extend(result.result.context || {}, additional_context); + self.do_action(result.result); + }); } }); @@ -1884,10 +1872,13 @@ openerp.base.form.One2ManyListView = openerp.base.ListView.extend({ pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{ auto_write: false, alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - parent_view: self.o2m.view + parent_view: self.o2m.view, + read_function: function() { + return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); + } }); pop.on_write.add(function(id, data) { - self.o2m.dataset.write(id, data, function(r) { + self.o2m.dataset.write(id, data, {}, function(r) { self.o2m.reload_current_view(); }); }); @@ -2156,6 +2147,7 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ * options: * - alternative_form_view * - auto_write (default true) + * - read_function * - parent_view */ show_element: function(model, row_id, context, options) { @@ -2171,7 +2163,8 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ }, start: function() { this._super(); - this.dataset = new openerp.base.ReadOnlyDataSetSearch(this, this.model, this.context); + this.dataset = new openerp.base.form.FormOpenDataset(this, this.model, this.context); + this.dataset.fop = this; this.dataset.ids = [this.row_id]; this.dataset.index = 0; this.dataset.parent_view = this.options.parent_view; @@ -2184,7 +2177,7 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ var self = this; var wdataset = new openerp.base.DataSetSearch(this, this.model, this.context, this.domain); wdataset.parent_view = this.options.parent_view; - wdataset.write(id, data, function(r) { + wdataset.write(id, data, {}, function(r) { self.on_write_completed(); }); }, @@ -2213,6 +2206,16 @@ openerp.base.form.FormOpenPopup = openerp.base.OldWidget.extend({ } }); +openerp.base.form.FormOpenDataset = openerp.base.ReadOnlyDataSetSearch.extend({ + read_ids: function() { + if (this.fop.options.read_function) { + return this.fop.options.read_function.apply(null, arguments); + } else { + return this._super.apply(this, arguments); + } + } +}); + openerp.base.form.FieldReference = openerp.base.form.Field.extend({ init: function(view, node) { this._super(view, node); @@ -2440,9 +2443,9 @@ openerp.base.form.widgets = new openerp.base.Registry({ 'reference' : 'openerp.base.form.FieldReference', 'boolean' : 'openerp.base.form.FieldBoolean', 'float' : 'openerp.base.form.FieldFloat', - 'integer': 'openerp.base.form.FieldInteger', + 'integer': 'openerp.base.form.FieldFloat', + 'float_time': 'openerp.base.form.FieldFloat', 'progressbar': 'openerp.base.form.FieldProgressBar', - 'float_time': 'openerp.base.form.FieldFloatTime', 'image': 'openerp.base.form.FieldBinaryImage', 'binary': 'openerp.base.form.FieldBinaryFile' }); diff --git a/addons/base/static/src/js/formats.js b/addons/base/static/src/js/formats.js index c141aad908c..bf54290bf45 100644 --- a/addons/base/static/src/js/formats.js +++ b/addons/base/static/src/js/formats.js @@ -28,8 +28,10 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { case 'float': var precision = descriptor.digits ? descriptor.digits[1] : 2; var int_part = Math.floor(value); - var dec_part = Math.floor((value % 1) * Math.pow(10, precision)); - return _.sprintf('%d' + openerp.base._t.database.parameters.decimal_point + '%d', int_part, dec_part); + var dec_part = Math.abs(Math.floor((value % 1) * Math.pow(10, precision))); + return _.sprintf('%d%s%d', + int_part, + openerp.base._t.database.parameters.decimal_point, dec_part); case 'float_time': return _.sprintf("%02d:%02d", Math.floor(value), @@ -45,8 +47,8 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { if (typeof(value) == "string") value = openerp.base.str_to_datetime(value); try { - return value.format(_.sprintf("%s %s", openerp.base._t.database.parameters.date_format, - openerp.base._t.database.parameters.time_format)); + return value.toString(_.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, + Date.CultureInfo.formatPatterns.longTime)); } catch (e) { return value.format("%m/%d/%Y %H:%M:%S"); } @@ -55,7 +57,7 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { if (typeof(value) == "string") value = openerp.base.str_to_date(value); try { - return value.format(openerp.base._t.database.parameters.date_format); + return value.toString(Date.CultureInfo.formatPatterns.shortDate); } catch (e) { return value.format("%m/%d/%Y"); } @@ -63,7 +65,7 @@ openerp.base.format_value = function (value, descriptor, value_if_empty) { if (typeof(value) == "string") value = openerp.base.str_to_time(value); try { - return value.format(openerp.base._t.database.parameters.time_format); + return value.toString(Date.CultureInfo.formatPatterns.longTime); } catch (e) { return value.format("%H:%M:%S"); } @@ -108,17 +110,27 @@ openerp.base.parse_value = function (value, descriptor, value_if_empty) { case 'progressbar': return openerp.base.parse_value(value, {type: "float"}); case 'datetime': - var tmp = Date.parse(value); + var tmp = Date.parseExact(value, _.sprintf("%s %s", Date.CultureInfo.formatPatterns.shortDate, + Date.CultureInfo.formatPatterns.longTime)); + if (tmp !== null) + return tmp; + tmp = Date.parse(value); if (tmp !== null) return tmp; throw value + " is not a valid datetime"; case 'date': - var tmp = Date.parse(value); + var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.shortDate); + if (tmp !== null) + return tmp; + tmp = Date.parse(value); if (tmp !== null) return tmp; throw value + " is not a valid date"; case 'time': - var tmp = Date.parse(value); + var tmp = Date.parseExact(value, Date.CultureInfo.formatPatterns.longTime); + if (tmp !== null) + return tmp; + tmp = Date.parse(value); if (tmp !== null) return tmp; throw value + " is not a valid time"; diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 9797b5dff99..786c8cb2068 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -365,7 +365,6 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi reload_view: function (grouped, context, initial) { var self = this; var callback = function (field_view_get) { - console.log('loaded', initial ? 'initial' : 'subsequent'); self.on_loaded(field_view_get, grouped); }; if (this.embedded_view) { @@ -383,10 +382,19 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi * re-renders the content of the list view */ reload_content: function () { + var self = this; this.records.reset(); this.$element.find('.oe-listview-content').append( - this.groups.render( - $.proxy(this, 'compute_aggregates'))); + this.groups.render(function () { + if (self.dataset.index == null) { + var has_one = false; + self.records.each(function () { has_one = true; }); + if (has_one) { + self.dataset.index = 0; + } + } + self.compute_aggregates(); + })); }, /** * Event handler for a search, asks for the computation/folding of domains @@ -399,8 +407,8 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi */ do_search: function (domains, contexts, groupbys) { return this.rpc('/base/session/eval_domain_and_context', { - domains: domains, - contexts: contexts, + domains: [this.dataset.get_domain()].concat(domains), + contexts: [this.dataset.get_context()].concat(contexts), group_by_seq: groupbys }, $.proxy(this, 'do_actual_search')); }, @@ -589,8 +597,11 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi $footer_cells.filter(_.sprintf('[data-field=%s]', column.id)) .html(openerp.base.format_cell(aggregation, column)); }); + }, + get_selected_ids: function() { + var ids = this.groups.get_selection().ids; + return ids; } - // TODO: implement reorder (drag and drop rows) }); openerp.base.ListView.List = openerp.base.Class.extend( /** @lends openerp.base.ListView.List# */{ /** @@ -1040,25 +1051,25 @@ openerp.base.ListView.Groups = openerp.base.Class.extend( /** @lends openerp.bas var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name'); var options = { offset: page * limit, limit: limit }; dataset.read_slice(fields, options , function (records) { - if (!self.datagroup.openable) { - view.configure_pager(dataset); - } else { - var pages = Math.ceil(dataset.ids.length / limit); - self.$row - .find('.oe-pager-state') - .text(_.sprintf('%d/%d', page + 1, pages)) - .end() - .find('button[data-pager-action=previous]') - .attr('disabled', page === 0) - .end() - .find('button[data-pager-action=next]') - .attr('disabled', page === pages - 1); - } + if (!self.datagroup.openable) { + view.configure_pager(dataset); + } else { + var pages = Math.ceil(dataset.ids.length / limit); + self.$row + .find('.oe-pager-state') + .text(_.sprintf('%d/%d', page + 1, pages)) + .end() + .find('button[data-pager-action=previous]') + .attr('disabled', page === 0) + .end() + .find('button[data-pager-action=next]') + .attr('disabled', page === pages - 1); + } - self.records.add(records, {silent: true}); - list.render(); - d.resolve(list); - }); + self.records.add(records, {silent: true}); + list.render(); + d.resolve(list); + }); return d.promise(); }, setup_resequence_rows: function (list, dataset) { diff --git a/addons/base/static/src/js/search.js b/addons/base/static/src/js/search.js index 99f07784160..e3d51e2b439 100644 --- a/addons/base/static/src/js/search.js +++ b/addons/base/static/src/js/search.js @@ -801,6 +801,8 @@ openerp.base.search.ManyToOneField = openerp.base.search.CharField.extend({ render: function (defaults) { if (defaults[this.attrs.name]) { this.id = defaults[this.attrs.name]; + if (this.id instanceof Array) + this.id = this.id[0]; // TODO: maybe this should not be completely removed delete defaults[this.attrs.name]; this.dataset.name_get([this.id], $.proxy(this, 'on_name_get')); diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index 7320d09d251..11b45cbfeb0 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -123,6 +123,14 @@ openerp.base.ActionManager = openerp.base.Widget.extend({ this.content_stop(); var ClientWidget = openerp.base.client_actions.get_object(action.tag); (this.client_widget = new ClientWidget(this, action.params)).appendTo(this); + }, + ir_actions_report_xml: function(action) { + this.rpc('/base/report/get_report', { + action: action, + context: {} + }).then(function(result) { + debugger; + }); } }); @@ -350,20 +358,22 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ shortcut_add_remove: function() { var self = this; var $shortcut_toggle = this.$element.find('.oe-shortcut-toggle'); - $shortcut_toggle.click(function() { - if ($shortcut_toggle.hasClass("oe-shortcut-remove")) { - $(self.session.shortcuts.binding).trigger('remove-current'); - $shortcut_toggle.removeClass("oe-shortcut-remove"); - } else { - $(self.session.shortcuts.binding).trigger('add', { - 'user_id': self.session.uid, - 'res_id': self.session.active_id, - 'resource': 'ir.ui.menu', - 'name': self.action.name - }); - $shortcut_toggle.addClass("oe-shortcut-remove"); - } - }); + $shortcut_toggle + .unbind("click") + .click(function() { + if ($shortcut_toggle.hasClass("oe-shortcut-remove")) { + $(self.session.shortcuts.binding).trigger('remove-current'); + $shortcut_toggle.removeClass("oe-shortcut-remove"); + } else { + $(self.session.shortcuts.binding).trigger('add', { + 'user_id': self.session.uid, + 'res_id': self.session.active_id, + 'resource': 'ir.ui.menu', + 'name': self.action.name + }); + $shortcut_toggle.addClass("oe-shortcut-remove"); + } + }); } }); @@ -374,8 +384,8 @@ openerp.base.Sidebar = openerp.base.Widget.extend({ this.sections = {}; }, start: function() { + this._super(this); var self = this; - this._super(this, arguments); this.$element.html(QWeb.render('Sidebar')); this.$element.find(".toggle-sidebar").click(function(e) { self.do_toggle(); @@ -431,9 +441,26 @@ openerp.base.Sidebar = openerp.base.Widget.extend({ item.callback(); } if (item.action) { - item.action.flags = item.action.flags || {}; - item.action.flags.new_window = true; - self.do_action(item.action); + var ids = self.widget_parent.get_selected_ids(); + if (ids.length == 0) { + //TODO niv: maybe show a warning? + return false; + } + var additional_context = { + active_id: ids[0], + active_ids: ids, + active_model: self.widget_parent.dataset.model + }; + self.rpc("/base/action/load", { + action_id: item.action.id, + context: additional_context + }, function(result) { + result.result.context = _.extend(result.result.context || {}, + additional_context); + result.result.flags = result.result.flags || {}; + result.result.flags.new_window = true; + self.do_action(result.result); + }); } return false; }); @@ -456,6 +483,9 @@ openerp.base.Sidebar = openerp.base.Widget.extend({ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ dialog_title: _t("Translations"), init: function(view) { + // TODO fme: should add the language to fields_view_get because between the fields view get + // and the moment the user opens the translation dialog, the user language could have been changed + this.view_language = view.session.user_context.lang; this['on_button' + _t("Save")] = this.on_button_Save; this['on_button' + _t("Close")] = this.on_button_Close; this._super(view, { @@ -484,6 +514,9 @@ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ self.select_tab('view'); } self.$fields_form = self.$element.find('.oe_translation_form'); + self.$fields_form.find('.oe_trad_field').change(function() { + $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value'))); + }); }); return this; }, @@ -498,19 +531,27 @@ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ _.each(self.languages, function(lg) { var deff = $.Deferred(); deffered.push(deff); - self.rpc('/base/dataset/get', { - model: self.view.dataset.model, - ids: [self.view.datarecord.id], - fields: self.translatable_fields_keys, - context: self.view.dataset.get_context({ - 'lang': lg.code - }) - }, function(values) { + var callback = function(values) { _.each(self.translatable_fields_keys, function(f) { - self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || ''); + self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || ''); }); deff.resolve(); - }); + } + if (lg.code === self.view_language) { + var values = {}; + _.each(self.translatable_fields_keys, function(field) { + values[field] = self.view.fields[field].get_value(); + }); + callback([values]); + } else { + self.rpc('/base/dataset/get', { + model: self.view.dataset.model, + ids: [self.view.datarecord.id], + fields: self.translatable_fields_keys, + context: self.view.dataset.get_context({ + 'lang': lg.code + })}, callback); + } }); $.when.apply(null, deffered).then(callback); }, @@ -531,21 +572,39 @@ openerp.base.TranslateDialog = openerp.base.Dialog.extend({ }, open: function(field) { var self = this, - super = this._super; + sup = this._super; $.when(this.languages_loaded).then(function() { if (self.view.translatable_fields && self.view.translatable_fields.length) { self.do_load_fields_values(function() { - super.call(self); + sup.call(self); if (field) { // TODO: focus and scroll to field } }); } else { - super.call(self); + sup.call(self); } }); }, on_button_Save: function() { + var trads = {}, + self = this; + self.$fields_form.find('.oe_trad_field.touched').each(function() { + var field = $(this).attr('name').split('-'); + if (!trads[field[0]]) { + trads[field[0]] = {}; + } + trads[field[0]][field[1]] = $(this).val(); + }); + _.each(trads, function(data, code) { + if (code === self.view_language) { + _.each(data, function(value, field) { + self.view.fields[field].set_value(value); + }); + } else { + self.view.dataset.write(self.view.datarecord.id, data, { 'lang': code }); + } + }); this.close(); }, on_button_Close: function() { @@ -651,7 +710,7 @@ openerp.base.View = openerp.base.Widget.extend({ }, { label: "Translate", callback: this.on_sidebar_translate, - classname: 'oe_sidebar_translate' + classname: 'oe_sidebar_translate oe_hide' }, { label: "View Log", callback: this.on_sidebar_view_log, diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index 69aac831386..c6bd9a1ee19 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -326,15 +326,19 @@ - -