diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 0fb6af05e58..87e6a66740c 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -221,8 +221,8 @@ class account_invoice(osv.osv): 'type': { }, 'state': { - 'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'paid' and obj['type'] in ('out_invoice', 'out_refund'), - 'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open' and obj['type'] in ('out_invoice', 'out_refund'), + 'account.mt_invoice_paid': lambda self, cr, uid, obj, ctx=None: obj.state == 'paid' and obj.type in ('out_invoice', 'out_refund'), + 'account.mt_invoice_validated': lambda self, cr, uid, obj, ctx=None: obj.state == 'open' and obj.type in ('out_invoice', 'out_refund'), }, } _columns = { diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py index ce79c4d5ffb..df852181709 100644 --- a/addons/analytic/analytic.py +++ b/addons/analytic/analytic.py @@ -33,9 +33,9 @@ class account_analytic_account(osv.osv): _description = 'Analytic Account' _track = { 'state': { - 'analytic.mt_account_pending': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'pending', - 'analytic.mt_account_closed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'close', - 'analytic.mt_account_opened': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open', + 'analytic.mt_account_pending': lambda self, cr, uid, obj, ctx=None: obj.state == 'pending', + 'analytic.mt_account_closed': lambda self, cr, uid, obj, ctx=None: obj.state == 'close', + 'analytic.mt_account_opened': lambda self, cr, uid, obj, ctx=None: obj.state == 'open', }, } diff --git a/addons/auth_openid/controllers/main.py b/addons/auth_openid/controllers/main.py index 1fcc34c82a0..a4ef1bfa07b 100644 --- a/addons/auth_openid/controllers/main.py +++ b/addons/auth_openid/controllers/main.py @@ -38,6 +38,8 @@ import openerp from openerp import SUPERUSER_ID from openerp.modules.registry import RegistryManager from openerp.addons.web.controllers.main import login_and_redirect, set_cookie_and_redirect +import openerp.addons.web.http as http +from openerp.addons.web.http import request from .. import utils @@ -88,20 +90,19 @@ class GoogleAppsAwareConsumer(consumer.GenericConsumer): return super(GoogleAppsAwareConsumer, self).complete(message, endpoint, return_to) -class OpenIDController(openerp.addons.web.http.Controller): - _cp_path = '/auth_openid/login' +class OpenIDController(http.Controller): _store = filestore.FileOpenIDStore(_storedir) _REQUIRED_ATTRIBUTES = ['email'] _OPTIONAL_ATTRIBUTES = 'nickname fullname postcode country language timezone'.split() - def _add_extensions(self, request): - """Add extensions to the request""" + def _add_extensions(self, oidrequest): + """Add extensions to the oidrequest""" sreg_request = sreg.SRegRequest(required=self._REQUIRED_ATTRIBUTES, optional=self._OPTIONAL_ATTRIBUTES) - request.addExtension(sreg_request) + oidrequest.addExtension(sreg_request) ax_request = ax.FetchRequest() for alias in self._REQUIRED_ATTRIBUTES: @@ -111,7 +112,7 @@ class OpenIDController(openerp.addons.web.http.Controller): uri = utils.SREG2AX[alias] ax_request.add(ax.AttrInfo(uri, required=False, alias=alias)) - request.addExtension(ax_request) + oidrequest.addExtension(ax_request) def _get_attributes_from_success_response(self, success_response): attrs = {} @@ -133,58 +134,58 @@ class OpenIDController(openerp.addons.web.http.Controller): attrs[attr] = value return attrs - def _get_realm(self, req): - return req.httprequest.host_url + def _get_realm(self): + return request.httprequest.host_url - @openerp.addons.web.http.httprequest - def verify_direct(self, req, db, url): - result = self._verify(req, db, url) + @http.route('/auth_openid/login/verify_direct', type='http', auth='none') + def verify_direct(self, db, url): + result = self._verify(db, url) if 'error' in result: return werkzeug.exceptions.BadRequest(result['error']) if result['action'] == 'redirect': return werkzeug.utils.redirect(result['value']) return result['value'] - @openerp.addons.web.http.jsonrequest - def verify(self, req, db, url): - return self._verify(req, db, url) + @http.route('/auth_openid/login/verify', type='json', auth='none') + def verify(self, db, url): + return self._verify(db, url) - def _verify(self, req, db, url): - redirect_to = werkzeug.urls.Href(req.httprequest.host_url + 'auth_openid/login/process')(session_id=req.session_id) - realm = self._get_realm(req) + def _verify(self, db, url): + redirect_to = werkzeug.urls.Href(request.httprequest.host_url + 'auth_openid/login/process')(session_id=request.session_id) + realm = self._get_realm() session = dict(dbname=db, openid_url=url) # TODO add origin page ? oidconsumer = consumer.Consumer(session, self._store) try: - request = oidconsumer.begin(url) + oidrequest = oidconsumer.begin(url) except consumer.DiscoveryFailure, exc: fetch_error_string = 'Error in discovery: %s' % (str(exc[0]),) return {'error': fetch_error_string, 'title': 'OpenID Error'} - if request is None: + if oidrequest is None: return {'error': 'No OpenID services found', 'title': 'OpenID Error'} - req.session.openid_session = session - self._add_extensions(request) + request.session.openid_session = session + self._add_extensions(oidrequest) - if request.shouldSendRedirect(): - redirect_url = request.redirectURL(realm, redirect_to) - return {'action': 'redirect', 'value': redirect_url, 'session_id': req.session_id} + if oidrequest.shouldSendRedirect(): + redirect_url = oidrequest.redirectURL(realm, redirect_to) + return {'action': 'redirect', 'value': redirect_url, 'session_id': request.session_id} else: - form_html = request.htmlMarkup(realm, redirect_to) - return {'action': 'post', 'value': form_html, 'session_id': req.session_id} + form_html = oidrequest.htmlMarkup(realm, redirect_to) + return {'action': 'post', 'value': form_html, 'session_id': request.session_id} - @openerp.addons.web.http.httprequest - def process(self, req, **kw): - session = getattr(req.session, 'openid_session', None) + @http.route('/auth_openid/login/process', type='http', auth='none') + def process(self, **kw): + session = getattr(request.session, 'openid_session', None) if not session: - return set_cookie_and_redirect(req, '/') + return set_cookie_and_redirect('/') oidconsumer = consumer.Consumer(session, self._store, consumer_class=GoogleAppsAwareConsumer) - query = req.httprequest.args - info = oidconsumer.complete(query, req.httprequest.base_url) + query = request.httprequest.args + info = oidconsumer.complete(query, request.httprequest.base_url) display_identifier = info.getDisplayIdentifier() session['status'] = info.status @@ -225,7 +226,7 @@ class OpenIDController(openerp.addons.web.http.Controller): # TODO fill empty fields with the ones from sreg/ax cr.commit() - return login_and_redirect(req, dbname, login, key) + return login_and_redirect(dbname, login, key) session['message'] = 'This OpenID identifier is not associated to any active users' @@ -241,11 +242,11 @@ class OpenIDController(openerp.addons.web.http.Controller): # information in a log. session['message'] = 'Verification failed.' - return set_cookie_and_redirect(req, '/#action=login&loginerror=1') + return set_cookie_and_redirect('/#action=login&loginerror=1') - @openerp.addons.web.http.jsonrequest - def status(self, req): - session = getattr(req.session, 'openid_session', {}) + @http.route('/auth_openid/login/status', type='json', auth='none') + def status(self): + session = getattr(request.session, 'openid_session', {}) return {'status': session.get('status'), 'message': session.get('message')} diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py index 33c5aa863bf..6b8578bbf47 100644 --- a/addons/base_setup/res_config.py +++ b/addons/base_setup/res_config.py @@ -36,6 +36,8 @@ class base_config_settings(osv.osv_memory): help="""Enable the public part of openerp, openerp becomes a public website."""), 'module_auth_oauth': fields.boolean('Use external authentication providers, sign in with google, facebook, ...'), 'module_base_import': fields.boolean("Allow users to import data from CSV files"), + 'module_google_drive': fields.boolean('Attach Google documents to any record', + help="""This installs the module google_docs."""), } def open_company(self, cr, uid, ids, context=None): diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml index 8c48f5d7a6b..73685be95df 100644 --- a/addons/base_setup/res_config_view.xml +++ b/addons/base_setup/res_config_view.xml @@ -81,6 +81,15 @@ + + diff --git a/addons/crm/crm.py b/addons/crm/crm.py index 4c9a60634a5..d1194508b71 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -222,17 +222,13 @@ class crm_case_section(osv.osv): return res def create(self, cr, uid, vals, context=None): - mail_alias = self.pool.get('mail.alias') - if not vals.get('alias_id'): - alias_name = vals.pop('alias_name', None) or vals.get('name') # prevent errors during copy() - alias_id = mail_alias.create_unique_alias(cr, uid, - {'alias_name': alias_name}, - model_name="crm.lead", - context=context) - vals['alias_id'] = alias_id - res = super(crm_case_section, self).create(cr, uid, vals, context) - mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'section_id': res, 'type': 'lead'}}, context) - return res + if context is None: + context = {} + create_context = dict(context, alias_model_name='crm.lead', alias_parent_model_name=self._name) + section_id = super(crm_case_section, self).create(cr, uid, vals, context=create_context) + section = self.browse(cr, uid, section_id, context=context) + self.pool.get('mail.alias').write(cr, uid, [section.alias_id.id], {'alias_parent_thread_id': section_id, 'alias_defaults': {'section_id': section_id, 'type': 'lead'}}, context=context) + return section_id def unlink(self, cr, uid, ids, context=None): # Cascade-delete mail aliases as well, as they should not exist without the sales team. diff --git a/addons/crm/crm_case_section_view.xml b/addons/crm/crm_case_section_view.xml index be5611c03c4..c957311b47f 100644 --- a/addons/crm/crm_case_section_view.xml +++ b/addons/crm/crm_case_section_view.xml @@ -94,7 +94,7 @@

- %% + %%
@@ -168,17 +168,6 @@

-
-
@@ -187,12 +176,25 @@ - - + + diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 44ada2ffcc0..1070af22b3c 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -77,12 +77,12 @@ class crm_lead(base_stage, format_address, osv.osv): _track = { 'state': { - 'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'], - 'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', - 'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancel', + 'crm.mt_lead_create': lambda self, cr, uid, obj, ctx=None: obj.state in ['new', 'draft'], + 'crm.mt_lead_won': lambda self, cr, uid, obj, ctx=None: obj.state == 'done', + 'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.state == 'cancel', }, 'stage_id': { - 'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'cancel', 'done'], + 'crm.mt_lead_stage': lambda self, cr, uid, obj, ctx=None: obj.state not in ['new', 'draft', 'cancel', 'done'], }, } diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml index e7e6beae33e..4c6cf47c4f7 100644 --- a/addons/crm/crm_lead_demo.xml +++ b/addons/crm/crm_lead_demo.xml @@ -709,5 +709,6 @@ Andrew eval="[ ref('msg_case18_1'), ref('msg_case18_2')], True, {}" /> + diff --git a/addons/event/event.py b/addons/event/event.py index eb18afd747c..3c04f5c1baa 100644 --- a/addons/event/event.py +++ b/addons/event/event.py @@ -172,6 +172,12 @@ class event_event(osv.osv): continue return res + def _get_visibility_selection(self, cr, uid, context=None): + return [('public', 'All Users'), + ('employees', 'Employees Only')] + # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field + _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs) + _columns = { 'name': fields.char('Name', size=64, required=True, translate=True, readonly=False, states={'done': [('readonly', True)]}), 'user_id': fields.many2one('res.users', 'Responsible User', readonly=False, states={'done': [('readonly', True)]}), @@ -209,11 +215,14 @@ class event_event(osv.osv): 'note': fields.text('Description', readonly=False, states={'done': [('readonly', True)]}), 'company_id': fields.many2one('res.company', 'Company', required=False, change_default=True, readonly=False, states={'done': [('readonly', True)]}), 'is_subscribed' : fields.function(_subscribe_fnc, type="boolean", string='Subscribed'), + 'visibility': fields.selection(_visibility_selection, 'Privacy / Visibility', + select=True, required=True), } _defaults = { 'state': 'draft', 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'event.event', context=c), 'user_id': lambda obj, cr, uid, context: uid, + 'visibility': 'employees', } def subscribe_to_event(self, cr, uid, ids, context=None): diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml index 4f5e33eeafd..a9831720abf 100644 --- a/addons/event/event_view.xml +++ b/addons/event/event_view.xml @@ -76,6 +76,7 @@
diff --git a/addons/event/security/event_security.xml b/addons/event/security/event_security.xml index 83039ca4686..90a232731a7 100644 --- a/addons/event/security/event_security.xml +++ b/addons/event/security/event_security.xml @@ -25,25 +25,35 @@ - - Event multi-company + + Event: multi-company - ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + ['|', + ('company_id', '=', False), + ('company_id', 'child_of', [user.company_id.id]), + ] + - - - Event Registration multi-company + + Event/Registration: multi-company - ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + ['|', + ('company_id', '=', False), + ('company_id', 'child_of', [user.company_id.id]), + ] + - - - Report Event Registration multi-company + + Event/Report Registration: multi-company - ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + ['|', + ('company_id', '=', False), + ('company_id', 'child_of', [user.company_id.id]), + ] + diff --git a/addons/google_base_account/__init__.py b/addons/google_base_account/__init__.py index e1a476431af..44a5aabe277 100644 --- a/addons/google_base_account/__init__.py +++ b/addons/google_base_account/__init__.py @@ -20,7 +20,6 @@ ############################################################################## import google_base_account -import wizard # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/google_base_account/__openerp__.py b/addons/google_base_account/__openerp__.py index b4d996f2807..99678d2785d 100644 --- a/addons/google_base_account/__openerp__.py +++ b/addons/google_base_account/__openerp__.py @@ -30,10 +30,9 @@ The module adds google user in res user. """, 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', - 'depends': ['base'], + 'depends': ['base_setup'], 'data': [ - 'google_base_account_view.xml', - 'wizard/google_login_view.xml', + 'google_base_account_data.xml', ], 'demo': [], 'installable': True, diff --git a/addons/google_base_account/google_base_account.py b/addons/google_base_account/google_base_account.py index 06e5643344c..a6cf67bbe62 100644 --- a/addons/google_base_account/google_base_account.py +++ b/addons/google_base_account/google_base_account.py @@ -19,14 +19,48 @@ # ############################################################################## -from openerp.osv import fields,osv - -class res_users(osv.osv): - _inherit = "res.users" - _columns = { - 'gmail_user': fields.char('Username', size=64,), - 'gmail_password': fields.char('Password', size=64), - } +from openerp.osv import osv +from openerp import SUPERUSER_ID +from openerp.tools.translate import _ + +import urllib +import urllib2 +import simplejson + + +class google_service(osv.osv): + _name = 'google.service' + + def generate_refresh_token(self, cr, uid, service, authorization_code, context=None): + if authorization_code: + ir_config = self.pool['ir.config_parameter'] + client_id = ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service) + client_secret = ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_secret' % service) + redirect_uri = ir_config.get_param(cr, SUPERUSER_ID, 'google_redirect_uri') + + #Get the Refresh Token From Google And store it in ir.config_parameter + headers = {"Content-type": "application/x-www-form-urlencoded"} + data = dict(code=authorization_code, client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri, grant_type="authorization_code") + data = urllib.urlencode(data) + try: + req = urllib2.Request("https://accounts.google.com/o/oauth2/token", data, headers) + content = urllib2.urlopen(req).read() + except urllib2.HTTPError: + raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during your token generation. Maybe your Authorization Code is invalid or already expired"), context=context) + + content = simplejson.loads(content) + return content.get('refresh_token') + + def _get_google_token_uri(self, cr, uid, service, context=None): + ir_config = self.pool['ir.config_parameter'] + params = { + 'scope': 'https://www.googleapis.com/auth/drive', + 'redirect_uri': ir_config.get_param(cr, SUPERUSER_ID, 'google_redirect_uri'), + 'client_id': ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service), + 'response_type': 'code', + 'client_id': ir_config.get_param(cr, SUPERUSER_ID, 'google_%s_client_id' % service), + } + uri = 'https://accounts.google.com/o/oauth2/auth?%s' % urllib.urlencode(params) + return uri # vim:expandtab:smartindent:toabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/google_base_account/google_base_account_data.xml b/addons/google_base_account/google_base_account_data.xml new file mode 100644 index 00000000000..c6748df7dc9 --- /dev/null +++ b/addons/google_base_account/google_base_account_data.xml @@ -0,0 +1,9 @@ + + + + + google_redirect_uri + urn:ietf:wg:oauth:2.0:oob + + + \ No newline at end of file diff --git a/addons/google_base_account/google_base_account_view.xml b/addons/google_base_account/google_base_account_view.xml deleted file mode 100644 index 8613582692f..00000000000 --- a/addons/google_base_account/google_base_account_view.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - res.users.google.form1 - res.users - - - - - - - - - - - - - - diff --git a/addons/google_base_account/wizard/google_login.py b/addons/google_base_account/wizard/google_login.py deleted file mode 100644 index 1771d99b51e..00000000000 --- a/addons/google_base_account/wizard/google_login.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp.osv import fields,osv -from openerp.tools.translate import _ -try: - import gdata.contacts.service - import gdata.contacts.client - import gdata.calendar.service -except ImportError: - raise osv.except_osv(_('Google Contacts Import Error!'), _('Please install gdata-python-client from http://code.google.com/p/gdata-python-client/downloads/list')) - -class google_login(osv.osv_memory): - _description ='Google Contact' - _name = 'google.login' - _columns = { - 'user': fields.char('Google Username', size=64, required=True), - 'password': fields.char('Google Password', size=64), - } - - def google_login(self, user, password, type='', context=None): - if type == 'group': - gd_client = gdata.contacts.service.ContactsService() - elif type == 'contact': - gd_client = gdata.contacts.service.ContactsService() - elif type == 'calendar': - gd_client = gdata.calendar.service.CalendarService() - elif type =='docs_client': - gd_client = gdata.docs.client.DocsClient() - else: - gd_client = gdata.contacts.service.ContactsService() - try: - gd_client.ClientLogin(user, password, gd_client.source) - except Exception: - return False - return gd_client - - - def default_get(self, cr, uid, fields, context=None): - res = super(google_login, self).default_get(cr, uid, fields, context=context) - user_obj = self.pool.get('res.users').browse(cr, uid, uid) - if 'user' in fields: - res.update({'user': user_obj.gmail_user}) - if 'password' in fields: - res.update({'password': user_obj.gmail_password}) - return res - - def login(self, cr, uid, ids, context=None): - data = self.read(cr, uid, ids)[0] - user = data['user'] - password = data['password'] - if self.google_login(user, password): - res = { - 'gmail_user': user, - 'gmail_password': password - } - self.pool.get('res.users').write(cr, uid, uid, res, context=context) - else: - raise osv.except_osv(_('Error!'), _("Authentication failed. Check the user and password.")) - - return self._get_next_action(cr, uid, context=context) - - def _get_next_action(self, cr, uid, context=None): - return {'type': 'ir.actions.act_window_close'} - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/google_base_account/wizard/google_login_view.xml b/addons/google_base_account/wizard/google_login_view.xml deleted file mode 100644 index ed345ab5a0f..00000000000 --- a/addons/google_base_account/wizard/google_login_view.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - google.login.form - google.login - -
- - - - -
-
-
-
-
- - - - Google Login - ir.actions.act_window - google.login - form - form - new - - -
-
diff --git a/addons/google_docs/__init__.py b/addons/google_docs/__init__.py deleted file mode 100644 index dd6285502c0..00000000000 --- a/addons/google_docs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import google_docs diff --git a/addons/google_docs/google_docs.py b/addons/google_docs/google_docs.py deleted file mode 100644 index 2fd1976ae3b..00000000000 --- a/addons/google_docs/google_docs.py +++ /dev/null @@ -1,193 +0,0 @@ -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2012 OpenERP SA (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import logging -from datetime import datetime - -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT -from openerp.osv import fields, osv -from openerp.tools.translate import _ - -_logger = logging.getLogger(__name__) - -try: - import gdata.docs.data - import gdata.docs.client - - # API breakage madness in the gdata API - those guys are insane. - try: - # gdata 2.0.15+ - gdata.docs.client.DocsClient.copy_resource - except AttributeError: - # gdata 2.0.14- : copy_resource() was copy() - gdata.docs.client.DocsClient.copy_resource = gdata.docs.client.DocsClient.copy - - try: - # gdata 2.0.16+ - gdata.docs.client.DocsClient.get_resource_by_id - except AttributeError: - try: - # gdata 2.0.15+ - gdata.docs.client.DocsClient.get_resource_by_self_link - def get_resource_by_id_2_0_16(self, resource_id, **kwargs): - return self.GetResourceBySelfLink( - gdata.docs.client.RESOURCE_FEED_URI + ('/%s' % resource_id), **kwargs) - gdata.docs.client.DocsClient.get_resource_by_id = get_resource_by_id_2_0_16 - except AttributeError: - # gdata 2.0.14- : alias get_resource_by_id() - gdata.docs.client.DocsClient.get_resource_by_id = gdata.docs.client.DocsClient.get_doc - - try: - import atom.http_interface - _logger.info('GData lib version `%s` detected' % atom.http_interface.USER_AGENT) - except (ImportError, AttributeError): - _logger.debug('GData lib version could not be detected', exc_info=True) - -except ImportError: - _logger.warning("Please install latest gdata-python-client from http://code.google.com/p/gdata-python-client/downloads/list") - - -class google_docs_ir_attachment(osv.osv): - _inherit = 'ir.attachment' - - def _auth(self, cr, uid, context=None): - ''' - Connexion with google base account - @return client object for connexion - ''' - #pool the google.login in google_base_account - google_pool = self.pool.get('google.login') - #get gmail password and login. We use default_get() instead of a create() followed by a read() on the - # google.login object, because it is easier. The keys 'user' and 'password' ahve to be passed in the dict - # but the values will be replaced by the user gmail password and login. - user_config = google_pool.default_get(cr, uid, {'user' : '' , 'password' : ''}, context=context) - #login gmail account - client = google_pool.google_login(user_config['user'], user_config['password'], type='docs_client', context=context) - if not client: - raise osv.except_osv(_('Google Docs Error!'), _("Check your google configuration in Users/Users/Synchronization tab.")) - _logger.info('Logged into google docs as %s', user_config['user']) - return client - - def create_empty_google_doc(self, cr, uid, res_model, res_id, context=None): - '''Create a new google document, empty and with a default type (txt) - :param res_model: the object for which the google doc is created - :param res_id: the Id of the object for which the google doc is created - :return: the ID of the google document object created - ''' - #login with the base account google module - client = self._auth(cr, uid, context=context) - # create the document in google docs - title = "%s %s" % (context.get("name","Untitled Document."), datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT)) - local_resource = gdata.docs.data.Resource(gdata.docs.data.DOCUMENT_LABEL,title=title) - #create a new doc in Google Docs - gdocs_resource = client.post(entry=local_resource, uri='https://docs.google.com/feeds/default/private/full/') - # create an ir.attachment into the db - self.create(cr, uid, { - 'res_model': res_model, - 'res_id': res_id, - 'type': 'url', - 'name': title, - 'url': gdocs_resource.get_alternate_link().href, - }, context=context) - return {'resource_id': gdocs_resource.resource_id.text, - 'title': title, - 'url': gdocs_resource.get_alternate_link().href} - - def copy_gdoc(self, cr, uid, res_model, res_id, name_gdocs, gdoc_template_id, context=None): - ''' - copy an existing document in google docs - :param res_model: the object for which the google doc is created - :param res_id: the Id of the object for which the google doc is created - :param name_gdocs: the name of the future ir.attachment that will be created. Based on the google doc template foun. - :param gdoc_template_id: the id of the google doc document to copy - :return: the ID of the google document object created - ''' - #login with the base account google module - client = self._auth(cr, uid) - # fetch and copy the original document - try: - doc = client.get_resource_by_id(gdoc_template_id) - #copy the document you choose in the configuration - copy_resource = client.copy_resource(doc, name_gdocs) - except: - raise osv.except_osv(_('Google Docs Error!'), _("Your resource id is not correct. You can find the id in the google docs URL.")) - # create an ir.attachment - self.create(cr, uid, { - 'res_model': res_model, - 'res_id': res_id, - 'type': 'url', - 'name': name_gdocs, - 'url': copy_resource.get_alternate_link().href - }, context=context) - return copy_resource.resource_id.text - - def google_doc_get(self, cr, uid, res_model, ids, context=None): - ''' - Function called by the js, when no google doc are yet associated with a record, with the aim to create one. It - will first seek for a google.docs.config associated with the model `res_model` to find out what's the template - of google doc to copy (this is usefull if you want to start with a non-empty document, a type or a name - different than the default values). If no config is associated with the `res_model`, then a blank text document - with a default name is created. - :param res_model: the object for which the google doc is created - :param ids: the list of ids of the objects for which the google doc is created. This list is supposed to have - a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it) - :return: the google document object created - ''' - if len(ids) != 1: - raise osv.except_osv(_('Google Docs Error!'), _("Creating google docs may only be done by one at a time.")) - res_id = ids[0] - pool_ir_attachment = self.pool.get('ir.attachment') - pool_gdoc_config = self.pool.get('google.docs.config') - name_gdocs = '' - model_fields_dic = self.pool[res_model].read(cr, uid, res_id, [], context=context) - - # check if a model is configured with a template - google_docs_config = pool_gdoc_config.search(cr, uid, [('model_id', '=', res_model)], context=context) - if google_docs_config: - name_gdocs = pool_gdoc_config.browse(cr, uid, google_docs_config, context=context)[0].name_template - try: - name_gdocs = name_gdocs % model_fields_dic - except: - raise osv.except_osv(_('Key Error!'), _("Your Google Doc Name Pattern's key does not found in object.")) - google_template_id = pool_gdoc_config.browse(cr, uid, google_docs_config[0], context=context).gdocs_resource_id - google_document = pool_ir_attachment.copy_gdoc(cr, uid, res_model, res_id, name_gdocs, google_template_id, context=context) - else: - google_document = pool_ir_attachment.create_empty_google_doc(cr, uid, res_model, res_id, context=context) - return google_document - -class config(osv.osv): - _name = 'google.docs.config' - _description = "Google Docs templates config" - - _columns = { - 'model_id': fields.many2one('ir.model', 'Model', required=True), - 'gdocs_resource_id': fields.char('Google Resource ID to Use as Template', size=64, help=''' -This is the id of the template document, on google side. You can find it thanks to its URL: -*for a text document with url like `https://docs.google.com/a/openerp.com/document/d/123456789/edit`, the ID is `document:123456789` -*for a spreadsheet document with url like `https://docs.google.com/a/openerp.com/spreadsheet/ccc?key=123456789#gid=0`, the ID is `spreadsheet:123456789` -*for a presentation (slide show) document with url like `https://docs.google.com/a/openerp.com/presentation/d/123456789/edit#slide=id.p`, the ID is `presentation:123456789` -*for a drawing document with url like `https://docs.google.com/a/openerp.com/drawings/d/123456789/edit`, the ID is `drawings:123456789` -... -''', required=True), - 'name_template': fields.char('Google Doc Name Pattern', size=64, help='Choose how the new google docs will be named, on google side. Eg. gdoc_%(field_name)s', required=True), - } - - _defaults = { - 'name_template': 'gdoc_%(name)s', - } diff --git a/addons/google_docs/i18n/google_docs.pot b/addons/google_docs/i18n/google_docs.pot deleted file mode 100644 index 8da7c5d2f70..00000000000 --- a/addons/google_docs/i18n/google_docs.pot +++ /dev/null @@ -1,159 +0,0 @@ -# Translation of OpenERP Server. -# This file contains the translation of the following modules: -# * google_docs -# -msgid "" -msgstr "" -"Project-Id-Version: OpenERP Server 7.0alpha\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-12-21 17:05+0000\n" -"PO-Revision-Date: 2012-12-21 17:05+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: google_docs -#: code:addons/google_docs/google_docs.py:139 -#, python-format -msgid "Key Error!" -msgstr "" - -#. module: google_docs -#: view:google.docs.config:0 -msgid "for a presentation (slide show) document with url like `https://docs.google.com/a/openerp.com/presentation/d/123456789/edit#slide=id.p`, the ID is `presentation:123456789`" -msgstr "" - -#. module: google_docs -#: view:google.docs.config:0 -msgid "for a text document with url like `https://docs.google.com/a/openerp.com/document/d/123456789/edit`, the ID is `document:123456789`" -msgstr "" - -#. module: google_docs -#: field:google.docs.config,gdocs_resource_id:0 -msgid "Google Resource ID to Use as Template" -msgstr "" - -#. module: google_docs -#: view:google.docs.config:0 -msgid "for a drawing document with url like `https://docs.google.com/a/openerp.com/drawings/d/123456789/edit`, the ID is `drawings:123456789`" -msgstr "" - -#. module: google_docs -#. openerp-web -#: code:addons/google_docs/static/src/xml/gdocs.xml:6 -#, python-format -msgid "Add Google Doc..." -msgstr "" - -#. module: google_docs -#: view:google.docs.config:0 -msgid "This is the id of the template document, on google side. You can find it thanks to its URL:" -msgstr "" - -#. module: google_docs -#: model:ir.model,name:google_docs.model_google_docs_config -msgid "Google Docs templates config" -msgstr "" - -#. module: google_docs -#. openerp-web -#: code:addons/google_docs/static/src/js/gdocs.js:25 -#, python-format -msgid "The user google credentials are not set yet. Contact your administrator for help." -msgstr "" - -#. module: google_docs -#: view:google.docs.config:0 -msgid "for a spreadsheet document with url like `https://docs.google.com/a/openerp.com/spreadsheet/ccc?key=123456789#gid=0`, the ID is `spreadsheet:123456789`" -msgstr "" - -#. module: google_docs -#: code:addons/google_docs/google_docs.py:101 -#, python-format -msgid "Your resource id is not correct. You can find the id in the google docs URL." -msgstr "" - -#. module: google_docs -#: code:addons/google_docs/google_docs.py:125 -#, python-format -msgid "Creating google docs may only be done by one at a time." -msgstr "" - -#. module: google_docs -#: code:addons/google_docs/google_docs.py:56 -#: code:addons/google_docs/google_docs.py:101 -#: code:addons/google_docs/google_docs.py:125 -#, python-format -msgid "Google Docs Error!" -msgstr "" - -#. module: google_docs -#: code:addons/google_docs/google_docs.py:56 -#, python-format -msgid "Check your google configuration in Users/Users/Synchronization tab." -msgstr "" - -#. module: google_docs -#: model:ir.ui.menu,name:google_docs.menu_gdocs_config -msgid "Google Docs configuration" -msgstr "" - -#. module: google_docs -#: model:ir.actions.act_window,name:google_docs.action_google_docs_users_config -#: model:ir.ui.menu,name:google_docs.menu_gdocs_model_config -msgid "Models configuration" -msgstr "" - -#. module: google_docs -#: field:google.docs.config,model_id:0 -msgid "Model" -msgstr "" - -#. module: google_docs -#. openerp-web -#: code:addons/google_docs/static/src/js/gdocs.js:28 -#, python-format -msgid "User Google credentials are not yet set." -msgstr "" - -#. module: google_docs -#: code:addons/google_docs/google_docs.py:139 -#, python-format -msgid "Your Google Doc Name Pattern's key does not found in object." -msgstr "" - -#. module: google_docs -#: help:google.docs.config,name_template:0 -msgid "Choose how the new google docs will be named, on google side. Eg. gdoc_%(field_name)s" -msgstr "" - -#. module: google_docs -#: view:google.docs.config:0 -msgid "Google Docs Configuration" -msgstr "" - -#. module: google_docs -#: help:google.docs.config,gdocs_resource_id:0 -msgid "\n" -"This is the id of the template document, on google side. You can find it thanks to its URL: \n" -"*for a text document with url like `https://docs.google.com/a/openerp.com/document/d/123456789/edit`, the ID is `document:123456789`\n" -"*for a spreadsheet document with url like `https://docs.google.com/a/openerp.com/spreadsheet/ccc?key=123456789#gid=0`, the ID is `spreadsheet:123456789`\n" -"*for a presentation (slide show) document with url like `https://docs.google.com/a/openerp.com/presentation/d/123456789/edit#slide=id.p`, the ID is `presentation:123456789`\n" -"*for a drawing document with url like `https://docs.google.com/a/openerp.com/drawings/d/123456789/edit`, the ID is `drawings:123456789`\n" -"...\n" -"" -msgstr "" - -#. module: google_docs -#: model:ir.model,name:google_docs.model_ir_attachment -msgid "ir.attachment" -msgstr "" - -#. module: google_docs -#: field:google.docs.config,name_template:0 -msgid "Google Doc Name Pattern" -msgstr "" - diff --git a/addons/google_docs/res_config_user_view.xml b/addons/google_docs/res_config_user_view.xml deleted file mode 100644 index e6c4223735f..00000000000 --- a/addons/google_docs/res_config_user_view.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - google_docs.config.tree - google.docs.config - - - - - - - - - - google_docs.config.form - google.docs.config - -
- - - -
-
-
- - - Models configuration - google.docs.config - ir.actions.act_window - form - - - - -
-
diff --git a/addons/google_docs/security/ir.model.access.csv b/addons/google_docs/security/ir.model.access.csv deleted file mode 100644 index 9393d586fb8..00000000000 --- a/addons/google_docs/security/ir.model.access.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_google_docs,google.docs.config,model_google_docs_config,,1,0,0,0 -access_google_docs,google.docs.config,model_google_docs_config,base.group_system,1,1,1,1 \ No newline at end of file diff --git a/addons/google_docs/static/src/js/gdocs.js b/addons/google_docs/static/src/js/gdocs.js deleted file mode 100644 index 1cc511ef4d9..00000000000 --- a/addons/google_docs/static/src/js/gdocs.js +++ /dev/null @@ -1,40 +0,0 @@ -openerp.google_docs = function(instance, m) { -var _t = instance.web._t, - QWeb = instance.web.qweb; - - instance.web.Sidebar.include({ - redraw: function() { - var self = this; - this._super.apply(this, arguments); - self.$el.find('.oe_sidebar_add_attachment').after(QWeb.render('AddGoogleDocumentItem', {widget: self})) - self.$el.find('.oe_sidebar_add_google_doc').on('click', function (e) { - self.on_google_doc(); - }); - }, - on_google_doc: function() { - var self = this; - var view = self.getParent(); - var ids = ( view.fields_view.type != "form" )? view.groups.get_selection().ids : [ view.datarecord.id ]; - if( !_.isEmpty(ids) ){ - view.sidebar_eval_context().done(function (context) { - var ds = new instance.web.DataSet(this, 'ir.attachment', context); - ds.call('google_doc_get', [view.dataset.model, ids, context]).done(function(r) { - if (r == 'False') { - var params = { - error: response, - message: _t("The user google credentials are not set yet. Contact your administrator for help.") - } - $(openerp.web.qweb.render("DialogWarning", params)).dialog({ - title: _t("User Google credentials are not yet set."), - modal: true, - }); - } - }).done(function(r){ - window.open(r.url,"_blank"); - view.reload(); - }); - }); - } - } - }); -}; diff --git a/addons/google_drive/__init__.py b/addons/google_drive/__init__.py new file mode 100644 index 00000000000..e1cf2d00f3c --- /dev/null +++ b/addons/google_drive/__init__.py @@ -0,0 +1 @@ +import google_drive diff --git a/addons/google_docs/__openerp__.py b/addons/google_drive/__openerp__.py similarity index 63% rename from addons/google_docs/__openerp__.py rename to addons/google_drive/__openerp__.py index 2d064fdb8a9..9e6c73d2649 100644 --- a/addons/google_docs/__openerp__.py +++ b/addons/google_drive/__openerp__.py @@ -20,22 +20,31 @@ ############################################################################## { - 'name': 'Google Docs integration', + 'name': 'Google Drive™ integration', 'version': '0.2', 'author': 'OpenERP SA', 'website': 'http://openerp.com', 'category': 'Tools', 'installable': True, 'auto_install': False, - 'js': ['static/src/js/gdocs.js'], - 'qweb': ['static/src/xml/gdocs.xml'], + 'js': [ + 'static/lib/gapi/client.js', + 'static/src/js/gdrive.js', + ], 'data': [ 'security/ir.model.access.csv', - 'res_config_user_view.xml' + 'res_config_user_view.xml', + 'google_drive_data.xml' ], - 'depends': ['google_base_account','document'], + 'demo': [ + 'google_drive_demo.xml' + ], + 'depends': ['base_setup', 'google_base_account'], 'description': """ -Module to attach a google document to any model. -================================================ +Integrate google document to OpenERP record. +============================================ + +This module allows you to integrate google documents to any of your OpenERP record quickly and easily using OAuth 2.0 for Installed Applications, +You can configure your google Authorization Code from Settings > Configuration > General Settings by clicking on "Generate Google Authorization Code" """ } diff --git a/addons/google_drive/google_drive.py b/addons/google_drive/google_drive.py new file mode 100644 index 00000000000..1dc6b25f27c --- /dev/null +++ b/addons/google_drive/google_drive.py @@ -0,0 +1,211 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2012 OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import logging + +from openerp import SUPERUSER_ID +from openerp.osv import fields, osv +from openerp.tools.translate import _ + +import urllib +import urllib2 +import json +import re + +_logger = logging.getLogger(__name__) + + +class config(osv.osv): + _name = 'google.drive.config' + _description = "Google Drive templates config" + + def get_google_drive_url(self, cr, uid, config_id, res_id, template_id, context=None): + config = self.browse(cr, SUPERUSER_ID, config_id, context=context) + model = config.model_id + filter_name = config.filter_id and config.filter_id.name or False + record = self.pool.get(model.model).read(cr, uid, res_id, [], context=context) + record.update({'model': model.name, 'filter': filter_name}) + name_gdocs = config.name_template + try: + name_gdocs = name_gdocs % record + except: + raise osv.except_osv(_('Key Error!'), _("At least one key cannot be found in your Google Drive name pattern")) + + attach_pool = self.pool.get("ir.attachment") + attach_ids = attach_pool.search(cr, uid, [('res_model', '=', model.model), ('name', '=', name_gdocs), ('res_id', '=', res_id)]) + url = False + if attach_ids: + attachment = attach_pool.browse(cr, uid, attach_ids[0], context) + url = attachment.url + else: + url = self.copy_doc(cr, uid, res_id, template_id, name_gdocs, model.model, context) + return url + + def copy_doc(self, cr, uid, res_id, template_id, name_gdocs, res_model, context=None): + ir_config = self.pool['ir.config_parameter'] + google_drive_refresh_token = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_refresh_token') + if not google_drive_refresh_token: + raise self.pool.get('res.config.settings').get_config_warning(cr, _("You haven't configured 'Authorization Code' generated from google, Please generate and configure it in %(menu:base_setup.menu_general_configuration)s."), context=context) + google_drive_client_id = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_client_id') + google_drive_client_secret = ir_config.get_param(cr, SUPERUSER_ID, 'google_drive_client_secret') + google_web_base_url = ir_config.get_param(cr, SUPERUSER_ID, 'web.base.url') + + #For Getting New Access Token With help of old Refresh Token + headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"} + data = dict(client_id=google_drive_client_id, + refresh_token=google_drive_refresh_token, + client_secret=google_drive_client_secret, + grant_type="refresh_token") + + data = urllib.urlencode(data) + try: + req = urllib2.Request('https://accounts.google.com/o/oauth2/token', data, headers) + content = urllib2.urlopen(req).read() + except urllib2.HTTPError: + raise self.pool.get('res.config.settings').get_config_warning(cr, _("Something went wrong during the token generation. Please request again an authorization code in %(menu:base_setup.menu_general_configuration)s."), context=context) + content = json.loads(content) + + # Copy template in to drive with help of new access token + if 'access_token' in content: + request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, content['access_token']) + try: + req = urllib2.Request(request_url, None, headers) + parents = urllib2.urlopen(req).read() + except urllib2.HTTPError: + raise self.pool.get('res.config.settings').get_config_warning(cr, _("The Google Template cannot be found. Maybe it has been deleted."), context=context) + parents_dict = json.loads(parents) + + record_url = "Click on link to open Record in OpenERP\n %s/?db=%s#id=%s&model=%s" % (google_web_base_url, cr.dbname, res_id, res_model) + data = {"title": name_gdocs, "description": record_url, "parents": parents_dict['parents']} + request_url = "https://www.googleapis.com/drive/v2/files/%s/copy?access_token=%s" % (template_id, content['access_token']) + headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} + data_json = json.dumps(data) + # resp, content = Http().request(request_url, "POST", data_json, headers) + req = urllib2.Request(request_url, data_json, headers) + content = urllib2.urlopen(req).read() + content = json.loads(content) + res = False + if 'alternateLink' in content.keys(): + attach_pool = self.pool.get("ir.attachment") + attach_vals = {'res_model': res_model, 'name': name_gdocs, 'res_id': res_id, 'type': 'url', 'url': content['alternateLink']} + attach_pool.create(cr, uid, attach_vals) + res = content['alternateLink'] + return res + + def get_google_drive_config(self, cr, uid, res_model, res_id, context=None): + ''' + Function called by the js, when no google doc are yet associated with a record, with the aim to create one. It + will first seek for a google.docs.config associated with the model `res_model` to find out what's the template + of google doc to copy (this is usefull if you want to start with a non-empty document, a type or a name + different than the default values). If no config is associated with the `res_model`, then a blank text document + with a default name is created. + :param res_model: the object for which the google doc is created + :param ids: the list of ids of the objects for which the google doc is created. This list is supposed to have + a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it) + :return: the config id and config name + ''' + if not res_id: + raise osv.except_osv(_('Google Drive Error!'), _("Creating google drive may only be done by one at a time.")) + # check if a model is configured with a template + config_ids = self.search(cr, uid, [('model_id', '=', res_model)], context=context) + configs = [] + for config in self.browse(cr, uid, config_ids, context=context): + if config.filter_id: + if (config.filter_id.user_id and config.filter_id.user_id.id != uid): + #Private + continue + domain = [('id', 'in', [res_id])] + eval(config.filter_id.domain) + local_context = context and context.copy() or {} + local_context.update(eval(config.filter_id.context)) + google_doc_configs = self.pool.get(config.filter_id.model_id).search(cr, uid, domain, context=local_context) + if google_doc_configs: + configs.append({'id': config.id, 'name': config.name}) + else: + configs.append({'id': config.id, 'name': config.name}) + return configs + + def _resource_get(self, cr, uid, ids, name, arg, context=None): + result = {} + for data in self.browse(cr, uid, ids, context): + mo = re.search("(key=|/d/)([A-Za-z0-9-_]+)", data.google_drive_template_url) + if mo: + result[data.id] = mo.group(2) + else: + raise osv.except_osv(_('Incorrect URL!'), _("Please enter a valid Google Document URL.")) + return result + + def _client_id_get(self, cr, uid, ids, name, arg, context=None): + result = {} + client_id = self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'google_drive_client_id') + for config_id in ids: + result[config_id] = client_id + return result + + _columns = { + 'name': fields.char('Template Name', required=True, size=1024), + 'model_id': fields.many2one('ir.model', 'Model', ondelete='set null', required=True), + 'model': fields.related('model_id', 'model', type='char', string='Model', readonly=True), + 'filter_id': fields.many2one('ir.filters', 'Filter', domain="[('model_id', '=', model)]"), + 'google_drive_template_url': fields.char('Template URL', required=True, size=1024), + 'google_drive_resource_id': fields.function(_resource_get, type="char", string='Resource Id'), + 'google_drive_client_id': fields.function(_client_id_get, type="char", string='Google Client '), + 'name_template': fields.char('Google Drive Name Pattern', size=64, help='Choose how the new google drive will be named, on google side. Eg. gdoc_%(field_name)s', required=True), + } + + def onchange_model_id(self, cr, uid, ids, model_id, context=None): + res = {} + if model_id: + model = self.pool['ir.model'].browse(cr, uid, model_id, context=context) + res['value'] = {'model': model.model} + else: + res['value'] = {'filter_id': False, 'model': False} + return res + + _defaults = { + 'name_template': 'Document %(name)s', + } + + def _check_model_id(self, cr, uid, ids, context=None): + config_id = self.browse(cr, uid, ids[0], context=context) + if config_id.filter_id and config_id.model_id.model != config_id.filter_id.model_id: + return False + return True + + _constraints = [ + (_check_model_id, 'Model of selected filter is not matching with model of current template.', ['model_id', 'filter_id']), + ] + +config() + + +class base_config_settings(osv.osv): + _inherit = "base.config.settings" + + _columns = { + 'google_drive_authorization_code': fields.char('Authorization Code', size=124), + 'google_drive_uri': fields.char('URI', readonly=True, help="The URL to generate the authorization code from Google"), + } + _defaults = { + 'google_drive_uri': lambda s, cr, uid, c: s.pool['google.service']._get_google_token_uri(cr, uid, 'drive', context=c), + } + + def set_google_authorization_code(self, cr, uid, ids, context=None): + config = self.browse(cr, uid, ids[0], context) + refresh_token = self.pool['google.service'].generate_refresh_token(cr, uid, 'drive', config.google_drive_authorization_code, context=context) + self.pool['ir.config_parameter'].set_param(cr, uid, 'google_drive_refresh_token', refresh_token) diff --git a/addons/google_drive/google_drive_data.xml b/addons/google_drive/google_drive_data.xml new file mode 100644 index 00000000000..c9266547969 --- /dev/null +++ b/addons/google_drive/google_drive_data.xml @@ -0,0 +1,16 @@ + + + + + + google_drive_client_id + 39623646228-eg3ggo3mk6o40m7rguobi3rkl9frh4tb.apps.googleusercontent.com + + + + google_drive_client_secret + Ul-PtmnSWs3euWs20fdono0e + + + + \ No newline at end of file diff --git a/addons/google_drive/google_drive_demo.xml b/addons/google_drive/google_drive_demo.xml new file mode 100644 index 00000000000..ebe73f40ec2 --- /dev/null +++ b/addons/google_drive/google_drive_demo.xml @@ -0,0 +1,24 @@ + + + + + + + Customer + res.partner + [['customer', '=', 1]] + + + + + + Partner Review + + + https://docs.google.com/spreadsheet/ccc?key=0Ah2qnrLAoZmUdGRvdVdmS1VoSDctWk1kd18taGZ4ckE#gid=0 + Partner Review %(name)s + + + + + diff --git a/addons/google_docs/i18n/ar.po b/addons/google_drive/i18n/ar.po similarity index 100% rename from addons/google_docs/i18n/ar.po rename to addons/google_drive/i18n/ar.po diff --git a/addons/google_docs/i18n/cs.po b/addons/google_drive/i18n/cs.po similarity index 100% rename from addons/google_docs/i18n/cs.po rename to addons/google_drive/i18n/cs.po diff --git a/addons/google_docs/i18n/de.po b/addons/google_drive/i18n/de.po similarity index 100% rename from addons/google_docs/i18n/de.po rename to addons/google_drive/i18n/de.po diff --git a/addons/google_docs/i18n/es.po b/addons/google_drive/i18n/es.po similarity index 100% rename from addons/google_docs/i18n/es.po rename to addons/google_drive/i18n/es.po diff --git a/addons/google_docs/i18n/fr.po b/addons/google_drive/i18n/fr.po similarity index 100% rename from addons/google_docs/i18n/fr.po rename to addons/google_drive/i18n/fr.po diff --git a/addons/google_drive/i18n/google_drive.pot b/addons/google_drive/i18n/google_drive.pot new file mode 100644 index 00000000000..4472e9c0ed1 --- /dev/null +++ b/addons/google_drive/i18n/google_drive.pot @@ -0,0 +1,227 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * google_drive +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 8.0alpha1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-06-27 16:03+0000\n" +"PO-Revision-Date: 2013-06-27 16:03+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: google_drive +#: model:ir.ui.menu,name:google_drive.menu_google_drive_config +msgid "Google Drive configuration" +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:48 +#, python-format +msgid "Key Error!" +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "The name of the attached document can use fixed or variable data. To distinguish between documents in\n" +" Google Drive, use fixed words and fields. For instance, in the example above, if you wrote Agrolait_%(name)s_Sales\n" +" in the Google Drive name field, the document in your Google Drive and in OpenERP attachment will be named\n" +" 'Agrolait_SO0001_Sales'." +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "- If filter is not specified, link of google document will appear in \"More\" option for all users for all opportunities." +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "To create a new filter:" +msgstr "" + +#. module: google_drive +#: model:ir.model,name:google_drive.model_base_config_settings +msgid "base.config.settings" +msgstr "" + +#. module: google_drive +#: model:ir.actions.act_window,help:google_drive.action_google_drive_users_config +msgid "

\n" +" Click to add a new template.\n" +"

\n" +"

\n" +" Link your own google drive templates to any record of OpenERP. If you have really specific documents you want your collaborator fill in, e.g. Use a spreadsheet to control the quality of your product or review the delivery checklist for each order in a foreign country, ... Its very easy to manage them, link them to OpenERP and use them to collaborate with your employees.\n" +"

\n" +" " +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:150 +#, python-format +msgid "Incorrect URL!" +msgstr "" + +#. module: google_drive +#: view:base.config.settings:0 +msgid "Configure your templates" +msgstr "" + +#. module: google_drive +#: help:google.drive.config,name_template:0 +msgid "Choose how the new google drive will be named, on google side. Eg. gdoc_%(field_name)s" +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "- Go to the OpenERP document you want to filter. For instance, go to Opportunities and search on Sales Department." +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "- In this \"Search\" view, select the option \"Save Current Filter\", enter the name (Ex: Sales Department)" +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "- If you select \"Share with all users\", link of google document in \"More\" options will appear for all users in opportunities of Sales Department." +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "- If you don't select \"Share with all users\", link of google document in \"More\" options will not appear for other users in opportunities of Sales Department." +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:48 +#, python-format +msgid "At least one key cannot be found in your Google Drive name pattern" +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:150 +#, python-format +msgid "Please enter a valid Google Document URL." +msgstr "" + +#. module: google_drive +#: field:google.drive.config,google_drive_client_id:0 +msgid "Google Client " +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "https://docs.google.com/document/d/1vOtpJK9scIQz6taD9tJRIETWbEw3fSiaQHArsJYcua4/edit" +msgstr "" + +#. module: google_drive +#: field:google.drive.config,filter_id:0 +msgid "Filter" +msgstr "" + +#. module: google_drive +#: field:google.drive.config,name_template:0 +msgid "Google Drive Name Pattern" +msgstr "" + +#. module: google_drive +#: help:base.config.settings,google_drive_uri:0 +msgid "The URL to generate the authorization code from Google" +msgstr "" + +#. module: google_drive +#: model:ir.filters,name:google_drive.filter_partner +msgid "Customer" +msgstr "" + +#. module: google_drive +#: field:google.drive.config,google_drive_resource_id:0 +msgid "Resource Id" +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:91 +#, python-format +msgid "The Google Template cannot be found. Maybe it has been deleted." +msgstr "" + +#. module: google_drive +#: model:ir.actions.act_window,name:google_drive.action_google_drive_users_config +#: model:ir.ui.menu,name:google_drive.menu_google_drive_model_config +msgid "Google Drive Templates" +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:81 +#, python-format +msgid "Something went wrong during the token generation. Please request again an authorization code in %(menu:base_setup.menu_general_configuration)s." +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:124 +#, python-format +msgid "Google Drive Error!" +msgstr "" + +#. module: google_drive +#: field:base.config.settings,google_drive_uri:0 +msgid "URI" +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:124 +#, python-format +msgid "Creating google drive may only be done by one at a time." +msgstr "" + +#. module: google_drive +#: field:google.drive.config,model:0 +#: field:google.drive.config,model_id:0 +msgid "Model" +msgstr "" + +#. module: google_drive +#: view:google.drive.config:0 +msgid "Google Drive Configuration" +msgstr "" + +#. module: google_drive +#: field:google.drive.config,name:0 +msgid "Template Name" +msgstr "" + +#. module: google_drive +#: constraint:google.drive.config:0 +msgid "Model of selected filter is not matching with model of current template." +msgstr "" + +#. module: google_drive +#: field:google.drive.config,google_drive_template_url:0 +msgid "Template URL" +msgstr "" + +#. module: google_drive +#: view:base.config.settings:0 +msgid "and paste it here" +msgstr "" + +#. module: google_drive +#: field:base.config.settings,google_drive_authorization_code:0 +msgid "Authorization Code" +msgstr "" + +#. module: google_drive +#: model:ir.model,name:google_drive.model_google_drive_config +msgid "Google Drive templates config" +msgstr "" + +#. module: google_drive +#: code:addons/google_drive/google_drive.py:64 +#, python-format +msgid "You haven't configured 'Authorization Code' generated from google, Please generate and configure it in %(menu:base_setup.menu_general_configuration)s." +msgstr "" + diff --git a/addons/google_docs/i18n/hr.po b/addons/google_drive/i18n/hr.po similarity index 100% rename from addons/google_docs/i18n/hr.po rename to addons/google_drive/i18n/hr.po diff --git a/addons/google_docs/i18n/hu.po b/addons/google_drive/i18n/hu.po similarity index 100% rename from addons/google_docs/i18n/hu.po rename to addons/google_drive/i18n/hu.po diff --git a/addons/google_docs/i18n/it.po b/addons/google_drive/i18n/it.po similarity index 100% rename from addons/google_docs/i18n/it.po rename to addons/google_drive/i18n/it.po diff --git a/addons/google_docs/i18n/ln.po b/addons/google_drive/i18n/ln.po similarity index 100% rename from addons/google_docs/i18n/ln.po rename to addons/google_drive/i18n/ln.po diff --git a/addons/google_docs/i18n/mk.po b/addons/google_drive/i18n/mk.po similarity index 100% rename from addons/google_docs/i18n/mk.po rename to addons/google_drive/i18n/mk.po diff --git a/addons/google_docs/i18n/mn.po b/addons/google_drive/i18n/mn.po similarity index 100% rename from addons/google_docs/i18n/mn.po rename to addons/google_drive/i18n/mn.po diff --git a/addons/google_docs/i18n/nl.po b/addons/google_drive/i18n/nl.po similarity index 100% rename from addons/google_docs/i18n/nl.po rename to addons/google_drive/i18n/nl.po diff --git a/addons/google_docs/i18n/pl.po b/addons/google_drive/i18n/pl.po similarity index 100% rename from addons/google_docs/i18n/pl.po rename to addons/google_drive/i18n/pl.po diff --git a/addons/google_docs/i18n/pt.po b/addons/google_drive/i18n/pt.po similarity index 100% rename from addons/google_docs/i18n/pt.po rename to addons/google_drive/i18n/pt.po diff --git a/addons/google_docs/i18n/pt_BR.po b/addons/google_drive/i18n/pt_BR.po similarity index 100% rename from addons/google_docs/i18n/pt_BR.po rename to addons/google_drive/i18n/pt_BR.po diff --git a/addons/google_docs/i18n/ro.po b/addons/google_drive/i18n/ro.po similarity index 100% rename from addons/google_docs/i18n/ro.po rename to addons/google_drive/i18n/ro.po diff --git a/addons/google_docs/i18n/ru.po b/addons/google_drive/i18n/ru.po similarity index 100% rename from addons/google_docs/i18n/ru.po rename to addons/google_drive/i18n/ru.po diff --git a/addons/google_docs/i18n/sl.po b/addons/google_drive/i18n/sl.po similarity index 100% rename from addons/google_docs/i18n/sl.po rename to addons/google_drive/i18n/sl.po diff --git a/addons/google_docs/i18n/sv.po b/addons/google_drive/i18n/sv.po similarity index 100% rename from addons/google_docs/i18n/sv.po rename to addons/google_drive/i18n/sv.po diff --git a/addons/google_docs/i18n/tr.po b/addons/google_drive/i18n/tr.po similarity index 100% rename from addons/google_docs/i18n/tr.po rename to addons/google_drive/i18n/tr.po diff --git a/addons/google_docs/i18n/zh_CN.po b/addons/google_drive/i18n/zh_CN.po similarity index 100% rename from addons/google_docs/i18n/zh_CN.po rename to addons/google_drive/i18n/zh_CN.po diff --git a/addons/google_drive/res_config_user_view.xml b/addons/google_drive/res_config_user_view.xml new file mode 100644 index 00000000000..728ef03c5dd --- /dev/null +++ b/addons/google_drive/res_config_user_view.xml @@ -0,0 +1,93 @@ + + + + + + + + google_drive.config.tree + google.drive.config + + + + + + + + + + google_drive.config.form + google.drive.config + +
+ + + + + + +
+
+ + + Google Drive Templates + google.drive.config + ir.actions.act_window + form + + +

+ Click to add a new template. +

+

+ Link your own google drive templates to any record of OpenERP. If you have really specific documents you want your collaborator fill in, e.g. Use a spreadsheet to control the quality of your product or review the delivery checklist for each order in a foreign country, ... Its very easy to manage them, link them to OpenERP and use them to collaborate with your employees. +

+
+
+ + + General Settings + base.config.settings + + + +
+
+ + and paste it here + +
+
+
+
+
+ + + +
+
diff --git a/addons/google_drive/security/ir.model.access.csv b/addons/google_drive/security/ir.model.access.csv new file mode 100644 index 00000000000..ae98cb36976 --- /dev/null +++ b/addons/google_drive/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_google_drive_all,google.drive.config,model_google_drive_config,,1,0,0,0 +access_google_drive,google.drive.config,model_google_drive_config,base.group_system,1,1,1,1 \ No newline at end of file diff --git a/addons/google_drive/static/lib/gapi/client.js b/addons/google_drive/static/lib/gapi/client.js new file mode 100644 index 00000000000..7f40ec55ef9 --- /dev/null +++ b/addons/google_drive/static/lib/gapi/client.js @@ -0,0 +1,7 @@ +var gapi=window.gapi=window.gapi||{};gapi._bs=new Date().getTime();(function(){var f=null,g=encodeURIComponent,k=window,m=decodeURIComponent,n="push",r="test",t="shift",u="replace",y="length",B="split",C="join";var D=k,E=document,aa=D.location,ba=function(){},ca=/\[native code\]/,G=function(a,b,c){return a[b]=a[b]||c},da=function(a){for(var b=0;bda.call(b,e)&&c[n](e)}return c},sa=function(a){"loading"!=E.readyState?Z(a):E.write("<"+X+' src="'+encodeURI(a)+'">")},Z=function(a){var b=E.createElement(X);b.setAttribute("src",a);b.async="true";(a=E.getElementsByTagName(X)[0])?a.parentNode.insertBefore(b,a):(E.head||E.body||E.documentElement).appendChild(b)},ta=function(a,b){var c=b&&b._c;if(c)for(var d=0;d', + config_id: res.id, + res_id: res_id, + res_model: view.dataset.model, + callback: self.on_google_doc, + classname: 'oe_share_gdoc' + }, + ]); + }) + } + }); + }); + } + }, + + fetch: function (model, fields, domain, ctx) { + return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all() + }, + + on_google_doc: function (doc_item) { + var self = this; + self.config = doc_item; + var loaded = self.fetch('google.drive.config', ['google_drive_resource_id', 'google_drive_client_id'], [['id', '=', doc_item.config_id]]) + .then(function (configs) { + var ds = new instance.web.DataSet(self, 'google.drive.config'); + ds.call('get_google_drive_url', [doc_item.config_id, doc_item.res_id,configs[0].google_drive_resource_id]).done(function (url) { + if (url){ + window.open(url, '_blank'); + } + }); + }); + }, + + }); +}; \ No newline at end of file diff --git a/addons/google_docs/static/src/xml/gdocs.xml b/addons/google_drive/static/src/xml/gdocs.xml similarity index 100% rename from addons/google_docs/static/src/xml/gdocs.xml rename to addons/google_drive/static/src/xml/gdocs.xml diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index 19a6d70839f..eb1bbe451b4 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -348,12 +348,10 @@

- + - - diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py index 8d707baa50e..5d5be0eec97 100644 --- a/addons/hr_expense/hr_expense.py +++ b/addons/hr_expense/hr_expense.py @@ -65,9 +65,9 @@ class hr_expense_expense(osv.osv): _order = "id desc" _track = { 'state': { - 'hr_expense.mt_expense_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'accepted', - 'hr_expense.mt_expense_refused': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancelled', - 'hr_expense.mt_expense_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirm', + 'hr_expense.mt_expense_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'accepted', + 'hr_expense.mt_expense_refused': lambda self, cr, uid, obj, ctx=None: obj.state == 'cancelled', + 'hr_expense.mt_expense_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirm', }, } diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 0eb3c0cbf16..f2be4844d27 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -110,9 +110,9 @@ class hr_holidays(osv.osv): _inherit = ['mail.thread', 'ir.needaction_mixin'] _track = { 'state': { - 'hr_holidays.mt_holidays_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'validate', - 'hr_holidays.mt_holidays_refused': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'refuse', - 'hr_holidays.mt_holidays_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirm', + 'hr_holidays.mt_holidays_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'validate', + 'hr_holidays.mt_holidays_refused': lambda self, cr, uid, obj, ctx=None: obj.state == 'refuse', + 'hr_holidays.mt_holidays_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirm', }, } diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index e26c96d41d9..d8c00c40a31 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -94,11 +94,11 @@ class hr_applicant(base_stage, osv.Model): _inherit = ['mail.thread', 'ir.needaction_mixin'] _track = { 'state': { - 'hr_recruitment.mt_applicant_hired': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', - 'hr_recruitment.mt_applicant_refused': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'cancel', + 'hr_recruitment.mt_applicant_hired': lambda self, cr, uid, obj, ctx=None: obj.state == 'done', + 'hr_recruitment.mt_applicant_refused': lambda self, cr, uid, obj, ctx=None: obj.state == 'cancel', }, 'stage_id': { - 'hr_recruitment.mt_stage_changed': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['done', 'cancel'], + 'hr_recruitment.mt_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.state not in ['done', 'cancel'], }, } @@ -506,33 +506,18 @@ class hr_job(osv.osv): help="Email alias for this job position. New emails will automatically " "create new applicants for this job position."), } - _defaults = { - 'alias_domain': False, # always hide alias during creation - } def _auto_init(self, cr, context=None): """Installation hook to create aliases for all jobs and avoid constraint errors.""" - if context is None: - context = {} - alias_context = dict(context, alias_model_name='hr.applicant') - res = self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job, self)._auto_init, - self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=alias_context) - return res + return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job, self)._auto_init, + 'hr.applicant', self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=context) def create(self, cr, uid, vals, context=None): - mail_alias = self.pool.get('mail.alias') - if not vals.get('alias_id'): - vals.pop('alias_name', None) # prevent errors during copy() - alias_id = mail_alias.create_unique_alias(cr, uid, - # Using '+' allows using subaddressing for those who don't - # have a catchall domain setup. - {'alias_name': 'jobs+'+vals['name']}, - model_name="hr.applicant", - context=context) - vals['alias_id'] = alias_id - res = super(hr_job, self).create(cr, uid, vals, context) - mail_alias.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context) - return res + alias_context = dict(context, alias_model_name='hr.applicant', alias_parent_model_name=self._name) + job_id = super(hr_job, self).create(cr, uid, vals, context=alias_context) + job = self.browse(cr, uid, job_id, context=context) + self.pool.get('mail.alias').write(cr, uid, [job.alias_id.id], {'alias_parent_thread_id': job_id, "alias_defaults": {'job_id': job_id}}, context) + return job_id def unlink(self, cr, uid, ids, context=None): # Cascade-delete mail aliases as well, as they should not exist without the job position. @@ -550,15 +535,16 @@ class hr_job(osv.osv): if record.survey_id: datas['ids'] = [record.survey_id.id] datas['model'] = 'survey.print' - context.update({'response_id': [0], 'response_no': 0,}) + context.update({'response_id': [0], 'response_no': 0}) return { 'type': 'ir.actions.report.xml', 'report_name': 'survey.form', 'datas': datas, - 'context' : context, - 'nodestroy':True, + 'context': context, + 'nodestroy': True, } + class applicant_category(osv.osv): """ Category of applicant """ _name = "hr.applicant_category" diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index f6fb202252c..9cbfd4560e7 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -316,18 +316,19 @@ attrs="{'invisible':[('survey_id','=',False)]}"/>
- -
+ -
+
diff --git a/addons/idea/__init__.py b/addons/idea/__init__.py index e6f7333e055..2b8966f9e7a 100644 --- a/addons/idea/__init__.py +++ b/addons/idea/__init__.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# Copyright (C) 2004-Today OpenERP S.A. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -19,5 +19,4 @@ # ############################################################################## -import idea - +import models diff --git a/addons/idea/__openerp__.py b/addons/idea/__openerp__.py index 8f32e1e91c4..7d8560a2cf4 100644 --- a/addons/idea/__openerp__.py +++ b/addons/idea/__openerp__.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# Copyright (C) 2004-Today OpenERP S.A. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -22,29 +22,39 @@ { 'name': 'Ideas', - 'version': '0.1', + 'summary': 'Share and Discuss your Ideas', + 'version': '1.0', 'category': 'Tools', 'description': """ -This module allows user to easily and efficiently participate in enterprise innovation. -======================================================================================= +Share your ideas and participate in enterprise innovation +========================================================= -It allows everybody to express ideas about different subjects. -Then, other users can comment on these ideas and vote for particular ideas. -Each idea has a score based on the different votes. +The Ideas module give users a way to express and discuss ideas, allowing everybody +to participate in enterprise innovation. Every user can suggest, comment ideas. The managers can obtain an easy view of best ideas from all the users. Once installed, check the menu 'Ideas' in the 'Tools' main menu.""", 'author': 'OpenERP SA', - 'website': 'http://openerp.com', + 'website': 'http://www.openerp.com', 'depends': ['mail'], 'data': [ - 'security/idea_security.xml', + 'security/idea.xml', 'security/ir.model.access.csv', - 'idea_view.xml', - 'idea_workflow.xml', + 'views/idea.xml', + 'views/category.xml', + 'data/idea.xml', + 'data/idea_workflow.xml', + ], + 'demo': [ + 'demo/idea.xml', ], - 'demo': ['idea_data.xml'], - 'test':[], 'installable': True, + 'application': True, 'images': [], + 'css': [ + 'static/src/css/idea_idea.css', + ], + 'js': [], + 'qweb': [], } + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/idea/data/idea.xml b/addons/idea/data/idea.xml new file mode 100644 index 00000000000..c057868d6de --- /dev/null +++ b/addons/idea/data/idea.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/addons/idea/data/idea_workflow.xml b/addons/idea/data/idea_workflow.xml new file mode 100644 index 00000000000..b460c73a6ed --- /dev/null +++ b/addons/idea/data/idea_workflow.xml @@ -0,0 +1,57 @@ + + + + + idea.wkf + idea.idea + True + + + + + True + normal + function + idea_set_normal_priority() + + + + + low + function + idea_set_low_priority() + + + + + high + function + idea_set_high_priority() + + + + + + idea_set_low_priority + + + + + + idea_set_normal_priority + + + + + + idea_set_high_priority + + + + + + idea_set_normal_priority + + + + diff --git a/addons/idea/demo/idea.xml b/addons/idea/demo/idea.xml new file mode 100644 index 00000000000..2b4ff58b973 --- /dev/null +++ b/addons/idea/demo/idea.xml @@ -0,0 +1,68 @@ + + + + + + Sales + + + Organization + + + Technical + + + + Docking station along with tablet PC + When you sell a tablet PC, maybe we could propose a docking station with it. I offer 20% on the docking stating (not the tablet). + + + + + + Communicate using emails + I start communicating with prospects more by email than phonecalls. I send an email to create a sense of emergency, like "can I call you this week about our quote?" and I call only those that answer this email. + + open + + + + + + Use a two-stages testing phase + We should perform testing using two levels of validation. + + open + + + + + + Write some functional documentation about procurements + We receive many questions about OpenChatter. Maybe some functional doc could save us some time. + + open + + + + + Better management of smtp errors + There should be away to store the reason why some emails are not sent. + + close + + + + + + Kitten mode enabled by default + As this is the most loved feature, the kitten mode should be enabled by default. And maybe even impossible to remove. + + cancel + + + + + + + diff --git a/addons/idea/idea.py b/addons/idea/idea.py deleted file mode 100644 index d1e250ce9a5..00000000000 --- a/addons/idea/idea.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp.osv import osv -from openerp.osv import fields -from openerp.tools.translate import _ -import time - -VoteValues = [('-1', 'Not Voted'), ('0', 'Very Bad'), ('25', 'Bad'), \ - ('50', 'Normal'), ('75', 'Good'), ('100', 'Very Good') ] -DefaultVoteValue = '50' - -class idea_category(osv.osv): - """ Category of Idea """ - _name = "idea.category" - _description = "Idea Category" - _columns = { - 'name': fields.char('Category Name', size=64, required=True), - } - _sql_constraints = [ - ('name', 'unique(name)', 'The name of the category must be unique') - ] - _order = 'name asc' - - -class idea_idea(osv.osv): - """ Idea """ - _name = 'idea.idea' - _inherit = ['mail.thread'] - _columns = { - 'create_uid': fields.many2one('res.users', 'Creator', required=True, readonly=True), - 'name': fields.char('Idea Summary', size=64, required=True, readonly=True, oldname='title', states={'draft': [('readonly', False)]}), - 'description': fields.text('Description', help='Content of the idea', readonly=True, states={'draft': [('readonly', False)]}), - 'category_ids': fields.many2many('idea.category', string='Tags', readonly=True, states={'draft': [('readonly', False)]}), - 'state': fields.selection([('draft', 'New'), - ('open', 'Accepted'), - ('cancel', 'Refused'), - ('close', 'Done')], - 'Status', readonly=True, track_visibility='onchange', - ) - } - _sql_constraints = [ - ('name', 'unique(name)', 'The name of the idea must be unique') - ] - _defaults = { - 'state': lambda *a: 'draft', - } - _order = 'name asc' - - def idea_cancel(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) - - def idea_open(self, cr, uid, ids, context={}): - return self.write(cr, uid, ids, {'state': 'open'}, context=context) - - def idea_close(self, cr, uid, ids, context={}): - return self.write(cr, uid, ids, {'state': 'close'}, context=context) - - def idea_draft(self, cr, uid, ids, context={}): - return self.write(cr, uid, ids, {'state': 'draft'}, context=context) diff --git a/addons/idea/idea_data.xml b/addons/idea/idea_data.xml deleted file mode 100644 index 421cb6a9eef..00000000000 --- a/addons/idea/idea_data.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Sales - - - Organization - - - Technical - - - - - - - - - diff --git a/addons/idea/idea_view.xml b/addons/idea/idea_view.xml deleted file mode 100644 index 53734d53919..00000000000 --- a/addons/idea/idea_view.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - idea.category.search - idea.category - - - - - - - - - - idea.category.form - idea.category - -
- - - -
-
-
- - - - idea.category.tree - idea.category - - - - - - - - - - - - Categories - idea.category - form - tree,form - - - - - - - - - - - - - - idea.idea.form - idea.idea - -
-
-
- - -
- - -
-
-
-
- - - - - idea.idea.tree - idea.idea - - - - - - - - - - - - - idea.idea.search - idea.idea - - - - - - - - - - - - - - - - - - Ideas - idea.idea - form - tree,form - - - - - -
-
diff --git a/addons/idea/idea_workflow.xml b/addons/idea/idea_workflow.xml deleted file mode 100644 index 649541ae349..00000000000 --- a/addons/idea/idea_workflow.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - idea.wkf - idea.idea - True - - - - - True - draft - function - idea_draft() - - - - - open - function - idea_open() - - - - - close - function - idea_close() - True - - - - - cancel - function - idea_cancel() - True - - - - - - idea_open - - - - - - idea_close - - - - - - idea_cancel - - - - diff --git a/addons/idea/models/__init__.py b/addons/idea/models/__init__.py new file mode 100644 index 00000000000..e8a48771771 --- /dev/null +++ b/addons/idea/models/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2013-TODAY OpenERP S.A. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import idea diff --git a/addons/idea/models/idea.py b/addons/idea/models/idea.py new file mode 100644 index 00000000000..61934cfc3b2 --- /dev/null +++ b/addons/idea/models/idea.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-Today OpenERP S.A. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import osv +from openerp.osv import fields + + +class IdeaCategory(osv.Model): + """ Category of Idea """ + _name = "idea.category" + _description = "Idea Category" + + _order = 'name asc' + + _columns = { + 'name': fields.char('Category Name', size=64, required=True), + } + + _sql_constraints = [ + ('name', 'unique(name)', 'The name of the category must be unique') + ] + + +class IdeaIdea(osv.Model): + """ Model of an Idea """ + _name = 'idea.idea' + _description = 'Propose and Share your Ideas' + + _rec_name = 'name' + _order = 'name asc' + + def _get_state_list(self, cr, uid, context=None): + return [('draft', 'New'), + ('open', 'In discussion'), + ('close', 'Accepted'), + ('cancel', 'Refused')] + + def _get_color(self, cr, uid, ids, fields, args, context=None): + res = dict.fromkeys(ids, 3) + for idea in self.browse(cr, uid, ids, context=context): + if idea.priority == 'low': + res[idea.id] = 0 + elif idea.priority == 'high': + res[idea.id] = 7 + return res + + _columns = { + 'user_id': fields.many2one('res.users', 'Responsible', required=True), + 'name': fields.char('Summary', required=True, readonly=True, + states={'draft': [('readonly', False)]}, + oldname='title'), + 'description': fields.text('Description', required=True, + states={'draft': [('readonly', False)]}, + help='Content of the idea'), + 'category_ids': fields.many2many('idea.category', string='Tags'), + 'state': fields.selection(_get_state_list, string='Status', required=True), + 'priority': fields.selection([('low', 'Low'), ('normal', 'Normal'), ('high', 'High')], + string='Priority', required=True), + 'color': fields.function(_get_color, type='integer', string='Color Index'), + } + + _sql_constraints = [ + ('name', 'unique(name)', 'The name of the idea must be unique') + ] + + _defaults = { + 'user_id': lambda self, cr, uid, ctx=None: uid, + 'state': lambda self, cr, uid, ctx=None: self._get_state_list(cr, uid, ctx)[0][0], + 'priority': 'normal', + } + + #------------------------------------------------------ + # Technical stuff + #------------------------------------------------------ + + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): + """ Override read_group to always display all states. """ + if groupby and groupby[0] == "state": + # Default result structure + states = self._get_state_list(cr, uid, context=context) + read_group_all_states = [{ + '__context': {'group_by': groupby[1:]}, + '__domain': domain + [('state', '=', state_value)], + 'state': state_value, + 'state_count': 0, + } for state_value, state_name in states] + # Get standard results + read_group_res = super(IdeaIdea, self).read_group(cr, uid, domain, fields, groupby, offset, limit, context, orderby) + # Update standard results with default results + result = [] + for state_value, state_name in states: + res = filter(lambda x: x['state'] == state_value, read_group_res) + if not res: + res = filter(lambda x: x['state'] == state_value, read_group_all_states) + res[0]['state'] = [state_value, state_name] + result.append(res[0]) + return result + else: + return super(IdeaIdea, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + + #------------------------------------------------------ + # Workflow / Actions + #------------------------------------------------------ + + def idea_set_low_priority(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'priority': 'low'}, context=context) + + def idea_set_normal_priority(self, cr, uid, ids, context={}): + return self.write(cr, uid, ids, {'priority': 'normal'}, context=context) + + def idea_set_high_priority(self, cr, uid, ids, context={}): + return self.write(cr, uid, ids, {'priority': 'high'}, context=context) diff --git a/addons/idea/security/idea.xml b/addons/idea/security/idea.xml new file mode 100644 index 00000000000..e7a0d800187 --- /dev/null +++ b/addons/idea/security/idea.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/idea/security/idea_security.xml b/addons/idea/security/idea_security.xml deleted file mode 100644 index 8743252c9e5..00000000000 --- a/addons/idea/security/idea_security.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - User - - - - - - diff --git a/addons/idea/security/ir.model.access.csv b/addons/idea/security/ir.model.access.csv index 78b8deeaf57..761ada8132c 100644 --- a/addons/idea/security/ir.model.access.csv +++ b/addons/idea/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_idea_category_user,idea.category user,model_idea_category,base.group_tool_user,1,1,1,1 -access_idea_idea_user,idea.idea user,model_idea_idea,base.group_tool_user,1,1,1,1 +access_idea_category_user,idea.category.user,model_idea_category,base.group_user,1,1,1,1 +access_idea_idea_user,idea.idea.user,model_idea_idea,base.group_user,1,1,1,1 diff --git a/addons/idea/static/src/css/idea_idea.css b/addons/idea/static/src/css/idea_idea.css new file mode 100644 index 00000000000..08daffd902a --- /dev/null +++ b/addons/idea/static/src/css/idea_idea.css @@ -0,0 +1,21 @@ +.openerp .oe_kanban_view .oe_kanban_idea_idea { + width: 200px; +} + +.openerp .oe_kanban_view .oe_kanban_idea_idea .oe_avatars { + text-align: right; + margin: -5px 0 -10px 0; +} + +.openerp .oe_kanban_view .oe_kanban_idea_idea .oe_avatars img { + width: 30px; + height: 30px; + padding-left: 0px; + margin-top: 3px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} diff --git a/addons/idea/static/src/img/icon.png b/addons/idea/static/src/img/icon.png new file mode 100644 index 00000000000..1f0cba223bb Binary files /dev/null and b/addons/idea/static/src/img/icon.png differ diff --git a/addons/google_base_account/wizard/__init__.py b/addons/idea/tests/__init__.py similarity index 82% rename from addons/google_base_account/wizard/__init__.py rename to addons/idea/tests/__init__.py index a6b9c644a1e..a53567a380b 100644 --- a/addons/google_base_account/wizard/__init__.py +++ b/addons/idea/tests/__init__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# +# OpenERP, Open Source Business Applications +# Copyright (c) 2013-TODAY OpenERP S.A. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -15,11 +15,14 @@ # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . +# along with this program. If not, see . # ############################################################################## -import google_login +from openerp.addons.idea.tests import test_idea + +checks = [ + test_idea, +] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/portal_event/event.py b/addons/idea/tests/test_idea.py similarity index 54% rename from addons/portal_event/event.py rename to addons/idea/tests/test_idea.py index 45026da4eba..4ca9bb9dcb9 100644 --- a/addons/portal_event/event.py +++ b/addons/idea/tests/test_idea.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# OpenERP, Open Source Business Applications +# Copyright (c) 2013-TODAY OpenERP S.A. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -19,21 +19,21 @@ # ############################################################################## -from openerp.osv import fields, osv +from openerp.tests import common -class event_event(osv.osv): - _description = 'Portal event' - _inherit = 'event.event' - """ - ``visibility``: defines if the event appears on the portal's event page - - 'public' means the event will appear for everyone (anonymous) - - 'private' means the event won't appear - """ - _columns = { - 'visibility': fields.selection([('public', 'Public'),('private', 'Private')], - string='Visibility', help='Event\'s visibility in the portal\'s contact page'), - } - _defaults = { - 'visibility': 'private', - } +class TestIdeaBase(common.TransactionCase): + + def setUp(self): + super(TestIdeaBase, self).setUp() + cr, uid = self.cr, self.uid + + # Usefull models + self.idea_category = self.registry('idea.category') + self.idea_idea = self.registry('idea.idea') + + def tearDown(self): + super(TestIdeaBase, self).tearDown() + + def test_OO(self): + pass diff --git a/addons/idea/views/category.xml b/addons/idea/views/category.xml new file mode 100644 index 00000000000..79725fad73f --- /dev/null +++ b/addons/idea/views/category.xml @@ -0,0 +1,56 @@ + + + + + + + + idea.category.search + idea.category + + + + + + + + + idea.category.form + idea.category + +
+ + + +
+
+
+ + + idea.category.tree + idea.category + + + + + + + + + + Categories + idea.category + form + tree,form + + + + + + + +
+
diff --git a/addons/idea/views/idea.xml b/addons/idea/views/idea.xml new file mode 100644 index 00000000000..b228af2da45 --- /dev/null +++ b/addons/idea/views/idea.xml @@ -0,0 +1,118 @@ + + + + + + idea.idea.kanban + idea.idea + + + + + + +
+
+ í + +
+
+

+
+ +
+ +
+
+
+
+
+
+
+
+ + + idea.idea.form + idea.idea + +
+
+
+ + +
+
+
+ + + idea.idea.tree + idea.idea + + + + + + + + + + + + idea.idea.search + idea.idea + + + + + + + + + + + + + + + + + + + Ideas + idea.idea + form + kanban,tree,form + + + + + + + +
+
diff --git a/addons/l10n_in_hr_payroll/i18n/zh_CN.po b/addons/l10n_in_hr_payroll/i18n/zh_CN.po new file mode 100644 index 00000000000..8ee2c7f2028 --- /dev/null +++ b/addons/l10n_in_hr_payroll/i18n/zh_CN.po @@ -0,0 +1,1013 @@ +# Chinese (Simplified) translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-11-24 02:53+0000\n" +"PO-Revision-Date: 2013-06-24 10:15+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Chinese (Simplified) \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-06-25 05:14+0000\n" +"X-Generator: Launchpad (build 16677)\n" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "E-mail Address" +msgstr "E-mail地址" + +#. module: l10n_in_hr_payroll +#: field:payment.advice.report,employee_bank_no:0 +msgid "Employee Bank Account" +msgstr "员工银行帐号" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Payment Advices which are in draft state" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Title" +msgstr "职位" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "Payment Advice from" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_yearly_salary_detail +msgid "Hr Salary Employee By Category Report" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Payslips which are paid" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: view:payment.advice.report:0 +#: view:payslip.report:0 +msgid "Group By..." +msgstr "分组于..." + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Allowances with Basic:" +msgstr "基本补贴:" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Payslips which are in done state" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Department" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Deductions:" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "A/C no." +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.contract,driver_salay:0 +msgid "Driver Salary" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,name:l10n_in_hr_payroll.action_yearly_salary_detail +#: model:ir.actions.report.xml,name:l10n_in_hr_payroll.yearly_salary +#: model:ir.ui.menu,name:l10n_in_hr_payroll.menu_yearly_salary_detail +msgid "Yearly Salary by Employee" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,name:l10n_in_hr_payroll.act_hr_emp_payslip_list +msgid "Payslips" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "March" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: field:hr.payroll.advice,company_id:0 +#: field:hr.payroll.advice.line,company_id:0 +#: view:payment.advice.report:0 +#: field:payment.advice.report,company_id:0 +#: view:payslip.report:0 +#: field:payslip.report,company_id:0 +msgid "Company" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "The Manager" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Letter Details" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Set to Draft" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "to" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "Total :" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payslip.run,available_advice:0 +msgid "Made Payment Advice?" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Advices which are paid using NEFT transfer" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:payslip.report,nbr:0 +msgid "# Payslip lines" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.contract,tds:0 +msgid "Amount for Tax Deduction at Source" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_hr_payslip +msgid "Pay Slip" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +#: field:payment.advice.report,day:0 +#: view:payslip.report:0 +#: field:payslip.report,day:0 +msgid "Day" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Month of Payment Advices" +msgstr "" + +#. module: l10n_in_hr_payroll +#: constraint:hr.payslip:0 +msgid "Payslip 'Date From' must be before 'Date To'." +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,batch_id:0 +msgid "Batch" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Code" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Other Information" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:hr.payroll.advice,state:0 +#: selection:payment.advice.report,state:0 +msgid "Cancelled" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,help:l10n_in_hr_payroll.action_payslip_report_all +msgid "This report performs analysis on Payslip" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "For" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Details by Salary Rule Category:" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,number:0 +#: report:paylip.details.in:0 +msgid "Reference" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.contract,medical_insurance:0 +msgid "Medical Insurance" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Identification No" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +#: field:payslip.report,struct_id:0 +msgid "Structure" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "form period" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:hr.payroll.advice,state:0 +#: selection:payment.advice.report,state:0 +msgid "Confirmed" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +#: report:salary.employee.bymonth:0 +msgid "From" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice.line,bysal:0 +#: field:payment.advice.report,bysal:0 +#: report:payroll.advice:0 +msgid "By Salary" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: view:payment.advice.report:0 +msgid "Confirm" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,chaque_nos:0 +#: field:payment.advice.report,cheque_nos:0 +msgid "Cheque Numbers" +msgstr "" + +#. module: l10n_in_hr_payroll +#: constraint:res.company:0 +msgid "Error! You can not create recursive companies." +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,name:l10n_in_hr_payroll.action_salary_employee_month +#: model:ir.actions.report.xml,name:l10n_in_hr_payroll.hr_salary_employee_bymonth +#: model:ir.ui.menu,name:l10n_in_hr_payroll.menu_salary_employee_month +msgid "Yearly Salary by Head" +msgstr "" + +#. module: l10n_in_hr_payroll +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:134 +#, python-format +msgid "You can not confirm Payment advice without advice lines." +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "Yours Sincerely" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "# Payslip Lines" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.contract,medical_insurance:0 +msgid "Deduction towards company provided medical insurance" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_hr_payroll_advice_line +msgid "Bank Advice Lines" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Day of Payslip" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Email" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.payslip.run,available_advice:0 +msgid "" +"If this box is checked which means that Payment Advice exists for current " +"batch" +msgstr "" + +#. module: l10n_in_hr_payroll +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:108 +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:134 +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:190 +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:207 +#, python-format +msgid "Error !" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:payslip.report,paid:0 +msgid "Made Payment Order ? " +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.salary.employee.month:0 +#: view:yearly.salary.detail:0 +msgid "Print" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payslip.report,state:0 +msgid "Rejected" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Year of Payslip" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_hr_payslip_run +msgid "Payslip Batches" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice.line,debit_credit:0 +#: report:payroll.advice:0 +msgid "C/D" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.employee.bymonth:0 +msgid "Yearly Salary Details" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.report.xml,name:l10n_in_hr_payroll.payroll_advice +msgid "Print Advice" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,line_ids:0 +msgid "Employee Salary" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "July" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:res.company:0 +msgid "Configuration" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Payslip Line" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,name:l10n_in_hr_payroll.action_view_hr_bank_advice_tree +#: model:ir.ui.menu,name:l10n_in_hr_payroll.hr_menu_payment_advice +msgid "Payment Advices" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,name:l10n_in_hr_payroll.action_payment_advice_report_all +#: model:ir.ui.menu,name:l10n_in_hr_payroll.menu_reporting_payment_advice +#: view:payment.advice.report:0 +msgid "Advices Analysis" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.salary.employee.month:0 +msgid "" +"This wizard will print report which displays employees break-up of Net Head " +"for a specified dates." +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice.line,ifsc:0 +msgid "IFSC" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +#: field:payslip.report,date_to:0 +msgid "Date To" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.contract,tds:0 +msgid "TDS" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Confirm Advices" +msgstr "" + +#. module: l10n_in_hr_payroll +#: constraint:hr.contract:0 +msgid "Error! Contract start-date must be less than contract end-date." +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:res.company,dearness_allowance:0 +msgid "Dearness Allowance" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "August" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.contract:0 +msgid "Deduction" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "SI. No." +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Payment Advices which are in confirm state" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "December" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Confirm Sheet" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +#: field:payment.advice.report,month:0 +#: view:payslip.report:0 +#: field:payslip.report,month:0 +msgid "Month" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Employee Code" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.salary.employee.month:0 +#: view:yearly.salary.detail:0 +msgid "or" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_hr_salary_employee_month +msgid "Hr Salary Employee By Month Report" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.salary.employee.month,category_id:0 +#: view:payslip.report:0 +#: field:payslip.report,category_id:0 +msgid "Category" +msgstr "" + +#. module: l10n_in_hr_payroll +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:190 +#, python-format +msgid "" +"Payment advice already exists for %s, 'Set to Draft' to create a new advice." +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payslip.run:0 +msgid "To Advice" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Note" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Salary Rule Category" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: selection:hr.payroll.advice,state:0 +#: view:payment.advice.report:0 +#: selection:payment.advice.report,state:0 +#: view:payslip.report:0 +#: selection:payslip.report,state:0 +msgid "Draft" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +#: field:payslip.report,date_from:0 +msgid "Date From" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Employee Name" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_payment_advice_report +msgid "Payment Advice Analysis" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: field:hr.payroll.advice,state:0 +#: view:payment.advice.report:0 +#: field:payment.advice.report,state:0 +#: view:payslip.report:0 +#: field:payslip.report,state:0 +msgid "Status" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:res.company,dearness_allowance:0 +msgid "Check this box if your company provide Dearness Allowance to employee" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice.line,ifsc_code:0 +#: field:payment.advice.report,ifsc_code:0 +#: report:payroll.advice:0 +msgid "IFSC Code" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "June" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Paid" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.contract,voluntary_provident_fund:0 +msgid "" +"VPF is a safe option wherein you can contribute more than the PF ceiling of " +"12% that has been mandated by the government and VPF computed as " +"percentage(%)" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +#: field:payment.advice.report,nbr:0 +msgid "# Payment Lines" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.report.xml,name:l10n_in_hr_payroll.payslip_details_report +msgid "PaySlip Details" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Payment Lines" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,date:0 +#: field:payment.advice.report,date:0 +msgid "Date" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "November" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +#: view:payslip.report:0 +msgid "Extended Filters..." +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,help:l10n_in_hr_payroll.action_payment_advice_report_all +msgid "This report performs analysis on Payment Advices" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "October" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +#: report:salary.detail.byyear:0 +msgid "Designation" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Month of Payslip" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "January" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:yearly.salary.detail:0 +msgid "Pay Head Employee Breakup" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_res_company +msgid "Companies" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +#: report:payroll.advice:0 +msgid "Authorized Signature" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.model,name:l10n_in_hr_payroll.model_hr_contract +msgid "Contract" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.contract,supplementary_allowance:0 +msgid "Supplementary Allowance" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice.line:0 +msgid "Advice Lines" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "To," +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.contract,driver_salay:0 +msgid "Check this box if you provide allowance for driver" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +msgid "Payslips which are in draft state" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: field:hr.payroll.advice.line,advice_id:0 +#: field:hr.payslip,advice_id:0 +#: model:ir.model,name:l10n_in_hr_payroll.model_hr_payroll_advice +msgid "Bank Advice" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Other No." +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Draft Advices" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.payroll.advice,neft:0 +msgid "Check this box if your company use online transfer for salary" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:payment.advice.report,number:0 +#: field:payslip.report,number:0 +msgid "Number" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "September" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payslip.report:0 +#: selection:payslip.report,state:0 +msgid "Done" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: view:hr.salary.employee.month:0 +#: view:yearly.salary.detail:0 +msgid "Cancel" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Day of Payment Advices" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Search Payment advice" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:yearly.salary.detail:0 +msgid "" +"This wizard will print report which display a pay head employee breakup for " +"a specified dates." +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Pay Slip Details" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Total Salary" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice.line,employee_id:0 +#: view:payment.advice.report:0 +#: field:payment.advice.report,employee_id:0 +#: view:payslip.report:0 +#: field:payslip.report,employee_id:0 +msgid "Employee" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +msgid "Compute Advice" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "Dear Sir/Madam," +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,note:0 +msgid "Description" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "May" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:res.company:0 +msgid "Payroll" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "NEFT" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +#: report:salary.detail.byyear:0 +msgid "Address" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: field:hr.payroll.advice,bank_id:0 +#: view:payment.advice.report:0 +#: field:payment.advice.report,bank_id:0 +#: report:payroll.advice:0 +#: report:salary.detail.byyear:0 +msgid "Bank" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.salary.employee.month,end_date:0 +#: field:yearly.salary.detail,date_to:0 +msgid "End Date" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "February" +msgstr "" + +#. module: l10n_in_hr_payroll +#: sql_constraint:res.company:0 +msgid "The company name must be unique !" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payroll.advice:0 +#: field:hr.payroll.advice,name:0 +#: report:paylip.details.in:0 +#: field:payment.advice.report,name:0 +#: field:payslip.report,name:0 +#: report:salary.employee.bymonth:0 +msgid "Name" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.salary.employee.month:0 +#: field:hr.salary.employee.month,employee_ids:0 +#: view:yearly.salary.detail:0 +#: field:yearly.salary.detail,employee_ids:0 +msgid "Employees" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Bank Account" +msgstr "" + +#. module: l10n_in_hr_payroll +#: model:ir.actions.act_window,name:l10n_in_hr_payroll.action_payslip_report_all +#: model:ir.model,name:l10n_in_hr_payroll.model_payslip_report +#: model:ir.ui.menu,name:l10n_in_hr_payroll.menu_reporting_payslip +#: view:payslip.report:0 +msgid "Payslip Analysis" +msgstr "" + +#. module: l10n_in_hr_payroll +#: selection:payment.advice.report,month:0 +#: selection:payslip.report,month:0 +msgid "April" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:payroll.advice:0 +msgid "Name of the Employe" +msgstr "" + +#. module: l10n_in_hr_payroll +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:108 +#: code:addons/l10n_in_hr_payroll/l10n_in_hr_payroll.py:207 +#, python-format +msgid "Please define bank account for the %s employee" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.salary.employee.month,start_date:0 +#: field:yearly.salary.detail,date_from:0 +msgid "Start Date" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.contract:0 +msgid "Allowance" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.contract,voluntary_provident_fund:0 +msgid "Voluntary Provident Fund (%)" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.contract,house_rent_allowance_metro_nonmetro:0 +msgid "House Rent Allowance (%)" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.payroll.advice,bank_id:0 +msgid "Select the Bank from which the salary is going to be paid" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.salary.employee.month:0 +msgid "Employee Pay Head Breakup" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:salary.detail.byyear:0 +msgid "Phone No." +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +msgid "Credit" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice.line,name:0 +#: report:payroll.advice:0 +msgid "Bank Account No." +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.payroll.advice,date:0 +msgid "Advice Date is used to search Payslips" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payslip.run:0 +msgid "Payslip Batches ready to be Adviced" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:hr.payslip.run:0 +msgid "Create Advice" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +#: field:payment.advice.report,year:0 +#: view:payslip.report:0 +#: field:payslip.report,year:0 +msgid "Year" +msgstr "" + +#. module: l10n_in_hr_payroll +#: field:hr.payroll.advice,neft:0 +#: field:payment.advice.report,neft:0 +msgid "NEFT Transaction" +msgstr "" + +#. module: l10n_in_hr_payroll +#: report:paylip.details.in:0 +#: field:payslip.report,total:0 +#: report:salary.detail.byyear:0 +#: report:salary.employee.bymonth:0 +msgid "Total" +msgstr "" + +#. module: l10n_in_hr_payroll +#: help:hr.contract,house_rent_allowance_metro_nonmetro:0 +msgid "" +"HRA is an allowance given by the employer to the employee for taking care of " +"his rental or accommodation expenses for metro city it is 50 % and for non " +"metro 40%.HRA computed as percentage(%)" +msgstr "" + +#. module: l10n_in_hr_payroll +#: view:payment.advice.report:0 +msgid "Year of Payment Advices" +msgstr "" diff --git a/addons/mail/mail_alias.py b/addons/mail/mail_alias.py index 870695e074e..97d2842d15c 100644 --- a/addons/mail/mail_alias.py +++ b/addons/mail/mail_alias.py @@ -39,6 +39,7 @@ def remove_accents(input_str): nkfd_form = unicodedata.normalize('NFKD', input_str) return u''.join([c for c in nkfd_form if not unicodedata.combining(c)]) + class mail_alias(osv.Model): """A Mail Alias is a mapping of an email address with a given OpenERP Document model. It is used by OpenERP's mail gateway when processing incoming emails @@ -47,7 +48,7 @@ class mail_alias(osv.Model): of that alias. If the message is a reply it will be attached to the existing discussion on the corresponding record, otherwise a new record of the corresponding model will be created. - + This is meant to be used in combination with a catch-all email configuration on the company's mail server, so that as soon as a new mail.alias is created, it becomes immediately usable and OpenERP will accept email for it. @@ -63,9 +64,8 @@ class mail_alias(osv.Model): return dict.fromkeys(ids, domain or "") _columns = { - 'alias_name': fields.char('Alias', required=True, - help="The name of the email alias, e.g. 'jobs' " - "if you want to catch emails for ",), + 'alias_name': fields.char('Alias', + help="The name of the email alias, e.g. 'jobs' if you want to catch emails for ",), 'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade", help="The model (OpenERP Document Kind) to which this alias " "corresponds. Any incoming email that does not reply to an " @@ -87,13 +87,29 @@ class mail_alias(osv.Model): "messages will be attached, even if they did not reply to it. " "If set, this will disable the creation of new records completely."), 'alias_domain': fields.function(_get_alias_domain, string="Alias domain", type='char', size=None), + 'alias_parent_model_id': fields.many2one('ir.model', 'Parent Model', + help="Parent model holding the alias. The model holding the alias reference\n" + "is not necessarily the model given by alias_model_id\n" + "(example: project (parent_model) and task (model))"), + 'alias_parent_thread_id': fields.integer('Parent Record Thread ID', + help="ID of the parent record holding the alias (example: project holding the task creation alias)"), + 'alias_contact': fields.selection([ + ('everyone', 'Everyone'), + ('partners', 'Authenticated Partners'), + ('followers', 'Followers only'), + ], string='Alias Contact Security', required=True, + help="Policy to post a message on the document using the mailgateway.\n" + "- everyone: everyone can post\n" + "- partners: only authenticated partners\n" + "- followers: only followers of the related document\n"), } _defaults = { 'alias_defaults': '{}', - 'alias_user_id': lambda self,cr,uid,context: uid, + 'alias_user_id': lambda self, cr, uid, context: uid, # looks better when creating new aliases - even if the field is informative only - 'alias_domain': lambda self,cr,uid,context: self._get_alias_domain(cr, SUPERUSER_ID,[1],None,None)[1] + 'alias_domain': lambda self, cr, uid, context: self._get_alias_domain(cr, SUPERUSER_ID, [1], None, None)[1], + 'alias_contact': 'everyone', } _sql_constraints = [ @@ -139,13 +155,15 @@ class mail_alias(osv.Model): return new_name def migrate_to_alias(self, cr, child_model_name, child_table_name, child_model_auto_init_fct, - alias_id_column, alias_key, alias_prefix='', alias_force_key='', alias_defaults={}, context=None): + alias_model_name, alias_id_column, alias_key, alias_prefix='', alias_force_key='', alias_defaults={}, + alias_generate_name=False, context=None): """ Installation hook to create aliases for all users and avoid constraint errors. :param child_model_name: model name of the child class (i.e. res.users) :param child_table_name: table name of the child class (i.e. res_users) :param child_model_auto_init_fct: pointer to the _auto_init function (i.e. super(res_users,self)._auto_init(cr, context=context)) + :param alias_model_name: name of the aliased model :param alias_id_column: alias_id column (i.e. self._columns['alias_id']) :param alias_key: name of the column used for the unique name (i.e. 'login') :param alias_prefix: prefix for the unique name (i.e. 'jobs' + ...) @@ -153,6 +171,8 @@ class mail_alias(osv.Model): if empty string, not taken into account :param alias_defaults: dict, keys = mail.alias columns, values = child model column name used for default values (i.e. {'job_id': 'id'}) + :param alias_generate_name: automatically generate alias name using prefix / alias key; + default alias_name value is False because since 8.0 it is not required anymore """ if context is None: context = {} @@ -170,13 +190,17 @@ class mail_alias(osv.Model): no_alias_ids = child_class_model.search(cr, SUPERUSER_ID, [('alias_id', '=', False)], context={'active_test': False}) # Use read() not browse(), to avoid prefetching uninitialized inherited fields for obj_data in child_class_model.read(cr, SUPERUSER_ID, no_alias_ids, [alias_key]): - alias_vals = {'alias_name': '%s%s' % (alias_prefix, obj_data[alias_key])} + alias_vals = {'alias_name': False} + if alias_generate_name: + alias_vals['alias_name'] = '%s%s' % (alias_prefix, obj_data[alias_key]) if alias_force_key: alias_vals['alias_force_thread_id'] = obj_data[alias_force_key] alias_vals['alias_defaults'] = dict((k, obj_data[v]) for k, v in alias_defaults.iteritems()) - alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, alias_vals, model_name=context.get('alias_model_name', child_model_name)) + alias_vals['alias_parent_thread_id'] = obj_data['id'] + alias_create_ctx = dict(context, alias_model_name=alias_model_name, alias_parent_model_name=child_model_name) + alias_id = mail_alias.create(cr, SUPERUSER_ID, alias_vals, context=alias_create_ctx) child_class_model.write(cr, SUPERUSER_ID, obj_data['id'], {'alias_id': alias_id}) - _logger.info('Mail alias created for %s %s (uid %s)', child_model_name, obj_data[alias_key], obj_data['id']) + _logger.info('Mail alias created for %s %s (id %s)', child_model_name, obj_data[alias_key], obj_data['id']) # Finally attempt to reinstate the missing constraint try: @@ -189,22 +213,53 @@ class mail_alias(osv.Model): # set back the unique alias_id constraint alias_id_column.required = True - return res - def create_unique_alias(self, cr, uid, vals, model_name=None, context=None): - """Creates an email.alias record according to the values provided in ``vals``, - with 2 alterations: the ``alias_name`` value may be suffixed in order to - make it unique (and certain unsafe characters replaced), and - he ``alias_model_id`` value will set to the model ID of the ``model_name`` - value, if provided, + def create(self, cr, uid, vals, context=None): + """ Creates an email.alias record according to the values provided in ``vals``, + with 2 alterations: the ``alias_name`` value may be suffixed in order to + make it unique (and certain unsafe characters replaced), and + he ``alias_model_id`` value will set to the model ID of the ``model_name`` + context value, if provided. """ - # when an alias name appears to already be an email, we keep the local part only - alias_name = remove_accents(vals['alias_name']).lower().split('@')[0] - alias_name = re.sub(r'[^\w+.]+', '-', alias_name) - alias_name = self._find_unique(cr, uid, alias_name, context=context) - vals['alias_name'] = alias_name + if context is None: + context = {} + model_name = context.get('alias_model_name') + parent_model_name = context.get('alias_parent_model_name') + if vals.get('alias_name'): + # when an alias name appears to already be an email, we keep the local part only + alias_name = remove_accents(vals['alias_name']).lower().split('@')[0] + alias_name = re.sub(r'[^\w+.]+', '-', alias_name) + alias_name = self._find_unique(cr, uid, alias_name, context=context) + vals['alias_name'] = alias_name if model_name: model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0] vals['alias_model_id'] = model_id - return self.create(cr, uid, vals, context=context) + if parent_model_name: + model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', parent_model_name)], context=context)[0] + vals['alias_parent_model_id'] = model_id + return super(mail_alias, self).create(cr, uid, vals, context=context) + + def open_document(self, cr, uid, ids, context=None): + alias = self.browse(cr, uid, ids, context=context)[0] + if not alias.alias_model_id or not alias.alias_force_thread_id: + return False + return { + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': alias.alias_model_id.model, + 'res_id': alias.alias_force_thread_id, + 'type': 'ir.actions.act_window', + } + + def open_parent_document(self, cr, uid, ids, context=None): + alias = self.browse(cr, uid, ids, context=context)[0] + if not alias.alias_parent_model_id or not alias.alias_parent_thread_id: + return False + return { + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': alias.alias_parent_model_id.model, + 'res_id': alias.alias_parent_thread_id, + 'type': 'ir.actions.act_window', + } diff --git a/addons/mail/mail_alias_view.xml b/addons/mail/mail_alias_view.xml index e0d8e173ebc..248bf7e8f3a 100644 --- a/addons/mail/mail_alias_view.xml +++ b/addons/mail/mail_alias_view.xml @@ -9,13 +9,23 @@
-
@@ -32,6 +42,7 @@ + @@ -44,8 +55,13 @@ + + + + + - + @@ -55,6 +71,10 @@ Aliases mail.alias + { + 'search_default_active': True, + } +

- %% + %%
@@ -78,17 +78,19 @@

-
-
+
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index c6b0dd9f5fa..1b5d520cd42 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -25,7 +25,6 @@ import dateutil import email import logging import pytz -import re import time import xmlrpclib from email.message import Message @@ -102,21 +101,22 @@ class mail_thread(osv.AbstractModel): if catchall_domain and model and res_id: # specific res_id -> find its alias (i.e. section_id specified) object_id = self.pool.get(model).browse(cr, uid, res_id, context=context) # check that the alias effectively creates new records - if object_id.alias_id and object_id.alias_id.alias_model_id and \ + if object_id.alias_id and object_id.alias_id.alias_name and \ + object_id.alias_id.alias_model_id and \ object_id.alias_id.alias_model_id.model == self._name and \ object_id.alias_id.alias_force_thread_id == 0: alias = object_id.alias_id elif catchall_domain and model: # no specific res_id given -> generic help message, take an example alias (i.e. alias of some section_id) model_id = self.pool.get('ir.model').search(cr, uid, [("model", "=", self._name)], context=context)[0] alias_obj = self.pool.get('mail.alias') - alias_ids = alias_obj.search(cr, uid, [("alias_model_id", "=", model_id), ('alias_force_thread_id', '=', 0)], context=context, order='id ASC') + alias_ids = alias_obj.search(cr, uid, [("alias_model_id", "=", model_id), ("alias_name", "!=", False), ('alias_force_thread_id', '=', 0)], context=context, order='id ASC') if alias_ids and len(alias_ids) == 1: # if several aliases -> incoherent to propose one guessed from nowhere, therefore avoid if several aliases alias = alias_obj.browse(cr, uid, alias_ids[0], context=context) if alias: alias_email = alias.name_get()[0][1] return _("""

- Click here to add a new %(document)s or send an email to: %(email)s + Click here to add new %(document)s or send an email to: %(email)s

%(static_help)s""" ) % { @@ -126,7 +126,7 @@ class mail_thread(osv.AbstractModel): } if document_name != 'document' and help and help.find("oe_view_nocontent_create") == -1: - return _("

Click here to add a new %(document)s

%(static_help)s") % { + return _("

Click here to add new %(document)s

%(static_help)s") % { 'document': document_name, 'static_help': help or '', } @@ -257,7 +257,6 @@ class mail_thread(osv.AbstractModel): def _search_is_follower(self, cr, uid, obj, name, args, context): """Search function for message_is_follower""" - fol_obj = self.pool.get('mail.followers') res = [] for field, operator, value in args: assert field == name @@ -329,8 +328,8 @@ class mail_thread(osv.AbstractModel): # Track initial values of tracked fields tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=context) if tracked_fields: - initial = self.read(cr, uid, ids, tracked_fields.keys(), context=context) - initial_values = dict((item['id'], item) for item in initial) + records = self.browse(cr, uid, ids, context=context) + initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records) # Perform write, update followers result = super(mail_thread, self).write(cr, uid, ids, values, context=context) @@ -389,7 +388,7 @@ class mail_thread(osv.AbstractModel): if not value: return '' if col_info['type'] == 'many2one': - return value[1] + return value.name_get()[0][1] if col_info['type'] == 'selection': return dict(col_info['selection'])[value] return value @@ -408,23 +407,26 @@ class mail_thread(osv.AbstractModel): if not tracked_fields: return True - for record in self.read(cr, uid, ids, tracked_fields.keys(), context=context): - initial = initial_values[record['id']] - changes = [] + for browse_record in self.browse(cr, uid, ids, context=context): + initial = initial_values[browse_record.id] + changes = set() tracked_values = {} # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}} for col_name, col_info in tracked_fields.items(): - if record[col_name] == initial[col_name] and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always': + initial_value = initial[col_name] + record_value = getattr(browse_record, col_name) + + if record_value == initial_value and getattr(self._all_columns[col_name].column, 'track_visibility', None) == 'always': tracked_values[col_name] = dict(col_info=col_info['string'], - new_value=convert_for_display(record[col_name], col_info)) - elif record[col_name] != initial[col_name]: + new_value=convert_for_display(record_value, col_info)) + elif record_value != initial_value and (record_value or initial_value): # because browse null != False if getattr(self._all_columns[col_name].column, 'track_visibility', None) in ['always', 'onchange']: tracked_values[col_name] = dict(col_info=col_info['string'], - old_value=convert_for_display(initial[col_name], col_info), - new_value=convert_for_display(record[col_name], col_info)) + old_value=convert_for_display(initial_value, col_info), + new_value=convert_for_display(record_value, col_info)) if col_name in tracked_fields: - changes.append(col_name) + changes.add(col_name) if not changes: continue @@ -434,7 +436,7 @@ class mail_thread(osv.AbstractModel): if field not in changes: continue for subtype, method in track_info.items(): - if method(self, cr, uid, record, context): + if method(self, cr, uid, browse_record, context): subtypes.append(subtype) posted = False @@ -445,11 +447,11 @@ class mail_thread(osv.AbstractModel): _logger.debug('subtype %s not found, giving error "%s"' % (subtype, e)) continue message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values) - self.message_post(cr, uid, record['id'], body=message, subtype=subtype, context=context) + self.message_post(cr, uid, browse_record.id, body=message, subtype=subtype, context=context) posted = True if not posted: message = format_message('', tracked_values) - self.message_post(cr, uid, record['id'], body=message, context=context) + self.message_post(cr, uid, browse_record.id, body=message, context=context) return True #------------------------------------------------------ @@ -564,6 +566,8 @@ class mail_thread(osv.AbstractModel): #------------------------------------------------------ def message_get_reply_to(self, cr, uid, ids, context=None): + """ Returns the preferred reply-to email address that is basically + the alias of the document, if it exists. """ if not self._inherits.get('mail.alias'): return [False for id in ids] return ["%s@%s" % (record['alias_name'], record['alias_domain']) @@ -587,27 +591,123 @@ class mail_thread(osv.AbstractModel): def _message_find_partners(self, cr, uid, message, header_fields=['From'], context=None): """ Find partners related to some header fields of the message. - TDE TODO: merge me with other partner finding methods in 8.0 """ - partner_obj = self.pool.get('res.partner') - partner_ids = [] + :param string message: an email.message instance """ s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)]) - for email_address in tools.email_split(s): - related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context) - if not related_partners: - related_partners = partner_obj.search(cr, uid, [('email', 'ilike', email_address)], limit=1, context=context) - partner_ids += related_partners - return partner_ids + return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context)) - def _message_find_user_id(self, cr, uid, message, context=None): - """ TDE TODO: check and maybe merge me with other user finding methods in 8.0 """ - from_local_part = tools.email_split(decode(message.get('From')))[0] - # FP Note: canonification required, the minimu: .lower() - user_ids = self.pool.get('res.users').search(cr, uid, ['|', - ('login', '=', from_local_part), - ('email', '=', from_local_part)], context=context) - return user_ids[0] if user_ids else uid + def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None): + """ Verify route validity. Check and rules: + 1 - if thread_id -> check that document effectively exists; otherwise + fallback on a message_new by resetting thread_id + 2 - check that message_update exists if thread_id is set; or at least + that message_new exist + [ - find author_id if udpate_author is set] + 3 - if there is an alias, check alias_contact: + 'followers' and thread_id: + check on target document that the author is in the followers + 'followers' and alias_parent_thread_id: + check on alias parent document that the author is in the + followers + 'partners': check that author_id id set + """ - def message_route(self, cr, uid, message, model=None, thread_id=None, + assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple' + assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record' + + message_id = message.get('Message-Id') + email_from = decode_header(message, 'From') + author_id = message_dict.get('author_id') + model, thread_id, alias = route[0], route[1], route[4] + model_pool = None + + def _create_bounce_email(): + mail_mail = self.pool.get('mail.mail') + mail_id = mail_mail.create(cr, uid, { + 'body_html': '

Hello,

' + '

The following email sent to %s cannot be accepted because this is ' + 'a private email address. Only allowed people can contact us at this address.

' + '
%s
' % (message.get('to'), message_dict.get('body')), + 'subject': 'Re: %s' % message.get('subject'), + 'email_to': message.get('from'), + 'auto_delete': True, + }, context=context) + mail_mail.send(cr, uid, [mail_id], context=context) + + def _warn(message): + _logger.warning('Routing mail with Message-Id %s: route %s: %s', + message_id, route, message) + + # Wrong model + if model and not model in self.pool: + if assert_model: + assert model in self.pool, 'Routing: unknown target model %s' % model + _warn('unknown target model %s' % model) + return () + elif model: + model_pool = self.pool[model] + + # Private message: should not contain any thread_id + if not model and thread_id: + if assert_model: + assert thread_id == 0, 'Routing: posting a message without model should be with a null res_id (private message).' + _warn('posting a message without model should be with a null res_id (private message), resetting thread_id') + thread_id = 0 + + # Existing Document: check if exists; if not, fallback on create if allowed + if thread_id and not model_pool.exists(cr, uid, thread_id): + if create_fallback: + _warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id)) + thread_id = None + elif assert_model: + assert model_pool.exists(cr, uid, thread_id), 'Routing: reply to missing document (%s,%s)' % (model, thread_id) + else: + _warn('reply to missing document (%s,%s), skipping' % (model, thread_id)) + return () + + # Existing Document: check model accepts the mailgateway + if thread_id and not hasattr(model_pool, 'message_update'): + if create_fallback: + _warn('model %s does not accept document update, fall back on document creation' % model) + thread_id = None + elif assert_model: + assert hasattr(model_pool, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model + else: + _warn('model %s does not accept document update, skipping' % model) + return () + + # New Document: check model accepts the mailgateway + if not thread_id and not hasattr(model_pool, 'message_new'): + if assert_model: + assert hasattr(model_pool, 'message_new'), 'Model %s does not accept document creation, crashing' % model + _warn('model %s does not accept document creation, skipping' % model) + return () + + # Update message author if asked + # We do it now because we need it for aliases (contact settings) + if not author_id and update_author: + author_ids = self._find_partner_from_emails(cr, uid, thread_id, [email_from], model=model, context=context) + if author_ids: + author_id = author_ids[0] + message_dict['author_id'] = author_id + + # Alias: check alias_contact settings + if alias and alias.alias_contact == 'followers' and (thread_id or alias.alias_parent_thread_id): + if thread_id: + obj = self.pool[model].browse(cr, uid, thread_id, context=context) + else: + obj = self.pool[alias.alias_parent_model_id.model].browse(cr, uid, alias.alias_parent_thread_id, context=context) + if not author_id or not author_id in [fol.id for fol in obj.message_follower_ids]: + _warn('alias %s restricted to internal followers, skipping' % alias.alias_name) + _create_bounce_email() + return () + elif alias and alias.alias_contact == 'partners' and not author_id: + _warn('alias %s does not accept unknown author, skipping' % alias.alias_name) + _create_bounce_email() + return () + + return (model, thread_id, route[2], route[3], route[4]) + + def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None, custom_values=None, context=None): """Attempt to figure out the correct target model, thread_id, custom_values and user_id to use for an incoming message. @@ -627,6 +727,7 @@ class mail_thread(osv.AbstractModel): 4. If all the above fails, raise an exception. :param string message: an email.message instance + :param dict message_dict: dictionary holding message variables :param string model: the fallback model to use if the message does not match any of the currently configured mail aliases (may be None if a matching alias is supposed to be present) @@ -637,9 +738,12 @@ class mail_thread(osv.AbstractModel): :param int thread_id: optional ID of the record/thread from ``model`` to which this mail should be attached. Only used if the message does not reply to an existing thread and does not match any mail alias. - :return: list of [model, thread_id, custom_values, user_id] + :return: list of [model, thread_id, custom_values, user_id, alias] """ assert isinstance(message, Message), 'message must be an email.message.Message at this point' + fallback_model = model + + # Get email.message.Message variables for future processing message_id = message.get('Message-Id') email_from = decode_header(message, 'From') email_to = decode_header(message, 'To') @@ -649,18 +753,20 @@ class mail_thread(osv.AbstractModel): # 1. Verify if this is a reply to an existing thread thread_references = references or in_reply_to ref_match = thread_references and tools.reference_re.search(thread_references) - if ref_match: thread_id = int(ref_match.group(1)) - model = ref_match.group(2) or model + model = ref_match.group(2) or fallback_model if thread_id and model in self.pool: model_obj = self.pool[model] if model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'): _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s', email_from, email_to, message_id, model, thread_id, custom_values, uid) - return [(model, thread_id, custom_values, uid)] + route = self.message_route_verify(cr, uid, message, message_dict, + (model, thread_id, custom_values, uid, None), + update_author=True, assert_model=True, create_fallback=True, context=context) + return route and [route] or [] - # Verify whether this is a reply to a private message + # 2. Reply to a private message if in_reply_to: message_ids = self.pool.get('mail.message').search(cr, uid, [ ('message_id', '=', in_reply_to), @@ -670,9 +776,12 @@ class mail_thread(osv.AbstractModel): message = self.pool.get('mail.message').browse(cr, uid, message_ids[0], context=context) _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s', email_from, email_to, message_id, message.id, custom_values, uid) - return [(message.model, message.res_id, custom_values, uid)] + route = self.message_route_verify(cr, uid, message, message_dict, + (message.model, message.res_id, custom_values, uid, None), + update_author=True, assert_model=True, create_fallback=True, context=context) + return route and [route] or [] - # 2. Look for a matching mail.alias entry + # 3. Look for a matching mail.alias entry # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value. rcpt_tos = \ @@ -697,14 +806,16 @@ class mail_thread(osv.AbstractModel): # user_id = self._message_find_user_id(cr, uid, message, context=context) user_id = uid _logger.info('No matching user_id for the alias %s', alias.alias_name) - routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \ - eval(alias.alias_defaults), user_id)) - _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r', - email_from, email_to, message_id, routes) + route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias) + _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r', + email_from, email_to, message_id, route) + route = self.message_route_verify(cr, uid, message, message_dict, route, + update_author=True, assert_model=True, create_fallback=True, context=context) + if route: + routes.append(route) return routes - # 3. Fallback to the provided parameters, if they work - model_pool = self.pool.get(model) + # 4. Fallback to the provided parameters, if they work if not thread_id: # Legacy: fallback to matching [ID] in the Subject match = tools.res_re.search(decode_header(message, 'Subject')) @@ -714,16 +825,18 @@ class mail_thread(osv.AbstractModel): thread_id = int(thread_id) except: thread_id = False - assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \ + _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s', + email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid) + route = self.message_route_verify(cr, uid, message, message_dict, + (fallback_model, thread_id, custom_values, uid, None), + update_author=True, assert_model=True, context=context) + if route: + return [route] + + # AssertionError if no routes found and if no bounce occured + assert False, \ "No possible route found for incoming message from %s to %s (Message-Id %s:)." \ "Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id) - if thread_id and not model_pool.exists(cr, uid, thread_id): - _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s', - thread_id, message_id) - thread_id = None - _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s', - email_from, email_to, message_id, model, thread_id, custom_values, uid) - return [(model, thread_id, custom_values, uid)] def message_process(self, cr, uid, model, message, custom_values=None, save_original=False, strip_attachments=False, @@ -777,25 +890,21 @@ class mail_thread(osv.AbstractModel): msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context) if strip_attachments: msg.pop('attachments', None) + # postpone setting msg.partner_ids after message_post, to avoid double notifications + partner_ids = msg.pop('partner_ids', []) if msg.get('message_id'): # should always be True as message_parse generate one if missing existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [ ('message_id', '=', msg.get('message_id')), ], context=context) if existing_msg_ids: - _logger.info('Ignored mail from %s to %s with Message-Id %s:: found duplicated Message-Id during processing', + _logger.info('Ignored mail from %s to %s with Message-Id %s: found duplicated Message-Id during processing', msg.get('from'), msg.get('to'), msg.get('message_id')) return False # find possible routes for the message - routes = self.message_route(cr, uid, msg_txt, model, - thread_id, custom_values, - context=context) - - # postpone setting msg.partner_ids after message_post, to avoid double notifications - partner_ids = msg.pop('partner_ids', []) - + routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context) thread_id = False - for model, thread_id, custom_values, user_id in routes: + for model, thread_id, custom_values, user_id, alias in routes: if self._name == 'mail.thread': context.update({'thread_model': model}) if model: @@ -806,11 +915,10 @@ class mail_thread(osv.AbstractModel): # disabled subscriptions during message_new/update to avoid having the system user running the # email gateway become a follower of all inbound messages - nosub_ctx = dict(context, mail_create_nosubscribe=True) + nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True) if thread_id and hasattr(model_pool, 'message_update'): model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx) else: - nosub_ctx = dict(nosub_ctx, mail_create_nolog=True) thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx) else: assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." @@ -947,7 +1055,6 @@ class mail_thread(osv.AbstractModel): """ msg_dict = { 'type': 'email', - 'author_id': False, } if not isinstance(message, Message): if isinstance(message, unicode): @@ -970,12 +1077,7 @@ class mail_thread(osv.AbstractModel): msg_dict['from'] = decode(message.get('from')) msg_dict['to'] = decode(message.get('to')) msg_dict['cc'] = decode(message.get('cc')) - - if message.get('From'): - author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context) - if author_ids: - msg_dict['author_id'] = author_ids[0] - msg_dict['email_from'] = decode(message.get('from')) + msg_dict['email_from'] = decode(message.get('from')) partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context) msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids] @@ -1029,7 +1131,7 @@ class mail_thread(osv.AbstractModel): partner_id, partner_name or partner_name, reason """ if email and not partner: # get partner info from email - partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context, res_id=obj.id)[0] + partner_info = self.message_partner_info_from_emails(cr, uid, obj.id, [email], context=context)[0] if partner_info.get('partner_id'): partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info.get('partner_id')], context=context)[0] if email and email in [val[1] for val in result[obj.id]]: # already existing email -> skip @@ -1057,53 +1159,76 @@ class mail_thread(osv.AbstractModel): self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context) return result - def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None, res_id=None): - """ Wrapper with weird order parameter because of 7.0 fix. + def _find_partner_from_emails(self, cr, uid, id, emails, model=None, context=None, check_followers=True): + """ Utility method to find partners from email addresses. The rules are : + 1 - check in document (model | self, id) followers + 2 - try to find a matching partner that is also an user + 3 - try to find a matching partner - TDE TODO: remove me in 8.0 """ - return self.message_find_partner_from_emails(cr, uid, res_id, emails, link_mail=link_mail, context=context) - - def message_find_partner_from_emails(self, cr, uid, id, emails, link_mail=False, context=None): - """ Convert a list of emails into a list partner_ids and a list - new_partner_ids. The return value is non conventional because - it is meant to be used by the mail widget. - - :return dict: partner_ids and new_partner_ids - - TDE TODO: merge me with other partner finding methods in 8.0 """ - mail_message_obj = self.pool.get('mail.message') - partner_obj = self.pool.get('res.partner') - result = list() - if id and self._name != 'mail.thread': - obj = self.browse(cr, SUPERUSER_ID, id, context=context) - else: - obj = None - for email in emails: - partner_info = {'full_name': email, 'partner_id': False} - m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL) - if not m: + :param list emails: list of email addresses + :param string model: model to fetch related record; by default self + is used. + :param boolean check_followers: check in document followers + """ + partner_obj = self.pool['res.partner'] + partner_ids = [] + obj = None + if id and (model or self._name != 'mail.thread') and check_followers: + if model: + obj = self.pool[model].browse(cr, uid, id, context=context) + else: + obj = self.browse(cr, uid, id, context=context) + for contact in emails: + partner_id = False + email_address = tools.email_split(contact) + if not email_address: + partner_ids.append(partner_id) continue - email_address = m.group(3) + email_address = email_address[0] # first try: check in document's followers if obj: for follower in obj.message_follower_ids: if follower.email == email_address: - partner_info['partner_id'] = follower.id - # second try: check in partners - if not partner_info.get('partner_id'): - ids = partner_obj.search(cr, SUPERUSER_ID, [('email', 'ilike', email_address), ('user_ids', '!=', False)], limit=1, context=context) - if not ids: - ids = partner_obj.search(cr, SUPERUSER_ID, [('email', 'ilike', email_address)], limit=1, context=context) + partner_id = follower.id + # second try: check in partners that are also users + if not partner_id: + ids = partner_obj.search(cr, SUPERUSER_ID, [ + ('email', 'ilike', email_address), + ('user_ids', '!=', False) + ], limit=1, context=context) if ids: - partner_info['partner_id'] = ids[0] + partner_id = ids[0] + # third try: check in partners + if not partner_id: + ids = partner_obj.search(cr, SUPERUSER_ID, [ + ('email', 'ilike', email_address) + ], limit=1, context=context) + if ids: + partner_id = ids[0] + partner_ids.append(partner_id) + return partner_ids + + def message_partner_info_from_emails(self, cr, uid, id, emails, link_mail=False, context=None): + """ Convert a list of emails into a list partner_ids and a list + new_partner_ids. The return value is non conventional because + it is meant to be used by the mail widget. + + :return dict: partner_ids and new_partner_ids """ + mail_message_obj = self.pool.get('mail.message') + partner_ids = self._find_partner_from_emails(cr, uid, id, emails, context=context) + result = list() + for idx in range(len(emails)): + email_address = emails[idx] + partner_id = partner_ids[idx] + partner_info = {'full_name': email_address, 'partner_id': partner_id} result.append(partner_info) # link mail with this from mail to the new partner id if link_mail and partner_info['partner_id']: message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [ '|', - ('email_from', '=', email), - ('email_from', 'ilike', '<%s>' % email), + ('email_from', '=', email_address), + ('email_from', 'ilike', '<%s>' % email_address), ('author_id', '=', False) ], context=context) if message_ids: @@ -1156,18 +1281,7 @@ class mail_thread(osv.AbstractModel): del context['thread_model'] return self.pool[model].message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs) - # 0: Parse email-from, try to find a better author_id based on document's followers for incoming emails - email_from = kwargs.get('email_from') - if email_from and thread_id and type == 'email' and kwargs.get('author_id'): - email_list = tools.email_split(email_from) - doc = self.browse(cr, uid, thread_id, context=context) - if email_list and doc: - author_ids = self.pool.get('res.partner').search(cr, uid, [ - ('email', 'ilike', email_list[0]), - ('id', 'in', [f.id for f in doc.message_follower_ids]) - ], limit=1, context=context) - if author_ids: - kwargs['author_id'] = author_ids[0] + #0: Find the message's author, because we need it for private discussion author_id = kwargs.get('author_id') if author_id is None: # keep False values author_id = self.pool.get('mail.message')._get_default_author(cr, uid, context=context) @@ -1278,21 +1392,6 @@ class mail_thread(osv.AbstractModel): self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context) return msg_id - #------------------------------------------------------ - # Compatibility methods: do not use - # TDE TODO: remove me in 8.0 - #------------------------------------------------------ - - def message_create_partners_from_emails(self, cr, uid, emails, context=None): - return {'partner_ids': [], 'new_partner_ids': []} - - def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False, - attachment_ids=None, content_subtype='plaintext', - context=None, **kwargs): - return self.message_post(cr, uid, thread_id, body=body, parent_id=parent_id, - attachment_ids=attachment_ids, content_subtype=content_subtype, - context=context, **kwargs) - #------------------------------------------------------ # Followers API #------------------------------------------------------ diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index b9883489ca6..7bc39c438d1 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -23,6 +23,7 @@ from openerp.osv import fields, osv from openerp import SUPERUSER_ID from openerp.tools.translate import _ + class res_users(osv.Model): """ Update of res.users class - add a preference about sending emails about notifications @@ -42,7 +43,6 @@ class res_users(osv.Model): } _defaults = { - 'alias_domain': False, # always hide alias during creation 'display_groups_suggestions': True, } @@ -63,25 +63,20 @@ class res_users(osv.Model): def _auto_init(self, cr, context=None): """ Installation hook: aliases, partner following themselves """ # create aliases for all users and avoid constraint errors - res = self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(res_users, self)._auto_init, - self._columns['alias_id'], 'login', alias_force_key='id', context=context) - return res + return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(res_users, self)._auto_init, + self._name, self._columns['alias_id'], 'login', alias_force_key='id', context=context) def create(self, cr, uid, data, context=None): - # create default alias same as the login if not data.get('login', False): raise osv.except_osv(_('Invalid Action!'), _('You may not create a user. To create new users, you should use the "Settings > Users" menu.')) + if context is None: + context = {} - mail_alias = self.pool.get('mail.alias') - alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context) - data['alias_id'] = alias_id - data.pop('alias_name', None) # prevent errors during copy() - - # create user - user_id = super(res_users, self).create(cr, uid, data, context=context) + create_context = dict(context, alias_model_name=self._name, alias_parent_model_name=self._name) + user_id = super(res_users, self).create(cr, uid, data, context=create_context) user = self.browse(cr, uid, user_id, context=context) - # alias - mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context) + self.pool.get('mail.alias').write(cr, SUPERUSER_ID, [user.alias_id.id], {"alias_force_thread_id": user_id, "alias_parent_thread_id": user_id}, context) + # create a welcome message self._create_welcome_message(cr, uid, user, context=context) return user_id @@ -95,12 +90,6 @@ class res_users(osv.Model): return self.pool.get('res.partner').message_post(cr, SUPERUSER_ID, [user.partner_id.id], body=body, context=context) - def write(self, cr, uid, ids, vals, context=None): - # User alias is sync'ed with login - if vals.get('login'): - vals['alias_name'] = vals['login'] - return super(res_users, self).write(cr, uid, ids, vals, context=context) - def unlink(self, cr, uid, ids, context=None): # Cascade-delete mail aliases as well, as they should not exist without the user. alias_pool = self.pool.get('mail.alias') diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml index babe922d975..2bf54b4bafd 100644 --- a/addons/mail/res_users_view.xml +++ b/addons/mail/res_users_view.xml @@ -22,21 +22,25 @@ res.users - - - - - - - - - - - - - - + + + + + + diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index c969d3f3b16..211133afaf8 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -26,6 +26,16 @@ border-radius: 0px; } +/* ---- GENERIC FOR MAIL-RELATED STUFF ---- */ +.openerp .oe_e.oe_e_alias { + font-size: 30px; + line-height: 15px; + vertical-align: top; + margin-right: 3px; + color: white; + text-shadow: 0px 0px 2px black; +} + /* ------------ MAIL WIDGET --------------- */ .openerp .oe_mail, .openerp .oe_mail *{ -webkit-box-sizing: border-box; diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 83ac92404f1..21b0f5a9a87 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -632,10 +632,7 @@ openerp.mail = function (session) { // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id var find_done = $.Deferred(); if (names_to_find.length > 0) { - var values = { - 'res_id': this.context.default_res_id, - } - find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [names_to_find], values); + find_done = self.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [this.context.default_res_id, names_to_find]); } else { find_done.resolve([]); @@ -681,11 +678,7 @@ openerp.mail = function (session) { var new_names_to_find = _.difference(names_to_find, names_to_remove); find_done = $.Deferred(); if (new_names_to_find.length > 0) { - var values = { - 'link_mail': true, - 'res_id': self.context.default_res_id, - } - find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [new_names_to_find], values); + find_done = self.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [self.context.default_res_id, new_names_to_find, true]); } else { find_done.resolve([]); diff --git a/addons/mail/tests/test_mail_base.py b/addons/mail/tests/test_mail_base.py index 7ca8b93135c..b0f9f72b6f7 100644 --- a/addons/mail/tests/test_mail_base.py +++ b/addons/mail/tests/test_mail_base.py @@ -71,9 +71,9 @@ class TestMailBase(common.TransactionCase): # Test users to use through the various tests self.res_users.write(cr, uid, uid, {'name': 'Administrator'}) self.user_raoul_id = self.res_users.create(cr, uid, - {'name': 'Raoul Grosbedon', 'signature': 'SignRaoul', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'groups_id': [(6, 0, [self.group_employee_id])]}) + {'name': 'Raoul Grosbedon', 'signature': 'SignRaoul', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'alias_name': 'raoul', 'groups_id': [(6, 0, [self.group_employee_id])]}) self.user_bert_id = self.res_users.create(cr, uid, - {'name': 'Bert Tartignole', 'signature': 'SignBert', 'email': 'bert@bert.fr', 'login': 'bert', 'groups_id': [(6, 0, [])]}) + {'name': 'Bert Tartignole', 'signature': 'SignBert', 'email': 'bert@bert.fr', 'login': 'bert', 'alias_name': 'bert', 'groups_id': [(6, 0, [])]}) self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id) self.user_bert = self.res_users.browse(cr, uid, self.user_bert_id) self.user_admin = self.res_users.browse(cr, uid, uid) @@ -83,7 +83,7 @@ class TestMailBase(common.TransactionCase): # Test 'pigs' group to use through the various tests self.group_pigs_id = self.mail_group.create(cr, uid, - {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}, + {'name': 'Pigs', 'description': 'Fans of Pigs, unite !', 'alias_name': 'group+pigs'}, {'mail_create_nolog': True}) self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index ca9c156c8cb..148b5faba87 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -32,17 +32,17 @@ class test_mail(TestMailBase): """ Test basic mail.alias setup works, before trying to use them for routing """ cr, uid = self.cr, self.uid self.user_valentin_id = self.res_users.create(cr, uid, - {'name': 'Valentin Cognito', 'email': 'valentin.cognito@gmail.com', 'login': 'valentin.cognito'}) + {'name': 'Valentin Cognito', 'email': 'valentin.cognito@gmail.com', 'login': 'valentin.cognito', 'alias_name': 'valentin.cognito'}) self.user_valentin = self.res_users.browse(cr, uid, self.user_valentin_id) self.assertEquals(self.user_valentin.alias_name, self.user_valentin.login, "Login should be used as alias") self.user_pagan_id = self.res_users.create(cr, uid, - {'name': 'Pagan Le Marchant', 'email': 'plmarchant@gmail.com', 'login': 'plmarchant@gmail.com'}) + {'name': 'Pagan Le Marchant', 'email': 'plmarchant@gmail.com', 'login': 'plmarchant@gmail.com', 'alias_name': 'plmarchant@gmail.com'}) self.user_pagan = self.res_users.browse(cr, uid, self.user_pagan_id) self.assertEquals(self.user_pagan.alias_name, 'plmarchant', "If login is an email, the alias should keep only the local part") self.user_barty_id = self.res_users.create(cr, uid, - {'name': 'Bartholomew Ironside', 'email': 'barty@gmail.com', 'login': 'b4r+_#_R3wl$$'}) + {'name': 'Bartholomew Ironside', 'email': 'barty@gmail.com', 'login': 'b4r+_#_R3wl$$', 'alias_name': 'b4r+_#_R3wl$$'}) self.user_barty = self.res_users.browse(cr, uid, self.user_barty_id) self.assertEquals(self.user_barty.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens') @@ -739,18 +739,21 @@ class test_mail(TestMailBase): self.ir_model_data.create(cr, uid, {'name': 'mt_private', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_private_id}) mt_name_supername_id = self.mail_message_subtype.create(cr, uid, {'name': 'name_supername', 'description': 'Supername name'}) self.ir_model_data.create(cr, uid, {'name': 'mt_name_supername', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_name_supername_id}) + mt_group_public_set_id = self.mail_message_subtype.create(cr, uid, {'name': 'group_public_set', 'description': 'Group set'}) + self.ir_model_data.create(cr, uid, {'name': 'mt_group_public_set', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public_set_id}) mt_group_public_id = self.mail_message_subtype.create(cr, uid, {'name': 'group_public', 'description': 'Group changed'}) self.ir_model_data.create(cr, uid, {'name': 'mt_group_public', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public_id}) # Data: alter mail_group model for testing purposes (test on classic, selection and many2one fields) self.mail_group._track = { 'public': { - 'mail.mt_private': lambda self, cr, uid, obj, ctx=None: obj['public'] == 'private', + 'mail.mt_private': lambda self, cr, uid, obj, ctx=None: obj.public == 'private', }, 'name': { - 'mail.mt_name_supername': lambda self, cr, uid, obj, ctx=None: obj['name'] == 'supername', + 'mail.mt_name_supername': lambda self, cr, uid, obj, ctx=None: obj.name == 'supername', }, 'group_public_id': { + 'mail.mt_group_public_set': lambda self, cr, uid, obj, ctx=None: obj.group_public_id, 'mail.mt_group_public': lambda self, cr, uid, obj, ctx=None: True, }, } @@ -787,21 +790,37 @@ class test_mail(TestMailBase): self.assertIn(u'Public\u2192Private', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect') self.assertIn(u'Pigs\u2192supername', _strip_string_spaces(last_msg.body), 'tracked feature: message body does not hold always tracked field') - # Test: change public as public, group_public_id -> 1 subtype, name always tracked + # Test: change public as public, group_public_id -> 2 subtypes, name always tracked self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'public': 'public', 'group_public_id': group_system_id}) self.group_pigs.refresh() - self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: one message should have been produced') - # Test: first produced message: mt_group_public_id, with name always tracked, public tracked on change + self.assertEqual(len(self.group_pigs.message_ids), 5, 'tracked: one message should have been produced') + # Test: first produced message: mt_group_public_set_id, with name always tracked, public tracked on change last_msg = self.group_pigs.message_ids[-4] - self.assertEqual(last_msg.subtype_id.id, mt_group_public_id, 'tracked: message should not be linked to any subtype') + self.assertEqual(last_msg.subtype_id.id, mt_group_public_set_id, 'tracked: message should be linked to mt_group_public_set_id') + self.assertIn('Group set', last_msg.body, 'tracked: message body does not hold the subtype description') + self.assertIn(u'Private\u2192Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field') + self.assertIn(u'HumanResources/Employee\u2192Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') + # Test: second produced message: mt_group_public_id, with name always tracked, public tracked on change + last_msg = self.group_pigs.message_ids[-5] + self.assertEqual(last_msg.subtype_id.id, mt_group_public_id, 'tracked: message should be linked to mt_group_public_id') self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description') self.assertIn(u'Private\u2192Public', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field') self.assertIn(u'HumanResources/Employee\u2192Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') + # Test: change group_public_id to False -> 1 subtype, name always tracked + self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'group_public_id': False}) + self.group_pigs.refresh() + self.assertEqual(len(self.group_pigs.message_ids), 6, 'tracked: one message should have been produced') + # Test: first produced message: mt_group_public_set_id, with name always tracked, public tracked on change + last_msg = self.group_pigs.message_ids[-6] + self.assertEqual(last_msg.subtype_id.id, mt_group_public_id, 'tracked: message should be linked to mt_group_public_id') + self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description') + self.assertIn(u'Administration/Settings\u2192', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') + # Test: change not tracked field, no tracking message self.mail_group.write(cr, self.user_raoul_id, [self.group_pigs_id], {'description': 'Dummy'}) self.group_pigs.refresh() - self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: No message should have been produced') + self.assertEqual(len(self.group_pigs.message_ids), 6, 'tracked: No message should have been produced') # Data: removed changes public_col.track_visibility = None diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index 9e05bb049c1..2e0ba336dc4 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -99,20 +99,20 @@ class TestMailgateway(TestMailBase): # -------------------------------------------------- # Do: find partner with email -> first partner should be found - partner_info = self.mail_thread.message_find_partner_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0] + partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0] self.assertEqual(partner_info['full_name'], 'Maybe Raoul ', - 'mail_thread: message_find_partner_from_emails did not handle email') + 'mail_thread: message_partner_info_from_emails did not handle email') self.assertEqual(partner_info['partner_id'], p_a_id, - 'mail_thread: message_find_partner_from_emails wrong partner found') + 'mail_thread: message_partner_info_from_emails wrong partner found') # Data: add some data about partners # 2 - User BRaoul p_b_id = self.res_partner.create(cr, uid, {'name': 'BRaoul', 'email': 'test@test.fr', 'user_ids': [(4, user_raoul.id)]}) # Do: find partner with email -> first user should be found - partner_info = self.mail_thread.message_find_partner_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0] + partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul '], link_mail=False)[0] self.assertEqual(partner_info['partner_id'], p_b_id, - 'mail_thread: message_find_partner_from_emails wrong partner found') + 'mail_thread: message_partner_info_from_emails wrong partner found') # -------------------------------------------------- # CASE1: with object @@ -120,9 +120,9 @@ class TestMailgateway(TestMailBase): # Do: find partner in group where there is a follower with the email -> should be taken self.mail_group.message_subscribe(cr, uid, [group_pigs.id], [p_b_id]) - partner_info = self.mail_group.message_find_partner_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul '], link_mail=False)[0] + partner_info = self.mail_group.message_partner_info_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul '], link_mail=False)[0] self.assertEqual(partner_info['partner_id'], p_b_id, - 'mail_thread: message_find_partner_from_emails wrong partner found') + 'mail_thread: message_partner_info_from_emails wrong partner found') def test_05_mail_message_mail_mail(self): """ Tests designed for testing email values based on mail.message, aliases, ... """ @@ -189,6 +189,7 @@ class TestMailgateway(TestMailBase): # Data: set catchall domain self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', alias_domain) + self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')])) # Update message self.mail_message.write(cr, user_raoul_id, [msg_id], {'email_from': False, 'reply_to': False}) @@ -220,87 +221,6 @@ class TestMailgateway(TestMailBase): self.assertEqual(mail.reply_to, msg.email_from, 'mail_mail: incorrect reply_to: should be message email_from') - def test_05_mail_message_mail_mail(self): - """ Tests designed for testing email values based on mail.message, aliases, ... """ - cr, uid = self.cr, self.uid - - # Data: clean catchall domain - param_ids = self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.domain')]) - self.registry('ir.config_parameter').unlink(cr, uid, param_ids) - - # Do: create a mail_message with a reply_to, without message-id - msg_id = self.mail_message.create(cr, uid, {'subject': 'Subject', 'body': 'Body', 'reply_to': 'custom@example.com'}) - msg = self.mail_message.browse(cr, uid, msg_id) - # Test: message content - self.assertIn('reply_to', msg.message_id, - 'mail_message: message_id should be specific to a mail_message with a given reply_to') - self.assertEqual('custom@example.com', msg.reply_to, - 'mail_message: incorrect reply_to') - # Do: create a mail_mail with the previous mail_message and specified reply_to - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'reply_to': 'other@example.com', 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, 'other@example.com', - 'mail_mail: reply_to should be equal to the one coming from creation values') - # Do: create a mail_mail with the previous mail_message - self.mail_message.write(cr, uid, [msg_id], {'reply_to': 'custom@example.com'}) - msg.refresh() - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, msg.reply_to, - 'mail_mail: reply_to should be equal to the one coming from the mail_message') - - # Do: create a mail_message without a reply_to - msg_id = self.mail_message.create(cr, uid, {'subject': 'Subject', 'body': 'Body', 'model': 'mail.group', 'res_id': self.group_pigs_id, 'email_from': False}) - msg = self.mail_message.browse(cr, uid, msg_id) - # Test: message content - self.assertIn('mail.group', msg.message_id, - 'mail_message: message_id should contain model') - self.assertIn('%s' % self.group_pigs_id, msg.message_id, - 'mail_message: message_id should contain res_id') - self.assertFalse(msg.reply_to, - 'mail_message: should not generate a reply_to address when not specified') - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertFalse(mail.reply_to, - 'mail_mail: reply_to should not have been guessed') - # Update message - self.mail_message.write(cr, uid, [msg_id], {'email_from': 'someone@example.com'}) - msg.refresh() - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(email_split(mail.reply_to), email_split(msg.email_from), - 'mail_mail: reply_to should be equal to mail_message.email_from when having no document or default alias') - - # Data: set catchall domain - self.registry('ir.config_parameter').set_param(cr, uid, 'mail.catchall.domain', 'schlouby.fr') - self.registry('ir.config_parameter').unlink(cr, uid, self.registry('ir.config_parameter').search(cr, uid, [('key', '=', 'mail.catchall.alias')])) - - # Update message - self.mail_message.write(cr, uid, [msg_id], {'email_from': False, 'reply_to': False}) - msg.refresh() - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, '"Followers of Pigs" ', - 'mail_mail: reply_to should equal the mail.group alias') - - # Update message - self.mail_message.write(cr, uid, [msg_id], {'res_id': False, 'email_from': 'someone@schlouby.fr', 'reply_to': False}) - msg.refresh() - # Do: create a mail_mail based on the previous mail_message - mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) - mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content - self.assertEqual(mail.reply_to, msg.email_from, - 'mail_mail: reply_to should equal the mail_message email_from') - # Data: set catchall alias self.registry('ir.config_parameter').set_param(self.cr, self.uid, 'mail.catchall.alias', 'gateway') @@ -310,7 +230,7 @@ class TestMailgateway(TestMailBase): # Do: create a mail_mail based on the previous mail_message mail_id = self.mail_mail.create(cr, uid, {'mail_message_id': msg_id, 'state': 'cancel'}) mail = self.mail_mail.browse(cr, uid, mail_id) - # Test: mail_mail content + # Test: mail_mail Content-Type self.assertEqual(mail.reply_to, 'gateway@schlouby.fr', 'mail_mail: reply_to should equal the catchall email alias') @@ -351,7 +271,10 @@ class TestMailgateway(TestMailBase): alias_id = self.mail_alias.create(cr, uid, { 'alias_name': 'groups', 'alias_user_id': False, - 'alias_model_id': self.mail_group_model_id}) + 'alias_model_id': self.mail_group_model_id, + 'alias_parent_model_id': self.mail_group_model_id, + 'alias_parent_thread_id': self.group_pigs_id, + 'alias_contact': 'everyone'}) # -------------------------------------------------- # Test1: new record creation @@ -392,12 +315,42 @@ class TestMailgateway(TestMailBase): # Data: unlink group frog_group.unlink() - # Do: incoming email from a known partner on an alias with known recipients, alias is owned by user that can create a group - self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': self.user_raoul_id}) - p1id = self.res_partner.create(cr, uid, {'name': 'Sylvie Lelitre', 'email': 'test.sylvie.lelitre@agrolait.com'}) - p2id = self.res_partner.create(cr, uid, {'name': 'Other Poilvache', 'email': 'other@gmail.com'}) + # Do: incoming email from an unknown partner on a Partners only alias -> bounce self._init_mock_build_email() - frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com') + self.mail_alias.write(cr, uid, [alias_id], {'alias_contact': 'partners'}) + frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other2@gmail.com') + # Test: no group created + self.assertTrue(len(frog_groups) == 0) + # Test: email bounced + sent_emails = self._build_email_kwargs_list + self.assertEqual(len(sent_emails), 1, + 'message_process: incoming email on Partners alias should send a bounce email') + self.assertIn('Frogs', sent_emails[0].get('subject'), + 'message_process: bounce email on Partners alias should contain the original subject') + self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to'), + 'message_process: bounce email on Partners alias should have original email sender as recipient') + + # Do: incoming email from an unknown partner on a Followers only alias -> bounce + self._init_mock_build_email() + self.mail_alias.write(cr, uid, [alias_id], {'alias_contact': 'followers'}) + frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other3@gmail.com') + # Test: no group created + self.assertTrue(len(frog_groups) == 0) + # Test: email bounced + sent_emails = self._build_email_kwargs_list + self.assertEqual(len(sent_emails), 1, + 'message_process: incoming email on Followers alias should send a bounce email') + self.assertIn('Frogs', sent_emails[0].get('subject'), + 'message_process: bounce email on Followers alias should contain the original subject') + self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to'), + 'message_process: bounce email on Followers alias should have original email sender as recipient') + + # Do: incoming email from a known partner on a Partners alias -> ok (+ test on alias.user_id) + self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': self.user_raoul_id, 'alias_contact': 'partners'}) + p1id = self.res_partner.create(cr, uid, {'name': 'Sylvie Lelitre', 'email': 'test.sylvie.lelitre@agrolait.com'}) + p2id = self.res_partner.create(cr, uid, {'name': 'Other Poilvache', 'email': 'other4@gmail.com'}) + self._init_mock_build_email() + frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other4@gmail.com') sent_emails = self._build_email_kwargs_list # Test: one group created by Raoul self.assertEqual(len(frog_groups), 1, 'message_process: a new mail.group should have been created') @@ -409,24 +362,37 @@ class TestMailgateway(TestMailBase): self.assertEqual(len(frog_group.message_ids), 1, 'message_process: newly created group should have the incoming email in message_ids') msg = frog_group.message_ids[0] - # Test: message: unknown email address -> message has email_from, not author_id + # Test: message: author found self.assertEqual(p1id, msg.author_id.id, 'message_process: message on created group should have Sylvie as author_id') self.assertIn('Sylvie Lelitre ', msg.email_from, 'message_process: message on created group should have have an email_from') - # Test: author (not recipient and not raoul (as alias owner)) added as follower + # Test: author (not recipient and not Raoul (as alias owner)) added as follower frog_follower_ids = set([p.id for p in frog_group.message_follower_ids]) self.assertEqual(frog_follower_ids, set([p1id]), 'message_process: newly created group should have 1 follower (author, not creator, not recipients)') # Test: sent emails: no-one, no bounce effet + sent_emails = self._build_email_kwargs_list self.assertEqual(len(sent_emails), 0, 'message_process: should not bounce incoming emails') # Data: unlink group frog_group.unlink() - # Do: incoming email from a known partner that is also an user that can create a mail.group - self.res_users.create(cr, uid, {'partner_id': p1id, 'login': 'sylvie', 'groups_id': [(6, 0, [self.group_employee_id])]}) - frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other@gmail.com') + # Do: incoming email from a not follower Partner on a Followers only alias -> bounce + self._init_mock_build_email() + self.mail_alias.write(cr, uid, [alias_id], {'alias_user_id': False, 'alias_contact': 'followers'}) + frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other5@gmail.com') + # Test: no group created + self.assertTrue(len(frog_groups) == 0) + # Test: email bounced + sent_emails = self._build_email_kwargs_list + self.assertEqual(len(sent_emails), 1, + 'message_process: incoming email on Partners alias should send a bounce email') + + # Do: incoming email from a parent document follower on a Followers only alias -> ok + self._init_mock_build_email() + self.mail_group.message_subscribe(cr, uid, [self.group_pigs_id], [p1id]) + frog_groups = format_and_process(MAIL_TEMPLATE, to='groups@example.com, other6@gmail.com') # Test: one group created by Raoul (or Sylvie maybe, if we implement it) self.assertEqual(len(frog_groups), 1, 'message_process: a new mail.group should have been created') frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) @@ -438,15 +404,63 @@ class TestMailgateway(TestMailBase): self.assertEqual(frog_follower_ids, set([p1id]), 'message_process: newly created group should have 1 follower (author, not creator, not recipients)') # Test: sent emails: no-one, no bounce effet + sent_emails = self._build_email_kwargs_list self.assertEqual(len(sent_emails), 0, 'message_process: should not bounce incoming emails') # -------------------------------------------------- - # Test2: discussion update + # Test2: update-like alias + # -------------------------------------------------- + + # Do: Pigs alias is restricted, should bounce + self._init_mock_build_email() + self.mail_group.write(cr, uid, [frog_group.id], {'alias_name': 'frogs', 'alias_contact': 'followers', 'alias_force_thread_id': frog_group.id}) + frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com', + msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>', + to='frogs@example.com>', subject='Re: news') + # Test: no group 'Re: news' created, still only 1 Frogs group + self.assertEqual(len(frog_groups), 0, + 'message_process: reply on Frogs should not have created a new group with new subject') + frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')]) + self.assertEqual(len(frog_groups), 1, + 'message_process: reply on Frogs should not have created a duplicate group with old subject') + frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) + # Test: email bounced + sent_emails = self._build_email_kwargs_list + self.assertEqual(len(sent_emails), 1, + 'message_process: incoming email on Followers alias should send a bounce email') + self.assertIn('Re: news', sent_emails[0].get('subject'), + 'message_process: bounce email on Followers alias should contain the original subject') + + # Do: Pigs alias is restricted, should accept Followers + self._init_mock_build_email() + self.mail_group.message_subscribe(cr, uid, [frog_group.id], [p2id]) + frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com', + msg_id='<1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>', + to='frogs@example.com>', subject='Re: cats') + # Test: no group 'Re: news' created, still only 1 Frogs group + self.assertEqual(len(frog_groups), 0, + 'message_process: reply on Frogs should not have created a new group with new subject') + frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')]) + self.assertEqual(len(frog_groups), 1, + 'message_process: reply on Frogs should not have created a duplicate group with old subject') + frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) + # Test: one new message + self.assertEqual(len(frog_group.message_ids), 2, 'message_process: group should contain 2 messages after reply') + # Test: sent emails: 1 (Sylvie copy of the incoming email, but no bounce) + sent_emails = self._build_email_kwargs_list + self.assertEqual(len(sent_emails), 1, + 'message_process: one email should have been generated') + self.assertIn('test.sylvie.lelitre@agrolait.com', sent_emails[0].get('email_to')[0], + 'message_process: email should be sent to Sylvie') + self.mail_group.message_unsubscribe(cr, uid, [frog_group.id], [p2id]) + + # -------------------------------------------------- + # Test3: discussion and replies # -------------------------------------------------- # Do: even with a wrong destination, a reply should end up in the correct thread - frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other@gmail.com', + frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com', msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>', to='erroneous@example.com>', subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id) @@ -458,14 +472,14 @@ class TestMailgateway(TestMailBase): 'message_process: reply on Frogs should not have created a duplicate group with old subject') frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) # Test: one new message - self.assertEqual(len(frog_group.message_ids), 2, 'message_process: group should contain 2 messages after reply') + self.assertEqual(len(frog_group.message_ids), 3, 'message_process: group should contain 2 messages after reply') # Test: author (and not recipient) added as follower frog_follower_ids = set([p.id for p in frog_group.message_follower_ids]) self.assertEqual(frog_follower_ids, set([p1id, p2id]), 'message_process: after reply, group should have 2 followers') # Do: due to some issue, same email goes back into the mailgateway - frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other@gmail.com', + frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com', msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>', subject='Re: news', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id) # Test: no group 'Re: news' created, still only 1 Frogs group @@ -476,20 +490,18 @@ class TestMailgateway(TestMailBase): 'message_process: reply on Frogs should not have created a duplicate group with old subject') frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) # Test: no new message - self.assertEqual(len(frog_group.message_ids), 2, 'message_process: message with already existing message_id should not have been duplicated') + self.assertEqual(len(frog_group.message_ids), 3, 'message_process: message with already existing message_id should not have been duplicated') # Test: message_id is still unique msg_ids = self.mail_message.search(cr, uid, [('message_id', 'ilike', '<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>')]) self.assertEqual(len(msg_ids), 1, 'message_process: message with already existing message_id should not have been duplicated') # -------------------------------------------------- - # Test3: email_from and partner finding + # Test4: email_from and partner finding # -------------------------------------------------- # Data: extra partner with Raoul's email -> test the 'better author finding' extra_partner_id = self.res_partner.create(cr, uid, {'name': 'A-Raoul', 'email': 'test_raoul@email.com'}) - # extra_user_id = self.res_users.create(cr, uid, {'name': 'B-Raoul', 'email': self.user_raoul.email}) - # extra_user_pid = self.res_users.browse(cr, uid, extra_user_id).partner_id.id # Do: post a new message, with a known partner -> duplicate emails -> partner format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ', @@ -534,7 +546,7 @@ class TestMailgateway(TestMailBase): self.res_users.write(cr, uid, self.user_raoul_id, {'email': raoul_email}) # -------------------------------------------------- - # Test4: misc gateway features + # Test5: misc gateway features # -------------------------------------------------- # Do: incoming email with model that does not accepts incoming emails must raise @@ -568,7 +580,7 @@ class TestMailgateway(TestMailBase): frog_group = self.mail_group.browse(cr, uid, frog_groups[0]) msg = frog_group.message_ids[0] # Test: plain text content should be wrapped and stored as html - self.assertEqual(msg.body, '
\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
', + self.assertIn('
\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n
', msg.body, 'message_process: plaintext incoming email incorrectly parsed') @mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm') diff --git a/addons/portal_anonymous/i18n/en_AU.po b/addons/portal_anonymous/i18n/en_AU.po index f5b491b2574..feb30ee1137 100644 --- a/addons/portal_anonymous/i18n/en_AU.po +++ b/addons/portal_anonymous/i18n/en_AU.po @@ -14,7 +14,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2013-06-24 04:43+0000\n" +"X-Launchpad-Export-Date: 2013-06-25 05:14+0000\n" "X-Generator: Launchpad (build 16677)\n" #. module: portal_anonymous diff --git a/addons/portal_event/__init__.py b/addons/portal_event/__init__.py index 60fb460545a..71376240d03 100644 --- a/addons/portal_event/__init__.py +++ b/addons/portal_event/__init__.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# Copyright (C) 2004-TODAY OpenERP SA () # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -18,5 +18,3 @@ # along with this program. If not, see . # ############################################################################## - -import event diff --git a/addons/portal_event/__openerp__.py b/addons/portal_event/__openerp__.py index f97ad81efd9..05d84875647 100644 --- a/addons/portal_event/__openerp__.py +++ b/addons/portal_event/__openerp__.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). +# Copyright (C) 2004-TODAY OpenERP SA () # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -30,11 +30,13 @@ This module adds event menu and features to your portal if event and portal are ========================================================================================== """, 'author': 'OpenERP SA', - 'depends': ['event','portal'], + 'depends': [ + 'event', + 'portal', + ], 'data': [ - 'event_view.xml', - 'security/portal_security.xml', 'portal_event_view.xml', + 'security/portal_security.xml', 'security/ir.model.access.csv', ], 'installable': True, diff --git a/addons/portal_event/event_view.xml b/addons/portal_event/event_view.xml deleted file mode 100644 index 3dec56fa526..00000000000 --- a/addons/portal_event/event_view.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - portal.event.form - event.event - - - - - - - - - - - - - diff --git a/addons/portal_event/security/portal_security.xml b/addons/portal_event/security/portal_security.xml index 926e4fdd0f2..6177ae5944e 100644 --- a/addons/portal_event/security/portal_security.xml +++ b/addons/portal_event/security/portal_security.xml @@ -2,17 +2,21 @@ - - Portal Visible Events - - ['|', ('visibility', '=', 'public'), ('message_follower_ids','in', [user.partner_id.id])] + + Event: portal and anonymous users: public only + + ['|', + ('visibility', '=', 'public'), + ('message_follower_ids', 'in', [user.partner_id.id]) + ] + - - Portal Personal Registrations - - [('user_id','=',user.id)] + + Event/Registration: portal and anonymous users: personal only + + [('user_id', '=', user.id)] diff --git a/addons/portal_project/__openerp__.py b/addons/portal_project/__openerp__.py index d502d278c38..8d24b4a2c84 100644 --- a/addons/portal_project/__openerp__.py +++ b/addons/portal_project/__openerp__.py @@ -30,12 +30,13 @@ This module adds project menu and features (tasks) to your portal if project and ====================================================================================================== """, 'author': 'OpenERP SA', - 'depends': ['project','portal'], + 'depends': ['project', 'portal'], 'data': [ 'security/portal_security.xml', 'security/ir.model.access.csv', 'portal_project_view.xml', ], + 'demo': [], 'installable': True, 'auto_install': True, 'category': 'Hidden', diff --git a/addons/portal_project/project.py b/addons/portal_project/project.py index fb783c8a531..71eb00914e0 100644 --- a/addons/portal_project/project.py +++ b/addons/portal_project/project.py @@ -30,7 +30,7 @@ class portal_project(osv.Model): """ Override to add portal option. """ selection = super(portal_project, self)._get_visibility_selection(cr, uid, context=context) idx = [item[0] for item in selection].index('public') - selection.insert((idx + 1), ('portal', 'Portal Users and Employees')) + selection.insert((idx + 1), ('portal', 'Customer related project: visible through portal')) return selection # return [('public', 'All Users'), # ('portal', 'Portal Users and Employees'), diff --git a/addons/project/i18n/zh_CN.po b/addons/project/i18n/zh_CN.po index b0618c0233d..a544153f2d2 100644 --- a/addons/project/i18n/zh_CN.po +++ b/addons/project/i18n/zh_CN.po @@ -7,14 +7,14 @@ msgstr "" "Project-Id-Version: OpenERP Server 6.0dev\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2012-12-21 17:04+0000\n" -"PO-Revision-Date: 2012-12-14 14:12+0000\n" -"Last-Translator: sum1201 \n" +"PO-Revision-Date: 2013-06-27 03:25+0000\n" +"Last-Translator: Alan \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2013-03-16 04:57+0000\n" -"X-Generator: Launchpad (build 16532)\n" +"X-Launchpad-Export-Date: 2013-06-27 05:36+0000\n" +"X-Generator: Launchpad (build 16681)\n" #. module: project #: view:project.project:0 @@ -82,7 +82,7 @@ msgid "Start Task" msgstr "开始任务" #. module: project -#: code:addons/project/project.py:932 +#: code:addons/project/project.py:944 #, python-format msgid "Warning !" msgstr "警告!" @@ -132,8 +132,8 @@ msgid "" msgstr "保存复杂的摘要(消息数量,……等)。为了插入到看板视图,这一摘要直接是是HTML格式。" #. module: project -#: code:addons/project/project.py:432 -#: code:addons/project/project.py:1318 +#: code:addons/project/project.py:444 +#: code:addons/project/project.py:1332 #, python-format msgid "Warning!" msgstr "警告!" @@ -383,7 +383,7 @@ msgid "Assigned to" msgstr "指派到" #. module: project -#: code:addons/project/project.py:1021 +#: code:addons/project/project.py:1033 #, python-format msgid "Delegated User should be specified" msgstr "应该制定委派用户" @@ -433,7 +433,7 @@ msgstr "使用集成的协作记事簿任务" #: model:mail.message.subtype,name:project.mt_project_task_blocked #: model:mail.message.subtype,name:project.mt_task_blocked msgid "Task Blocked" -msgstr "" +msgstr "延期的任务" #. module: project #: model:process.node,note:project.process_node_opentask0 @@ -453,7 +453,7 @@ msgstr "oe_kanban_text_red" #. module: project #: model:mail.message.subtype,description:project.mt_task_blocked msgid "Task blocked" -msgstr "" +msgstr "延期的任务" #. module: project #: view:project.task:0 @@ -866,7 +866,7 @@ msgstr "公司" #. module: project #: field:project.task.type,fold:0 msgid "Folded by Default" -msgstr "" +msgstr "默认折叠" #. module: project #: field:project.task.history,date:0 @@ -1195,9 +1195,9 @@ msgid "Computed as: Time Spent + Remaining Time." msgstr "计算:花费的时间 + 剩余的时间" #. module: project -#: code:addons/project/project.py:356 -#: code:addons/project/project.py:377 -#: code:addons/project/project.py:709 +#: code:addons/project/project.py:368 +#: code:addons/project/project.py:389 +#: code:addons/project/project.py:721 #, python-format msgid "%s (copy)" msgstr "%s (副本)" @@ -1397,7 +1397,7 @@ msgid "" msgstr "做任务的估计时间.通常由项目经理在任务草稿阶段设定" #. module: project -#: code:addons/project/project.py:220 +#: code:addons/project/project.py:230 #, python-format msgid "Attachments" msgstr "附件" @@ -1447,7 +1447,7 @@ msgstr "剩余的小时数" #. module: project #: model:mail.message.subtype,description:project.mt_task_stage msgid "Stage changed" -msgstr "" +msgstr "任务阶段已改变" #. module: project #: constraint:project.task:0 @@ -1524,7 +1524,7 @@ msgid "Overpassed Tasks" msgstr "拖期任务" #. module: project -#: code:addons/project/project.py:932 +#: code:addons/project/project.py:944 #, python-format msgid "" "Child task still open.\n" @@ -1556,7 +1556,7 @@ msgstr "委派你的任务给其它用户" #. module: project #: model:mail.message.subtype,description:project.mt_task_started msgid "Task started" -msgstr "" +msgstr "已开始的任务" #. module: project #: help:project.task.reevaluate,remaining_hours:0 @@ -1585,7 +1585,7 @@ msgid "CHECK: " msgstr "CHECK: " #. module: project -#: code:addons/project/project.py:432 +#: code:addons/project/project.py:444 #, python-format msgid "You must assign members on the project '%s' !" msgstr "您必须为项目“%s”指定成员!" @@ -1811,7 +1811,7 @@ msgid "Starting Date" msgstr "开始日期" #. module: project -#: code:addons/project/project.py:398 +#: code:addons/project/project.py:410 #: model:ir.actions.act_window,name:project.open_view_project_all #: model:ir.ui.menu,name:project.menu_projects #: view:project.project:0 @@ -1972,7 +1972,7 @@ msgid "Reevaluation Task" msgstr "任务重估" #. module: project -#: code:addons/project/project.py:1318 +#: code:addons/project/project.py:1332 #, python-format msgid "Please delete the project linked with this account first." msgstr "请删除与此科目关联的项目" diff --git a/addons/project/project.py b/addons/project/project.py index 28e4bd6e5d2..587d60ff86f 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -182,10 +182,10 @@ class project(osv.osv): _('You cannot delete a project containing tasks. You can either delete all the project\'s tasks and then delete the project or simply deactivate the project.')) elif proj.alias_id: alias_ids.append(proj.alias_id.id) - res = super(project, self).unlink(cr, uid, ids, context=context) + res = super(project, self).unlink(cr, uid, ids, context=context) mail_alias.unlink(cr, uid, alias_ids, context=context) return res - + def _get_attached_docs(self, cr, uid, ids, field_name, arg, context): res = {} attachment = self.pool.get('ir.attachment') @@ -196,7 +196,7 @@ class project(osv.osv): task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True) res[id] = (project_attachments or 0) + (task_attachments or 0) return res - + def _task_count(self, cr, uid, ids, field_name, arg, context=None): if context is None: context = {} @@ -209,22 +209,21 @@ class project(osv.osv): return res def _get_alias_models(self, cr, uid, context=None): - """Overriden in project_issue to offer more options""" + """ Overriden in project_issue to offer more options """ return [('project.task', "Tasks")] def _get_visibility_selection(self, cr, uid, context=None): """ Overriden in portal_project to offer more options """ - return [('public', 'Public'), - ('employees', 'Employees Only'), - ('followers', 'Followers Only')] + return [('public', 'Public project'), + ('employees', 'Internal project: all employees can access'), + ('followers', 'Private project: followers Only')] def attachment_tree_view(self, cr, uid, ids, context): task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)]) domain = [ - '|', + '|', '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids), - '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids) - ] + '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)] res_id = ids and ids[0] or False return { 'name': _('Attachments'), @@ -237,6 +236,7 @@ class project(osv.osv): 'limit': 80, 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id) } + # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field _alias_models = lambda self, *args, **kwargs: self._get_alias_models(*args, **kwargs) _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs) @@ -370,13 +370,11 @@ class project(osv.osv): default['state'] = 'open' default['line_ids'] = [] default['tasks'] = [] - default.pop('alias_name', None) - default.pop('alias_id', None) proj = self.browse(cr, uid, id, context=context) if not default.get('name', False): default.update(name=_("%s (copy)") % (proj.name)) res = super(project, self).copy(cr, uid, id, default, context) - self.map_tasks(cr,uid,id,res,context) + self.map_tasks(cr, uid, id, res, context=context) return res def duplicate_template(self, cr, uid, ids, context=None): @@ -527,7 +525,7 @@ def Project(): for project in projects: project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,)) for task in project.tasks: - if task.state in ('done','cancelled'): + if task.state in ('done', 'cancelled'): continue p = getattr(project_gantt, 'Task_%d' % (task.id,)) @@ -547,23 +545,18 @@ def Project(): # ------------------------------------------------ def create(self, cr, uid, vals, context=None): - if context is None: context = {} - # Prevent double project creation when 'use_tasks' is checked! - context = dict(context, project_creation_in_progress=True) - mail_alias = self.pool.get('mail.alias') - if not vals.get('alias_id') and vals.get('name', False): - alias_name = vals.pop('alias_name', None) # prevent errors during copy() - alias_id = mail_alias.create_unique_alias(cr, uid, - # Using '+' allows using subaddressing for those who don't - # have a catchall domain setup. - {'alias_name': alias_name or "project+"+short_name(vals['name'])}, - model_name=vals.get('alias_model', 'project.task'), - context=context) - vals['alias_id'] = alias_id - if vals.get('type', False) not in ('template','contract'): + if context is None: + context = {} + # Prevent double project creation when 'use_tasks' is checked + alias management + create_context = dict(context, project_creation_in_progress=True, + alias_model_name=vals.get('alias_model', 'project.task'), alias_parent_model_name=self._name) + + if vals.get('type', False) not in ('template', 'contract'): vals['type'] = 'contract' - project_id = super(project, self).create(cr, uid, vals, context) - mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'project_id': project_id} }, context) + + project_id = super(project, self).create(cr, uid, vals, context=create_context) + project_rec = self.browse(cr, uid, project_id, context=context) + self.pool.get('mail.alias').write(cr, uid, [project_rec.alias_id.id], {'alias_parent_thread_id': project_id, 'alias_defaults': {'project_id': project_id}}, context) return project_id def write(self, cr, uid, ids, vals, context=None): @@ -581,15 +574,15 @@ class task(base_stage, osv.osv): _track = { 'state': { - 'project.mt_task_new': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'], - 'project.mt_task_started': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open', - 'project.mt_task_closed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', + 'project.mt_task_new': lambda self, cr, uid, obj, ctx=None: obj.state in ['new', 'draft'], + 'project.mt_task_started': lambda self, cr, uid, obj, ctx=None: obj.state == 'open', + 'project.mt_task_closed': lambda self, cr, uid, obj, ctx=None: obj.state == 'done', }, 'stage_id': { - 'project.mt_task_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'done', 'open'], + 'project.mt_task_stage': lambda self, cr, uid, obj, ctx=None: obj.state not in ['new', 'draft', 'done', 'open'], }, 'kanban_state': { # kanban state: tracked, but only block subtype - 'project.mt_task_blocked': lambda self, cr, uid, obj, ctx=None: obj['kanban_state'] == 'blocked', + 'project.mt_task_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked', }, } diff --git a/addons/project/project_demo.xml b/addons/project/project_demo.xml index b7dcc2bdb83..b7e1524c2c8 100644 --- a/addons/project/project_demo.xml +++ b/addons/project/project_demo.xml @@ -46,9 +46,12 @@ Research & Development - public + followers project.task + @@ -75,6 +78,7 @@ Website Design Templates + followers project.task employees diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index f6765b3462b..33842a9a19c 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -90,17 +90,6 @@

-
-
- - - + attrs="{'readonly':[('state','in',['close', 'cancelled'])]}" + context="{'default_groups_ref': ['base.group_user', 'project.group_project_manager']}"/> -

- To invoice or setup invoicing and renewal options, go to the related contract: . + +

+ To invoice or setup invoicing and renewal options, go to the related contract: + .

+ +
@@ -146,20 +149,22 @@
- - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -253,7 +258,7 @@

- %% + %%
diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 8bcac4581c3..a69d9356002 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -50,15 +50,15 @@ class project_issue(base_stage, osv.osv): _track = { 'state': { - 'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['new', 'draft'], - 'project_issue.mt_issue_closed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', - 'project_issue.mt_issue_started': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'open', + 'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj.state in ['new', 'draft'], + 'project_issue.mt_issue_closed': lambda self, cr, uid, obj, ctx=None: obj.state == 'done', + 'project_issue.mt_issue_started': lambda self, cr, uid, obj, ctx=None: obj.state == 'open', }, 'stage_id': { - 'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj['state'] not in ['new', 'draft', 'done', 'open'], + 'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj.state not in ['new', 'draft', 'done', 'open'], }, 'kanban_state': { - 'project_issue.mt_issue_blocked': lambda self, cr, uid, obj, ctx=None: obj['kanban_state'] == 'blocked', + 'project_issue.mt_issue_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked', }, } @@ -578,7 +578,7 @@ class project_issue(base_stage, osv.osv): if context is None: context = {} res = super(project_issue, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, content_subtype=content_subtype, **kwargs) - if thread_id: + if thread_id and subtype: self.write(cr, SUPERUSER_ID, thread_id, {'date_action_last': time.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) return res @@ -653,6 +653,14 @@ class project_project(osv.Model): elif vals.get('use_issues') and not vals.get('use_tasks'): vals['alias_model'] = 'project.issue' + def on_change_use_tasks_or_issues(self, cr, uid, ids, use_tasks, use_issues, context=None): + values = {} + if use_tasks and not use_issues: + values['alias_model'] = 'project.task' + elif not use_tasks and use_issues: + values['alias_model'] = 'project.issues' + return {'value': values} + def create(self, cr, uid, vals, context=None): self._check_create_write_values(cr, uid, vals, context=context) return super(project_project, self).create(cr, uid, vals, context=context) diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 9f5dc8135bb..da3034c72f5 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -309,7 +309,8 @@ - + @@ -318,21 +319,12 @@ {'invisible': [('use_tasks', '=', False),('use_issues','=',False)]} + + {'on_change': 'on_change_use_tasks_or_issues(use_tasks, use_issues)'} + - - - - @ - - - - - diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 707012fc063..77151026765 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -162,9 +162,9 @@ class purchase_order(osv.osv): ] _track = { 'state': { - 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirmed', - 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'approved', - 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', + 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed', + 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved', + 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done', }, } _columns = { diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 67c6f82ced3..55bd34009bf 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -34,8 +34,8 @@ class sale_order(osv.osv): _description = "Sales Order" _track = { 'state': { - 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['manual'], - 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj['state'] in ['sent'] + 'sale.mt_order_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state in ['manual'], + 'sale.mt_order_sent': lambda self, cr, uid, obj, ctx=None: obj.state in ['sent'] }, }