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.
+
+
+
+
+
+
+
+
+ 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 @@
Aliasesmail.alias
+ {
+ 'search_default_active': True,
+ }
+
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
+ followersproject.task
+
@@ -75,6 +78,7 @@
Website Design Templates
+ followersproject.taskemployees
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 @@