[FIX] project,hr: improved alias creation/duplication
[odoo/odoo.git] / addons / mail / mail_alias.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (C) 2012 OpenERP S.A. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import re
23 import unicodedata
24
25 from openerp.osv import fields, osv
26 from openerp.tools import ustr
27
28 # Inspired by http://stackoverflow.com/questions/517923
29 def remove_accents(input_str):
30     """Suboptimal-but-better-than-nothing way to replace accented
31     latin letters by an ASCII equivalent. Will obviously change the
32     meaning of input_str and work only for some cases"""
33     input_str = ustr(input_str)
34     nkfd_form = unicodedata.normalize('NFKD', input_str)
35     return u''.join([c for c in nkfd_form if not unicodedata.combining(c)])
36
37 class mail_alias(osv.Model):
38     """A Mail Alias is a mapping of an email address with a given OpenERP Document
39        model. It is used by OpenERP's mail gateway when processing incoming emails
40        sent to the system. If the recipient address (To) of the message matches
41        a Mail Alias, the message will be either processed following the rules
42        of that alias. If the message is a reply it will be attached to the
43        existing discussion on the corresponding record, otherwise a new
44        record of the corresponding model will be created.
45        
46        This is meant to be used in combination with a catch-all email configuration
47        on the company's mail server, so that as soon as a new mail.alias is
48        created, it becomes immediately usable and OpenERP will accept email for it.
49      """
50     _name = 'mail.alias'
51     _description = "Email Aliases"
52     _rec_name = 'alias_name'
53     _order = 'alias_model_id, alias_name'
54
55     def _get_alias_domain(self, cr, uid, ids, name, args, context=None):
56         ir_config_parameter = self.pool.get("ir.config_parameter")
57         domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)   
58         return dict.fromkeys(ids, domain or "")
59
60     _columns = {
61         'alias_name': fields.char('Alias', required=True,
62                             help="The name of the email alias, e.g. 'jobs' "
63                                  "if you want to catch emails for <jobs@example.my.openerp.com>",),
64         'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True,
65                                           help="The model (OpenERP Document Kind) to which this alias "
66                                                "corresponds. Any incoming email that does not reply to an "
67                                                "existing record will cause the creation of a new record "
68                                                "of this model (e.g. a Project Task)",
69                                           # hack to only allow selecting mail_thread models (we might
70                                           # (have a few false positives, though)
71                                           domain="[('field_id.name', '=', 'message_ids')]"),
72         'alias_user_id': fields.many2one('res.users', 'Owner',
73                                            help="The owner of records created upon receiving emails on this alias. "
74                                                 "If this field is not set the system will attempt to find the right owner "
75                                                 "based on the sender (From) address, or will use the Administrator account "
76                                                 "if no system user is found for that address."),
77         'alias_defaults': fields.text('Default Values', required=True,
78                                       help="A Python dictionary that will be evaluated to provide "
79                                            "default values when creating new records for this alias."),
80         'alias_force_thread_id': fields.integer('Record Thread ID',
81                                       help="Optional ID of a thread (record) to which all incoming "
82                                            "messages will be attached, even if they did not reply to it. "
83                                            "If set, this will disable the creation of new records completely."),
84         'alias_domain': fields.function(_get_alias_domain, string="Alias Domain", type='char', size=None),
85     }
86
87     _defaults = {
88         'alias_defaults': '{}',
89         'alias_user_id': lambda self,cr,uid,context: uid,
90         
91         # looks better when creating new aliases - even if the field is informative only
92         'alias_domain': lambda self,cr,uid,context: self._get_alias_domain(cr,1,[1],None,None)[1]
93     }
94
95     _sql_constraints = [
96         ('alias_unique', 'UNIQUE(alias_name)', 'Unfortunately this email alias is already used, please choose a unique one')
97     ]
98
99     def _check_alias_defaults(self, cr, uid, ids, context=None):
100         try:
101             for record in self.browse(cr, uid, ids, context=context):
102                 dict(eval(record.alias_defaults))
103         except Exception:
104             return False
105         return True
106
107     _constraints = [
108         (_check_alias_defaults, '''Invalid expression, it must be a literal python dictionary definition e.g. "{'field': 'value'}"''', ['alias_defaults']),
109     ]
110
111     def name_get(self, cr, uid, ids, context=None):
112         """Return the mail alias display alias_name, inclusing the implicit
113            mail catchall domain from config.
114            e.g. `jobs@openerp.my.openerp.com` or `sales@openerp.my.openerp.com`
115         """
116         return [(record['id'], "%s@%s" % (record['alias_name'], record['alias_domain']))
117                     for record in self.read(cr, uid, ids, ['alias_name', 'alias_domain'], context=context)]
118
119     def _find_unique(self, cr, uid, name, context=None):
120         """Find a unique alias name similar to ``name``. If ``name`` is
121            already taken, make a variant by adding an integer suffix until
122            an unused alias is found.
123         """
124         sequence = None
125         while True:
126             new_name = "%s%s" % (name, sequence) if sequence is not None else name
127             if not self.search(cr, uid, [('alias_name', '=', new_name)]):
128                 break
129             sequence = (sequence + 1) if sequence else 2
130         return new_name
131
132     def create_unique_alias(self, cr, uid, vals, model_name=None, context=None):
133         """Creates an email.alias record according to the values provided in ``vals``,
134         with 2 alterations: the ``alias_name`` value may be suffixed in order to
135         make it unique, and the ``alias_model_id`` value will set to the
136         model ID of the ``model_name`` value, if provided, 
137         """
138         alias_name = re.sub(r'[^\w+]', '_', remove_accents(vals['alias_name'])).lower()
139         alias_name = self._find_unique(cr, uid, alias_name, context=context)
140         vals['alias_name'] = alias_name
141         if model_name:
142             model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0]
143             vals['alias_model_id'] = model_id
144         return self.create(cr, uid, vals, context=context)