diff --git a/addons/account/account.py b/addons/account/account.py index bf072507aa0..ab3d065421f 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -155,6 +155,7 @@ class account_account_type(osv.osv): return res def _save_report_type(self, cr, uid, account_type_id, field_name, field_value, arg, context=None): + field_value = field_value or 'none' obj_data = self.pool.get('ir.model.data') obj_financial_report = self.pool.get('account.financial.report') #unlink if it exists somewhere in the financial reports related to BS or PL @@ -596,10 +597,7 @@ class account_account(osv.osv): return res def copy(self, cr, uid, id, default=None, context=None, done_list=None, local=False): - if default is None: - default = {} - else: - default = default.copy() + default = {} if default is None else default.copy() if done_list is None: done_list = [] account = self.browse(cr, uid, id, context=context) @@ -781,10 +779,7 @@ class account_journal(osv.osv): ] def copy(self, cr, uid, id, default=None, context=None, done_list=None, local=False): - if default is None: - default = {} - else: - default = default.copy() + default = {} if default is None else default.copy() if done_list is None: done_list = [] journal = self.browse(cr, uid, id, context=context) @@ -1185,6 +1180,7 @@ class account_fiscalyear(osv.osv): } def copy(self, cr, uid, id, default=None, context=None): + default = {} if default is None else default.copy() default.update({ 'period_ids': [], 'end_journal_period_id': False @@ -1444,14 +1440,8 @@ class account_move(osv.osv): return result def copy(self, cr, uid, id, default=None, context=None): - if context is None: - default = {} - else: - default = default.copy() - if context is None: - context = {} - else: - context = context.copy() + default = {} if default is None else default.copy() + context = {} if context is None else context.copy() default.update({ 'state':'draft', 'name':'/', diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index 423026251c2..f97f3f6af3c 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -1070,7 +1070,8 @@ class account_voucher(osv.osv): if line.amount == line.amount_unreconciled: if not line.move_line_id.amount_residual: raise osv.except_osv(_('Wrong bank statement line'),_("You have to delete the bank statement line which the payment was reconciled to manually. Please check the payment of the partner %s by the amount of %s.")%(line.voucher_id.partner_id.name, line.voucher_id.amount)) - currency_rate_difference = line.move_line_id.amount_residual - amount + sign = voucher_brw.type in ('payment', 'purchase') and -1 or 1 + currency_rate_difference = sign * (line.move_line_id.amount_residual - amount) else: currency_rate_difference = 0.0 move_line = { diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index ae01df5f6f7..7baac5d67c4 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -43,7 +43,7 @@ class OAuthController(openerpweb.Controller): url = "/#action=login&oauth_error=1" except Exception,e: # signup error - _logger.exception('oops') + _logger.exception("OAuth2: %s" % str(e)) url = "/#action=login&oauth_error=2" return openerp.addons.web.controllers.main.set_cookie_and_redirect(req, url) diff --git a/addons/auth_oauth/res_users.py b/addons/auth_oauth/res_users.py index fd26e89c2e3..9c1f88bd7f4 100644 --- a/addons/auth_oauth/res_users.py +++ b/addons/auth_oauth/res_users.py @@ -15,13 +15,17 @@ class res_users(osv.Model): _inherit = 'res.users' _columns = { - 'oauth_provider_id': fields.many2one('auth.oauth.provider','OAuth Provider'), + 'oauth_provider_id': fields.many2one('auth.oauth.provider', 'OAuth Provider'), 'oauth_uid': fields.char('OAuth User ID', help="Oauth Provider user_id"), - 'oauth_access_token': fields.char('OAuth Token', readonly=True), + 'oauth_access_token': fields.char('OAuth Access Token', readonly=True), } + _sql_constraints = [ + ('uniq_users_oauth_provider_oauht_uid', 'unique(auth_provider_id, auth_uid)', 'OAuth UID must be unique per provider'), + ] + def auth_oauth_rpc(self, cr, uid, endpoint, access_token, context=None): - params = urllib.urlencode({ 'access_token': access_token }) + params = urllib.urlencode({'access_token': access_token}) if urlparse.urlparse(endpoint)[4]: url = endpoint + '&' + params else: @@ -41,22 +45,26 @@ class res_users(osv.Model): validation = self.auth_oauth_rpc(cr, uid, p.validation_endpoint, access_token) if validation.get("error"): - raise openerp.exceptions.AccessDenied + raise Exception(validation['error']) if p.data_endpoint: data = self.auth_oauth_rpc(cr, uid, p.data_endpoint, access_token) validation.update(data) # required oauth_uid = validation['user_id'] if not oauth_uid: - raise openerp.exceptions.AccessDenied + raise openerp.exceptions.AccessDenied() email = validation.get('email', 'provider_%d_user_%d' % (p.id, oauth_uid)) # optional name = validation.get('name', email) - res = self.search(cr, uid, [("oauth_uid", "=", oauth_uid)]) + res = self.search(cr, uid, [("oauth_uid", "=", oauth_uid), ('oauth_provider_id', '=', provider)]) if res: - self.write(cr, uid, res[0], { 'oauth_access_token': access_token }) + assert len(res) == 1 + self.write(cr, uid, res[0], {'oauth_access_token': access_token}) else: - # New user + # New user if signup module available + if not hasattr(self, '_signup_create_user'): + raise openerp.exceptions.AccessDenied() + new_user = { 'name': name, 'login': email, @@ -66,7 +74,9 @@ class res_users(osv.Model): 'oauth_access_token': access_token, 'active': True, } - self.auth_signup_create(cr, uid, new_user) + # TODO pass signup token to allow attach new user to right partner + self._signup_create_user(cr, uid, new_user) + credentials = (cr.dbname, email, access_token) return credentials @@ -74,7 +84,7 @@ class res_users(osv.Model): try: return super(res_users, self).check_credentials(cr, uid, password) except openerp.exceptions.AccessDenied: - res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('oauth_access_token','=',password)]) + res = self.search(cr, SUPERUSER_ID, [('id', '=', uid), ('oauth_access_token', '=', password)]) if not res: raise diff --git a/addons/base_import/__openerp__.py b/addons/base_import/__openerp__.py index eff20bc90ab..f5a707650d8 100644 --- a/addons/base_import/__openerp__.py +++ b/addons/base_import/__openerp__.py @@ -26,13 +26,14 @@ Re-implement openerp's file import system: 'author': 'OpenERP SA', 'depends': ['base'], 'installable': True, - 'auto_install': False, # set to true and allow uninstall? + 'auto_install': True, 'css': [ 'static/lib/select2/select2.css', 'static/src/css/import.css', ], 'js': [ 'static/lib/select2/select2.js', + 'static/lib/javascript-state-machine/state-machine.js', 'static/src/js/import.js', ], 'qweb': ['static/src/xml/import.xml'], diff --git a/addons/base_import/models.py b/addons/base_import/models.py index 1e5ad002b84..996dbf7964d 100644 --- a/addons/base_import/models.py +++ b/addons/base_import/models.py @@ -83,6 +83,10 @@ class ir_import(orm.TransientModel): }] fields_got = self.pool[model].fields_get(cr, uid, context=context) for name, field in fields_got.iteritems(): + # an empty string means the field is deprecated, @deprecated must + # be absent or False to mean not-deprecated + if field.get('deprecated', False) is not False: + continue if field.get('readonly'): states = field.get('states') if not states: @@ -97,7 +101,7 @@ class ir_import(orm.TransientModel): 'id': name, 'name': name, 'string': field['string'], - # Y U NO ALWAYS HAVE REQUIRED + # Y U NO ALWAYS HAS REQUIRED 'required': bool(field.get('required')), 'fields': [], } @@ -124,8 +128,8 @@ class ir_import(orm.TransientModel): """ csv_iterator = csv.reader( StringIO(record.file), - quotechar=options['quoting'], - delimiter=options['separator']) + quotechar=str(options['quoting']), + delimiter=str(options['separator'])) csv_nonempty = itertools.ifilter(None, csv_iterator) # TODO: guess encoding with chardet? Or https://github.com/aadsm/jschardet encoding = options.get('encoding', 'utf-8') @@ -307,22 +311,14 @@ class ir_import(orm.TransientModel): except ValueError, e: return [{ 'type': 'error', - 'message': str(e), + 'message': unicode(e), 'record': False, }] - try: - _logger.info('importing %d rows...', len(data)) - (code, record, message, _wat) = self.pool[record.res_model].import_data( - cr, uid, import_fields, data, context=context) - _logger.info('done') - - except Exception, e: - _logger.exception("Import failed") - # TODO: remove when exceptions stop being an "expected" - # behavior of import_data on some (most) invalid - # input. - code, record, message = -1, None, str(e) + _logger.info('importing %d rows...', len(data)) + import_result = self.pool[record.res_model].load( + cr, uid, import_fields, data, context=context) + _logger.info('done') # If transaction aborted, RELEASE SAVEPOINT is going to raise # an InternalError (ROLLBACK should work, maybe). Ignore that. @@ -339,14 +335,4 @@ class ir_import(orm.TransientModel): except psycopg2.InternalError: pass - if code != -1: - return [] - - # TODO: add key for error location? - # TODO: error not within normal preview, how to display? Re-preview - # with higher ``count``? - return [{ - 'type': 'error', - 'message': message, - 'record': record or False - }] + return import_result['messages'] diff --git a/addons/base_import/static/lib/javascript-state-machine/LICENSE b/addons/base_import/static/lib/javascript-state-machine/LICENSE new file mode 100644 index 00000000000..8ad703ca4ef --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Jake Gordon and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/addons/base_import/static/lib/javascript-state-machine/README.md b/addons/base_import/static/lib/javascript-state-machine/README.md new file mode 100644 index 00000000000..64c045e69d7 --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/README.md @@ -0,0 +1,327 @@ +Javascript Finite State Machine (v2.1.0) +======================================== + +This standalone javascript micro-framework provides a finite state machine for your pleasure. + + * You can find the [code here](https://github.com/jakesgordon/javascript-state-machine) + * You can find a [description here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + * You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/) + +Download +======== + +You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js), +or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js) + +Alternatively: + + git clone git@github.com:jakesgordon/javascript-state-machine + + + * All code is in state-machine.js + * Minified version provided in state-machine.min.js + * No 3rd party library is required + * Demo can be found in /index.html + * QUnit tests can be found in /test/index.html + +Usage +===== + +Include `state-machine.min.js` in your application. + +In its simplest form, create a standalone state machine using: + + var fsm = StateMachine.create({ + initial: 'green', + events: [ + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ]}); + +... will create an object with a method for each event: + + * fsm.warn() - transition from 'green' to 'yellow' + * fsm.panic() - transition from 'yellow' to 'red' + * fsm.calm() - transition from 'red' to 'yellow' + * fsm.clear() - transition from 'yellow' to 'green' + +along with the following members: + + * fsm.current - contains the current state + * fsm.is(s) - return true if state `s` is the current state + * fsm.can(e) - return true if event `e` can be fired in the current state + * fsm.cannot(e) - return true if event `e` cannot be fired in the current state + +Multiple 'from' and 'to' states for a single event +================================================== + +If an event is allowed **from** multiple states, and always transitions to the same +state, then simply provide an array of states in the `from` attribute of an event. However, +if an event is allowed from multiple states, but should transition **to** a different +state depending on the current state, then provide multiple event entries with +the same name: + + var fsm = StateMachine.create({ + initial: 'hungry', + events: [ + { name: 'eat', from: 'hungry', to: 'satisfied' }, + { name: 'eat', from: 'satisfied', to: 'full' }, + { name: 'eat', from: 'full', to: 'sick' }, + { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' }, + ]}); + +This example will create an object with 2 event methods: + + * fsm.eat() + * fsm.rest() + +The `rest` event will always transition to the `hungry` state, while the `eat` event +will transition to a state that is dependent on the current state. + +>> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be +allowed from any current state. + +>> NOTE: The `rest` event in the above example can also be specified as multiple events with +the same name if you prefer the verbose approach. + +Callbacks +========= + +4 callbacks are available if your state machine has methods using the following naming conventions: + + * onbefore**event** - fired before the event + * onleave**state** - fired when leaving the old state + * onenter**state** - fired when entering the new state + * onafter**event** - fired after the event + +You can affect the event in 3 ways: + + * return `false` from an `onbeforeevent` handler to cancel the event. + * return `false` from an `onleavestate` handler to cancel the event. + * return `ASYNC` from an `onleavestate` handler to perform an asynchronous state transition (see next section) + +For convenience, the 2 most useful callbacks can be shortened: + + * on**event** - convenience shorthand for onafter**event** + * on**state** - convenience shorthand for onenter**state** + +In addition, a generic `onchangestate()` callback can be used to call a single function for _all_ state changes: + +All callbacks will be passed the same arguments: + + * **event** name + * **from** state + * **to** state + * _(followed by any arguments you passed into the original event method)_ + +Callbacks can be specified when the state machine is first created: + + var fsm = StateMachine.create({ + initial: 'green', + events: [ + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ], + callbacks: { + onpanic: function(event, from, to, msg) { alert('panic! ' + msg); }, + onclear: function(event, from, to, msg) { alert('thanks to ' + msg); }, + ongreen: function(event, from, to) { document.body.className = 'green'; }, + onyellow: function(event, from, to) { document.body.className = 'yellow'; }, + onred: function(event, from, to) { document.body.className = 'red'; }, + } + }); + + fsm.panic('killer bees'); + fsm.clear('sedatives in the honey pots'); + ... + +Additionally, they can be added and removed from the state machine at any time: + + fsm.ongreen = null; + fsm.onyellow = null; + fsm.onred = null; + fsm.onchangestate = function(event, from, to) { document.body.className = to; }; + +Asynchronous State Transitions +============================== + +Sometimes, you need to execute some asynchronous code during a state transition and ensure the +new state is not entered until your code has completed. + +A good example of this is when you transition out of a `menu` state, perhaps you want to gradually +fade the menu away, or slide it off the screen and don't want to transition to your `game` state +until after that animation has been performed. + +You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine +will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()` +method. + +For example, using jQuery effects: + + var fsm = StateMachine.create({ + + initial: 'menu', + + events: [ + { name: 'play', from: 'menu', to: 'game' }, + { name: 'quit', from: 'game', to: 'menu' } + ], + + callbacks: { + + onentermenu: function() { $('#menu').show(); }, + onentergame: function() { $('#game').show(); }, + + onleavemenu: function() { + $('#menu').fadeOut('fast', function() { + fsm.transition(); + }); + return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above) + }, + + onleavegame: function() { + $('#game').slideDown('slow', function() { + fsm.transition(); + }; + return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above) + } + + } + }); + + +State Machine Classes +===================== + +You can also turn all instances of a _class_ into an FSM by applying +the state machine functionality to the prototype, including your callbacks +in your prototype, and providing a `startup` event for use when constructing +instances: + + MyFSM = function() { // my constructor function + this.startup(); + }; + + MyFSM.prototype = { + + onpanic: function(event, from, to) { alert('panic'); }, + onclear: function(event, from, to) { alert('all is clear'); }, + + // my other prototype methods + + }; + + StateMachine.create({ + target: MyFSM.prototype, + events: [ + { name: 'startup', from: 'none', to: 'green' }, + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ]}); + + +This should be easy to adjust to fit your appropriate mechanism for object construction. + +Initialization Options +====================== + +How the state machine should initialize can depend on your application requirements, so +the library provides a number of simple options. + +By default, if you dont specify any initial state, the state machine will be in the `'none'` +state and you would need to provide an event to take it out of this state: + + var fsm = StateMachine.create({ + events: [ + { name: 'startup', from: 'none', to: 'green' }, + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "none" + fsm.startup(); + alert(fsm.current); // "green" + +If you specify the name of your initial event (as in all the earlier examples), then an +implicit `startup` event will be created for you and fired when the state machine is constructed. + + var fsm = StateMachine.create({ + initial: 'green', + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "green" + +If your object already has a `startup` method you can use a different name for the initial event + + var fsm = StateMachine.create({ + initial: { state: 'green', event: 'init' }, + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "green" + +Finally, if you want to wait to call the initial state transition event until a later date you +can `defer` it: + + var fsm = StateMachine.create({ + initial: { state: 'green', event: 'init', defer: true }, + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.current); // "none" + fsm.init(); + alert(fsm.current); // "green" + +Of course, we have now come full circle, this last example is pretty much functionally the +same as the first example in this section where you simply define your own startup event. + +So you have a number of choices available to you when initializing your state machine. + +Handling Failures +====================== + +By default, if you try to call an event method that is not allowed in the current state, the +state machine will throw an exception. If you prefer to handle the problem yourself, you can +define a custom `error` handler: + + var fsm = StateMachine.create({ + initial: 'green', + error: function(eventName, from, to, args, errorCode, errorMessage) { + return 'event ' + eventName + ' was naughty :- ' + errorMessage; + }, + events: [ + { name: 'panic', from: 'green', to: 'red' }, + { name: 'calm', from: 'red', to: 'green' }, + ]}); + alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green" + +Release Notes +============= + +See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file. + +License +======= + +See [LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file. + +Contact +======= + +If you have any ideas, feedback, requests or bug reports, you can reach me at +[jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via +my website: [Code inComplete](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + + + + + diff --git a/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md b/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md new file mode 100644 index 00000000000..06abf402d38 --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/RELEASE_NOTES.md @@ -0,0 +1,32 @@ +Version 2.1.0 (January 7th 2012) +-------------------------------- + + * Wrapped in self executing function to be more easily used with loaders like `require.js` or `curl.js` (issue #15) + * Allow event to be cancelled by returning `false` from `onleavestate` handler (issue #13) - WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`) + * Added explicit return values for event methods (issue #12) + * Added support for wildcard events that can be fired 'from' any state (issue #11) + * Added support for no-op events that transition 'to' the same state (issue #5) + * extended custom error callback to handle any exceptions caused by caller provided callbacks + * added custom error callback to override exception when an illegal state transition is attempted (thanks to cboone) + * fixed typos (thanks to cboone) + * fixed issue #4 - ensure before/after event hooks are called even if the event doesn't result in a state change + +Version 2.0.0 (August 19th 2011) +-------------------------------- + + * adding support for asynchronous state transitions (see README) - with lots of qunit tests (see test/async.js). + * consistent arguments for ALL callbacks, first 3 args are ALWAYS event name, from state and to state, followed by whatever arguments the user passed to the original event method. + * added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function. + * allow callbacks to be declared at creation time (instead of having to attach them afterwards) + * renamed 'hooks' => 'callbacks' + * [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/) + +Version 1.2.0 (June 21st 2011) +------------------------------ + * allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md) + * [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/) + +Version 1.0.0 (June 1st 2011) +----------------------------- + * initial version + * [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/) diff --git a/addons/base_import/static/lib/javascript-state-machine/Rakefile b/addons/base_import/static/lib/javascript-state-machine/Rakefile new file mode 100644 index 00000000000..beb8702a7f0 --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/Rakefile @@ -0,0 +1,8 @@ + +desc "create minified version of state-machine.js" +task :minify do + require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier')) + Minifier.enabled = true + Minifier.minify('state-machine.js') +end + diff --git a/addons/base_import/static/lib/javascript-state-machine/index.html b/addons/base_import/static/lib/javascript-state-machine/index.html new file mode 100644 index 00000000000..2d6cb62617b --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/index.html @@ -0,0 +1,39 @@ + + + + Javascript Finite State Machine + + + + + + +
+ +

Finite State Machine

+ +
+ + + + +
+ +
+
+ +
+ dashed lines are asynchronous state transitions (3 seconds) +
+ + + +
+ + + + + + + diff --git a/addons/base_import/static/lib/javascript-state-machine/state-machine.js b/addons/base_import/static/lib/javascript-state-machine/state-machine.js new file mode 100644 index 00000000000..0d503ee7bdb --- /dev/null +++ b/addons/base_import/static/lib/javascript-state-machine/state-machine.js @@ -0,0 +1,155 @@ +(function (window) { + + StateMachine = { + + //--------------------------------------------------------------------------- + + VERSION: "2.1.0", + + //--------------------------------------------------------------------------- + + Result: { + SUCCEEDED: 1, // the event transitioned successfully from one state to another + NOTRANSITION: 2, // the event was successfull but no state transition was necessary + CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback + ASYNC: 4, // the event is asynchronous and the caller is in control of when the transition occurs + }, + + Error: { + INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state + PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending + INVALID_CALLBACK: 300, // caller provided callback function threw an exception + }, + + WILDCARD: '*', + ASYNC: 'async', + + //--------------------------------------------------------------------------- + + create: function(cfg, target) { + + var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false } + var fsm = target || cfg.target || {}; + var events = cfg.events || []; + var callbacks = cfg.callbacks || {}; + var map = {}; + + var add = function(e) { + var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified + map[e.name] = map[e.name] || {}; + for (var n = 0 ; n < from.length ; n++) + map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified + }; + + if (initial) { + initial.event = initial.event || 'startup'; + add({ name: initial.event, from: 'none', to: initial.state }); + } + + for(var n = 0 ; n < events.length ; n++) + add(events[n]); + + for(var name in map) { + if (map.hasOwnProperty(name)) + fsm[name] = StateMachine.buildEvent(name, map[name]); + } + + for(var name in callbacks) { + if (callbacks.hasOwnProperty(name)) + fsm[name] = callbacks[name] + } + + fsm.current = 'none'; + fsm.is = function(state) { return this.current == state; }; + fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); } + fsm.cannot = function(event) { return !this.can(event); }; + fsm.error = cfg.error || function(name, from, to, args, error, msg) { throw msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3) + + if (initial && !initial.defer) + fsm[initial.event](); + + return fsm; + + }, + + //=========================================================================== + + doCallback: function(fsm, func, name, from, to, args) { + if (func) { + try { + return func.apply(fsm, [name, from, to].concat(args)); + } + catch(e) { + return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function"); + } + } + }, + + beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); }, + afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); }, + leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); }, + enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); }, + changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); }, + + + buildEvent: function(name, map) { + return function() { + + var from = this.current; + var to = map[from] || map[StateMachine.WILDCARD] || from; + var args = Array.prototype.slice.call(arguments); // turn arguments into pure array + + if (this.transition) + return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); + + if (this.cannot(name)) + return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); + + if (false === StateMachine.beforeEvent(this, name, from, to, args)) + return StateMachine.CANCELLED; + + if (from === to) { + StateMachine.afterEvent(this, name, from, to, args); + return StateMachine.NOTRANSITION; + } + + // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState) + var fsm = this; + this.transition = function() { + fsm.transition = null; // this method should only ever be called once + fsm.current = to; + StateMachine.enterState( fsm, name, from, to, args); + StateMachine.changeState(fsm, name, from, to, args); + StateMachine.afterEvent( fsm, name, from, to, args); + }; + + var leave = StateMachine.leaveState(this, name, from, to, args); + if (false === leave) { + this.transition = null; + return StateMachine.CANCELLED; + } + else if ("async" === leave) { + return StateMachine.ASYNC; + } + else { + if (this.transition) + this.transition(); // in case user manually called transition() but forgot to return ASYNC + return StateMachine.SUCCEEDED; + } + + }; + } + + }; // StateMachine + + //=========================================================================== + + if ("function" === typeof define) { + define("statemachine", [], function() { return StateMachine; }); + } + else { + window.StateMachine = StateMachine; + } + +}(this)); + diff --git a/addons/base_import/static/src/css/import.css b/addons/base_import/static/src/css/import.css index f6258f5aa50..21e598ff707 100644 --- a/addons/base_import/static/src/css/import.css +++ b/addons/base_import/static/src/css/import.css @@ -10,7 +10,8 @@ .oe_import .oe_import_grid, .oe_import .oe_import_error_report, .oe_import .oe_import_with_file, -.oe_import .oe_import_noheaders { +.oe_import .oe_import_noheaders, +.oe_import .oe_import_report_more { display: none; } @@ -19,7 +20,8 @@ } .oe_import.oe_import_error .oe_import_error_report, .oe_import.oe_import_with_file .oe_import_with_file, -.oe_import.oe_import_noheaders .oe_import_noheaders { +.oe_import.oe_import_noheaders .oe_import_noheaders, +.oe_import .oe_import_report_showmore .oe_import_report_more { display: block; } @@ -29,6 +31,9 @@ .oe_import .oe_import_error_report ul .oe_import_report_warning { background-color: #FEFFD9; } +.oe_import .oe_import_error_report ul .oe_import_report_info { + background-color: #d3ffd3; +} .oe_import .oe_import_noheaders { color: #888; @@ -47,7 +52,7 @@ } .oe_import .oe_import_options label { display: inline-block; - width: 10em; + width: 8em; text-align: right; } diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index c2deb660ff2..0a60fe18ffb 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -47,31 +47,35 @@ openerp.base_import = function (instance) { this._super.apply(this, arguments); if(add_button) { this.$buttons.on('click', '.oe_list_button_import', function() { - new instance.web.DataImport(self, self.dataset).open(); + self.do_action({ + type: 'ir.actions.client', + tag: 'import', + params: { + model: self.dataset.model + } + }, void 0, void 0, function () { + self.reload(); + }); return false; }); } } }); - instance.web.DataImport = instance.web.Dialog.extend({ + instance.web.client_actions.add( + 'import', 'instance.web.DataImport'); + instance.web.DataImport = instance.web.Widget.extend({ template: 'ImportView', - dialog_title: _lt("Import Data"), opts: [ {name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'}, {name: 'separator', label: _lt("Separator:"), value: ','}, {name: 'quoting', label: _lt("Quoting:"), value: '"'} ], events: { - 'change .oe_import_grid input': 'import_dryrun', - 'change input.oe_import_file': 'file_update', - 'change input.oe_import_has_header, .oe_import_options input': 'settings_updated', - 'click a.oe_import_csv': function (e) { - e.preventDefault(); - }, - 'click a.oe_import_export': function (e) { - e.preventDefault(); - }, + // 'change .oe_import_grid input': 'import_dryrun', + 'change .oe_import_file': 'loaded_file', + 'click .oe_import_file_reload': 'loaded_file', + 'change input.oe_import_has_header, .oe_import_options input': 'settings_changed', 'click a.oe_import_toggle': function (e) { e.preventDefault(); var $el = $(e.target); @@ -79,30 +83,84 @@ openerp.base_import = function (instance) { ? $el.next() : $el.parent().next()) .toggle(); + }, + 'click .oe_import_report a.oe_import_report_count': function (e) { + e.preventDefault(); + $(e.target).parent().toggleClass('oe_import_report_showmore'); + }, + 'click .oe_import_moreinfo_action a': function (e) { + e.preventDefault(); + // #data will parse the attribute on its own, we don't like + // that sort of things + var action = JSON.parse($(e.target).attr('data-action')); + // FIXME: when JS-side clean_action + action.views = _(action.views).map(function (view) { + var id = view[0], type = view[1]; + return [ + id, + type !== 'tree' ? type + : action.view_type === 'form' ? 'list' + : 'tree' + ]; + }); + this.do_action(_.extend(action, { + target: 'new', + flags: { + search_view: true, + display_title: true, + pager: true, + list: {selectable: false} + } + })); + }, + // buttons + 'click .oe_import_validate': 'validate', + 'click .oe_import_import': 'import', + 'click .oe_import_cancel': function (e) { + e.preventDefault(); + this.exit(); } }, - init: function (parent, dataset) { + init: function (parent, params) { var self = this; - this._super(parent, { - buttons: [ - {text: _t("Import File"), click: function () { - self.do_import(); - }, 'class': 'oe_import_dialog_button'} - ] - }); - this.res_model = parent.model; + this._super.apply(this, arguments); + this.res_model = params.model; // import object id this.id = null; this.Import = new instance.web.Model('base_import.import'); }, start: function () { var self = this; - return this.Import.call('create', [{ - 'res_model': this.res_model - }]).then(function (id) { - self.id = id; - self.$('input[name=import_id]').val(id); - }); + this.setup_encoding_picker(); + + return $.when( + this._super(), + this.Import.call('create', [{ + 'res_model': this.res_model + }]).then(function (id) { + self.id = id; + self.$('input[name=import_id]').val(id); + }) + ) + }, + setup_encoding_picker: function () { + this.$('input.oe_import_encoding').select2({ + width: '160px', + query: function (q) { + var make = function (term) { return {id: term, text: term}; }; + var suggestions = _.map( + ('utf-8 utf-16 windows-1252 latin1 latin2 big5 ' + + 'gb18030 shift_jis windows-1251 koir8_r').split(/\s+/), + make); + if (q.term) { + suggestions.unshift(make(q.term)); + } + q.callback({results: suggestions}); + }, + initSelection: function (e, c) { + return c({id: 'utf-8', text: 'utf-8'}); + } + }).select2('val', 'utf-8'); }, import_options: function () { @@ -118,34 +176,52 @@ openerp.base_import = function (instance) { }, //- File & settings change section - file_update: function (e) { + onfile_loaded: function () { + this.$('.oe_import_button').prop('disabled', true); if (!this.$('input.oe_import_file').val()) { return; } this.$el.removeClass('oe_import_preview oe_import_error'); jsonp(this.$el, { url: '/base_import/set_file' - }, this.proxy('settings_updated')); + }, this.proxy('settings_changed')); }, - settings_updated: function () { + onpreviewing: function () { + var self = this; + this.$('.oe_import_button').prop('disabled', true); this.$el.addClass('oe_import_with_file'); // TODO: test that write // succeeded? - this.Import.call( - 'parse_preview', [this.id, this.import_options()]) - .then(this.proxy('preview')); - }, - preview: function (result) { + this.$el.removeClass('oe_import_preview_error oe_import_error'); this.$el.toggleClass( 'oe_import_noheaders', !this.$('input.oe_import_has_header').prop('checked')); - if (result.error) { - this.$el.addClass('oe_import_error'); - this.$('.oe_import_error_report').html( + this.Import.call( + 'parse_preview', [this.id, this.import_options()]) + .then(function (result) { + var signal = result.error ? 'preview_failed' : 'preview_succeeded'; + self[signal](result); + }); + }, + onpreview_error: function (event, from, to, result) { + this.$('.oe_import_options').show(); + this.$el.addClass('oe_import_preview_error oe_import_error'); + this.$('.oe_import_error_report').html( QWeb.render('ImportView.preview.error', result)); - return; - } + }, + onpreview_success: function (event, from, to, result) { + this.$('.oe_import_import').removeClass('oe_highlight'); + this.$('.oe_import_validate').addClass('oe_highlight'); + this.$('.oe_import_button').prop('disabled', false); this.$el.addClass('oe_import_preview'); this.$('table').html(QWeb.render('ImportView.preview', result)); + if (result.headers.length === 1) { + this.$('.oe_import_options').show(); + this.onresults(null, null, null, [{ + type: 'warning', + message: _t("A single column was found in the file, this often means the file separator is incorrect") + }]); + } + var $fields = this.$('.oe_import_fields input'); this.render_fields_matches(result, $fields); var data = this.generate_fields_completion(result); @@ -180,7 +256,6 @@ openerp.base_import = function (instance) { width: 'resolve', dropdownCssClass: 'oe_import_selector' }); - this.import_dryrun(); }, generate_fields_completion: function (root) { var basic = []; @@ -252,40 +327,116 @@ openerp.base_import = function (instance) { //- import itself call_import: function (options) { - var self = this; var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) { return $(el).select2('val') || false; }).get(); return this.Import.call( 'do', [this.id, fields, this.import_options()], options); }, - import_dryrun: function () { -// this.call_import({ dryrun: true }) -// .then(this.proxy('render_import_errors')); + onvalidate: function () { + return this.call_import({ dryrun: true }) + .then(this.proxy('validated')); }, - do_import: function () { + onimport: function () { var self = this; - this.call_import({ dryrun: false }).then(function (errors) { - if (_.isEmpty(errors)) { - if (self.getParent().reload_content) { - self.getParent().reload_content(); - } - self.close(); + return this.call_import({ dryrun: false }).then(function (message) { + if (!_.any(message, function (message) { + return message.type === 'error' })) { + self['import_succeeded'](); return; } - self.render_import_errors(errors); + self['import_failed'](message); }); }, - render_import_errors: function (errors) { - if (_.isEmpty(errors)) { - this.$el.removeClass('oe_import_error'); - return; + onimported: function () { + this.exit(); + }, + exit: function () { + this.do_action({ + type: 'ir.actions.client', + tag: 'history_back' + }); + }, + onresults: function (event, from, to, message) { + var no_messages = _.isEmpty(message); + this.$('.oe_import_import').toggleClass('oe_highlight', no_messages); + this.$('.oe_import_validate').toggleClass('oe_highlight', !no_messages); + if (no_messages) { + message.push({ + type: 'info', + message: _t("Everything seems valid.") + }); } - // import failed (or maybe just warnings, if we ever get - // warnings?) + // row indexes come back 0-indexed, spreadsheets + // display 1-indexed. + var offset = 1; + // offset more if header + if (this.import_options().headers) { offset += 1; } + this.$el.addClass('oe_import_error'); this.$('.oe_import_error_report').html( - QWeb.render('ImportView.error', {errors: errors})); + QWeb.render('ImportView.error', { + errors: _(message).groupBy('message'), + at: function (rows) { + var from = rows.from + offset; + var to = rows.to + offset; + if (from === to) { + return _.str.sprintf(_t("at row %d"), from); + } + return _.str.sprintf(_t("between rows %d and %d"), + from, to); + }, + more: function (n) { + return _.str.sprintf(_t("(%d more)"), n); + }, + info: function (msg) { + if (typeof msg === 'string') { + return _.str.sprintf( + '
%s
', + _.str.escapeHTML(msg)); + } + if (msg instanceof Array) { + return _.str.sprintf( + '
%s
', + _.str.escapeHTML(_t("Here are the possible values:")), + _(msg).map(function (msg) { + return '
  • ' + + _.str.escapeHTML(msg) + + '
  • '; + }).join('')); + } + // Final should be object, action descriptor + return [ + '
    ', + _.str.sprintf('', + _.str.escapeHTML(JSON.stringify(msg))), + _.str.escapeHTML( + _t("Get all possible values")), + '', + '
    ' + ].join('') + }, + })).get(0).scrollIntoView(); }, }); + // FSM-ize DataImport + StateMachine.create({ + target: instance.web.DataImport.prototype, + events: [ + { name: 'loaded_file', + from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'], + to: 'file_loaded' }, + { name: 'settings_changed', + from: ['file_loaded', 'preview_error', 'preview_success', 'results'], + to: 'previewing' }, + { name: 'preview_failed', from: 'previewing', to: 'preview_error' }, + { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' }, + { name: 'validate', from: 'preview_success', to: 'validating' }, + { name: 'validate', from: 'results', to: 'validating' }, + { name: 'validated', from: 'validating', to: 'results' }, + { name: 'import', from: ['preview_success', 'results'], to: 'importing' }, + { name: 'import_succeeded', from: 'importing', to: 'imported'}, + { name: 'import_failed', from: 'importing', to: 'results' } + ] + }) }; diff --git a/addons/base_import/static/src/xml/import.xml b/addons/base_import/static/src/xml/import.xml index 386348bea4b..1c57c40c9db 100644 --- a/addons/base_import/static/src/xml/import.xml +++ b/addons/base_import/static/src/xml/import.xml @@ -2,17 +2,45 @@
    +
    + + + or + Cancel +

    Upload your file

    -

    Select the .CSV +

    Select the .CSV file to import. If you need a sample importable file, you - can use the export - tool to generate one.

    + can use the export tool to generate one.

    + +
    + + File Format Options… +
    +

    + + + +

    +
    +

    Map your data to OpenERP

    @@ -27,18 +55,6 @@
    - - File Format Options… -
    -

    - - - -

    -

    Frequently Asked Questions

    @@ -81,17 +97,37 @@ -

    Import preview failed due to:

    +

    Import preview failed due to: . The issue is + usually an incorrect file encoding.

    Here is the start of the file we could not import:

      -
    • - - +
    • + + + + + + + +
        +
      • + + + +
      • +
    + + + + + + + this.attr('t-if', 'widget.options.import_enabled'); diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index 43d7ca76d39..2dbd9f5c9cc 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -25,9 +25,9 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({ }).bind('sortstop', self.do_save_dashboard); var old_title = this.__parentedParent.get('title'); - this.__parentedParent.on_record_loaded.add_last(function(){ - self.__parentedParent.set({ 'title' : old_title}); - }); + this.__parentedParent.on('load_record', self, function(){ + self.__parentedParent.set({ 'title': old_title}); + }) // Events this.$el.find('.oe_dashboard_link_reset').click(this.on_reset); this.$el.find('.oe_dashboard_link_change_layout').click(this.on_change_layout); @@ -173,7 +173,7 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({ }, on_load_action: function(result, index, action_attrs) { var self = this, - action = result.result, + action = result, view_mode = action_attrs.view_mode; if (action_attrs.context && action_attrs.context['dashboard_merge_domains_contexts'] === false) { diff --git a/addons/l10n_uk/data/account.account.type.csv b/addons/l10n_uk/data/account.account.type.csv index 0827975b22f..379656ece39 100644 --- a/addons/l10n_uk/data/account.account.type.csv +++ b/addons/l10n_uk/data/account.account.type.csv @@ -10,4 +10,4 @@ "account_type_output_tax","Output Tax","Output Tax","liability","Unreconciled" "account_type_input_tax","Input Tax","Input Tax","asset","Unreconciled" "account_type_profit_and_loss","Profit and Loss","Profit and Loss","none","None" -"account_type_view","View","view",,"None" +"account_type_view","View","view","none","None" diff --git a/addons/process/static/src/js/process.js b/addons/process/static/src/js/process.js index c593c1569fc..386a9653c02 100644 --- a/addons/process/static/src/js/process.js +++ b/addons/process/static/src/js/process.js @@ -239,7 +239,7 @@ instance.web.ViewManager.include({ title: _t('Process') }); var form_controller = pop.view_form; - pop.on('on_write_complete', self, self.initialize_process_view); + pop.on('write_completed', self, self.initialize_process_view); } }); }; diff --git a/addons/project/project.py b/addons/project/project.py index a47b582be77..4f1d2826980 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -1101,7 +1101,7 @@ class task(base_stage, osv.osv): task_record = self.browse(cr, uid, task_id, context=context) if task_record.project_id: project_follower_ids = [follower.id for follower in task_record.project_id.message_follower_ids] - self.message_subscribe(cr, uid, [task_id], project_follower_ids, + self.message_subscribe(cr, uid, [task_id], project_follower_ids, context=context) self._store_history(cr, uid, [task_id], context=context) self.create_send_note(cr, uid, [task_id], context=context) @@ -1354,16 +1354,15 @@ class project_project(osv.osv): 'use_tasks': True } - -# -# Tasks History, used for cumulative flow charts (Lean/Agile) -# - class project_task_history(osv.osv): + """ + Tasks History, used for cumulative flow charts (Lean/Agile) + """ _name = 'project.task.history' _description = 'History of Tasks' _rec_name = 'task_id' _log_access = False + def _get_date(self, cr, uid, ids, name, arg, context=None): result = {} for history in self.browse(cr, uid, ids, context=context): @@ -1415,35 +1414,38 @@ class project_task_history(osv.osv): 'date': fields.date.context_today, } - class project_task_history_cumulative(osv.osv): _name = 'project.task.history.cumulative' _table = 'project_task_history_cumulative' _inherit = 'project.task.history' _auto = False + _columns = { 'end_date': fields.date('End Date'), - 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project') + 'project_id': fields.many2one('project.project', 'Project'), } + def init(self, cr): cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS ( SELECT - history.date::varchar||'-'||history.history_id::varchar as id, - history.date as end_date, + history.date::varchar||'-'||history.history_id::varchar AS id, + history.date AS end_date, * FROM ( SELECT - id as history_id, - date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date, - task_id, type_id, user_id, kanban_state, state, - greatest(remaining_hours,1) as remaining_hours, greatest(planned_hours,1) as planned_hours + h.id AS history_id, + h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date, + h.task_id, h.type_id, h.user_id, h.kanban_state, h.state, + greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours, + t.project_id FROM - project_task_history - ) as history + project_task_history AS h + JOIN project_task AS t ON (h.task_id = t.id) + + ) AS history ) """) - class project_category(osv.osv): """ Category of project's task (or issue) """ _name = "project.category" diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index b81db3c4b57..03c0ee2440b 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -103,7 +103,7 @@ - + diff --git a/addons/project/report/project_cumulative.xml b/addons/project/report/project_cumulative.xml index 8fd9c3ee1b6..c30c91fe6c0 100644 --- a/addons/project/report/project_cumulative.xml +++ b/addons/project/report/project_cumulative.xml @@ -18,10 +18,12 @@ + project.task.history.cumulative.graph project.task.history.cumulative + @@ -29,21 +31,6 @@ - - - - - - - - - - - - - - - project.task.history.cumulative.search @@ -51,18 +38,18 @@ - - - - - - - - - - + + + + + + + + + + @@ -73,27 +60,12 @@ form graph,tree - + {"search_default_open":1, "search_default_this_month": 1} - - - - - - - - - - - - - - - + parent="base.menu_project_report"/> diff --git a/addons/web_shortcuts/static/src/js/web_shortcuts.js b/addons/web_shortcuts/static/src/js/web_shortcuts.js index 743f3fc73aa..c6d5a2f41fb 100644 --- a/addons/web_shortcuts/static/src/js/web_shortcuts.js +++ b/addons/web_shortcuts/static/src/js/web_shortcuts.js @@ -108,7 +108,7 @@ instance.web.UserMenu.include({ }); instance.web.ViewManagerAction.include({ - on_mode_switch: function (view_type, no_store) { + switch_mode: function (view_type, no_store) { var self = this; this._super.apply(this, arguments).then(function() { self.shortcut_check(self.views[view_type]);