[MERGE] [IMP] Alias Improvement

mail_alias:
- alias_name is not required anymore (but still unique),
- added the concept of 'parent of alias' that is the record that owns the alias. For example project+RD is an alias whose parent is a project, and the aliased model is the project.task model (create alias). user+admin is an alias whose parent is the user, and whose aliased record is also the user (update alias).
- added 'alias_contact' selection field that holds the alias settings. Values are:
-- everyone: everyone can contact the alias; the behavior will be the same as before this revision
-- partners: only authenticated partners can contact the alias. If the author of an incoming emails is not a known partner, the email bounces.
-- followers: only followers of the record owning the alias can contact the alias. Otherwise the email bounces. For example on a project only project followers can create new tasks or issues using the alias. On a mail group, only group followers can discuss in the group using the alias.
- improved form view; added 'open document' and 'open parent document' buttons

mail:
- updated message_route to handle alias contact settings. There is notably a new message_route_verify method that performs various checks on the route (message_update / messsage_new attribute, related record, alias contact settings)
- added _find_partner_from_emails method that merge various partner finding methods that were present in mail_thread
- added some mail-related CSS coming from web
- added tests; removed duplicate tests introduced when merging saas-1 into trunk

crm.lead, hr.job, mail.group, res.users, project.project:
- updated create method overrides
- improved alias display in views (form and kanban if exists)

event: moved visibility field (public / employees) from portal_event to event module

bzr revid: tde@openerp.com-20130625094421-b4gv8lkzxn020oeh
This commit is contained in:
Thibault Delavallée 2013-06-25 11:44:21 +02:00
commit 742dc3f77d
32 changed files with 690 additions and 565 deletions

View File

@ -222,17 +222,13 @@ class crm_case_section(osv.osv):
return res
def create(self, cr, uid, vals, context=None):
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
alias_name = vals.pop('alias_name', None) or vals.get('name') # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
{'alias_name': alias_name},
model_name="crm.lead",
context=context)
vals['alias_id'] = alias_id
res = super(crm_case_section, self).create(cr, uid, vals, context)
mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'section_id': res, 'type': 'lead'}}, context)
return res
if context is None:
context = {}
create_context = dict(context, alias_model_name='crm.lead', alias_parent_model_name=self._name)
section_id = super(crm_case_section, self).create(cr, uid, vals, context=create_context)
section = self.browse(cr, uid, section_id, context=context)
self.pool.get('mail.alias').write(cr, uid, [section.alias_id.id], {'alias_parent_thread_id': section_id, 'alias_defaults': {'section_id': section_id, 'type': 'lead'}}, context=context)
return section_id
def unlink(self, cr, uid, ids, context=None):
# Cascade-delete mail aliases as well, as they should not exist without the sales team.

View File

@ -94,7 +94,7 @@
<div class="oe_kanban_content">
<h4 class="oe_center"><field name="name"/></h4>
<div class="oe_kanban_alias oe_center" t-if="record.use_leads.raw_value and record.alias_id.value">
<small><span class="oe_e" style="float: none;">%%</span><t t-raw="record.alias_id.raw_value[1]"/></small>
<small><span class="oe_e oe_e_alias" style="float: none;">%%</span><t t-raw="record.alias_id.raw_value[1]"/></small>
</div>
<div class="oe_items_list">
<div class="oe_salesteams_leads" t-if="record.use_leads.raw_value">
@ -168,17 +168,6 @@
<h1>
<field name="name" string="Salesteam"/>
</h1>
<div name="group_alias"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<label for="alias_id" string="Email Alias"/>
<field name="alias_id" class="oe_inline oe_read_only" required="0" nolabel="1"/>
<span name="edit_alias" class="oe_edit_only">
<field name="alias_name" class="oe_inline"
attrs="{'required': [('use_leads', '=', True), ('alias_id', '!=', False)]}"/>
@
<field name="alias_domain" class="oe_inline" readonly="1"/>
</span>
</div>
<div name="options_active">
<field name="use_leads" class="oe_inline"/><label for="use_leads"/>
</div>
@ -187,12 +176,25 @@
<group>
<field name="user_id"/>
<field name="code"/>
</group>
<group>
<field name="parent_id"/>
<field name="change_responsible"/>
<field name="active"/>
</group>
<group>
<label for="alias_name" string="Email Alias"
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<div name="alias_def"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_id" class="oe_read_only oe_inline"
string="Email Alias" required="0"/>
<div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
</div>
<field name="alias_contact" class="oe_inline"
string="Accept Emails From"
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
</group>
</group>
<notebook colspan="4">
<page string="Team Members">

View File

@ -172,6 +172,12 @@ class event_event(osv.osv):
continue
return res
def _get_visibility_selection(self, cr, uid, context=None):
return [('public', 'All Users'),
('employees', 'Employees Only')]
# Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
_visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs)
_columns = {
'name': fields.char('Name', size=64, required=True, translate=True, readonly=False, states={'done': [('readonly', True)]}),
'user_id': fields.many2one('res.users', 'Responsible User', readonly=False, states={'done': [('readonly', True)]}),
@ -209,11 +215,14 @@ class event_event(osv.osv):
'note': fields.text('Description', readonly=False, states={'done': [('readonly', True)]}),
'company_id': fields.many2one('res.company', 'Company', required=False, change_default=True, readonly=False, states={'done': [('readonly', True)]}),
'is_subscribed' : fields.function(_subscribe_fnc, type="boolean", string='Subscribed'),
'visibility': fields.selection(_visibility_selection, 'Privacy / Visibility',
select=True, required=True),
}
_defaults = {
'state': 'draft',
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'event.event', context=c),
'user_id': lambda obj, cr, uid, context: uid,
'visibility': 'employees',
}
def subscribe_to_event(self, cr, uid, ids, context=None):

View File

@ -76,6 +76,7 @@
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
<field name="visibility"/>
</div>
<group>
<group>

View File

@ -25,25 +25,35 @@
<data noupdate="1">
<!-- Multi - Company Rules -->
<record model="ir.rule" id="event_event_comp_rule">
<field name="name">Event multi-company</field>
<record model="ir.rule" id="event_event_company_rule">
<field name="name">Event: multi-company</field>
<field name="model_id" ref="model_event_event"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
<field name="domain_force">['|',
('company_id', '=', False),
('company_id', 'child_of', [user.company_id.id]),
]
</field>
</record>
<record model="ir.rule" id="event_registration_comp_rule">
<field name="name">Event Registration multi-company</field>
<record model="ir.rule" id="event_registration_company_rule">
<field name="name">Event/Registration: multi-company</field>
<field name="model_id" ref="model_event_registration"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
<field name="domain_force">['|',
('company_id', '=', False),
('company_id', 'child_of', [user.company_id.id]),
]
</field>
</record>
<record model="ir.rule" id="report_event_registration_comp_rule">
<field name="name">Report Event Registration multi-company</field>
<record model="ir.rule" id="report_event_registration_company_rule">
<field name="name">Event/Report Registration: multi-company</field>
<field name="model_id" ref="model_report_event_registration"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
<field name="domain_force">['|',
('company_id', '=', False),
('company_id', 'child_of', [user.company_id.id]),
]
</field>
</record>
</data>

View File

@ -348,12 +348,10 @@
<h1><field name="name" class="oe_inline"/></h1>
</div>
<group>
<group>
<group name="job_data">
<field name="no_of_employee" groups="base.group_user"/>
<field name="no_of_recruitment" on_change="on_change_expected_employee(no_of_recruitment,no_of_employee)"/>
<field name="expected_employees" groups="base.group_user"/>
</group>
<group>
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
<field name="department_id"/>
</group>

View File

@ -506,33 +506,18 @@ class hr_job(osv.osv):
help="Email alias for this job position. New emails will automatically "
"create new applicants for this job position."),
}
_defaults = {
'alias_domain': False, # always hide alias during creation
}
def _auto_init(self, cr, context=None):
"""Installation hook to create aliases for all jobs and avoid constraint errors."""
if context is None:
context = {}
alias_context = dict(context, alias_model_name='hr.applicant')
res = self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job, self)._auto_init,
self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=alias_context)
return res
return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(hr_job, self)._auto_init,
'hr.applicant', self._columns['alias_id'], 'name', alias_prefix='job+', alias_defaults={'job_id': 'id'}, context=context)
def create(self, cr, uid, vals, context=None):
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
vals.pop('alias_name', None) # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': 'jobs+'+vals['name']},
model_name="hr.applicant",
context=context)
vals['alias_id'] = alias_id
res = super(hr_job, self).create(cr, uid, vals, context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
return res
alias_context = dict(context, alias_model_name='hr.applicant', alias_parent_model_name=self._name)
job_id = super(hr_job, self).create(cr, uid, vals, context=alias_context)
job = self.browse(cr, uid, job_id, context=context)
self.pool.get('mail.alias').write(cr, uid, [job.alias_id.id], {'alias_parent_thread_id': job_id, "alias_defaults": {'job_id': job_id}}, context)
return job_id
def unlink(self, cr, uid, ids, context=None):
# Cascade-delete mail aliases as well, as they should not exist without the job position.
@ -550,15 +535,16 @@ class hr_job(osv.osv):
if record.survey_id:
datas['ids'] = [record.survey_id.id]
datas['model'] = 'survey.print'
context.update({'response_id': [0], 'response_no': 0,})
context.update({'response_id': [0], 'response_no': 0})
return {
'type': 'ir.actions.report.xml',
'report_name': 'survey.form',
'datas': datas,
'context' : context,
'nodestroy':True,
'context': context,
'nodestroy': True,
}
class applicant_category(osv.osv):
""" Category of applicant """
_name = "hr.applicant_category"

View File

@ -316,18 +316,19 @@
attrs="{'invisible':[('survey_id','=',False)]}"/>
</div>
</field>
<xpath expr="//div[@class='oe_title']//h1" position="after">
<div name="group_alias"
<xpath expr="//group[@name='job_data']" position="after">
<group name="group_alias"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<label for="alias_id" string="Email Alias"/>
<field name="alias_id" class="oe_inline oe_read_only" required="0" nolabel="1"/>
<span name="edit_alias" class="oe_edit_only">
<field name="alias_name" class="oe_inline"
attrs="{'required': [('alias_id', '!=', False)]}"/>
@
<field name="alias_domain" class="oe_inline" readonly="1"/>
</span>
</div>
<label for="alias_name" string="Email Alias"/>
<div name="alias_def">
<field name="alias_id" class="oe_read_only oe_inline"
string="Email Alias" required="0"/>
<div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
</div>
<field name="alias_contact" class="oe_inline" string="Accept Emails From"/>
</group>
</xpath>
</field>
</record>

View File

@ -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 <jobs@example.my.openerp.com>",),
'alias_name': fields.char('Alias',
help="The name of the email alias, e.g. 'jobs' if you want to catch emails for <jobs@example.my.openerp.com>",),
'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',
}

View File

@ -9,13 +9,23 @@
<field name="arch" type="xml">
<form string="Alias" version="7.0">
<sheet>
<label for="alias_name" class="oe_edit_only"/>
<h2><field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline"/></h2>
<div class="oe_right oe_button_box">
<button name="open_document" string="Open Document"
type="object" class="oe_link"
attrs="{'invisible': ['|', ('alias_model_id', '=', False), ('alias_force_thread_id', '=', 0)]}"/>
<button name="open_parent_document" string="Open Parent Document"
type="object" class="oe_link"
attrs="{'invisible': ['|', ('alias_parent_model_id', '=', False), ('alias_parent_thread_id', '=', 0)]}"/>
</div>
<group>
<field name="alias_model_id"/>
<field name="alias_user_id"/>
<field name="alias_force_thread_id"/>
<field name="alias_defaults"/>
<field name="alias_contact"/>
<field name="alias_user_id"/>
<field name="alias_parent_model_id"/>
<field name="alias_parent_thread_id"/>
</group>
</sheet>
</form>
@ -32,6 +42,7 @@
<field name="alias_model_id"/>
<field name="alias_user_id"/>
<field name="alias_defaults"/>
<field name="alias_contact"/>
</tree>
</field>
</record>
@ -44,8 +55,13 @@
<search string="Search Alias">
<field name="alias_name"/>
<field name="alias_model_id"/>
<field name="alias_force_thread_id"/>
<field name="alias_parent_model_id"/>
<field name="alias_parent_thread_id"/>
<separator/>
<filter string="Active" name="active" domain="[('alias_name', '!=', False)]"/>
<group expand="0" string="Group By...">
<filter string="User" name="User" icon="terp-personal" context="{'group_by':'alias_user_id'}"/>
<filter string="User" name="User" context="{'group_by':'alias_user_id'}"/>
<filter string="Model" name="Model" context="{'group_by':'alias_model_id'}"/>
</group>
</search>
@ -55,6 +71,10 @@
<record id="action_view_mail_alias" model="ir.actions.act_window">
<field name="name">Aliases</field>
<field name="res_model">mail.alias</field>
<field name="context">{
'search_default_active': True,
}
</field>
</record>
<menuitem id="mail_alias_menu"

View File

@ -93,17 +93,16 @@ class mail_group(osv.Model):
'public': 'groups',
'group_public_id': _get_default_employee_group,
'image': _get_default_image,
'alias_domain': False, # always hide alias during creation
}
def _generate_header_description(self, cr, uid, group, context=None):
header = ''
if group.description:
header = '%s' % group.description
if group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain:
if group.alias_id and group.alias_name and group.alias_domain:
if header:
header = '%s<br/>' % header
return '%sGroup email gateway: %s@%s' % (header, group.alias_id.alias_name, group.alias_id.alias_domain)
return '%sGroup email gateway: %s@%s' % (header, group.alias_name, group.alias_domain)
return header
def _subscribe_users(self, cr, uid, ids, context=None):
@ -114,15 +113,8 @@ class mail_group(osv.Model):
self.message_subscribe(cr, uid, ids, partner_ids, context=context)
def create(self, cr, uid, vals, context=None):
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
vals.pop('alias_name', None) # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': "group+" + vals['name']},
model_name=self._name, context=context)
vals['alias_id'] = alias_id
if context is None:
context = {}
# get parent menu
menu_parent = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'mail_group_root')
@ -134,8 +126,10 @@ class mail_group(osv.Model):
vals['menu_id'] = menu_id
# Create group and alias
mail_group_id = super(mail_group, self).create(cr, uid, vals, context=context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
create_context = dict(context, alias_model_name=self._name, alias_parent_model_name=self._name)
mail_group_id = super(mail_group, self).create(cr, uid, vals, context=create_context)
group = self.browse(cr, uid, mail_group_id, context=context)
self.pool.get('mail.alias').write(cr, uid, [group.alias_id.id], {"alias_force_thread_id": mail_group_id, 'alias_parent_thread_id': mail_group_id}, context)
group = self.browse(cr, uid, mail_group_id, context=context)
# Create client action for this group and link the menu to it

View File

@ -44,7 +44,7 @@
<div class="oe_group_details">
<h4><a type="open"><field name="name"/></a></h4>
<div class="oe_kanban_alias" t-if="record.alias_id.value">
<span class="oe_e">%%</span><small><field name="alias_id"/></small>
<span class="oe_e oe_e_alias">%%</span><small><field name="alias_id"/></small>
</div>
<div class="oe_grey">
<field name="description"/>
@ -78,17 +78,19 @@
<label for="name" string="Group Name"/>
</div>
<h1><field name="name" readonly="0"/></h1>
<div name="group_alias"
<group colspan="2" name="group_alias"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<label for="alias_id" string="Email Alias"/>
<field name="alias_id" class="oe_inline oe_read_only" required="0" nolabel="1"/>
<span name="edit_alias" class="oe_edit_only">
<field name="alias_name" class="oe_inline"
attrs="{'required': [('alias_id', '!=', False)]}"/>
@
<field name="alias_domain" class="oe_inline" readonly="1"/>
</span>
</div>
<label for="alias_id" string="%%" class="oe_e oe_e_alias" style="min-width: 20px;"/>
<div name="alias_def">
<field name="alias_id" class="oe_read_only oe_inline"
string="Email Alias" required="0"/>
<div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
</div>
<label for="alias_contact" string="V" class="oe_e oe_e_alias" style="min-width: 20px;"/>
<field name="alias_contact" class="oe_inline" nolabel="1"/>
</group>
</div>
<field name="description" placeholder="Topics discussed in this group..."/>
<div class="oe_clear"/>

View File

@ -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 _("""<p class='oe_view_nocontent_create'>
Click here to add a new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a>
Click here to add new %(document)s or send an email to: <a href='mailto:%(email)s'>%(email)s</a>
</p>
%(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 _("<p class='oe_view_nocontent_create'>Click here to add a new %(document)s</p>%(static_help)s") % {
return _("<p class='oe_view_nocontent_create'>Click here to add new %(document)s</p>%(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
@ -564,6 +563,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 +588,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': '<div><p>Hello,</p>'
'<p>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.</p></div>'
'<blockquote>%s</blockquote>' % (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 +724,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 +735,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 +750,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 +773,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 +803,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 +822,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 +887,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 +912,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 +1052,6 @@ class mail_thread(osv.AbstractModel):
"""
msg_dict = {
'type': 'email',
'author_id': False,
}
if not isinstance(message, Message):
if isinstance(message, unicode):
@ -970,12 +1074,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 +1128,7 @@ class mail_thread(osv.AbstractModel):
partner_id, partner_name<partner_email> 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 +1156,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 +1278,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 +1389,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
#------------------------------------------------------

View File

@ -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')

View File

@ -22,21 +22,25 @@
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<data>
<field name="signature" position="before">
<field name="notification_email_send"/>
</field>
<field name="signature" position="before">
<field name="alias_domain" invisible="1"/>
<field name="alias_id" readonly="1" required="0" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
</field>
<group string="Email preferences" position="after">
<group name="misc" string="Miscellaneous"
groups="base.group_no_one">
<field name="display_groups_suggestions"/>
</group>
</group>
</data>
<data>
<field name="signature" position="before">
<field name="notification_email_send"/>
</field>
<field name="signature" position="before">
<label for="alias_id" string="Messaging Alias" class="oe_read_only"
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<field name="alias_id" class="oe_read_only" required="0" nolabel="1"
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<label for="alias_name" string="Messaging Alias" class="oe_edit_only"
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<div class="oe_edit_only" attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
<field name="alias_contact" string="Alias Accepts Emails From"
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<field name="display_groups_suggestions" groups="base.group_no_one"/>
</field>
</data>
</field>
</record>

View File

@ -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;

View File

@ -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([]);

View File

@ -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)

View File

@ -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')

View File

@ -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 <test@test.fr>'], link_mail=False)[0]
partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul <test@test.fr>'], link_mail=False)[0]
self.assertEqual(partner_info['full_name'], 'Maybe Raoul <test@test.fr>',
'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 <test@test.fr>'], link_mail=False)[0]
partner_info = self.mail_thread.message_partner_info_from_emails(cr, uid, None, ['Maybe Raoul <test@test.fr>'], 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 <test@test.fr>'], link_mail=False)[0]
partner_info = self.mail_group.message_partner_info_from_emails(cr, uid, group_pigs.id, ['Maybe Raoul <test@test.fr>'], 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" <group+pigs@schlouby.fr>',
'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 <test.sylvie.lelitre@agrolait.com>', 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 <test_raoul@email.com>',
@ -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, '<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>',
self.assertIn('<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>', msg.body,
'message_process: plaintext incoming email incorrectly parsed')
@mute_logger('openerp.addons.mail.mail_thread', 'openerp.osv.orm')

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-TODAY OpenERP SA (<http://www.openerp.com>)
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import event

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-TODAY OpenERP SA (<http://www.openerp.com>)
#
# 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,

View File

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, osv
class event_event(osv.osv):
_description = 'Portal event'
_inherit = 'event.event'
"""
``visibility``: defines if the event appears on the portal's event page
- 'public' means the event will appear for everyone (anonymous)
- 'private' means the event won't appear
"""
_columns = {
'visibility': fields.selection([('public', 'Public'),('private', 'Private')],
string='Visibility', help='Event\'s visibility in the portal\'s contact page'),
}
_defaults = {
'visibility': 'private',
}

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- add visibility field to the event form view -->
<record id="view_event_form_portal" model="ir.ui.view">
<field name="name">portal.event.form</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_form"/>
<field name="arch" type="xml">
<xpath expr="//page[last()]" position="after">
<page string="Portal Settings" groups="base.group_user">
<group>
<field name="visibility"/>
</group>
</page>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -2,17 +2,21 @@
<openerp>
<data noupdate="1">
<record id="portal_event_rule" model="ir.rule">
<field name="name">Portal Visible Events</field>
<field ref="event.model_event_event" name="model_id"/>
<field name="domain_force">['|', ('visibility', '=', 'public'), ('message_follower_ids','in', [user.partner_id.id])]</field>
<record model="ir.rule" id="event_event_portal_anonymous_rule">
<field name="name">Event: portal and anonymous users: public only</field>
<field name="model_id" ref="event.model_event_event"/>
<field name="domain_force">['|',
('visibility', '=', 'public'),
('message_follower_ids', 'in', [user.partner_id.id])
]
</field>
<field name="groups" eval="[(4, ref('portal.group_portal')), (4, ref('portal.group_anonymous'))]"/>
</record>
<record id="portal_registration_rule" model="ir.rule">
<field name="name">Portal Personal Registrations</field>
<field ref="event.model_event_registration" name="model_id"/>
<field name="domain_force">[('user_id','=',user.id)]</field>
<record model="ir.rule" id="event_registration_portal_anonymous_rule">
<field name="name">Event/Registration: portal and anonymous users: personal only</field>
<field name="model_id" ref="event.model_event_registration"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('portal.group_portal')), (4, ref('portal.group_anonymous'))]"/>
</record>

View File

@ -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',

View File

@ -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'),

View File

@ -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):

View File

@ -46,9 +46,12 @@
<record id="project_project_2" model="project.project">
<field name="name">Research &amp; Development</field>
<field name="parent_id" ref="all_projects_account"/>
<field name="privacy_visibility">public</field>
<field name="privacy_visibility">followers</field>
<field name="user_id" ref="base.user_demo"/>
<field name="alias_model">project.task</field>
<field name="message_follower_ids" eval="[(6, 0, [
ref('base.user_root'),
ref('base.user_demo')])]"/>
</record>
<!-- We assign after so that default values applies -->
@ -75,6 +78,7 @@
<field name="parent_id" ref="all_projects_account"/>
<field name="name">Website Design Templates</field>
<field name="user_id" ref="base.user_root"/>
<field name="privacy_visibility">followers</field>
<field name="alias_model">project.task</field>
<field name="privacy_visibility">employees</field>
<field name="members" eval="[(4, ref('base.user_root')), (4,ref('base.user_demo'))]"/>

View File

@ -90,17 +90,6 @@
<h1>
<field name="name" string="Project Name"/>
</h1>
<div name="group_alias"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<label for="alias_id" string="Email Alias"/>
<field name="alias_id" class="oe_inline oe_read_only" required="0" nolabel="1"/>
<span name="edit_alias" class="oe_edit_only">
<field name="alias_name" class="oe_inline"
attrs="{'required': [('alias_id', '!=', False)]}"/>
@
<field name="alias_domain" class="oe_inline" readonly="1"/>
</span>
</div>
<div name="options_active">
<field name="use_tasks" class="oe_inline"/>
<label for="use_tasks"/>
@ -113,17 +102,31 @@
</div>
<group>
<group>
<field name="privacy_visibility"/>
<field name="user_id" string="Project Manager"
attrs="{'readonly':[('state','in',['close', 'cancelled'])]}"
context="{'default_groups_ref': ['base.group_user', 'project.group_project_manager']}"/>
</group>
<group>
attrs="{'readonly':[('state','in',['close', 'cancelled'])]}"
context="{'default_groups_ref': ['base.group_user', 'project.group_project_manager']}"/>
<field name="partner_id" on_change="onchange_partner_id(partner_id)"/>
<p colspan="2" attrs="{'invisible': [('analytic_account_id','=',False)]}">
To invoice or setup invoicing and renewal options, go to the related contract: <field name="analytic_account_id" readonly="1" required="0" class="oe_inline"/>.
<span></span>
<p attrs="{'invisible': [('analytic_account_id','=',False)]}">
To invoice or setup invoicing and renewal options, go to the related contract:
<field name="analytic_account_id" readonly="1" required="0" class="oe_inline" nolabel="1"/>.
</p>
</group>
<group name="group_alias"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<label for="alias_name" string="Email Alias"/>
<div name="alias_def">
<field name="alias_id" class="oe_read_only oe_inline"
string="Email Alias" required="0"/>
<div class="oe_edit_only oe_inline" name="edit_alias" style="display: inline;" >
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</div>
</div>
<label for="alias_model" string="Incoming Emails create"/>
<field name="alias_model" class="oe_inline" nolabel="1"/>
<field name="alias_contact" class="oe_inline"
string="Accept Emails From"/>
</group>
</group>
<notebook>
<page string="Team" name="team">
@ -146,20 +149,22 @@
</field>
</page>
<page string="Other Info">
<group>
<group string="Administration" groups="project.group_time_work_estimation_tasks">
<field name="planned_hours" widget="float_time"/>
<field name="effective_hours" widget="float_time"/>
<field name="resource_calendar_id"/>
</group>
<group string="Miscellaneous" name="misc">
<field name="date_start"/>
<field name="date" string="End Date"/>
<field name="priority" groups="base.group_no_one"/>
<field name="active" attrs="{'invisible':[('state','in',['open', 'pending', 'template'])]}"/>
<field name="currency_id" groups="base.group_multi_currency" required="1"/>
<field name="parent_id" string="Parent" help="Append this project to another one using analytic accounts hierarchy" domain="[('id','!=',analytic_account_id)]" context="{'current_model': 'project.project'}" />
</group>
<group string="Administration">
<field name="privacy_visibility" widget="radio"/>
<field name="planned_hours" widget="float_time"
groups="project.group_time_work_estimation_tasks"/>
<field name="effective_hours" widget="float_time"
groups="project.group_time_work_estimation_tasks"/>
<field name="resource_calendar_id"
groups="project.group_time_work_estimation_tasks"/>
</group>
<group string="Miscellaneous" name="misc">
<field name="date_start"/>
<field name="date" string="End Date"/>
<field name="priority" groups="base.group_no_one"/>
<field name="active" attrs="{'invisible':[('state','in',['open', 'pending', 'template'])]}"/>
<field name="currency_id" groups="base.group_multi_currency" required="1"/>
<field name="parent_id" string="Parent" help="Append this project to another one using analytic accounts hierarchy" domain="[('id','!=',analytic_account_id)]" context="{'current_model': 'project.project'}" />
</group>
</page>
<page string="Project Stages" attrs="{'invisible': [('use_tasks', '=', False)]}" name="project_stages">
@ -253,7 +258,7 @@
<div class="oe_kanban_content">
<h4><field name="name"/></h4>
<div class="oe_kanban_alias" t-if="record.alias_id.value">
<span class="oe_e">%%</span><small><field name="alias_id"/></small>
<span class="oe_e oe_e_alias">%%</span><small><field name="alias_id"/></small>
</div>
<div class="oe_kanban_project_list">
<a t-if="record.use_tasks.raw_value" name="%(act_project_project_2_project_task_all)d" type="action" style="margin-right: 10px">

View File

@ -653,6 +653,14 @@ class project_project(osv.Model):
elif vals.get('use_issues') and not vals.get('use_tasks'):
vals['alias_model'] = 'project.issue'
def on_change_use_tasks_or_issues(self, cr, uid, ids, use_tasks, use_issues, context=None):
values = {}
if use_tasks and not use_issues:
values['alias_model'] = 'project.task'
elif not use_tasks and use_issues:
values['alias_model'] = 'project.issues'
return {'value': values}
def create(self, cr, uid, vals, context=None):
self._check_create_write_values(cr, uid, vals, context=context)
return super(project_project, self).create(cr, uid, vals, context=context)

View File

@ -309,7 +309,8 @@
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr='//div[@name="options_active"]' position='inside'>
<field name="use_issues" class="oe_inline"/>
<field name="use_issues" class="oe_inline"
on_change="on_change_use_tasks_or_issues(use_tasks, use_issues)"/>
<label for="use_issues"/>
</xpath>
<xpath expr='//div[@name="buttons"]' position='inside'>
@ -318,21 +319,12 @@
<xpath expr='//page[@name="project_stages"]' position="attributes">
<attribute name="attrs">{'invisible': [('use_tasks', '=', False),('use_issues','=',False)]}</attribute>
</xpath>
<xpath expr='//field[@name="use_tasks"]' position="attributes">
<attribute name="attrs">{'on_change': 'on_change_use_tasks_or_issues(use_tasks, use_issues)'}</attribute>
</xpath>
<field name="priority" position="before">
<field name="project_escalation_id"/>
</field>
<xpath expr='//span[@name="edit_alias"]' position='replace'>
<span class="oe_edit_only" name="edit_alias">
<field name="alias_name" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>
@
<field class="oe_inline" name="alias_domain"/>
<span class="oe_inline"
attrs="{'invisible': ['|', ('use_issues', '!=', True), ('use_tasks', '!=', True)]}">
<label for="alias_model" string="creates"/>
<field name="alias_model" class="oe_inline"/>
</span>
</span>
</xpath>
</field>
</record>