-
-
-
-
-
-
- Send Mail
- crm.send.mail
- form
- form
-
- new
-
-
-
-
-
-
-
-
- crm.mail.reply.form
- crm.send.mail
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Reply to last Mail
- crm.send.mail
- form
- form
-
- new
-
-
-
-
diff --git a/addons/crm/wizard/mail_compose_message.py b/addons/crm/wizard/mail_compose_message.py
new file mode 100644
index 00000000000..512d9ece77a
--- /dev/null
+++ b/addons/crm/wizard/mail_compose_message.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from osv import osv
+from osv import fields
+import tools
+
+SUPPORTED_MODELS = ['crm.lead',]
+
+class mail_compose_message(osv.osv_memory):
+ _inherit = 'mail.compose.message'
+
+ def get_value(self, cr, uid, model, res_id, context=None):
+ """Returns a defaults-like dict with initial values for the composition
+ wizard when sending an email related to the document record identified
+ by ``model`` and ``res_id``.
+
+ Overrides the default implementation to provide more default field values
+ related to the corresponding CRM case.
+
+ :param str model: model name of the document record this mail is related to.
+ :param int res_id: id of the document record this mail is related to.
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
+ """
+ result = super(mail_compose_message, self).get_value(cr, uid, model, res_id, context=context)
+ if model in SUPPORTED_MODELS and res_id:
+ model_obj = self.pool.get(model)
+ data = model_obj.browse(cr, uid , res_id, context)
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ result.update({
+ 'subject' : data.name or False,
+ 'email_to' : data.email_from or False,
+ 'email_from' : user.user_email or tools.config.get('email_from', False),
+ 'body_text' : '\n' + tools.ustr(user.signature),
+ 'email_cc' : tools.ustr(data.email_cc or ''),
+ 'model': model,
+ 'res_id': res_id,
+ 'subtype': 'plain',
+ })
+ if hasattr(data, 'section_id'):
+ result.update({'reply_to' : data.section_id and data.section_id.reply_to or False})
+ return result
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/crm/wizard/wizard_history_event.py b/addons/crm/wizard/wizard_history_event.py
index caaf37a88d8..bb501e0b750 100644
--- a/addons/crm/wizard/wizard_history_event.py
+++ b/addons/crm/wizard/wizard_history_event.py
@@ -32,7 +32,7 @@ def _open_history_event(self, cr, uid, data, context=None):
if id2:
id2 = data_obj.browse(cr, uid, id2, context=context).res_id
res = ''
- if data.get('model',False) and data.get('ids',False):
+ if data.get('model') and data.get('ids'):
model_obj = pooler.get_pool(cr.dbname).get(data['model'])
res = model_obj.browse(cr, uid, data['ids'], context=context)
if len(res):
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index 401c43b0455..9700a5a24b2 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -22,10 +22,12 @@
from osv import fields, osv
from crm import crm
import time
+from crm import wizard
import binascii
import tools
from tools.translate import _
+wizard.mail_compose_message.SUPPORTED_MODELS.append('crm.claim')
CRM_CLAIM_PENDING_STATES = (
crm.AVAILABLE_STATES[2][0], # Cancelled
crm.AVAILABLE_STATES[3][0], # Done
@@ -40,69 +42,69 @@ class crm_claim(crm.crm_case, osv.osv):
_name = "crm.claim"
_description = "Claim"
_order = "priority,date desc"
- _inherit = ['mailgate.thread']
+ _inherit = ['mail.thread']
_columns = {
- 'id': fields.integer('ID', readonly=True),
- 'name': fields.char('Claim Subject', size=128, required=True),
+ 'id': fields.integer('ID', readonly=True),
+ 'name': fields.char('Claim Subject', size=128, required=True),
'action_next': fields.char('Next Action', size=200),
'date_action_next': fields.datetime('Next Action Date'),
- 'description': fields.text('Description'),
- 'resolution': fields.text('Resolution'),
- 'create_date': fields.datetime('Creation Date' , readonly=True),
- 'write_date': fields.datetime('Update Date' , readonly=True),
- 'date_deadline': fields.date('Deadline'),
- 'date_closed': fields.datetime('Closed', readonly=True),
- 'date': fields.datetime('Claim Date'),
- 'ref' : fields.reference('Reference', selection=crm._links_get, size=128),
+ 'description': fields.text('Description'),
+ 'resolution': fields.text('Resolution'),
+ 'create_date': fields.datetime('Creation Date' , readonly=True),
+ 'write_date': fields.datetime('Update Date' , readonly=True),
+ 'date_deadline': fields.date('Deadline'),
+ 'date_closed': fields.datetime('Closed', readonly=True),
+ 'date': fields.datetime('Claim Date'),
+ 'ref' : fields.reference('Reference', selection=crm._links_get, size=128),
'categ_id': fields.many2one('crm.case.categ', 'Category', \
domain="[('section_id','=',section_id),\
- ('object_id.model', '=', 'crm.claim')]"),
- 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
+ ('object_id.model', '=', 'crm.claim')]"),
+ 'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority'),
'type_action': fields.selection([('correction','Corrective Action'),('prevention','Preventive Action')], 'Action Type'),
- 'user_id': fields.many2one('res.users', 'Responsible'),
- 'user_fault': fields.char('Trouble Responsible', size=64),
+ 'user_id': fields.many2one('res.users', 'Responsible'),
+ 'user_fault': fields.char('Trouble Responsible', size=64),
'section_id': fields.many2one('crm.case.section', 'Sales Team', \
select=True, help="Sales team to which Case belongs to."\
"Define Responsible user and Email account for"\
- " mail gateway."),
- 'company_id': fields.many2one('res.company', 'Company'),
- 'partner_id': fields.many2one('res.partner', 'Partner'),
+ " mail gateway."),
+ 'company_id': fields.many2one('res.company', 'Company'),
+ 'partner_id': fields.many2one('res.partner', 'Partner'),
'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
# domain="[('partner_id','=',partner_id)]"
- ),
- 'email_cc': fields.text('Watchers Emails', size=252, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
- 'email_from': fields.char('Email', size=128, help="These people will receive email."),
- 'partner_phone': fields.char('Phone', size=32),
+ ),
+ 'email_cc': fields.text('Watchers Emails', size=252, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
+ 'email_from': fields.char('Email', size=128, help="These people will receive email."),
+ 'partner_phone': fields.char('Phone', size=32),
'stage_id': fields.many2one ('crm.case.stage', 'Stage', domain="[('section_ids','=',section_id)]"),
- 'cause': fields.text('Root Cause'),
- 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
+ 'cause': fields.text('Root Cause'),
+ 'state': fields.selection(crm.AVAILABLE_STATES, 'State', size=16, readonly=True,
help='The state is set to \'Draft\', when a case is created.\
\nIf the case is in progress the state is set to \'Open\'.\
\nWhen the case is over, the state is set to \'Done\'.\
- \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
- 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
+ \nIf the case needs to be reviewed then the state is set to \'Pending\'.'),
+ 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
}
_defaults = {
- 'user_id': crm.crm_case._get_default_user,
- 'partner_id': crm.crm_case._get_default_partner,
- 'partner_address_id': crm.crm_case._get_default_partner_address,
- 'email_from':crm.crm_case. _get_default_email,
- 'state': lambda *a: 'draft',
- 'section_id': crm.crm_case._get_section,
+ 'user_id': crm.crm_case._get_default_user,
+ 'partner_id': crm.crm_case._get_default_partner,
+ 'partner_address_id': crm.crm_case._get_default_partner_address,
+ 'email_from':crm.crm_case. _get_default_email,
+ 'state': lambda *a: 'draft',
+ 'section_id':crm.crm_case. _get_section,
'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
- 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.case', context=c),
+ 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.case', context=c),
'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
}
-
+
def onchange_partner_id(self, cr, uid, ids, part, email=False):
"""This function returns value of partner address based on partner
- @param part: Partner's id
- @email: Partner's email ID
+ :param part: Partner's id
+ :param email: ignored
"""
if not part:
return {'value': {'partner_address_id': False,
- 'email_from': False,
+ 'email_from': False,
'partner_phone': False,
'partner_mobile': False
}}
@@ -113,8 +115,8 @@ class crm_claim(crm.crm_case, osv.osv):
def onchange_partner_address_id(self, cr, uid, ids, add, email=False):
"""This function returns value of partner email based on Partner Address
- @param add: Id of Partner's address
- @email: Partner's email ID
+ :param part: Partner's id
+ :param email: ignored
"""
if not add:
return {'value': {'email_from': False}}
@@ -122,6 +124,7 @@ class crm_claim(crm.crm_case, osv.osv):
return {'value': {'email_from': address.email, 'partner_phone': address.phone, 'partner_mobile': address.mobile}}
def case_open(self, cr, uid, ids, *args):
+ """Opens Claim"""
for l in self.browse(cr, uid, ids):
# When coming from draft override date and stage otherwise just set state
if l.state == 'draft':
@@ -135,16 +138,13 @@ class crm_claim(crm.crm_case, osv.osv):
res = super(crm_claim, self).case_open(cr, uid, ids, *args)
return res
- def message_new(self, cr, uid, msg, context=None):
- """ Automatically calls when new email message arrives
- """
- mailgate_pool = self.pool.get('email.server.tools')
-
- subject = msg.get('subject') or _("No Subject")
- body = msg.get('body')
+ def message_new(self, cr, uid, msg, custom_values=None, context=None):
+ """Automatically called when new email message arrives"""
+ res_id = super(crm_claim,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ subject = msg.get('subject')
+ body = msg.get('body_text')
msg_from = msg.get('from')
priority = msg.get('priority')
-
vals = {
'name': subject,
'email_from': msg_from,
@@ -152,35 +152,18 @@ class crm_claim(crm.crm_case, osv.osv):
'description': body,
'user_id': False,
}
- if msg.get('priority', False):
+ if priority:
vals['priority'] = priority
+ vals.update(self.message_partner_by_email(cr, uid, msg.get('from', False)))
+ self.write(cr, uid, [res_id], vals, context=context)
+ return res_id
- res = mailgate_pool.get_partner(cr, uid, msg.get('from') or msg.get_unixfrom())
- if res:
- vals.update(res)
-
- res = self.create(cr, uid, vals, context)
- attachents = msg.get('attachments', [])
- for attactment in attachents or []:
- data_attach = {
- 'name': attactment,
- 'datas':binascii.b2a_base64(str(attachents.get(attactment))),
- 'datas_fname': attactment,
- 'description': 'Mail attachment',
- 'res_model': self._name,
- 'res_id': res,
- }
- self.pool.get('ir.attachment').create(cr, uid, data_attach)
-
- return res
-
- def message_update(self, cr, uid, ids, vals={}, msg="", default_act='pending', context=None):
- """
- @param ids: List of update mail’s IDs
- """
+ def message_update(self, cr, uid, ids, msg, vals={}, default_act='pending', context=None):
if isinstance(ids, (str, int, long)):
ids = [ids]
+ res_id = super(crm_claim,self).message_update(cr, uid, ids, msg, context=context)
+
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
vals['priority'] = msg.get('priority')
@@ -190,7 +173,7 @@ class crm_claim(crm.crm_case, osv.osv):
'probability':'probability'
}
vls = {}
- for line in msg['body'].split('\n'):
+ for line in msg['body_text'].split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
@@ -208,19 +191,10 @@ class crm_claim(crm.crm_case, osv.osv):
res = self.write(cr, uid, [case.id], values, context=context)
return res
- def msg_send(self, cr, uid, id, *args, **argv):
- """ Send The Message
- @param ids: List of email’s IDs
- """
- return True
-
-crm_claim()
-
class res_partner(osv.osv):
_inherit = 'res.partner'
_columns = {
'claims_ids': fields.one2many('crm.claim', 'partner_id', 'Claims'),
}
-res_partner()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml
index 72dcabf7c9a..21ec4cf028e 100644
--- a/addons/crm_claim/crm_claim_view.xml
+++ b/addons/crm_claim/crm_claim_view.xml
@@ -70,15 +70,10 @@
-
-
-
-
-
@@ -159,12 +154,12 @@
-
-
+
+
-
-
+
@@ -82,7 +80,7 @@
Define the delivery methods you are using and their pricing in order to reinvoice the delivery costs when you are doing invoicing based on delivery orders
-
+
@@ -132,7 +130,7 @@
tree,formThe delivery price list allows you to compute the cost and sales price of the delivery according to the weight of the products and other criteria. You can define several price lists for one delivery method, per country or a zone in a specific country defined by a postal code range.
-
+
delivery.grid.line.form
diff --git a/addons/document/document.py b/addons/document/document.py
index 69e123b87eb..ef9ca01bd1e 100644
--- a/addons/document/document.py
+++ b/addons/document/document.py
@@ -252,7 +252,7 @@ class document_file(osv.osv):
ids = ids2
if 'file_size' in vals: # only write that field using direct SQL calls
del vals['file_size']
- if len(ids) and len(vals):
+ if ids and vals:
result = super(document_file,self).write(cr, uid, ids, vals, context=context)
cr.commit() # ?
return result
diff --git a/addons/document_ftp/ftpserver/abstracted_fs.py b/addons/document_ftp/ftpserver/abstracted_fs.py
index b8ce97a7017..83cd1cde725 100644
--- a/addons/document_ftp/ftpserver/abstracted_fs.py
+++ b/addons/document_ftp/ftpserver/abstracted_fs.py
@@ -11,6 +11,7 @@ import fnmatch
import pooler
import netsvc
+import sql_db
from service import security
from osv import osv
@@ -68,7 +69,7 @@ class abstracted_fs(object):
db, cr = None, None
try:
try:
- db = pooler.get_db(db_name)
+ db = sql_db.db_connect(db_name)
cr = db.cursor()
cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
if not cr.fetchone():
diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py
index 5e01bf6a66c..d21dcdc0335 100644
--- a/addons/document_webdav/dav_fs.py
+++ b/addons/document_webdav/dav_fs.py
@@ -19,6 +19,7 @@
#
##############################################################################
import pooler
+import sql_db
import os
import time
@@ -361,7 +362,7 @@ class openerp_dav_handler(dav_interface):
for db_name in result:
cr = None
try:
- db = pooler.get_db(db_name)
+ db = sql_db.db_connect(db_name)
cr = db.cursor()
cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
res=cr.fetchone()
diff --git a/addons/email_template/__init__.py b/addons/email_template/__init__.py
index dd87232679d..97931ea8bd5 100644
--- a/addons/email_template/__init__.py
+++ b/addons/email_template/__init__.py
@@ -3,7 +3,7 @@
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2009 Sharoon Thomas
-# Copyright (C) 2010-2010 OpenERP SA ()
+# Copyright (C) 2010-Today OpenERP SA ()
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,9 +19,7 @@
# along with this program. If not, see
#
##############################################################################
-
-import email_template_account
import email_template
-import email_template_mailbox
import wizard
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py
index f56062f37ed..e5f83f9ac0c 100644
--- a/addons/email_template/__openerp__.py
+++ b/addons/email_template/__openerp__.py
@@ -3,7 +3,7 @@
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2009 Sharoon Thomas
-# Copyright (C) 2010-2010 OpenERP SA ()
+# Copyright (C) 2010-Today OpenERP SA ()
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,32 +21,47 @@
##############################################################################
{
- "name" : "Email Template for OpenERP",
- "version" : "0.7 RC",
+ "name" : "Email Templates",
+ "version" : "1.1",
"author" : "Openlabs",
"website" : "http://openerp.com",
"category" : "Tools",
'complexity': "expert",
- "depends" : ['marketing', 'base_tools'],
+ "depends" : ['mail'],
"description": """
-Email Template is extraction of Power Email basically just to send emails.
-==========================================================================
+Email Templating (simplified version of the original Power Email by Openlabs)
+=============================================================================
-You can define email accounts(server, port, mail format - HTML/Text/Both)
-and email templates (resource, recipient, subject, body, attachments).
+Lets you design complete email templates related to any OpenERP document (Sale
+Orders, Invoices and so on), including sender, recipient, subject, body (HTML and
+Text). You may also automatically attach files to your templates, or print and
+attach a report.
+
+For advanced use, the templates may include dynamic attributes of the document
+they are related to. For example, you may use the name of a Partner's country
+when writing to them, also providing a safe default in case the attribute is
+not defined. Each template contains a built-in assistant to help with the
+inclusion of these dynamic values.
+
+If you enable the option, a composition assistant will also appear in the sidebar
+of the OpenERP documents to which the template applies (e.g. Invoices).
+This serves as a quick way to send a new email based on the template, after
+reviewing and adapting the contents, if needed.
+This composition assistant will also turn into a mass mailing system when called
+for multiple documents at once.
+
+These email templates are also at the heart of the marketing campaign system
+(see the ``marketing_campaign`` application), if you need to automate larger
+campaigns on any OpenERP document.
+
+Technical note: only the templating system of the original Power Email by
+Openlabs was kept
-For each email template, you can have OpenERP generate a Wizard Action / Button
-that will be related to the object. So if you choose to do marketing campaigns
-for leads, the action will be added to the right side panel of the Lead form.
""",
- "init_xml": ['email_template_scheduler_data.xml'],
- "update_xml": [
- 'security/email_template_security.xml',
- 'email_template_workflow.xml',
- 'email_template_account_view.xml',
+ "data": [
+ 'wizard/email_template_preview_view.xml',
'email_template_view.xml',
- 'email_template_mailbox_view.xml',
- 'wizard/email_template_send_wizard_view.xml',
+ 'wizard/email_compose_message_view.xml',
'security/ir.model.access.csv'
],
"installable": True,
diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py
index aabc211df11..3917b3f2ddb 100644
--- a/addons/email_template/email_template.py
+++ b/addons/email_template/email_template.py
@@ -3,7 +3,7 @@
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2009 Sharoon Thomas
-# Copyright (C) 2010-2010 OpenERP SA ()
+# Copyright (C) 2010-Today OpenERP SA ()
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -21,277 +21,186 @@
##############################################################################
import base64
-import random
-import netsvc
import logging
-import re
-TEMPLATE_ENGINES = []
-
-from osv import osv, fields
+import netsvc
+from osv import osv
+from osv import fields
+import tools
from tools.translate import _
try:
from mako.template import Template as MakoTemplate
- TEMPLATE_ENGINES.append(('mako', 'Mako Templates'))
except ImportError:
- logging.getLogger('init').warning("module email_template: Mako templates not installed")
-
-try:
- from django.template import Context, Template as DjangoTemplate
- #Workaround for bug:
- #http://code.google.com/p/django-tagging/issues/detail?id=110
- from django.conf import settings
- settings.configure()
- #Workaround ends
- TEMPLATE_ENGINES.append(('django', 'Django Template'))
-except ImportError:
- logging.getLogger('init').warning("module email_template: Django templates not installed")
-
-import tools
-import pooler
-import logging
-
-def get_value(cursor, user, recid, message=None, template=None, context=None):
- """
- Evaluates an expression and returns its value
- @param cursor: Database Cursor
- @param user: ID of current user
- @param recid: ID of the target record under evaluation
- @param message: The expression to be evaluated
- @param template: BrowseRecord object of the current template
- @param context: OpenERP Context
- @return: Computed message (unicode) or u""
- """
- pool = pooler.get_pool(cursor.dbname)
- if message is None:
- message = {}
- #Returns the computed expression
- if message:
- try:
- message = tools.ustr(message)
- object = pool.get(template.model_int_name).browse(cursor, user, recid, context=context)
- env = {
- 'user':pool.get('res.users').browse(cursor, user, user, context=context),
- 'db':cursor.dbname
- }
- if template.template_language == 'mako':
- templ = MakoTemplate(message, input_encoding='utf-8')
- reply = MakoTemplate(message).render_unicode(object=object,
- peobject=object,
- env=env,
- format_exceptions=True)
- elif template.template_language == 'django':
- templ = DjangoTemplate(message)
- env['object'] = object
- env['peobject'] = object
- reply = templ.render(Context(env))
- return reply or False
- except Exception:
- logging.exception("can't render %r", message)
- return u""
- else:
- return message
+ logging.getLogger('init').warning("email_template: mako templates not available, templating features will not work!")
class email_template(osv.osv):
- "Templates for sending Email"
-
+ "Templates for sending email"
+ _inherit = 'mail.message'
_name = "email.template"
- _description = 'Email Templates for Models'
+ _description = 'Email Templates'
+ _rec_name = 'name' # override mail.message's behavior
- def change_model(self, cursor, user, ids, object_name, context=None):
- if object_name:
- mod_name = self.pool.get('ir.model').read(
- cursor,
- user,
- object_name,
- ['model'], context)['model']
+ def render_template(self, cr, uid, template, model, res_id, context=None):
+ """Render the given template text, replace mako expressions ``${expr}``
+ with the result of evaluating these expressions with
+ an evaluation context containing:
+
+ * ``user``: browse_record of the current user
+ * ``object``: browse_record of the document record this mail is
+ related to
+ * ``context``: the context passed to the mail composition wizard
+
+ :param str template: the template text to render
+ :param str model: model name of the document record this mail is related to.
+ :param int res_id: id of the document record this mail is related to.
+ """
+ if not template: return u""
+ try:
+ template = tools.ustr(template)
+ record = None
+ if res_id:
+ record = self.pool.get(model).browse(cr, uid, res_id, context=context)
+ user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ result = MakoTemplate(template).render_unicode(object=record,
+ user=user,
+ # context kw would clash with mako internals
+ ctx=context,
+ format_exceptions=True)
+ if result == u'False':
+ result = u''
+ return result
+ except Exception:
+ logging.exception("failed to render mako template value %r", template)
+ return u""
+
+ def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+ if context is None:
+ context = {}
+ if not template_id:
+ return False
+ template = self.browse(cr, uid, template_id, context)
+ lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
+ if lang:
+ # Use translated template if necessary
+ ctx = context.copy()
+ ctx['lang'] = lang
+ template = self.browse(cr, uid, template.id, ctx)
else:
- mod_name = False
- return {
- 'value':{'model_int_name':mod_name}
- }
+ template = self.browse(cr, uid, int(template_id), context)
+ return template
+
+ def onchange_model_id(self, cr, uid, ids, model_id, context=None):
+ mod_name = False
+ if model_id:
+ mod_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
+ return {'value':{'model': mod_name}}
_columns = {
- 'name' : fields.char('Name', size=100, required=True),
- 'object_name':fields.many2one('ir.model', 'Resource'),
- 'model_int_name':fields.char('Model Internal Name', size=200,),
- 'from_account':fields.many2one(
- 'email_template.account',
- string="Email Account",
- help="Emails will be sent from this approved account."),
- 'def_to':fields.char(
- 'Recipient (To)',
- size=250,
- help="The Recipient of email. "
- "Placeholders can be used here. "
- "e.g. ${object.email_to}"),
- 'def_cc':fields.char(
- 'CC',
- size=250,
- help="Carbon Copy address(es), comma-separated."
- " Placeholders can be used here. "
- "e.g. ${object.email_cc}"),
- 'def_bcc':fields.char(
- 'BCC',
- size=250,
- help="Blind Carbon Copy address(es), comma-separated."
- " Placeholders can be used here. "
- "e.g. ${object.email_bcc}"),
- 'reply_to':fields.char('Reply-To',
- size=250,
- help="The address recipients should reply to,"
- " if different from the From address."
- " Placeholders can be used here. "
- "e.g. ${object.email_reply_to}"),
- 'message_id':fields.char('Message-ID',
- size=250,
- help="Specify the Message-ID SMTP header to use in outgoing emails. Please note that this overrides the Resource tracking option! Placeholders can be used here."),
- 'track_campaign_item':fields.boolean('Resource Tracking',
- help="Enable this is you wish to include a special \
-tracking marker in outgoing emails so you can identify replies and link \
-them back to the corresponding resource record. \
-This is useful for CRM leads for example"),
- 'lang':fields.char(
- 'Language',
- size=250,
- help="The default language for the email."
- " Placeholders can be used here. "
- "eg. ${object.partner_id.lang}"),
- 'def_subject':fields.char(
- 'Subject',
- size=200,
- help="The subject of email."
- " Placeholders can be used here.",
- translate=True),
- 'def_body_text':fields.text(
- 'Standard Body (Text)',
- help="The text version of the mail",
- translate=True),
- 'def_body_html':fields.text(
- 'Body (Text-Web Client Only)',
- help="The text version of the mail",
- translate=True),
- 'use_sign':fields.boolean(
- 'Signature',
- help="the signature from the User details"
- " will be appended to the mail"),
- 'file_name':fields.char(
- 'Report Filename',
- size=200,
- help="Name of the generated report file. Placeholders can be used in the filename. eg: 2009_SO003.pdf",
- translate=True),
- 'report_template':fields.many2one(
- 'ir.actions.report.xml',
- 'Report to send'),
- 'attachment_ids': fields.many2many(
- 'ir.attachment',
- 'email_template_attachment_rel',
- 'email_template_id',
- 'attachment_id',
- 'Attached Files',
- help="You may attach existing files to this template, "
- "so they will be added in all emails created from this template"),
- 'ref_ir_act_window':fields.many2one(
- 'ir.actions.act_window',
- 'Window Action',
- help="Action that will open this email template on Resource records",
- readonly=True),
- 'ref_ir_value':fields.many2one(
- 'ir.values',
- 'Wizard Button',
- help="Button in the side bar of the form view of this Resource that will invoke the Window Action",
- readonly=True),
- 'allowed_groups':fields.many2many(
- 'res.groups',
- 'template_group_rel',
- 'templ_id', 'group_id',
- string="Allowed User Groups",
- help="Only users from these groups will be"
- " allowed to send mails from this Template"),
- 'model_object_field':fields.many2one(
- 'ir.model.fields',
- string="Field",
- help="Select the field from the model you want to use."
- "\nIf it is a relationship field you will be able to "
- "choose the nested values in the box below\n(Note:If "
- "there are no values make sure you have selected the"
- " correct model)",
- store=False),
- 'sub_object':fields.many2one(
- 'ir.model',
- 'Sub-model',
- help='When a relation field is used this field'
- ' will show you the type of field you have selected',
- store=False),
- 'sub_model_object_field':fields.many2one(
- 'ir.model.fields',
- 'Sub Field',
- help="When you choose relationship fields "
- "this field will specify the sub value you can use.",
- store=False),
- 'null_value':fields.char(
- 'Null Value',
- help="This Value is used if the field is empty",
- size=50, store=False),
- 'copyvalue':fields.char(
- 'Expression',
- size=100,
- help="Copy and paste the value in the "
- "location you want to use a system value.",
- store=False),
- 'table_html':fields.text(
- 'HTML code',
- help="Copy this html code to your HTML message"
- " body for displaying the info in your mail.",
- store=False),
- #Template language(engine eg.Mako) specifics
- 'template_language':fields.selection(
- TEMPLATE_ENGINES,
- 'Templating Language',
- required=True
- )
+ 'name': fields.char('Name', size=250),
+ 'model_id': fields.many2one('ir.model', 'Related document model'),
+ 'lang': fields.char('Language Selection', size=250,
+ help="Optional translation language (ISO code) to select when sending out an email. "
+ "If not set, the english version will be used. "
+ "This should usually be a placeholder expression "
+ "that provides the appropriate language code, e.g. "
+ "${object.partner_id.lang.code}."),
+ 'user_signature': fields.boolean('Add Signature',
+ help="If checked, the user's signature will be appended to the text version "
+ "of the message"),
+ 'report_name': fields.char('Report Filename', size=200, translate=True,
+ help="Name to use for the generated report file (may contain placeholders)\n"
+ "The extension can be omitted and will then come from the report type."),
+ 'report_template':fields.many2one('ir.actions.report.xml', 'Optional report to print and attach'),
+ 'ref_ir_act_window':fields.many2one('ir.actions.act_window', 'Sidebar action', readonly=True,
+ help="Sidebar action to make this template available on records "
+ "of the related document model"),
+ 'ref_ir_value':fields.many2one('ir.values', 'Sidebar button', readonly=True,
+ help="Sidebar button to open the sidebar action"),
+ 'track_campaign_item': fields.boolean('Resource Tracking',
+ help="Enable this is you wish to include a special tracking marker "
+ "in outgoing emails so you can identify replies and link "
+ "them back to the corresponding resource record. "
+ "This is useful for CRM leads for example"),
+
+ # Overridden mail.message.common fields for technical reasons:
+ 'model': fields.related('model_id','model', type='char', string='Related Document model',
+ size=128, select=True, store=True, readonly=True),
+ # we need a separate m2m table to avoid ID collisions with the original mail.message entries
+ 'attachment_ids': fields.many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
+ 'attachment_id', 'Files to attach',
+ help="You may attach files to this template, to be added to all "
+ "emails created from this template"),
+
+ # Overridden mail.message.common fields to make tooltips more appropriate:
+ 'subject':fields.char('Subject', size=512, translate=True, help="Subject (placeholders may be used here)",),
+ 'email_from': fields.char('From', size=128, help="Sender address (placeholders may be used here)"),
+ 'email_to': fields.char('To', size=256, help="Comma-separated recipient addresses (placeholders may be used here)"),
+ 'email_cc': fields.char('Cc', size=256, help="Carbon copy recipients (placeholders may be used here)"),
+ 'email_bcc': fields.char('Bcc', size=256, help="Blind carbon copy recipients (placeholders may be used here)"),
+ 'reply_to': fields.char('Reply-To', size=250, help="Preferred response address (placeholders may be used here)"),
+ 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
+ help="Optional preferred server for outgoing mails. If not set, the highest "
+ "priority one will be used."),
+ 'body_text': fields.text('Text contents', translate=True, help="Plaintext version of the message (placeholders may be used here)"),
+ 'body_html': fields.text('Rich-text contents', help="Rich-text/HTML version of the message (placeholders may be used here)"),
+ 'message_id': fields.char('Message-Id', size=256, help="Message-ID SMTP header to use in outgoing messages based on this template. "
+ "Please note that this overrides the 'Resource Tracking' option, "
+ "so if you simply need to track replies to outgoing emails, enable "
+ "that option instead.\n"
+ "Placeholders must be used here, as this value always needs to be unique!"),
+
+ # Fake fields used to implement the placeholder assistant
+ 'model_object_field': fields.many2one('ir.model.fields', string="Field",
+ help="Select target field from the related document model.\n"
+ "If it is a relationship field you will be able to select "
+ "a target field at the destination of the relationship."),
+ 'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
+ help="When a relationship field is selected as first field, "
+ "this field shows the document model the relationship goes to."),
+ 'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
+ help="When a relationship field is selected as first field, "
+ "this field lets you select the target field within the "
+ "destination document model (sub-model)."),
+ 'null_value': fields.char('Null value', help="Optional value to use if the target field is empty", size=128),
+ 'copyvalue': fields.char('Expression', size=256, help="Final placeholder expression, to be copy-pasted in the desired template field."),
}
_defaults = {
- 'template_language' : lambda *a:'mako',
-
+ 'track_campaign_item': True
}
- _sql_constraints = [
- ('name', 'unique (name)','The template name must be unique !')
- ]
-
def create_action(self, cr, uid, ids, context=None):
vals = {}
- if context is None:
- context = {}
- template_obj = self.browse(cr, uid, ids, context=context)[0]
- src_obj = template_obj.object_name.model
- vals['ref_ir_act_window'] = self.pool.get('ir.actions.act_window').create(cr, uid, {
- 'name': template_obj.name,
- 'type': 'ir.actions.act_window',
- 'res_model': 'email_template.send.wizard',
- 'src_model': src_obj,
- 'view_type': 'form',
- 'context': "{'src_model':'%s','template_id':'%d','src_rec_id':active_id,'src_rec_ids':active_ids}" % (src_obj, template_obj.id),
- 'view_mode':'form,tree',
- 'view_id': self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'email_template.send.wizard.form')], context=context)[0],
- 'target': 'new',
- 'auto_refresh':1
- }, context)
- ir_values_obj = self.pool.get('ir.values')
- vals['ref_ir_value'] = ir_values_obj.create(cr, uid, {
- 'name': _('Send Mail (%s)') % template_obj.name,
- 'model': src_obj,
- 'key2': 'client_action_multi',
- 'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
- 'object': True,
- }, context)
+ action_obj = self.pool.get('ir.actions.act_window')
+ data_obj = self.pool.get('ir.model.data')
+ for template in self.browse(cr, uid, ids, context=context):
+ src_obj = template.model_id.model
+ model_data_id = data_obj._get_id(cr, uid, 'mail', 'email_compose_message_wizard_form')
+ res_id = data_obj.browse(cr, uid, model_data_id, context=context).res_id
+ vals['ref_ir_act_window'] = action_obj.create(cr, uid, {
+ 'name': template.name,
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'mail.compose.message',
+ 'src_model': src_obj,
+ 'view_type': 'form',
+ 'context': "{'mail.compose.message.mode':'mass_mail'}",
+ 'view_mode':'form,tree',
+ 'view_id': res_id,
+ 'target': 'new',
+ 'auto_refresh':1
+ }, context)
+ vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
+ 'name': _('Send Mail (%s)') % template.name,
+ 'model': src_obj,
+ 'key2': 'client_action_multi',
+ 'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
+ 'object': True,
+ }, context)
self.write(cr, uid, ids, {
- 'ref_ir_act_window': vals['ref_ir_act_window'],
- 'ref_ir_value': vals['ref_ir_value'],
- }, context)
+ 'ref_ir_act_window': vals.get('ref_ir_act_window',False),
+ 'ref_ir_value': vals.get('ref_ir_value',False),
+ }, context)
return True
def unlink_action(self, cr, uid, ids, context=None):
@@ -304,9 +213,6 @@ This is useful for CRM leads for example"),
ir_values_obj.unlink(cr, uid, template.ref_ir_value.id, context)
except:
raise osv.except_osv(_("Warning"), _("Deletion of Record failed"))
-
- def delete_action(self, cr, uid, ids, context=None):
- self.unlink_action(cr, uid, ids, context=context)
return True
def unlink(self, cr, uid, ids, context=None):
@@ -314,511 +220,167 @@ This is useful for CRM leads for example"),
return super(email_template, self).unlink(cr, uid, ids, context=context)
def copy(self, cr, uid, id, default=None, context=None):
+ template = self.browse(cr, uid, id, context=context)
if default is None:
default = {}
default = default.copy()
- old = self.read(cr, uid, id, ['name'], context=context)
- new_name = _("Copy of template %s") % old.get('name', 'No Name')
- check = self.search(cr, uid, [('name', '=', new_name)], context=context)
- if check:
- new_name = new_name + '_' + random.choice('abcdefghij') + random.choice('lmnopqrs') + random.choice('tuvwzyz')
- default.update({'name':new_name})
+ default['name'] = template.name + _('(copy)')
return super(email_template, self).copy(cr, uid, id, default, context)
- def build_expression(self, field_name, sub_field_name, null_value, template_language='mako'):
- """
- Returns a template expression based on data provided
- @param field_name: field name
- @param sub_field_name: sub field name (M2O)
- @param null_value: default value if the target value is empty
- @param template_language: name of template engine
- @return: computed expression
- """
+ def build_expression(self, field_name, sub_field_name, null_value):
+ """Returns a placeholder expression for use in a template field,
+ based on the values provided in the placeholder assistant.
+ :param field_name: main field name
+ :param sub_field_name: sub field name (M2O)
+ :param null_value: default value if the target value is empty
+ :return: final placeholder expression
+ """
expression = ''
- if template_language == 'mako':
- if field_name:
- expression = "${object." + field_name
- if sub_field_name:
- expression += "." + sub_field_name
- if null_value:
- expression += " or '''%s'''" % null_value
- expression += "}"
- elif template_language == 'django':
- if field_name:
- expression = "{{object." + field_name
- if sub_field_name:
- expression += "." + sub_field_name
- if null_value:
- expression += "|default: '''%s'''" % null_value
- expression += "}}"
+ if field_name:
+ expression = "${object." + field_name
+ if sub_field_name:
+ expression += "." + sub_field_name
+ if null_value:
+ expression += " or '''%s'''" % null_value
+ expression += "}"
return expression
- def onchange_model_object_field(self, cr, uid, ids, model_object_field, template_language, context=None):
- if not model_object_field:
- return {}
- result = {}
- field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
- #Check if field is relational
- if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
- res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
- if res_ids:
- result['sub_object'] = res_ids[0]
- result['copyvalue'] = self.build_expression(False,
- False,
- False,
- template_language)
- result['sub_model_object_field'] = False
- result['null_value'] = False
- else:
- #Its a simple field... just compute placeholder
- result['sub_object'] = False
- result['copyvalue'] = self.build_expression(field_obj.name,
- False,
- False,
- template_language
- )
- result['sub_model_object_field'] = False
- result['null_value'] = False
+ def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, null_value=None, context=None):
+ result = {
+ 'sub_object': False,
+ 'copyvalue': False,
+ 'sub_model_object_field': False,
+ 'null_value': False
+ }
+ if model_object_field:
+ fields_obj = self.pool.get('ir.model.fields')
+ field_value = fields_obj.browse(cr, uid, model_object_field, context)
+ if field_value.ttype in ['many2one', 'one2many', 'many2many']:
+ res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
+ sub_field_value = False
+ if sub_model_object_field:
+ sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
+ if res_ids:
+ result.update({
+ 'sub_object': res_ids[0],
+ 'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False, null_value or False),
+ 'sub_model_object_field': sub_model_object_field or False,
+ 'null_value': null_value or False
+ })
+ else:
+ result.update({
+ 'copyvalue': self.build_expression(field_value.name, False, null_value or False),
+ 'null_value': null_value or False
+ })
return {'value':result}
- def onchange_sub_model_object_field(self, cr, uid, ids, model_object_field, sub_model_object_field, template_language, context=None):
- if not model_object_field or not sub_model_object_field:
- return {}
- result = {}
- field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
- if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
- res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
- sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
- if res_ids:
- result['sub_object'] = res_ids[0]
- result['copyvalue'] = self.build_expression(field_obj.name,
- sub_field_obj.name,
- False,
- template_language
- )
- result['sub_model_object_field'] = sub_model_object_field
- result['null_value'] = False
- else:
- #Its a simple field... just compute placeholder
- result['sub_object'] = False
- result['copyvalue'] = self.build_expression(field_obj.name,
- False,
- False,
- template_language
- )
- result['sub_model_object_field'] = False
- result['null_value'] = False
- return {'value':result}
- def onchange_null_value(self, cr, uid, ids, model_object_field, sub_model_object_field, null_value, template_language, context=None):
- if not model_object_field and not null_value:
- return {}
- result = {}
- field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context)
- if field_obj.ttype in ['many2one', 'one2many', 'many2many']:
- res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context)
- sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context)
- if res_ids:
- result['sub_object'] = res_ids[0]
- result['copyvalue'] = self.build_expression(field_obj.name,
- sub_field_obj.name,
- null_value,
- template_language
- )
- result['sub_model_object_field'] = sub_model_object_field
- result['null_value'] = null_value
- else:
- #Its a simple field... just compute placeholder
- result['sub_object'] = False
- result['copyvalue'] = self.build_expression(field_obj.name,
- False,
- null_value,
- template_language
- )
- result['sub_model_object_field'] = False
- result['null_value'] = null_value
- return {'value':result}
+ def generate_email(self, cr, uid, template_id, res_id, context=None):
+ """Generates an email from the template for given (model, res_id) pair.
- def _add_attachment(self, cursor, user, mailbox_id, name, data, filename, context=None):
+ :param template_id: id of the template to render.
+ :param res_id: id of the record to use for rendering the template (model
+ is taken from template definition)
+ :returns: a dict containing all relevant fields for creating a new
+ mail.message entry, with the addition one additional
+ special key ``attachments`` containing a list of
"""
- Add an attachment to a given mailbox entry.
-
- :param data: base64 encoded attachment data to store
- """
- attachment_obj = self.pool.get('ir.attachment')
- attachment_data = {
- 'name': (name or '') + _(' (Email Attachment)'),
- 'datas': data,
- 'datas_fname': filename,
- 'description': name or _('No Description'),
- 'res_model':'email_template.mailbox',
- 'res_id': mailbox_id,
+ if context is None:
+ context = {}
+ values = {
+ 'subject': False,
+ 'body_text': False,
+ 'body_html': False,
+ 'email_from': False,
+ 'email_to': False,
+ 'email_cc': False,
+ 'email_bcc': False,
+ 'reply_to': False,
+ 'auto_delete': False,
+ 'model': False,
+ 'res_id': False,
+ 'mail_server_id': False,
+ 'attachments': False,
+ 'attachment_ids': False,
+ 'message_id': False,
+ 'state': 'outgoing',
}
- attachment_id = attachment_obj.create(cursor,
- user,
- attachment_data,
- context)
- if attachment_id:
- self.pool.get('email_template.mailbox').write(
- cursor,
- user,
- mailbox_id,
- {
- 'attachments_ids':[(4, attachment_id)],
- 'mail_type':'multipart/mixed'
- },
- context)
+ if not template_id:
+ return values
- def generate_attach_reports(self,
- cursor,
- user,
- template,
- record_id,
- mail,
- context=None):
- """
- Generate report to be attached and attach it
- to the email, and add any directly attached files as well.
+ report_xml_pool = self.pool.get('ir.actions.report.xml')
+ template = self.get_email_template(cr, uid, template_id, res_id, context)
- @param cursor: Database Cursor
- @param user: ID of User
- @param template: Browse record of
- template
- @param record_id: ID of the target model
- for which this mail has
- to be generated
- @param mail: Browse record of email object
- @return: True
- """
+ for field in ['subject', 'body_text', 'body_html', 'email_from',
+ 'email_to', 'email_cc', 'email_bcc', 'reply_to',
+ 'message_id']:
+ values[field] = self.render_template(cr, uid, getattr(template, field),
+ template.model, res_id, context=context) \
+ or False
+
+ if template.user_signature:
+ signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
+ values['body_text'] += '\n\n' + signature
+
+ values.update(mail_server_id = template.mail_server_id.id or False,
+ auto_delete = template.auto_delete,
+ model=template.model,
+ res_id=res_id or False)
+
+ attachments = {}
+ # Add report as a Document
if template.report_template:
- reportname = 'report.' + \
- self.pool.get('ir.actions.report.xml').read(
- cursor,
- user,
- template.report_template.id,
- ['report_name'],
- context)['report_name']
- service = netsvc.LocalService(reportname)
- data = {}
- data['model'] = template.model_int_name
- (result, format) = service.create(cursor,
- user,
- [record_id],
- data,
- context)
- fname = tools.ustr(get_value(cursor, user, record_id,
- template.file_name, template, context)
- or 'Report')
- ext = '.' + format
- if not fname.endswith(ext):
- fname += ext
- self._add_attachment(cursor, user, mail.id, mail.subject, base64.b64encode(result), fname, context)
-
- if template.attachment_ids:
- for attachment in template.attachment_ids:
- self._add_attachment(cursor, user, mail.id, attachment.name, attachment.datas, attachment.datas_fname, context)
-
- return True
-
- def _generate_mailbox_item_from_template(self,
- cursor,
- user,
- template,
- record_id,
- context=None):
- """
- Generates an email from the template for
- record record_id of target object
-
- @param cursor: Database Cursor
- @param user: ID of User
- @param template: Browse record of
- template
- @param record_id: ID of the target model
- for which this mail has
- to be generated
- @return: ID of created object
- """
- if context is None:
- context = {}
- #If account to send from is in context select it, else use enforced account
- if 'account_id' in context.keys():
- from_account = self.pool.get('email_template.account').read(
- cursor,
- user,
- context.get('account_id'),
- ['name', 'email_id'],
- context
- )
- else:
- from_account = {
- 'id':template.from_account.id,
- 'name':template.from_account.name,
- 'email_id':template.from_account.email_id
- }
- lang = get_value(cursor,
- user,
- record_id,
- template.lang,
- template,
- context)
- if lang:
+ report_name = template.report_name
+ report_service = 'report.' + report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
+ # Ensure report is rendered using template's language
ctx = context.copy()
- ctx.update({'lang':lang})
- template = self.browse(cursor, user, template.id, context=ctx)
+ if template.lang:
+ ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
+ service = netsvc.LocalService(report_service)
+ (result, format) = service.create(cr, uid, [res_id], {'model': template.model}, ctx)
+ result = base64.b64encode(result)
+ if not report_name:
+ report_name = report_service
+ ext = "." + format
+ if not report_name.endswith(ext):
+ report_name += ext
+ attachments[report_name] = result
- # determine name of sender, either it is specified in email_id or we
- # use the account name
- email_id = from_account['email_id'].strip()
- email_from = re.findall(r'([^ ,<@]+@[^> ,]+)', email_id)[0]
- if email_from != email_id:
- # we should keep it all, name is probably specified in the address
- email_from = from_account['email_id']
- else:
- email_from = tools.ustr(from_account['name']) + "<" + tools.ustr(email_id) + ">"
+ # Add document attachments
+ for attach in template.attachment_ids:
+ # keep the bytes as fetched from the db, base64 encoded
+ attachments[attach.datas_fname] = attach.datas
- # FIXME: should do this in a loop and rename template fields to the corresponding
- # mailbox fields. (makes no sense to have different names I think.
- mailbox_values = {
- 'email_from': email_from,
- 'email_to':get_value(cursor,
- user,
- record_id,
- template.def_to,
- template,
- context),
- 'email_cc':get_value(cursor,
- user,
- record_id,
- template.def_cc,
- template,
- context),
- 'email_bcc':get_value(cursor,
- user,
- record_id,
- template.def_bcc,
- template,
- context),
- 'reply_to':get_value(cursor,
- user,
- record_id,
- template.reply_to,
- template,
- context),
- 'subject':get_value(cursor,
- user,
- record_id,
- template.def_subject,
- template,
- context),
- 'body_text':get_value(cursor,
- user,
- record_id,
- template.def_body_text,
- template,
- context),
- 'body_html':get_value(cursor,
- user,
- record_id,
- template.def_body_html,
- template,
- context),
- 'account_id' :from_account['id'],
- #This is a mandatory field when automatic emails are sent
- 'state':'na',
- 'folder':'drafts',
- 'mail_type':'multipart/alternative',
- }
+ values['attachments'] = attachments
+ return values
- if template['message_id']:
- # use provided message_id with placeholders
- mailbox_values.update({'message_id': get_value(cursor, user, record_id, template['message_id'], template, context)})
+ def send_mail(self, cr, uid, template_id, res_id, context=None):
+ """Generates a new mail message for the given template and record,
+ and schedule it for delivery through the ``mail`` module's scheduler.
- elif template['track_campaign_item']:
- # get appropriate message-id
- mailbox_values.update({'message_id': tools.misc.generate_tracking_message_id(record_id)})
-
- if not mailbox_values['account_id']:
- raise Exception("Unable to send the mail. No account linked to the template.")
- #Use signatures if allowed
- if template.use_sign:
- sign = self.pool.get('res.users').read(cursor,
- user,
- user,
- ['signature'],
- context)['signature']
- if mailbox_values['body_text']:
- mailbox_values['body_text'] += sign
- if mailbox_values['body_html']:
- mailbox_values['body_html'] += sign
- mailbox_id = self.pool.get('email_template.mailbox').create(
- cursor,
- user,
- mailbox_values,
- context)
-
- return mailbox_id
-
-
- def generate_mail(self,
- cursor,
- user,
- template_id,
- record_ids,
- context=None):
- if context is None:
- context = {}
- template = self.browse(cursor, user, template_id, context=context)
- if not template:
- raise Exception("The requested template could not be loaded")
- result = True
- mailbox_obj = self.pool.get('email_template.mailbox')
- for record_id in record_ids:
- mailbox_id = self._generate_mailbox_item_from_template(
- cursor,
- user,
- template,
- record_id,
- context)
- mail = mailbox_obj.browse(
- cursor,
- user,
- mailbox_id,
- context=context
- )
- if template.report_template or template.attachment_ids:
- self.generate_attach_reports(
- cursor,
- user,
- template,
- record_id,
- mail,
- context
- )
-
- self.pool.get('email_template.mailbox').write(
- cursor,
- user,
- mailbox_id,
- {'folder':'outbox'},
- context=context
- )
- # TODO : manage return value of all the records
- result = self.pool.get('email_template.mailbox').send_this_mail(cursor, user, [mailbox_id], context)
- return result
-
-email_template()
-
-
-## FIXME: this class duplicates a lot of features of the email template send wizard,
-## one of the 2 should inherit from the other!
-
-class email_template_preview(osv.osv_memory):
- _name = "email_template.preview"
- _description = "Email Template Preview"
-
- def _get_model_recs(self, cr, uid, context=None):
- if context is None:
- context = {}
- #Fills up the selection box which allows records from the selected object to be displayed
- self.context = context
- if 'template_id' in context:
- ref_obj_id = self.pool.get('email.template').read(cr, uid, context['template_id'], ['object_name'], context)
- ref_obj_name = self.pool.get('ir.model').read(cr, uid, ref_obj_id['object_name'][0], ['model'], context)['model']
- model_obj = self.pool.get(ref_obj_name)
- ref_obj_ids = model_obj.search(cr, uid, [], 0, 20, 'id', context=context)
- if not ref_obj_ids:
- ref_obj_ids = []
-
- # also add the default one if requested, otherwise it won't be available for selection:
- default_id = context.get('default_rel_model_ref')
- if default_id and default_id not in ref_obj_ids:
- ref_obj_ids.insert(0, default_id)
- return model_obj.name_get(cr, uid, ref_obj_ids, context)
- return []
-
- def default_get(self, cr, uid, fields, context=None):
- if context is None:
- context = {}
- result = super(email_template_preview, self).default_get(cr, uid, fields, context=context)
- if (not fields or 'rel_model_ref' in fields) and 'template_id' in context \
- and not result.get('rel_model_ref'):
- selectables = self._get_model_recs(cr, uid, context=context)
- result['rel_model_ref'] = selectables and selectables[0][0] or False
- return result
-
- def _default_model(self, cursor, user, context=None):
+ :param int template_id: id of the template to render
+ :param int res_id: id of the record to render the template with
+ (model is taken from the template)
"""
- Returns the default value for model field
- @param cursor: Database Cursor
- @param user: ID of current user
- @param context: OpenERP Context
- """
- return self.pool.get('email.template').read(
- cursor,
- user,
- context['template_id'],
- ['object_name'],
- context).get('object_name', False)
-
- _columns = {
- 'ref_template':fields.many2one(
- 'email.template',
- 'Template', readonly=True),
- 'rel_model':fields.many2one('ir.model', 'Model', readonly=True),
- 'rel_model_ref':fields.selection(_get_model_recs, 'Referred Document'),
- 'to':fields.char('To', size=250, readonly=True),
- 'cc':fields.char('CC', size=250, readonly=True),
- 'bcc':fields.char('BCC', size=250, readonly=True),
- 'reply_to':fields.char('Reply-To',
- size=250,
- help="The address recipients should reply to,"
- " if different from the From address."
- " Placeholders can be used here."),
- 'message_id':fields.char('Message-ID',
- size=250,
- help="The Message-ID header value, if you need to"
- "specify it, for example to automatically recognize the replies later."
- " Placeholders can be used here."),
- 'subject':fields.char('Subject', size=200, readonly=True),
- 'body_text':fields.text('Body', readonly=True),
- 'body_html':fields.text('Body', readonly=True),
- 'report':fields.char('Report Name', size=100, readonly=True),
- }
- _defaults = {
- 'ref_template': lambda self, cr, uid, ctx:ctx['template_id'] or False,
- 'rel_model': _default_model,
- }
- def on_change_ref(self, cr, uid, ids, rel_model_ref, context=None):
- if context is None:
- context = {}
- if not rel_model_ref:
- return {}
- vals = {}
- if context == {}:
- context = self.context
- template = self.pool.get('email.template').browse(cr, uid, context['template_id'], context)
- #Search translated template
- lang = get_value(cr, uid, rel_model_ref, template.lang, template, context)
- if lang:
- ctx = context.copy()
- ctx.update({'lang':lang})
- template = self.pool.get('email.template').browse(cr, uid, context['template_id'], ctx)
- vals['to'] = get_value(cr, uid, rel_model_ref, template.def_to, template, context)
- vals['cc'] = get_value(cr, uid, rel_model_ref, template.def_cc, template, context)
- vals['bcc'] = get_value(cr, uid, rel_model_ref, template.def_bcc, template, context)
- vals['reply_to'] = get_value(cr, uid, rel_model_ref, template.reply_to, template, context)
- if template.message_id:
- vals['message_id'] = get_value(cr, uid, rel_model_ref, template.message_id, template, context)
- elif template.track_campaign_item:
- vals['message_id'] = tools.misc.generate_tracking_message_id(rel_model_ref)
- vals['subject'] = get_value(cr, uid, rel_model_ref, template.def_subject, template, context)
- vals['body_text'] = get_value(cr, uid, rel_model_ref, template.def_body_text, template, context)
- vals['body_html'] = get_value(cr, uid, rel_model_ref, template.def_body_html, template, context)
- vals['report'] = get_value(cr, uid, rel_model_ref, template.file_name, template, context)
- return {'value':vals}
-
-email_template_preview()
+ mail_message = self.pool.get('mail.message')
+ ir_attachment = self.pool.get('ir.attachment')
+ template = self.browse(cr, uid, template_id, context)
+ values = self.generate_email(cr, uid, template_id, res_id, context=context)
+ attachments = values.pop('attachments') or {}
+ message_id = mail_message.create(cr, uid, values, context=context)
+ # link attachments
+ attachment_ids = []
+ for fname, fcontent in attachments.iteritems():
+ attachment_data = {
+ 'name': fname,
+ 'datas_fname': fname,
+ 'datas': fcontent,
+ 'res_model': mail_message._name,
+ 'res_id': message_id,
+ }
+ if context.has_key('default_type'):
+ del context['default_type']
+ attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context))
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/email_template_account.py b/addons/email_template/email_template_account.py
deleted file mode 100644
index a5f51400c8a..00000000000
--- a/addons/email_template/email_template_account.py
+++ /dev/null
@@ -1,470 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2009 Sharoon Thomas
-# Copyright (C) 2004-2010 OpenERP SA ()
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU 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 General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see
-#
-##############################################################################
-
-from osv import osv, fields
-import re
-import smtplib
-import base64
-from email import Encoders
-from email.mime.base import MIMEBase
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from email.header import decode_header, Header
-from email.utils import formatdate
-import netsvc
-import datetime
-from tools.translate import _
-import tools
-import logging
-
-EMAIL_PATTERN = re.compile(r'([^()\[\] ,<:\\>@";]+@[^()\[\] ,<:\\>@";]+)') # See RFC822
-def extract_emails(emails_str):
- """
- Returns a list of email addresses recognized in a string, ignoring the rest of the string.
- extract_emails('a@b.com,c@bcom, "John Doe" , e@b.com') -> ['a@b.com','c@bcom', 'd@b.com', 'e@b.com']"
- """
- return EMAIL_PATTERN.findall(emails_str)
-
-
-def extract_emails_from_dict(addresses={}):
- """
- Extracts email addresses from a dictionary with comma-separated address string values, handling
- separately the To, CC, BCC and Reply-To addresses.
-
- :param addresses: a dictionary of addresses in the form {'To': 'a@b.com,c@bcom; d@b.com;e@b.com' , 'CC': 'e@b.com;f@b.com', ... }
- :return: a dictionary with a list of separate addresses for each header (To, CC, BCC), with an additional key 'all-recipients'
- containing all addresses for the 'To', 'CC', 'BCC' entries.
- """
- result = {'all-recipients':[]}
- keys = ['To', 'CC', 'BCC', 'Reply-To']
- for each in keys:
- emails = extract_emails(addresses.get(each, u''))
- while u'' in emails:
- emails.remove(u'')
- result[each] = emails
- if each != 'Reply-To':
- result['all-recipients'].extend(emails)
- return result
-
-class email_template_account(osv.osv):
- """
- Object to store email account settings
- """
- _name = "email_template.account"
- _known_content_types = ['multipart/mixed',
- 'multipart/alternative',
- 'multipart/related',
- 'text/plain',
- 'text/html'
- ]
- _columns = {
- 'name': fields.char('Description',
- size=64, required=True,
- readonly=True, select=True,
- help="The description is used as the Sender name along with the provided From Email, \
-unless it is already specified in the From Email, e.g: John Doe ",
- states={'draft':[('readonly', False)]}),
- 'auto_delete': fields.boolean('Auto Delete', size=64, readonly=True,
- help="Permanently delete emails after sending",
- states={'draft':[('readonly', False)]}),
- 'user':fields.many2one('res.users',
- 'Related User', required=True,
- readonly=True, states={'draft':[('readonly', False)]}),
- 'email_id': fields.char('From Email',
- size=120, required=True,
- readonly=True, states={'draft':[('readonly', False)]} ,
- help="eg: 'john@doe.com' or 'John Doe '"),
- 'smtpserver': fields.char('Server',
- size=120, required=True,
- readonly=True, states={'draft':[('readonly', False)]},
- help="Enter name of outgoing server, eg: smtp.yourdomain.com"),
- 'smtpport': fields.integer('SMTP Port',
- size=64, required=True,
- readonly=True, states={'draft':[('readonly', False)]},
- help="Enter port number, eg: 25 or 587"),
- 'smtpuname': fields.char('User Name',
- size=120, required=False,
- readonly=True, states={'draft':[('readonly', False)]},
- help="Specify the username if your SMTP server requires authentication, "
- "otherwise leave it empty."),
- 'smtppass': fields.char('Password',
- size=120, invisible=True,
- required=False, readonly=True,
- states={'draft':[('readonly', False)]}),
- 'smtptls':fields.boolean('TLS',
- states={'draft':[('readonly', False)]}, readonly=True),
-
- 'smtpssl':fields.boolean('SSL/TLS (only in python 2.6)',
- states={'draft':[('readonly', False)]}, readonly=True),
- 'send_pref':fields.selection([
- ('html', 'HTML, otherwise Text'),
- ('text', 'Text, otherwise HTML'),
- ('alternative', 'Both HTML & Text (Alternative)'),
- ('mixed', 'Both HTML & Text (Mixed)')
- ], 'Mail Format', required=True),
- 'company':fields.selection([
- ('yes', 'Yes'),
- ('no', 'No')
- ], 'Corporate',
- readonly=True,
- help="Select if this mail account does not belong " \
- "to specific user but to the organization as a whole. " \
- "eg: info@companydomain.com",
- required=True, states={
- 'draft':[('readonly', False)]
- }),
-
- 'state':fields.selection([
- ('draft', 'Initiated'),
- ('suspended', 'Suspended'),
- ('approved', 'Approved')
- ],
- 'State', required=True, readonly=True),
- }
-
- _defaults = {
- 'name':lambda self, cursor, user, context:self.pool.get(
- 'res.users'
- ).read(
- cursor,
- user,
- user,
- ['name'],
- context
- )['name'],
- 'state':lambda * a:'draft',
- 'smtpport':lambda *a:25,
- 'smtpserver':lambda *a:'localhost',
- 'company':lambda *a:'yes',
- 'user':lambda self, cursor, user, context:user,
- 'send_pref':lambda *a: 'html',
- 'smtptls':lambda *a:True,
- }
-
- _sql_constraints = [
- (
- 'email_uniq',
- 'unique (email_id)',
- 'Another setting already exists with this email ID !')
- ]
-
- def name_get(self, cr, uid, ids, context=None):
- return [(a["id"], "%s (%s)" % (a['email_id'], a['name'])) for a in self.read(cr, uid, ids, ['name', 'email_id'], context=context)]
-
- def _constraint_unique(self, cursor, user, ids, context=None):
- """
- This makes sure that you dont give personal
- users two accounts with same ID (Validated in sql constaints)
- However this constraint exempts company accounts.
- Any no of co accounts for a user is allowed
- """
- if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no':
- accounts = self.search(cursor, user, [
- ('user', '=', user),
- ('company', '=', 'no')
- ])
- if len(accounts) > 1 :
- return False
- else :
- return True
- else:
- return True
-
- _constraints = [
- (_constraint_unique,
- 'Error: You are not allowed to have more than 1 account.',
- [])
- ]
-
- def get_outgoing_server(self, cursor, user, ids, context=None):
- """
- Returns the Out Going Connection (SMTP) object
-
- @attention: DO NOT USE except_osv IN THIS METHOD
- @param cursor: Database Cursor
- @param user: ID of current user
- @param ids: ID/list of ids of current object for
- which connection is required
- First ID will be chosen from lists
- @param context: Context
-
- @return: SMTP server object or Exception
- """
- #Type cast ids to integer
- if type(ids) == list:
- ids = ids[0]
- this_object = self.browse(cursor, user, ids, context=context)
- if this_object:
- if this_object.smtpserver and this_object.smtpport:
- try:
- if this_object.smtpssl:
- serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport)
- else:
- serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport)
- if this_object.smtptls:
- serv.ehlo()
- serv.starttls()
- serv.ehlo()
- except Exception, error:
- raise error
- try:
- if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass:
- serv.login(str(this_object.smtpuname), str(this_object.smtppass))
- except Exception, error:
- raise error
- return serv
- raise Exception(_("SMTP SERVER or PORT not specified"))
- raise Exception(_("Core connection for the given ID does not exist"))
-
- def check_outgoing_connection(self, cursor, user, ids, context=None):
- """
- checks SMTP credentials and confirms if outgoing connection works
- (Attached to button)
- @param cursor: Database Cursor
- @param user: ID of current user
- @param ids: list of ids of current object for
- which connection is required
- @param context: Context
- """
- try:
- self.get_outgoing_server(cursor, user, ids, context)
- raise osv.except_osv(_("SMTP Test Connection Was Successful"), '')
- except osv.except_osv, success_message:
- raise success_message
- except Exception, error:
- raise osv.except_osv(
- _("Out going connection test failed"),
- _("Reason: %s") % tools.ustr(error)
- )
-
- def do_approval(self, cr, uid, ids, context=None):
- #TODO: Check if user has rights
- self.write(cr, uid, ids, {'state':'approved'}, context=context)
-# wf_service = netsvc.LocalService("workflow")
-
- def smtp_connection(self, cursor, user, id, context=None):
- """
- This method should now wrap smtp_connection
- """
- #This function returns a SMTP server object
- logger = netsvc.Logger()
- core_obj = self.browse(cursor, user, id, context=context)
- if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved':
- try:
- serv = self.get_outgoing_server(cursor, user, id, context)
- except Exception, error:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed on login. Probable Reason:Could not login to server\nError: %s") % (id, tools.ustr(error)))
- return False
- #Everything is complete, now return the connection
- return serv
- else:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
- return False
-
-#**************************** MAIL SENDING FEATURES ***********************#
-
-
-
-
- def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, message_id=None, context=None):
- #TODO: Replace all this with a single email object
- if body is None:
- body = {}
- if payload is None:
- payload = {}
- if context is None:
- context = {}
- logger = netsvc.Logger()
- for id in ids:
- core_obj = self.browse(cr, uid, id, context)
- serv = self.smtp_connection(cr, uid, id)
- if serv:
- try:
- # Prepare multipart containers depending on data
- text_subtype = (core_obj.send_pref == 'alternative') and 'alternative' or 'mixed'
- # Need a multipart/mixed wrapper for attachments if content is alternative
- if payload and text_subtype == 'alternative':
- payload_part = MIMEMultipart(_subtype='mixed')
- text_part = MIMEMultipart(_subtype=text_subtype)
- payload_part.attach(text_part)
- else:
- # otherwise a single multipart/mixed will do the whole job
- payload_part = text_part = MIMEMultipart(_subtype=text_subtype)
-
- if subject:
- payload_part['Subject'] = subject
- from_email = core_obj.email_id
- if '<' in from_email:
- # We have a structured email address, keep it untouched
- payload_part['From'] = Header(core_obj.email_id, 'utf-8').encode()
- else:
- # Plain email address, construct a structured one based on the name:
- sender_name = Header(core_obj.name, 'utf-8').encode()
- payload_part['From'] = sender_name + " <" + core_obj.email_id + ">"
- payload_part['Organization'] = tools.ustr(core_obj.user.company_id.name)
- payload_part['Date'] = formatdate()
- addresses_l = extract_emails_from_dict(addresses)
- if addresses_l['To']:
- payload_part['To'] = u','.join(addresses_l['To'])
- if addresses_l['CC']:
- payload_part['CC'] = u','.join(addresses_l['CC'])
- if addresses_l['Reply-To']:
- payload_part['Reply-To'] = addresses_l['Reply-To'][0]
- if message_id:
- payload_part['Message-ID'] = message_id
- if body.get('text', False):
- temp_body_text = body.get('text', '')
- l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', ''))
- if l == 0:
- body['text'] = u'No Mail Message'
- # Attach parts into message container.
- # According to RFC 2046, the last part of a multipart message, in this case
- # the HTML message, is best and preferred.
- if core_obj.send_pref in ('text', 'mixed', 'alternative'):
- body_text = body.get('text', u'')
- body_text = tools.ustr(body_text)
- text_part.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8'))
- if core_obj.send_pref in ('html', 'mixed', 'alternative'):
- html_body = body.get('html', u'')
- if len(html_body) == 0 or html_body == u'':
- html_body = body.get('text', u'
<Empty Message>
').replace('\n', ' ').replace('\r', ' ')
- html_body = tools.ustr(html_body)
- text_part.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8'))
-
- #Now add attachments if any, wrapping into a container multipart/mixed if needed
- if payload:
- for file in payload:
- part = MIMEBase('application', "octet-stream")
- part.set_payload(base64.decodestring(payload[file]))
- part.add_header('Content-Disposition', 'attachment; filename="%s"' % file)
- Encoders.encode_base64(part)
- payload_part.attach(part)
- except Exception, error:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, tools.ustr(error)))
- return {'error_msg': _("Server Send Error\nDescription: %s")%error}
- try:
- serv.sendmail(payload_part['From'], addresses_l['all-recipients'], payload_part.as_string())
- except Exception, error:
- logging.getLogger('email_template').error(_("Mail from Account %s failed. Probable Reason: Server Send Error\n Description: %s"), id, tools.ustr(error), exc_info=True)
- return {'error_msg': _("Server Send Error\nDescription: %s") % tools.ustr(error)}
- #The mail sending is complete
- serv.close()
- logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id))
- return True
- else:
- logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id)
- return {'nodestroy':True,'error_msg': _("Mail from Account %s failed. Probable Reason:Account not approved")% id}
-
- def extracttime(self, time_as_string):
- """
- TODO: DOC THis
- """
- logger = netsvc.Logger()
- #The standard email dates are of format similar to:
- #Thu, 8 Oct 2009 09:35:42 +0200
- date_as_date = False
- convertor = {'+':1, '-':-1}
- try:
- time_as_string = time_as_string.replace(',', '')
- date_list = time_as_string.split(' ')
- date_temp_str = ' '.join(date_list[1:5])
- if len(date_list) >= 6:
- sign = convertor.get(date_list[5][0], False)
- else:
- sign = False
- try:
- dt = datetime.datetime.strptime(
- date_temp_str,
- "%d %b %Y %H:%M:%S")
- except:
- try:
- dt = datetime.datetime.strptime(
- date_temp_str,
- "%d %b %Y %H:%M")
- except:
- return False
- if sign:
- try:
- offset = datetime.timedelta(
- hours=sign * int(
- date_list[5][1:3]
- ),
- minutes=sign * int(
- date_list[5][3:5]
- )
- )
- except Exception, e2:
- """Looks like UT or GMT, just forget decoding"""
- return False
- else:
- offset = datetime.timedelta(hours=0)
- dt = dt + offset
- date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S')
- except Exception, e:
- logger.notifyChannel(
- _("Email Template"),
- netsvc.LOG_WARNING,
- _(
- "Datetime Extraction failed.Date:%s \
- \tError:%s") % (
- time_as_string,
- tools.ustr(e))
- )
- return date_as_date
-
- def send_receive(self, cr, uid, ids, context=None):
- for id in ids:
- ctx = context.copy()
- ctx['filters'] = [('account_id', '=', id)]
- self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx)
- return True
-
- def decode_header_text(self, text):
- """ Decode internationalized headers RFC2822.
- To, CC, BCC, Subject fields can contain
- text slices with different encodes, like:
- =?iso-8859-1?Q?Enric_Mart=ED?= ,
- =?Windows-1252?Q?David_G=F3mez?=
- Sometimes they include extra " character at the beginning/
- end of the contact name, like:
- "=?iso-8859-1?Q?Enric_Mart=ED?="
- and decode_header() does not work well, so we use regular
- expressions (?= ? ? ?=) to split the text slices
- """
- if not text:
- return text
- p = re.compile("(=\?.*?\?.\?.*?\?=)")
- text2 = ''
- try:
- for t2 in p.split(text):
- text2 += ''.join(
- [s.decode(
- t or 'ascii'
- ) for (s, t) in decode_header(t2)]
- ).encode('utf-8')
- except:
- return text
- return text2
-
-email_template_account()
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/email_template/email_template_account_view.xml b/addons/email_template/email_template_account_view.xml
deleted file mode 100644
index 75111e626e5..00000000000
--- a/addons/email_template/email_template_account_view.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
-
-
-
-
-
- email_template.account.form
- email_template.account
- form
-
-
-
-
-
-
-
- email_template.mailbox.tree
- email_template.mailbox
- tree
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- email_template.mailbox.search
- email_template.mailbox
- search
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Emails
- email_template.mailbox
- form
- tree,form
-
- {'search_default_outbox': 1}
- An email template is an email document that will be sent as part of a marketing campaign. You can personalize it according to specific customer profile fields, so that a partner name or other partner related information may be inserted automatically.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/email_template/email_template_view.xml b/addons/email_template/email_template_view.xml
index 7d2a5f7ad5c..b4f76a03c6c 100644
--- a/addons/email_template/email_template_view.xml
+++ b/addons/email_template/email_template_view.xml
@@ -1,141 +1,99 @@
-
-
-
- email_template.preview.form
- email_template.preview
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Template Preview
- email_template.preview
- email_template.preview
- ir.actions.act_window
- form
- form
-
- new
- {'template_id':active_id}
-
-
email.template.formemail.templateform
-
-
-
+
diff --git a/addons/hr_recruitment/security/ir.model.access.csv b/addons/hr_recruitment/security/ir.model.access.csv
index 2907b96fd81..96baaf9e278 100644
--- a/addons/hr_recruitment/security/ir.model.access.csv
+++ b/addons/hr_recruitment/security/ir.model.access.csv
@@ -3,7 +3,7 @@
"access_hr_recruitment_report","hr.recruitment.report","model_hr_recruitment_report","base.group_hr_manager",1,1,1,1
"access_hr_recruitment_stage_user","hr.recruitment.stage.user","model_hr_recruitment_stage","base.group_hr_user",1,1,1,1
"access_hr_recruitment_degree","hr.recruitment.degree","model_hr_recruitment_degree","base.group_hr_user",1,1,1,1
-"access_mailgate_message_user","mailgate.message.user","mail_gateway.model_mailgate_message","base.group_hr_user",1,1,1,1
+"access_mail_message_user","mail.message.user","mail.model_mail_message","base.group_hr_user",1,1,1,1
"access_res_partner_hr_user","res.partner.user","base.model_res_partner","base.group_hr_user",1,1,1,1
"access_res_partner_address_hr_user","res.partner.address.user","base.model_res_partner_address","base.group_hr_user",1,1,1,1
"access_survey_hr_user","survey.hr.user","survey.model_survey","base.group_hr_user",1,1,1,0
diff --git a/addons/hr_recruitment/wizard/hr_recruitment_phonecall.py b/addons/hr_recruitment/wizard/hr_recruitment_phonecall.py
index edd08b5b531..cd9cbea52f7 100644
--- a/addons/hr_recruitment/wizard/hr_recruitment_phonecall.py
+++ b/addons/hr_recruitment/wizard/hr_recruitment_phonecall.py
@@ -32,7 +32,7 @@ class job2phonecall(osv.osv_memory):
'deadline': fields.datetime('Planned Date'),
'note': fields.text('Goals'),
'category_id': fields.many2one('crm.case.categ', 'Category', required=True),
- }
+ }
def _date_user(self, cr, uid, context=None):
case_obj = self.pool.get('hr.applicant')
@@ -50,7 +50,7 @@ class job2phonecall(osv.osv_memory):
return categ_id and categ_id[0] or case.categ_id and case.categ_id.id or False
def _get_note(self, cr, uid, context=None):
- msg_obj = self.pool.get('mailgate.message')
+ mail_message = self.pool.get('mail.message')
if context is None:
context = {}
if context.get('active_id'):
@@ -58,9 +58,9 @@ class job2phonecall(osv.osv_memory):
if case.description:
return case.description
else:
- msg_ids = msg_obj.search(cr, uid, [('model', '=', 'hr.applicant'), ('res_id', '=', case.id), ('email_from', '!=', ''), ('email_to', '!=', '')], limit=1)
+ msg_ids = mail_message.search(cr, uid, [('model', '=', 'hr.applicant'), ('res_id', '=', case.id), ('email_from', '!=', ''), ('email_to', '!=', '')], limit=1)
if msg_ids:
- return msg_obj.browse(cr, uid, msg_ids[0], context=context).description
+ return mail_message.browse(cr, uid, msg_ids[0], context=context).body_text
return False
diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py
index 2b33dfbda8d..4613c9251b5 100644
--- a/addons/hr_timesheet/hr_timesheet.py
+++ b/addons/hr_timesheet/hr_timesheet.py
@@ -66,8 +66,8 @@ class hr_analytic_timesheet(osv.osv):
_inherits = {'account.analytic.line': 'line_id'}
_order = "id desc"
_columns = {
- 'line_id': fields.many2one('account.analytic.line', 'Analytic line', ondelete='cascade', required=True),
- 'partner_id': fields.related('account_id', 'partner_id', type='many2one', string='Partner Id', relation='res.partner', store=True),
+ 'line_id': fields.many2one('account.analytic.line', 'Analytic Line', ondelete='cascade', required=True),
+ 'partner_id': fields.related('account_id', 'partner_id', type='many2one', string='Partner', relation='res.partner', store=True),
}
def unlink(self, cr, uid, ids, context=None):
diff --git a/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out.py b/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out.py
index 90c9a5d7b8c..e3f7f0c7acb 100644
--- a/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out.py
+++ b/addons/hr_timesheet/wizard/hr_timesheet_sign_in_out.py
@@ -27,7 +27,7 @@ class hr_so_project(osv.osv_memory):
_name = 'hr.sign.out.project'
_description = 'Sign Out By Project'
_columns = {
- 'account_id': fields.many2one('account.analytic.account', 'Analytic Account', domain=[('type','=','normal')]),
+ 'account_id': fields.many2one('account.analytic.account', 'Project / Analytic Account', domain=[('type','=','normal')]),
'info': fields.char('Work Description', size=256, required=True),
'date_start': fields.datetime('Starting Date', readonly=True),
'date': fields.datetime('Closing Date'),
diff --git a/addons/hr_timesheet_sheet/board_hr_timesheet_view.xml b/addons/hr_timesheet_sheet/board_hr_timesheet_view.xml
index 13193e66d99..134c7f99463 100644
--- a/addons/hr_timesheet_sheet/board_hr_timesheet_view.xml
+++ b/addons/hr_timesheet_sheet/board_hr_timesheet_view.xml
@@ -25,7 +25,7 @@
hr_timesheet_sheet.sheettree
-
+
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
index cf4351112a4..4e1eebb5f4c 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
@@ -264,7 +264,7 @@ class hr_timesheet_sheet(osv.osv):
return self.sign(cr,uid,ids,'sign_out',context=None)
_columns = {
- 'name': fields.char('Description', size=64, select=1,
+ 'name': fields.char('Note', size=64, select=1,
states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
'employee_id': fields.many2one('hr.employee', 'Employee', required=True),
'user_id': fields.related('employee_id', 'user_id', type="many2one", relation="res.users", store=True, string="User", required=False, readonly=True),#fields.many2one('res.users', 'User', required=True, select=1, states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
@@ -280,9 +280,9 @@ class hr_timesheet_sheet(osv.osv):
'attendances_ids' : one2many_mod2('hr.attendance', 'sheet_id', 'Attendances'),
'state' : fields.selection([
('new', 'New'),
- ('draft','Draft'),
- ('confirm','Confirmed'),
- ('done','Done')], 'State', select=True, required=True, readonly=True,
+ ('draft','Open'),
+ ('confirm','Waiting Approval'),
+ ('done','Approved')], 'State', select=True, required=True, readonly=True,
help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed timesheet. \
\n* The \'Confirmed\' state is used for to confirm the timesheet by user. \
\n* The \'Done\' state is used when users timesheet is accepted by his/her senior.'),
@@ -735,7 +735,7 @@ class hr_timesheet_sheet_sheet_account(osv.osv):
_auto = False
_order='name'
_columns = {
- 'name': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
+ 'name': fields.many2one('account.analytic.account', 'Project / Analytic Account', readonly=True),
'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True),
'total': fields.float('Total Time', digits=(16,2), readonly=True),
'invoice_rate': fields.many2one('hr_timesheet_invoice.factor', 'Invoice rate', readonly=True),
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
index be55c83e0fa..4fc5b2fb1b3 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_data.xml
@@ -7,7 +7,7 @@
-
+
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
index 321e4c2addd..9569149860c 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml
@@ -62,17 +62,17 @@
-
-
-
-
+
+
+
+
-
+
@@ -89,19 +89,19 @@
-
-
-
+
+
+
+
+
-
-
-
+
@@ -126,7 +126,7 @@
-
+
@@ -306,7 +306,7 @@
tree
-
+
diff --git a/addons/hr_timesheet_sheet/report/timesheet_report_view.xml b/addons/hr_timesheet_sheet/report/timesheet_report_view.xml
index 08abc6db63b..53575f19d29 100644
--- a/addons/hr_timesheet_sheet/report/timesheet_report_view.xml
+++ b/addons/hr_timesheet_sheet/report/timesheet_report_view.xml
@@ -18,7 +18,7 @@
timesheet.reporttree
-
+
diff --git a/addons/html_view/i18n/mk.po b/addons/html_view/i18n/mk.po
new file mode 100644
index 00000000000..dd98a0f9818
--- /dev/null
+++ b/addons/html_view/i18n/mk.po
@@ -0,0 +1,63 @@
+# Macedonian translation for openobject-addons
+# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2011.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2011-01-11 11:15+0000\n"
+"PO-Revision-Date: 2011-09-14 08:15+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Macedonian \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2011-09-15 04:47+0000\n"
+"X-Generator: Launchpad (build 13921)\n"
+
+#. module: html_view
+#: field:html.view,name:0
+msgid "Name"
+msgstr "Име"
+
+#. module: html_view
+#: field:html.view,comp_id:0
+msgid "Company"
+msgstr "Компанија"
+
+#. module: html_view
+#: model:ir.actions.act_window,name:html_view.action_html_view_form
+#: model:ir.ui.menu,name:html_view.html_form
+msgid "Html Test"
+msgstr "Html тест"
+
+#. module: html_view
+#: view:html.view:0
+msgid "Html Example"
+msgstr "Html пример"
+
+#. module: html_view
+#: model:ir.module.module,shortdesc:html_view.module_meta_information
+msgid "Html View"
+msgstr "Html преглед"
+
+#. module: html_view
+#: field:html.view,bank_ids:0
+msgid "Banks"
+msgstr "Банки"
+
+#. module: html_view
+#: model:ir.module.module,description:html_view.module_meta_information
+msgid ""
+"\n"
+" This is the test module which shows html tag supports in normal xml form "
+"view.\n"
+" "
+msgstr ""
+
+#. module: html_view
+#: model:ir.model,name:html_view.model_html_view
+msgid "html.view"
+msgstr "html.view"
diff --git a/addons/idea/idea_view.xml b/addons/idea/idea_view.xml
index e4cfa4efbe7..4111cd03962 100644
--- a/addons/idea/idea_view.xml
+++ b/addons/idea/idea_view.xml
@@ -263,7 +263,7 @@
idea.ideatree
-
+
diff --git a/addons/knowledge/i18n/en_GB.po b/addons/knowledge/i18n/en_GB.po
index 8d91cb25c17..6b24862c1f2 100644
--- a/addons/knowledge/i18n/en_GB.po
+++ b/addons/knowledge/i18n/en_GB.po
@@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME \n"
"POT-Creation-Date: 2011-01-11 11:15+0000\n"
-"PO-Revision-Date: 2011-08-25 17:36+0000\n"
-"Last-Translator: FULL NAME \n"
+"PO-Revision-Date: 2011-09-13 09:03+0000\n"
+"Last-Translator: John Bradshaw \n"
"Language-Team: English (United Kingdom) \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Launchpad-Export-Date: 2011-09-05 05:50+0000\n"
-"X-Generator: Launchpad (build 13830)\n"
+"X-Launchpad-Export-Date: 2011-09-14 04:41+0000\n"
+"X-Generator: Launchpad (build 13921)\n"
#. module: knowledge
#: model:ir.ui.menu,name:knowledge.menu_document
@@ -94,7 +94,7 @@ msgstr "Configuration"
#. module: knowledge
#: model:ir.actions.act_window,name:knowledge.action_knowledge_installer
msgid "Knowledge Modules Installation"
-msgstr ""
+msgstr "Knowledge Modules Installation"
#. module: knowledge
#: field:knowledge.installer,wiki_quality_manual:0
diff --git a/addons/lunch/lunch_view.xml b/addons/lunch/lunch_view.xml
index 5c19c840b78..09f1fcd92a6 100644
--- a/addons/lunch/lunch_view.xml
+++ b/addons/lunch/lunch_view.xml
@@ -49,7 +49,7 @@
lunch.ordertree
-
+
diff --git a/addons/mail_gateway/__init__.py b/addons/mail/__init__.py
similarity index 89%
rename from addons/mail_gateway/__init__.py
rename to addons/mail/__init__.py
index 6526541a068..4787883ef9c 100644
--- a/addons/mail_gateway/__init__.py
+++ b/addons/mail/__init__.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
+# Copyright (C) 2009-Today OpenERP SA ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -19,8 +19,10 @@
#
##############################################################################
-import mail_gateway
+import mail_message
+import mail_thread
import res_partner
+import wizard
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py
new file mode 100644
index 00000000000..e9716be9e8e
--- /dev/null
+++ b/addons/mail/__openerp__.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP S.A. ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+{
+ 'name': 'Email Subsystem',
+ 'version': '1.0',
+ 'category': 'Tools',
+ 'complexity': "easy",
+ 'description': """
+A generic email subsystem with message storage and queuing
+==========================================================
+
+This email subsystem is not intended to be used as as standalone
+application, but to provide a unified email abstraction that all
+other applications can use.
+
+The main features are:
+
+ * Relies on the global Outgoing Mail Servers configured in the
+ Administration menu for delivering outgoing mail
+ * Provides an API for sending messages and archiving them,
+ grouped by conversation
+ * Any OpenERP document can act as a conversation topic, provided
+ it includes the necessary support for handling incoming emails
+ (see the ``mail.thread`` class for more details).
+ * Includes queuing mechanism with automated configurable
+ scheduler-based processing
+ * Includes a generic email composition assistant, that can turn
+ into a mass-mailing assistant, and is capable of interpreting
+ simple *placeholder expressions* that will be replaced with
+ dynamic data when each email is actually sent.
+ This generic assistant is easily extensible to provide advanced
+ features (see ``email_template`` for example, which adds email
+ templating features to this assistant)
+
+ """,
+ 'author': 'OpenERP SA',
+ 'website': 'http://www.openerp.com',
+ 'depends': ['base', 'base_tools'],
+ 'data': [
+ "wizard/mail_compose_message_view.xml",
+ "mail_message_view.xml",
+ "mail_thread_view.xml",
+ "res_partner_view.xml",
+ 'security/ir.model.access.csv',
+ 'mail_data.xml',
+ ],
+ 'installable': True,
+ 'active': False,
+ 'certificate': '001056784984222247309',
+ 'images': ['images/customer_history.jpeg','images/messages_form.jpeg','images/messages_list.jpeg'],
+}
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail_gateway/i18n/bg.po b/addons/mail/i18n/bg.po
similarity index 100%
rename from addons/mail_gateway/i18n/bg.po
rename to addons/mail/i18n/bg.po
diff --git a/addons/mail_gateway/i18n/ca.po b/addons/mail/i18n/ca.po
similarity index 100%
rename from addons/mail_gateway/i18n/ca.po
rename to addons/mail/i18n/ca.po
diff --git a/addons/mail_gateway/i18n/de.po b/addons/mail/i18n/de.po
similarity index 100%
rename from addons/mail_gateway/i18n/de.po
rename to addons/mail/i18n/de.po
diff --git a/addons/mail_gateway/i18n/es.po b/addons/mail/i18n/es.po
similarity index 100%
rename from addons/mail_gateway/i18n/es.po
rename to addons/mail/i18n/es.po
diff --git a/addons/mail_gateway/i18n/es_PY.po b/addons/mail/i18n/es_PY.po
similarity index 100%
rename from addons/mail_gateway/i18n/es_PY.po
rename to addons/mail/i18n/es_PY.po
diff --git a/addons/mail_gateway/i18n/fi.po b/addons/mail/i18n/fi.po
similarity index 100%
rename from addons/mail_gateway/i18n/fi.po
rename to addons/mail/i18n/fi.po
diff --git a/addons/mail_gateway/i18n/fr.po b/addons/mail/i18n/fr.po
similarity index 100%
rename from addons/mail_gateway/i18n/fr.po
rename to addons/mail/i18n/fr.po
diff --git a/addons/mail_gateway/i18n/gl.po b/addons/mail/i18n/gl.po
similarity index 100%
rename from addons/mail_gateway/i18n/gl.po
rename to addons/mail/i18n/gl.po
diff --git a/addons/mail_gateway/i18n/hu.po b/addons/mail/i18n/hu.po
similarity index 100%
rename from addons/mail_gateway/i18n/hu.po
rename to addons/mail/i18n/hu.po
diff --git a/addons/mail_gateway/i18n/it.po b/addons/mail/i18n/it.po
similarity index 100%
rename from addons/mail_gateway/i18n/it.po
rename to addons/mail/i18n/it.po
diff --git a/addons/mail_gateway/i18n/lt.po b/addons/mail/i18n/lt.po
similarity index 100%
rename from addons/mail_gateway/i18n/lt.po
rename to addons/mail/i18n/lt.po
diff --git a/addons/mail_gateway/i18n/lv.po b/addons/mail/i18n/lv.po
similarity index 100%
rename from addons/mail_gateway/i18n/lv.po
rename to addons/mail/i18n/lv.po
diff --git a/addons/mail_gateway/i18n/mail_gateway.pot b/addons/mail/i18n/mail.pot
similarity index 100%
rename from addons/mail_gateway/i18n/mail_gateway.pot
rename to addons/mail/i18n/mail.pot
diff --git a/addons/mail_gateway/i18n/nl.po b/addons/mail/i18n/nl.po
similarity index 100%
rename from addons/mail_gateway/i18n/nl.po
rename to addons/mail/i18n/nl.po
diff --git a/addons/mail_gateway/i18n/pl.po b/addons/mail/i18n/pl.po
similarity index 100%
rename from addons/mail_gateway/i18n/pl.po
rename to addons/mail/i18n/pl.po
diff --git a/addons/mail_gateway/i18n/pt.po b/addons/mail/i18n/pt.po
similarity index 100%
rename from addons/mail_gateway/i18n/pt.po
rename to addons/mail/i18n/pt.po
diff --git a/addons/mail_gateway/i18n/pt_BR.po b/addons/mail/i18n/pt_BR.po
similarity index 100%
rename from addons/mail_gateway/i18n/pt_BR.po
rename to addons/mail/i18n/pt_BR.po
diff --git a/addons/mail_gateway/i18n/ro.po b/addons/mail/i18n/ro.po
similarity index 100%
rename from addons/mail_gateway/i18n/ro.po
rename to addons/mail/i18n/ro.po
diff --git a/addons/mail_gateway/i18n/ru.po b/addons/mail/i18n/ru.po
similarity index 100%
rename from addons/mail_gateway/i18n/ru.po
rename to addons/mail/i18n/ru.po
diff --git a/addons/mail_gateway/i18n/sl.po b/addons/mail/i18n/sl.po
similarity index 100%
rename from addons/mail_gateway/i18n/sl.po
rename to addons/mail/i18n/sl.po
diff --git a/addons/mail_gateway/i18n/sr@latin.po b/addons/mail/i18n/sr@latin.po
similarity index 100%
rename from addons/mail_gateway/i18n/sr@latin.po
rename to addons/mail/i18n/sr@latin.po
diff --git a/addons/mail_gateway/i18n/sv.po b/addons/mail/i18n/sv.po
similarity index 100%
rename from addons/mail_gateway/i18n/sv.po
rename to addons/mail/i18n/sv.po
diff --git a/addons/mail_gateway/i18n/zh_CN.po b/addons/mail/i18n/zh_CN.po
similarity index 100%
rename from addons/mail_gateway/i18n/zh_CN.po
rename to addons/mail/i18n/zh_CN.po
diff --git a/addons/email_template/email_template_scheduler_data.xml b/addons/mail/mail_data.xml
similarity index 59%
rename from addons/email_template/email_template_scheduler_data.xml
rename to addons/mail/mail_data.xml
index 45a9be04ac7..964552b951a 100644
--- a/addons/email_template/email_template_scheduler_data.xml
+++ b/addons/mail/mail_data.xml
@@ -1,15 +1,15 @@
-
- Email Template scheduler
+
+ Email Queue Manager1hours-1
-
-
+
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
new file mode 100644
index 00000000000..621eef59da5
--- /dev/null
+++ b/addons/mail/mail_message.py
@@ -0,0 +1,522 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-2011 OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import base64
+import dateutil.parser
+import email
+import logging
+import re
+import time
+from email.header import decode_header
+from email.message import Message
+
+import tools
+from osv import osv
+from osv import fields
+from tools.translate import _
+from tools.safe_eval import literal_eval
+
+_logger = logging.getLogger('mail')
+
+def format_date_tz(date, tz=None):
+ if not date:
+ return 'n/a'
+ format = tools.DEFAULT_SERVER_DATETIME_FORMAT
+ return tools.server_to_local_timestamp(date, format, format, tz)
+
+def truncate_text(text):
+ lines = text and text.split('\n') or []
+ if len(lines) > 3:
+ res = '\n\t'.join(lines[:3]) + '...'
+ else:
+ res = '\n\t'.join(lines)
+ return res
+
+def decode(text):
+ """Returns unicode() string conversion of the the given encoded smtp header text"""
+ if text:
+ text = decode_header(text.replace('\r', ''))
+ return ''.join([tools.ustr(x[0], x[1]) for x in text])
+
+def to_email(text):
+ """Return a list of the email addresses found in ``text``"""
+ if not text: return []
+ return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
+
+class mail_message_common(osv.osv_memory):
+ """Common abstract class for holding the main attributes of a
+ message object. It could be reused as parent model for any
+ database model or wizard screen that needs to hold a kind of
+ message"""
+
+ _name = 'mail.message.common'
+ _rec_name = 'subject'
+ _columns = {
+ 'subject': fields.char('Subject', size=512, required=True),
+ 'model': fields.char('Related Document model', size=128, select=1, readonly=1),
+ 'res_id': fields.integer('Related Document ID', select=1, readonly=1),
+ 'date': fields.datetime('Date'),
+ 'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences'),
+ 'email_to': fields.char('To', size=256, help='Message recipients'),
+ 'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
+ 'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
+ 'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
+ 'headers': fields.text('Message headers', readonly=1,
+ help="Full message headers, e.g. SMTP session headers "
+ "(usually available on inbound messages only)"),
+ 'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
+ 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
+ 'subtype': fields.char('Message type', size=32, help="Type of message, usually 'html' or 'plain', used to "
+ "select plaintext or rich text contents accordingly", readonly=1),
+ 'body_text': fields.text('Text contents', help="Plain-text version of the message"),
+ 'body_html': fields.text('Rich-text contents', help="Rich-text/HTML version of the message"),
+ }
+
+ _defaults = {
+ 'subtype': 'plain'
+ }
+
+class mail_message(osv.osv):
+ '''Model holding RFC2822 email messages, and providing facilities
+ to parse, queue and send new messages
+
+ Messages that do not have a value for the email_from column
+ are simple log messages (e.g. document state changes), while
+ actual e-mails have the email_from value set.
+ The ``display_text`` field will have a slightly different
+ presentation for real emails and for log messages.
+ '''
+
+ _name = 'mail.message'
+ _inherit = 'mail.message.common'
+ _description = 'Email Message'
+ _order = 'date desc'
+
+ # XXX to review - how to determine action to use?
+ def open_document(self, cr, uid, ids, context=None):
+ action_data = False
+ if ids:
+ msg = self.browse(cr, uid, ids[0], context=context)
+ model = msg.model
+ res_id = msg.res_id
+
+ ir_act_window = self.pool.get('ir.actions.act_window')
+ action_ids = ir_act_window.search(cr, uid, [('res_model', '=', model)])
+ if action_ids:
+ action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
+ action_data.update({
+ 'domain' : "[('id','=',%d)]"%(res_id),
+ 'nodestroy': True,
+ 'context': {}
+ })
+ return action_data
+
+ # XXX to review - how to determine action to use?
+ def open_attachment(self, cr, uid, ids, context=None):
+ action_data = False
+ action_pool = self.pool.get('ir.actions.act_window')
+ message = self.browse(cr, uid, ids, context=context)[0]
+ att_ids = [x.id for x in message.attachment_ids]
+ action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')])
+ if action_ids:
+ action_data = action_pool.read(cr, uid, action_ids[0], context=context)
+ action_data.update({
+ 'domain': [('id','in',att_ids)],
+ 'nodestroy': True
+ })
+ return action_data
+
+ def _get_display_text(self, cr, uid, ids, name, arg, context=None):
+ if context is None:
+ context = {}
+ tz = context.get('tz')
+ result = {}
+ for message in self.browse(cr, uid, ids, context=context):
+ msg_txt = ''
+ if message.email_from:
+ msg_txt += _('%s wrote on %s: \n Subject: %s \n\t') % (message.email_from or '/', format_date_tz(message.date, tz), message.subject)
+ if message.body_text:
+ msg_txt += truncate_text(message.body_text)
+ else:
+ msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz) + ':\n\t'
+ msg_txt += message.subject
+ result[message.id] = msg_txt
+ return result
+
+ _columns = {
+ 'partner_id': fields.many2one('res.partner', 'Related partner'),
+ 'user_id': fields.many2one('res.users', 'Related user', readonly=1),
+ 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
+ 'display_text': fields.function(_get_display_text, method=True, type='text', size="512", string='Display Text'),
+ 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
+ 'state': fields.selection([
+ ('outgoing', 'Outgoing'),
+ ('sent', 'Sent'),
+ ('received', 'Received'),
+ ('exception', 'Delivery Failed'),
+ ('cancel', 'Cancelled'),
+ ], 'State', readonly=True),
+ 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it, to save space"),
+ 'original': fields.binary('Original', help="Original version of the message, as it was sent on the network", readonly=1),
+ }
+
+ _defaults = {
+ 'state': 'received',
+ }
+
+ def init(self, cr):
+ cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
+ if not cr.fetchone():
+ cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ """Overridden to avoid duplicating fields that are unique to each email"""
+ if default is None:
+ default = {}
+ default.update(message_id=False,original=False,headers=False)
+ return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
+
+ def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None,
+ email_bcc=None, reply_to=False, attachments=None, message_id=False, references=False,
+ res_id=False, subtype='plain', headers=None, mail_server_id=False, auto_delete=False,
+ context=None):
+ """Schedule sending a new email message, to be sent the next time the mail scheduler runs, or
+ the next time :meth:`process_email_queue` is called explicitly.
+
+ :param string email_from: sender email address
+ :param list email_to: list of recipient addresses (to be joined with commas)
+ :param string subject: email subject (no pre-encoding/quoting necessary)
+ :param string body: email body, according to the ``subtype`` (by default, plaintext).
+ If html subtype is used, the message will be automatically converted
+ to plaintext and wrapped in multipart/alternative.
+ :param list email_cc: optional list of string values for CC header (to be joined with commas)
+ :param list email_bcc: optional list of string values for BCC header (to be joined with commas)
+ :param string model: optional model name of the document this mail is related to (this will also
+ be used to generate a tracking id, used to match any response related to the
+ same document)
+ :param int res_id: optional resource identifier this mail is related to (this will also
+ be used to generate a tracking id, used to match any response related to the
+ same document)
+ :param string reply_to: optional value of Reply-To header
+ :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
+ must match the format of the ``body`` parameter. Default is 'plain',
+ making the content part of the mail "text/plain".
+ :param dict attachments: map of filename to filecontents, where filecontents is a string
+ containing the bytes of the attachment
+ :param dict headers: optional map of headers to set on the outgoing mail (may override the
+ other headers, including Subject, Reply-To, Message-Id, etc.)
+ :param int mail_server_id: optional id of the preferred outgoing mail server for this mail
+ :param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been
+ successfully sent (default to False)
+
+ """
+ if context is None:
+ context = {}
+ if attachments is None:
+ attachments = {}
+ attachment_obj = self.pool.get('ir.attachment')
+ for param in (email_to, email_cc, email_bcc):
+ if param and not isinstance(param, list):
+ param = [param]
+ msg_vals = {
+ 'subject': subject,
+ 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'user_id': uid,
+ 'model': model,
+ 'res_id': res_id,
+ 'body_text': body if subtype != 'html' else False,
+ 'body_html': body if subtype == 'html' else False,
+ 'email_from': email_from,
+ 'email_to': email_to and ','.join(email_to) or '',
+ 'email_cc': email_cc and ','.join(email_cc) or '',
+ 'email_bcc': email_bcc and ','.join(email_bcc) or '',
+ 'reply_to': reply_to,
+ 'message_id': message_id,
+ 'references': references,
+ 'subtype': subtype,
+ 'headers': headers, # serialize the dict on the fly
+ 'mail_server_id': mail_server_id,
+ 'state': 'outgoing',
+ 'auto_delete': auto_delete
+ }
+ email_msg_id = self.create(cr, uid, msg_vals, context)
+ attachment_ids = []
+ for fname, fcontent in attachments.iteritems():
+ attachment_data = {
+ 'name': fname,
+ 'datas_fname': fname,
+ 'datas': fcontent,
+ 'res_model': self._name,
+ 'res_id': email_msg_id,
+ }
+ if context.has_key('default_type'):
+ del context['default_type']
+ attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
+ if attachment_ids:
+ self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
+ return email_msg_id
+
+ def mark_outgoing(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state':'outgoing'}, context)
+
+ def process_email_queue(self, cr, uid, ids=None, context=None):
+ """Send immediately queued messages, committing after each
+ message is sent - this is not transactional and should
+ not be called during another transaction!
+
+ :param list ids: optional list of emails ids to send. If passed
+ no search is performed, and these ids are used
+ instead.
+ :param dict context: if a 'filters' key is present in context,
+ this value will be used as an additional
+ filter to further restrict the outgoing
+ messages to send (by default all 'outgoing'
+ messages are sent).
+ """
+ if context is None:
+ context = {}
+ if not ids:
+ filters = [('state', '=', 'outgoing')]
+ if 'filters' in context:
+ filters.extend(context['filters'])
+ ids = self.search(cr, uid, filters, context=context)
+ res = None
+ try:
+ # Force auto-commit - this is meant to be called by
+ # the scheduler, and we can't allow rolling back the status
+ # of previously sent emails!
+ res = self.send(cr, uid, ids, auto_commit=True, context=context)
+ except Exception:
+ _logger.exception("Failed processing mail queue")
+ return res
+
+ def parse_message(self, message, save_original=False):
+ """Parses a string or email.message.Message representing an
+ RFC-2822 email, and returns a generic dict holding the
+ message details.
+
+ :param message: the message to parse
+ :type message: email.message.Message | string | unicode
+ :param bool save_original: whether the returned dict
+ should include an ``original`` entry with the base64
+ encoded source of the message.
+ :rtype: dict
+ :return: A dict with the following structure, where each
+ field may not be present if missing in original
+ message::
+
+ { 'message-id': msg_id,
+ 'subject': subject,
+ 'from': from,
+ 'to': to,
+ 'cc': cc,
+ 'headers' : { 'X-Mailer': mailer,
+ #.. all X- headers...
+ },
+ 'subtype': msg_mime_subtype,
+ 'body_text': plaintext_body
+ 'body_html': html_body,
+ 'attachments': { 'file1': 'bytes',
+ 'file2': 'bytes' }
+ # ...
+ 'original': source_of_email,
+ }
+ """
+ msg_txt = message
+ if isinstance(message, str):
+ msg_txt = email.message_from_string(message)
+
+ # Warning: message_from_string doesn't always work correctly on unicode,
+ # we must use utf-8 strings here :-(
+ if isinstance(message, unicode):
+ message = message.encode('utf-8')
+ msg_txt = email.message_from_string(message)
+
+ message_id = msg_txt.get('message-id', False)
+ msg = {}
+
+ if save_original:
+ # save original, we need to be able to read the original email sometimes
+ msg['original'] = message.as_string() if isinstance(message, Message) \
+ else message
+ msg['original'] = base64.b64encode(msg['original']) # binary fields are b64
+
+ if not message_id:
+ # Very unusual situation, be we should be fault-tolerant here
+ message_id = time.time()
+ msg_txt['message-id'] = message_id
+ _logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
+
+ fields = msg_txt.keys()
+ msg['id'] = message_id
+ msg['message-id'] = message_id
+
+ if 'Subject' in fields:
+ msg['subject'] = decode(msg_txt.get('Subject'))
+
+ if 'Content-Type' in fields:
+ msg['content-type'] = msg_txt.get('Content-Type')
+
+ if 'From' in fields:
+ msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
+
+ if 'To' in fields:
+ msg['to'] = decode(msg_txt.get('To'))
+ if 'Delivered-To' in fields:
+ msg['to'] = decode(msg_txt.get('Delivered-To'))
+
+ if 'CC' in fields:
+ msg['cc'] = decode(msg_txt.get('CC'))
+
+ if 'Reply-To' in fields:
+ msg['reply'] = decode(msg_txt.get('Reply-To'))
+
+ if 'Date' in fields:
+ date_hdr = decode(msg_txt.get('Date'))
+ msg['date'] = dateutil.parser.parse(date_hdr).strftime("%Y-%m-%d %H:%M:%S")
+
+ if 'Content-Transfer-Encoding' in fields:
+ msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
+
+ if 'References' in fields:
+ msg['references'] = msg_txt.get('References')
+
+ if 'In-Reply-To' in fields:
+ msg['in-reply-to'] = msg_txt.get('In-Reply-To')
+
+ msg['headers'] = {}
+ msg['subtype'] = 'plain'
+ for item in msg_txt.items():
+ if item[0].startswith('X-'):
+ msg['headers'].update({item[0]: item[1]})
+ if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
+ encoding = msg_txt.get_content_charset()
+ body = msg_txt.get_payload(decode=True)
+ if 'text/html' in msg.get('content-type', ''):
+ msg['body_html'] = body
+ msg['subtype'] = 'html'
+ body = tools.html2plaintext(body)
+ msg['body_text'] = tools.ustr(body, encoding)
+
+ attachments = {}
+ if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
+ body = ""
+ if 'multipart/alternative' in msg.get('content-type', ''):
+ msg['subtype'] = 'alternative'
+ else:
+ msg['subtype'] = 'mixed'
+ for part in msg_txt.walk():
+ if part.get_content_maintype() == 'multipart':
+ continue
+
+ encoding = part.get_content_charset()
+ filename = part.get_filename()
+ if part.get_content_maintype()=='text':
+ content = part.get_payload(decode=True)
+ if filename:
+ attachments[filename] = content
+ content = tools.ustr(content, encoding)
+ if part.get_content_subtype() == 'html':
+ msg['body_html'] = content
+ msg['subtype'] = 'html' # html version prevails
+ body = tools.ustr(tools.html2plaintext(content))
+ elif part.get_content_subtype() == 'plain':
+ body = content
+ elif part.get_content_maintype() in ('application', 'image'):
+ if filename :
+ attachments[filename] = part.get_payload(decode=True)
+ else:
+ res = part.get_payload(decode=True)
+ body += tools.ustr(res, encoding)
+
+ msg['body_text'] = body
+ msg['attachments'] = attachments
+
+ # for backwards compatibility:
+ msg['body'] = msg['body_text']
+ msg['sub_type'] = msg['subtype'] or 'plain'
+ return msg
+
+ def send(self, cr, uid, ids, auto_commit=False, context=None):
+ """Sends the selected emails immediately, ignoring their current
+ state (mails that have already been sent should not be passed
+ unless they should actually be re-sent).
+ Emails successfully delivered are marked as 'sent', and those
+ that fail to be deliver are marked as 'exception', and the
+ corresponding error message is output in the server logs.
+
+ :param bool auto_commit: whether to force a commit of the message
+ status after sending each message (meant
+ only for processing by the scheduler),
+ should never be True during normal
+ transactions (default: False)
+ :return: True
+ """
+ if context is None:
+ context = {}
+ ir_mail_server = self.pool.get('ir.mail_server')
+ self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
+ for message in self.browse(cr, uid, ids, context=context):
+ try:
+ attachments = []
+ for attach in message.attachment_ids:
+ attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
+ msg = ir_mail_server.build_email(
+ email_from=message.email_from,
+ email_to=to_email(message.email_to),
+ subject=message.subject,
+ body=message.body_html if message.subtype == 'html' else message.body_text,
+ email_cc=to_email(message.email_cc),
+ email_bcc=to_email(message.email_bcc),
+ reply_to=message.reply_to,
+ attachments=attachments, message_id=message.message_id,
+ references = message.references,
+ object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
+ subtype=message.subtype,
+ headers=message.headers and literal_eval(message.headers))
+ res = ir_mail_server.send_email(cr, uid, msg,
+ mail_server_id=message.mail_server_id.id,
+ context=context)
+ if res:
+ message.write({'state':'sent', 'message_id': res})
+ else:
+ message.write({'state':'exception'})
+
+ # if auto_delete=True then delete that sent messages as well as attachments
+ message.refresh()
+ if message.state == 'sent' and message.auto_delete:
+ self.pool.get('ir.attachment').unlink(cr, uid,
+ [x.id for x in message.attachment_ids],
+ context=context)
+ message.unlink()
+ except Exception:
+ _logger.exception('failed sending mail.message %s', message.id)
+ message.write({'state':'exception'})
+
+ if auto_commit == True:
+ cr.commit()
+ return True
+
+ def cancel(self, cr, uid, ids, context=None):
+ self.write(cr, uid, ids, {'state':'cancel'}, context=context)
+ return True
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
new file mode 100644
index 00000000000..83e7474fe1a
--- /dev/null
+++ b/addons/mail/mail_message_view.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+ mail.message.form
+ mail.message
+ form
+
+
+
+
+
+
+ mail.message.tree
+ mail.message
+ tree
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.message.search
+ mail.message
+ search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Messages
+ mail.message
+ form
+ tree,form
+ [('email_from', '!=', False)]
+ {'search_default_outgoing':1}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
new file mode 100644
index 00000000000..fa785c3517c
--- /dev/null
+++ b/addons/mail/mail_thread.py
@@ -0,0 +1,463 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2009-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import time
+import tools
+import base64
+import email
+from email.utils import parsedate
+
+import logging
+import xmlrpclib
+from osv import osv, fields
+from tools.translate import _
+from mail_message import decode, to_email
+
+_logger = logging.getLogger('mail')
+
+class mail_thread(osv.osv):
+ '''Mixin model, meant to be inherited by any model that needs to
+ act as a discussion topic on which messages can be attached.
+ Public methods are prefixed with ``message_`` in order to avoid
+ name collisions with methods of the models that will inherit
+ from this mixin.
+
+ ``mail.thread`` adds a one2many of mail.messages, acting as the
+ thread's history, and a few methods that may be overridden to
+ implement model-specific behavior upon arrival of new messages.
+
+ Inheriting classes are not required to implement any method, as the
+ default implementation will work for any model. However it is common
+ to override at least the ``message_new`` and ``message_update``
+ methods (calling ``super``) to add model-specific behavior at
+ creation and update of a thread.
+
+ '''
+ _name = 'mail.thread'
+ _description = 'Email Thread'
+
+ _columns = {
+ 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', readonly=True),
+ }
+
+ def message_thread_followers(self, cr, uid, ids, context=None):
+ """Returns a list of email addresses of the people following
+ this thread, including the sender of each mail, and the
+ people who were in CC of the messages, if any.
+ """
+ res = {}
+ if isinstance(ids, (str, int, long)):
+ ids = [long(ids)]
+ for thread in self.browse(cr, uid, ids, context=context):
+ l = set()
+ for message in thread.message_ids:
+ l.add((message.user_id and message.user_id.user_email) or '')
+ l.add(message.email_from or '')
+ l.add(message.email_cc or '')
+ res[thread.id] = filter(None, l)
+ return res
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ """Overrides default copy method to empty the thread of
+ messages attached to this record, as the copied object
+ will have its own thread and does not have to share it.
+ """
+ if default is None:
+ default = {}
+ default.update({
+ 'message_ids': [],
+ })
+ return super(mail_thread, self).copy(cr, uid, id, default, context=context)
+
+ def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
+ """Called by ``message_process`` when a new message is received
+ for a given thread model, if the message did not belong to
+ an existing thread.
+ The default behavior is to create a new record of the corresponding
+ model (based on some very basic info extracted from the message),
+ then attach the message to the newly created record
+ (by calling ``message_append_dict``).
+ Additional behavior may be implemented by overriding this method.
+
+ :param dict msg_dict: a map containing the email details and
+ attachments. See ``message_process`` and
+ ``mail.message.parse`` for details.
+ :param dict custom_values: optional dictionary of additional
+ field values to pass to create()
+ when creating the new thread record.
+ Be careful, these values may override
+ any other values coming from the message.
+ :param dict context: if a ``thread_model`` value is present
+ in the context, its value will be used
+ to determine the model of the record
+ to create (instead of the current model).
+ :rtype: int
+ :return: the id of the newly created thread object
+ """
+ if context is None:
+ context = {}
+ model = context.get('thread_model') or self._name
+ model_pool = self.pool.get(model)
+ fields = model_pool.fields_get(cr, uid, context=context)
+ data = model_pool.default_get(cr, uid, fields, context=context)
+ if 'name' in fields and not data.get('name'):
+ data['name'] = msg_dict.get('from','')
+ if custom_values and isinstance(custom_values, dict):
+ data.update(custom_values)
+ res_id = model_pool.create(cr, uid, data, context=context)
+ self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
+ return res_id
+
+ def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
+ """Called by ``message_process`` when a new message is received
+ for an existing thread. The default behavior is to create a
+ new mail.message in the given thread (by calling
+ ``message_append_dict``)
+ Additional behavior may be implemented by overriding this
+ method.
+
+ :param dict msg_dict: a map containing the email details and
+ attachments. See ``message_process`` and
+ ``mail.message.parse()`` for details.
+ :param dict context: if a ``thread_model`` value is present
+ in the context, its value will be used
+ to determine the model of the thread to
+ update (instead of the current model).
+ """
+ return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
+
+ def message_append(self, cr, uid, threads, subject, body_text=None, email_to=False,
+ email_from=False, email_cc=None, email_bcc=None, reply_to=None,
+ email_date=None, message_id=False, references=None,
+ attachments=None, body_html=None, subtype=None, headers=None,
+ original=None, context=None):
+ """Creates a new mail.message attached to the current mail.thread,
+ containing all the details passed as parameters. All attachments
+ will be attached to the thread record as well as to the actual
+ message.
+ If only the ``threads`` and ``subject`` parameters are provided,
+ a *event log* message is created, without the usual envelope
+ attributes (sender, recipients, etc.).
+
+ :param threads: list of thread ids, or list of browse_records representing
+ threads to which a new message should be attached
+ :param subject: subject of the message, or description of the event if this
+ is an *event log* entry.
+ :param email_to: Email-To / Recipient address
+ :param email_from: Email From / Sender address if any
+ :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
+ :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
+ :param reply_to: reply_to header
+ :param email_date: email date string if different from now, in server timezone
+ :param message_id: optional email identifier
+ :param references: optional email references
+ :param body_text: plaintext contents of the mail or log message
+ :param body_html: html contents of the mail or log message
+ :param subtype: optional type of message: 'plain' or 'html', corresponding to the main
+ body contents (body_text or body_html).
+ :param headers: mail headers to store
+ :param dict attachments: map of attachment filenames to binary contents, if any.
+ :param str original: optional full source of the RFC2822 email, for reference
+ :param dict context: if a ``thread_model`` value is present
+ in the context, its value will be used
+ to determine the model of the thread to
+ update (instead of the current model).
+ """
+ if context is None:
+ context = {}
+ if attachments is None:
+ attachments = {}
+
+ if email_date:
+ edate = parsedate(email_date)
+ if edate is not None:
+ email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
+
+ if all(isinstance(thread_id, (int, long)) for thread_id in threads):
+ model = context.get('thread_model') or self._name
+ model_pool = self.pool.get(model)
+ threads = model_pool.browse(cr, uid, threads, context=context)
+
+ ir_attachment = self.pool.get('ir.attachment')
+ mail_message = self.pool.get('mail.message')
+
+ for thread in threads:
+ to_attach = []
+ for fname, fcontent in attachments.items():
+ if isinstance(fcontent, unicode):
+ fcontent = fcontent.encode('utf-8')
+ data_attach = {
+ 'name': fname,
+ 'datas': base64.b64encode(str(fcontent)),
+ 'datas_fname': fname,
+ 'description': _('Mail attachment'),
+ 'res_model': thread._name,
+ 'res_id': thread.id,
+ }
+ to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
+
+ partner_id = hasattr(thread, 'partner_id') and (thread.partner_id and thread.partner_id.id or False) or False
+ if not partner_id and thread._name == 'res.partner':
+ partner_id = thread.id
+ data = {
+ 'subject': subject,
+ 'user_id': uid,
+ 'model' : thread._name,
+ 'partner_id': partner_id,
+ 'res_id': thread.id,
+ 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'message_id': message_id,
+ 'body_text': body_text or (hasattr(thread, 'description') and thread.description or False),
+ 'attachment_ids': [(6, 0, to_attach)],
+ 'state' : 'received',
+ }
+
+ if email_from:
+ for param in (email_to, email_cc, email_bcc):
+ if isinstance(param, list):
+ param = ", ".join(param)
+ data = {
+ 'subject': subject or _('History'),
+ 'user_id': uid,
+ 'model' : thread._name,
+ 'res_id': thread.id,
+ 'date': email_date or time.strftime('%Y-%m-%d %H:%M:%S'),
+ 'body_text': body_text,
+ 'email_to': email_to,
+ 'email_from': email_from or \
+ (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
+ 'email_cc': email_cc,
+ 'email_bcc': email_bcc,
+ 'partner_id': partner_id,
+ 'references': references,
+ 'message_id': message_id,
+ 'attachment_ids': [(6, 0, to_attach)],
+ 'state' : 'received',
+ 'body_html': body_html,
+ 'subtype': subtype,
+ 'headers': headers,
+ 'reply_to': reply_to,
+ 'original': original,
+ }
+ mail_message.create(cr, uid, data, context=context)
+ return True
+
+ def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
+ """Creates a new mail.message attached to the given threads (``ids``),
+ with the contents of ``msg_dict``, by calling ``message_append``
+ with the mail details. All attachments in msg_dict will be
+ attached to the object record as well as to the actual
+ mail message.
+
+ :param dict msg_dict: a map containing the email details and
+ attachments. See ``message_process()`` and
+ ``mail.message.parse()`` for details on
+ the dict structure.
+ :param dict context: if a ``thread_model`` value is present
+ in the context, its value will be used
+ to determine the model of the thread to
+ update (instead of the current model).
+ """
+ return self.message_append(cr, uid, ids,
+ subject = msg_dict.get('subject'),
+ body_text = msg_dict.get('body_text'),
+ email_to = msg_dict.get('to'),
+ email_from = msg_dict.get('from'),
+ email_cc = msg_dict.get('cc'),
+ email_bcc = msg_dict.get('bcc'),
+ reply_to = msg_dict.get('reply'),
+ email_date = msg_dict.get('date'),
+ message_id = msg_dict.get('message-id'),
+ references = msg_dict.get('references')\
+ or msg_dict.get('in-reply-to'),
+ attachments = msg_dict.get('attachments'),
+ body_html= msg_dict.get('body_html'),
+ subtype = msg_dict.get('subtype'),
+ headers = msg_dict.get('headers'),
+ original = msg_dict.get('original'),
+ context = context)
+
+
+ def message_process(self, cr, uid, model, message, custom_values=None,
+ save_original=False, strip_attachments=False,
+ context=None):
+ """Process an incoming RFC2822 email message related to the
+ given thread model, relying on ``mail.message.parse()``
+ for the parsing operation, and then calling ``message_new``
+ (if the thread record did not exist) or ``message_update``
+ (if it did), then calling ``message_forward`` to automatically
+ notify other people that should receive this message.
+
+ :param string model: the thread model for which a new message
+ must be processed
+ :param message: source of the RFC2822 mail
+ :type message: string or xmlrpclib.Binary
+ :type dict custom_values: optional dictionary of field values
+ to pass to ``message_new`` if a new
+ record needs to be created. Ignored
+ if the thread record already exists.
+ :param bool save_original: whether to keep a copy of the original
+ email source attached to the message after it is imported.
+ :param bool strip_attachments: whether to strip all attachments
+ before processing the message, in order to save some space.
+ """
+ # extract message bytes - we are forced to pass the message as binary because
+ # we don't know its encoding until we parse its headers and hence can't
+ # convert it to utf-8 for transport between the mailgate script and here.
+ if isinstance(message, xmlrpclib.Binary):
+ message = str(message.data)
+
+ model_pool = self.pool.get(model)
+ if self._name != model:
+ if context is None: context = {}
+ context.update({'thread_model': model})
+
+ mail_message = self.pool.get('mail.message')
+ res_id = False
+
+ # Parse Message
+ # Warning: message_from_string doesn't always work correctly on unicode,
+ # we must use utf-8 strings here :-(
+ if isinstance(message, unicode):
+ message = message.encode('utf-8')
+ msg_txt = email.message_from_string(message)
+ msg = mail_message.parse_message(msg_txt, save_original=save_original)
+
+ if strip_attachments and 'attachments' in msg:
+ del msg['attachments']
+
+ # Create New Record into particular model
+ def create_record(msg):
+ if hasattr(model_pool, 'message_new'):
+ return model_pool.message_new(cr, uid, msg,
+ custom_values,
+ context=context)
+ res_id = False
+ if msg.get('references') or msg.get('in-reply-to'):
+ references = msg.get('references') or msg.get('in-reply-to')
+ if '\r\n' in references:
+ references = references.split('\r\n')
+ else:
+ references = references.split(' ')
+ for ref in references:
+ ref = ref.strip()
+ res_id = tools.reference_re.search(ref)
+ if res_id:
+ res_id = res_id.group(1)
+ else:
+ res_id = tools.res_re.search(msg['subject'])
+ if res_id:
+ res_id = res_id.group(1)
+ if res_id:
+ res_id = int(res_id)
+ if model_pool.exists(cr, uid, res_id):
+ if hasattr(model_pool, 'message_update'):
+ model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
+ else:
+ # referenced thread was not found, we'll have to create a new one
+ res_id = False
+ if not res_id:
+ res_id = create_record(msg)
+ #To forward the email to other followers
+ self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
+ return res_id
+
+ # for backwards-compatibility with old scripts
+ process_email = message_process
+
+ def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
+ """Sends an email to all people following the given threads.
+ The emails are forwarded immediately, not queued for sending,
+ and not archived.
+
+ :param str model: thread model
+ :param list thread_ids: ids of the thread records
+ :param msg: email.message.Message object to forward
+ :param email_error: optional email address to notify in case
+ of any delivery error during the forward.
+ :return: True
+ """
+ model_pool = self.pool.get(model)
+ smtp_server_obj = self.pool.get('ir.mail_server')
+ mail_message = self.pool.get('mail.message')
+ for res in model_pool.browse(cr, uid, thread_ids, context=context):
+ if hasattr(model_pool, 'message_thread_followers'):
+ followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
+ else:
+ followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
+ message_followers_emails = to_email(','.join(filter(None, followers)))
+ message_recipients = to_email(','.join(filter(None,
+ [decode(msg['from']),
+ decode(msg['to']),
+ decode(msg['cc'])])))
+ forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
+ if forward_to:
+ # TODO: we need an interface for this for all types of objects, not just leads
+ if hasattr(res, 'section_id'):
+ del msg['reply-to']
+ msg['reply-to'] = res.section_id.reply_to
+
+ smtp_from, = to_email(msg['from'])
+ msg['from'] = smtp_from
+ msg['to'] = ", ".join(forward_to)
+ msg['message-id'] = tools.generate_tracking_message_id(res.id)
+ if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
+ subj = msg['subject']
+ del msg['subject'], msg['to'], msg['cc'], msg['bcc']
+ msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
+ msg['to'] = email_error
+ smtp_server_obj.send_email(cr, uid, msg)
+ return True
+
+ def message_partner_by_email(self, cr, uid, email, context=None):
+ """Attempts to return the id of a partner address matching
+ the given ``email``, and the corresponding partner id.
+ Can be used by classes using the ``mail.thread`` mixin
+ to lookup the partner and use it in their implementation
+ of ``message_new`` to link the new record with a
+ corresponding partner.
+ The keys used in the returned dict are meant to map
+ to usual names for relationships towards a partner
+ and one of its addresses.
+
+ :param email: email address for which a partner
+ should be searched for.
+ :rtype: dict
+ :return: a map of the following form::
+
+ { 'partner_address_id': id or False,
+ 'partner_id': pid or False }
+ """
+ address_pool = self.pool.get('res.partner.address')
+ res = {
+ 'partner_address_id': False,
+ 'partner_id': False
+ }
+ if email:
+ email = to_email(email)[0]
+ address_ids = address_pool.search(cr, uid, [('email', '=', email)])
+ if address_ids:
+ address = address_pool.browse(cr, uid, address_ids[0])
+ res['partner_address_id'] = address_ids[0]
+ res['partner_id'] = address.partner_id.id
+ return res
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_thread_view.xml b/addons/mail/mail_thread_view.xml
new file mode 100644
index 00000000000..bd10a72faf2
--- /dev/null
+++ b/addons/mail/mail_thread_view.xml
@@ -0,0 +1,72 @@
+
+
+
+
+ mail.thread.form
+ mail.thread
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.thread.tree
+ mail.thread
+ tree
+
+
+
+
+
+
+
+
+
+ Email Threads
+ mail.thread
+ tree,form
+ form
+
+
+
+
+ tree
+
+
+
+
+
+ form
+
+
+
+
+
+
diff --git a/addons/mail_gateway/res_partner.py b/addons/mail/res_partner.py
similarity index 86%
rename from addons/mail_gateway/res_partner.py
rename to addons/mail/res_partner.py
index c6f41ff0414..637922ff75c 100644
--- a/addons/mail_gateway/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -19,17 +19,16 @@
#
##############################################################################
-from osv import fields,osv
+from osv import osv
+from osv import fields
class res_partner(osv.osv):
""" Inherits partner and adds CRM information in the partner form """
_inherit = 'res.partner'
_columns = {
- 'emails': fields.one2many('mailgate.message', 'partner_id',\
- 'Emails', readonly=True, domain=[('history','=',True)]),
+ 'emails': fields.one2many('mail.message', 'partner_id', 'Emails', readonly=True, domain=[('email_from','!=',False)]),
}
res_partner()
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
new file mode 100644
index 00000000000..21da7d735b7
--- /dev/null
+++ b/addons/mail/res_partner_view.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ res.partner.crm.history.inherit1
+ res.partner
+ form
+
+
+
+ False
+
+
+
+
+ res.partner.emails.info.inherit
+ res.partner
+ form
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/mail_gateway/scripts/__init__.py b/addons/mail/scripts/__init__.py
similarity index 91%
rename from addons/mail_gateway/scripts/__init__.py
rename to addons/mail/scripts/__init__.py
index 9d7f6bf56c9..3b6a8c7e4b0 100644
--- a/addons/mail_gateway/scripts/__init__.py
+++ b/addons/mail/scripts/__init__.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
+# Copyright (C) 2009-2010 OpenERP SA ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -19,6 +19,4 @@
#
##############################################################################
-import openerp_mailgate
-
-
+import openerp_mailgate
\ No newline at end of file
diff --git a/addons/mail_gateway/scripts/openerp_mailgate/__init__.py b/addons/mail/scripts/openerp_mailgate/__init__.py
similarity index 93%
rename from addons/mail_gateway/scripts/openerp_mailgate/__init__.py
rename to addons/mail/scripts/openerp_mailgate/__init__.py
index 4f7fae0a190..de2e2bf390d 100644
--- a/addons/mail_gateway/scripts/openerp_mailgate/__init__.py
+++ b/addons/mail/scripts/openerp_mailgate/__init__.py
@@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
+# Copyright (C) 2009-2010 OpenERP SA ().
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
diff --git a/addons/mail_gateway/scripts/openerp_mailgate/openerp_mailgate.py b/addons/mail/scripts/openerp_mailgate/openerp_mailgate.py
similarity index 86%
rename from addons/mail_gateway/scripts/openerp_mailgate/openerp_mailgate.py
rename to addons/mail/scripts/openerp_mailgate/openerp_mailgate.py
index bcff408ef46..68ae20d35e3 100755
--- a/addons/mail_gateway/scripts/openerp_mailgate/openerp_mailgate.py
+++ b/addons/mail/scripts/openerp_mailgate/openerp_mailgate.py
@@ -46,13 +46,14 @@ class DefaultConfig(object):
OPENERP_PORT = 8069
OPENERP_DEFAULT_DATABASE = 'openerp'
MAIL_ERROR = 'error@example.com'
- MAIL_SERVER = 'localhost'
- MAIL_ADMINS = ('admin@example.com',)
+ MAIL_SERVER = 'smtp.example.com'
+ MAIL_SERVER_PORT = 25
+ MAIL_ADMINS = ('info@example.com',)
config = DefaultConfig()
-def send_mail(_from_, to_, subject, text, files=None, server=config.MAIL_SERVER):
+def send_mail(_from_, to_, subject, text, files=None, server=config.MAIL_SERVER, port=config.MAIL_SERVER_PORT):
assert isinstance(to_, (list, tuple))
if files is None:
@@ -74,7 +75,7 @@ def send_mail(_from_, to_, subject, text, files=None, server=config.MAIL_SERVER)
% file_name)
msg.attach(part)
- smtp = smtplib.SMTP(server)
+ smtp = smtplib.SMTP(server, port=port)
smtp.sendmail(_from_, to_, msg.as_string() )
smtp.close()
@@ -104,16 +105,15 @@ class EmailParser(object):
self.email_default = email_default
- def parse(self, message, custom_values=None):
- if custom_values is None:
- custom_values = {}
+ def parse(self, message, custom_values=None, save_original=None):
# pass message as bytes because we don't know its encoding until we parse its headers
# and hence can't convert it to utf-8 for transport
- self.rpc('email.server.tools',
- 'process_email',
- self.model,
- xmlrpclib.Binary(message),
- custom_values)
+ res_id = self.rpc('mail.thread',
+ 'message_process',
+ self.model,
+ xmlrpclib.Binary(message),
+ custom_values or {},
+ save_original or False)
def configure_parser():
parser = optparse.OptionParser(usage='usage: %prog [options]', version='%prog v1.1')
@@ -145,6 +145,10 @@ def configure_parser():
parser.add_option("--custom-values", dest="custom_values",
help="Add Custom Values to the object",
default=None)
+ parser.add_option("-s", dest="save_original",
+ action="store_true",
+ help="Attach a copy of original email to the message entry",
+ default=False)
return parser
@@ -171,10 +175,11 @@ def main():
try:
custom_values = dict(eval(options.custom_values or {} ))
except:
- pass
+ import traceback
+ traceback.print_exc()
try:
- email_parser.parse(msg_txt, custom_values)
+ email_parser.parse(msg_txt, custom_values, options.save_original or False)
except Exception:
msg = '\n'.join([
'parameters',
@@ -191,6 +196,7 @@ def main():
config.MAIL_ADMINS,
subject, msg, files=[('message.txt', msg_txt)]
)
+ sys.stderr.write("Failed to deliver email to OpenERP Server, sending error notification to %s\n" % config.MAIL_ADMINS)
if __name__ == '__main__':
main()
diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv
new file mode 100644
index 00000000000..b508cd68d83
--- /dev/null
+++ b/addons/mail/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_mail_message","mail.message","model_mail_message",,1,0,0,0
+"access_mail_thread","mail.thread","model_mail_thread",,1,0,0,0
diff --git a/addons/mail/wizard/__init__.py b/addons/mail/wizard/__init__.py
new file mode 100644
index 00000000000..88b9581f68e
--- /dev/null
+++ b/addons/mail/wizard/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import mail_compose_message
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
new file mode 100644
index 00000000000..0bfaf06890a
--- /dev/null
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -0,0 +1,272 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+import re
+
+import tools
+from mail.mail_message import to_email
+from osv import osv
+from osv import fields
+from tools.safe_eval import safe_eval as eval
+from tools.safe_eval import literal_eval
+from tools.translate import _
+
+# main mako-like expression pattern
+EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
+
+class mail_compose_message(osv.osv_memory):
+ """Generic E-mail composition wizard. This wizard is meant to be inherited
+ at model and view level to provide specific wizard features.
+
+ The behavior of the wizard can be modified through the use of context
+ parameters, among which are:
+
+ * mail.compose.message.mode: if set to 'reply', the wizard is in
+ reply mode and pre-populated with the original quote.
+ If set to 'mass_mail', the wizard is in mass mailing
+ where the mail details can contain template placeholders
+ that will be merged with actual data before being sent
+ to each recipient. Recipients will be derived from the
+ records determined via ``context['active_model']`` and
+ ``context['active_ids']``.
+ * active_model: model name of the document to which the mail being
+ composed is related
+ * active_id: id of the document to which the mail being composed is
+ related, or id of the message to which user is replying,
+ in case ``mail.compose.message.mode == 'reply'``
+ * active_ids: ids of the documents to which the mail being composed is
+ related, in case ``mail.compose.message.mode == 'mass_mail'``.
+ """
+ _name = 'mail.compose.message'
+ _inherit = 'mail.message.common'
+ _description = 'E-mail composition wizard'
+
+ def default_get(self, cr, uid, fields, context=None):
+ """Overridden to provide specific defaults depending on the context
+ parameters.
+
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
+ """
+ if context is None:
+ context = {}
+ result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
+ vals = {}
+ reply_mode = context.get('mail.compose.message.mode') == 'reply'
+ if (not reply_mode) and context.get('active_model') and context.get('active_id'):
+ # normal mode when sending an email related to any document, as specified by
+ # active_model and active_id in context
+ vals = self.get_value(cr, uid, context.get('active_model'), context.get('active_id'), context)
+ elif reply_mode and context.get('active_id'):
+ # reply mode, consider active_id is the ID of a mail.message to which we're
+ # replying
+ vals = self.get_message_data(cr, uid, int(context['active_id']), context)
+ else:
+ # default mode
+ result['model'] = context.get('active_model', False)
+ if vals:
+ for field in fields:
+ result.update({field : vals.get(field, False)})
+
+ # link to model and record if not done yet
+ if not result.get('model') or not result.get('res_id'):
+ active_model = context.get('active_model')
+ res_id = context.get('active_id')
+ if active_model and active_model not in (self._name, 'mail.message'):
+ result['model'] = active_model
+ if res_id:
+ result['res_id'] = res_id
+
+ # Try to provide default email_from if not specified yet
+ if not result.get('email_from'):
+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
+ result['email_from'] = current_user.user_email or False
+ return result
+
+ _columns = {
+ 'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
+ 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
+ 'filter_id': fields.many2one('ir.filters', 'Filters'),
+ }
+
+ def get_value(self, cr, uid, model, res_id, context=None):
+ """Returns a defaults-like dict with initial values for the composition
+ wizard when sending an email related to the document record identified
+ by ``model`` and ``res_id``.
+
+ The default implementation returns an empty dictionary, and is meant
+ to be overridden by subclasses.
+
+ :param str model: model name of the document record this mail is related to.
+ :param int res_id: id of the document record this mail is related to.
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
+ """
+ return {}
+
+ def get_message_data(self, cr, uid, message_id, context=None):
+ """Returns a defaults-like dict with initial values for the composition
+ wizard when replying to the given message (e.g. including the quote
+ of the initial message, and the correct recipient).
+ Should not be called unless ``context['mail.compose.message.mode'] == 'reply'``.
+
+ :param int message_id: id of the mail.message to which the user
+ is replying.
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
+ When calling this method, the ``'mail'`` value
+ in the context should be ``'reply'``.
+ """
+ if context is None:
+ context = {}
+ result = {}
+ mail_message = self.pool.get('mail.message')
+ if message_id:
+ message_data = mail_message.browse(cr, uid, message_id, context)
+ subject = tools.ustr(message_data.subject or '')
+ # we use the plain text version of the original mail, by default,
+ # as it is easier to quote than the HTML version.
+ # XXX TODO: make it possible to switch to HTML on the fly
+ current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
+ body = message_data.body_text or current_user.signature
+ if context.get('mail.compose.message.mode') == 'reply':
+ sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else ''
+ sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))}
+ quoted_body = '> %s' % tools.ustr(body.replace('\n', "\n> ") or '')
+ body = '\n'.join(["\n", (sent_date + sender), quoted_body])
+ body += "\n" + current_user.signature
+ re_prefix = _("Re:")
+ if not (subject.startswith('Re:') or subject.startswith(re_prefix)):
+ subject = "%s %s" % (re_prefix, subject)
+ result.update({
+ 'subtype' : 'plain', # default to the text version due to quoting
+ 'body_text' : body,
+ 'subject' : subject,
+ 'attachment_ids' : [],
+ 'model' : message_data.model or False,
+ 'res_id' : message_data.res_id or False,
+ 'email_from' : current_user.user_email or message_data.email_to or False,
+ 'email_to' : message_data.reply_to or message_data.email_from or False,
+ 'email_cc' : message_data.email_cc or False,
+ 'user_id' : uid,
+
+ # pass msg-id and references of mail we're replying to, to construct the
+ # new ones later when sending
+ 'message_id' : message_data.message_id or False,
+ 'references' : message_data.references and tools.ustr(message_data.references) or False,
+ })
+ return result
+
+ def send_mail(self, cr, uid, ids, context=None):
+ '''Process the wizard contents and proceed with sending the corresponding
+ email(s), rendering any template patterns on the fly if needed.
+ If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is
+ set to ``'mass_mail'``), the resulting email(s) are scheduled for being
+ sent the next time the mail.message scheduler runs, or the next time
+ ``mail.message.process_email_queue`` is called.
+ Otherwise the new message is sent immediately.
+
+ :param dict context: several context values will modify the behavior
+ of the wizard, cfr. the class description.
+ '''
+ if context is None:
+ context = {}
+ mail_message = self.pool.get('mail.message')
+ for mail in self.browse(cr, uid, ids, context=context):
+ attachment = {}
+ for attach in mail.attachment_ids:
+ attachment[attach.datas_fname] = attach.datas
+ references = None
+ headers = {}
+
+ body = mail.body_html if mail.subtype == 'html' else mail.body_text
+
+ # Reply Email
+ if context.get('mail.compose.message.mode') == 'reply' and mail.message_id:
+ references = (mail.references or '') + " " + mail.message_id
+ headers['In-Reply-To'] = mail.message_id
+
+ if context.get('mail.compose.message.mode') == 'mass_mail':
+ # Mass mailing: must render the template patterns
+ if context.get('active_ids') and context.get('active_model'):
+ active_ids = context['active_ids']
+ active_model = context['active_model']
+ else:
+ active_model = mail.model
+ active_model_pool = self.pool.get(active_model)
+ active_ids = active_model_pool.search(cr, uid, literal_eval(mail.filter_id.domain), context=literal_eval(mail.filter_id.context))
+
+ for active_id in active_ids:
+ subject = self.render_template(cr, uid, mail.subject, active_model, active_id)
+ rendered_body = self.render_template(cr, uid, body, active_model, active_id)
+ email_from = self.render_template(cr, uid, mail.email_from, active_model, active_id)
+ email_to = self.render_template(cr, uid, mail.email_to, active_model, active_id)
+ email_cc = self.render_template(cr, uid, mail.email_cc, active_model, active_id)
+ email_bcc = self.render_template(cr, uid, mail.email_bcc, active_model, active_id)
+ reply_to = self.render_template(cr, uid, mail.reply_to, active_model, active_id)
+
+ # in mass-mailing mode we only schedule the mail for sending, it will be
+ # processed as soon as the mail scheduler runs.
+ mail_message.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body,
+ model=mail.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
+ attachments=attachment, references=references, res_id=int(mail.res_id),
+ subtype=mail.subtype, headers=headers, context=context)
+ else:
+ # normal mode - no mass-mailing
+ msg_id = mail_message.schedule_with_attach(cr, uid, mail.email_from, to_email(mail.email_to), mail.subject, body,
+ model=mail.model, email_cc=to_email(mail.email_cc), email_bcc=to_email(mail.email_bcc), reply_to=mail.reply_to,
+ attachments=attachment, references=references, res_id=int(mail.res_id),
+ subtype=mail.subtype, headers=headers, context=context)
+ # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small)
+ mail_message.send(cr, uid, [msg_id], context)
+
+ return {'type': 'ir.actions.act_window_close'}
+
+ def render_template(self, cr, uid, template, model, res_id, context=None):
+ """Render the given template text, replace mako-like expressions ``${expr}``
+ with the result of evaluating these expressions with an evaluation context
+ containing:
+
+ * ``user``: browse_record of the current user
+ * ``object``: browse_record of the document record this mail is
+ related to
+ * ``context``: the context passed to the mail composition wizard
+
+ :param str template: the template text to render
+ :param str model: model name of the document record this mail is related to.
+ :param int res_id: id of the document record this mail is related to.
+ """
+ if context is None:
+ context = {}
+ def merge(match):
+ exp = str(match.group()[2:-1]).strip()
+ result = eval(exp,
+ {
+ 'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
+ 'object' : self.pool.get(model).browse(cr, uid, res_id, context=context),
+ 'context': dict(context), # copy context to prevent side-effects of eval
+ })
+ if result in (None, False):
+ return ""
+ return tools.ustr(result)
+ return template and EXPRESSION_PATTERN.sub(merge, template)
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
new file mode 100644
index 00000000000..d559d679c8b
--- /dev/null
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -0,0 +1,60 @@
+
+
+
+
+ mail.compose.message.form
+ mail.compose.message
+ form
+
+
+
+
+
+
+
+
project.task.treeproject.task
@@ -437,7 +494,7 @@
Tasksproject.taskform
- tree,form,calendar,gantt,graph
+ tree,form,calendar,gantt,graph,kanban{"search_default_user_id":uid, "search_default_current": 1}
@@ -450,7 +507,7 @@
Overpassed Tasksproject.taskform
- tree,form,calendar,graph
+ tree,form,calendar,graph,kanban[('date_deadline','<',time.strftime('%Y-%m-%d')),('state','in',('draft','pending','open'))]
@@ -461,7 +518,7 @@
project.taskProject's tasksform
- tree,form,calendar,graph,gantt
+ tree,form,calendar,graph,gantt,kanban[('project_id', 'child_of', [active_id])]{'project_id':active_id, 'active_test':False}
diff --git a/addons/project/wizard/__init__.py b/addons/project/wizard/__init__.py
index 69e1051e621..f5324d2b6aa 100644
--- a/addons/project/wizard/__init__.py
+++ b/addons/project/wizard/__init__.py
@@ -19,7 +19,7 @@
#
##############################################################################
-import project_task_close
+import mail_compose_message
import project_task_delegate
import project_task_reevaluate
diff --git a/addons/project/wizard/mail_compose_message.py b/addons/project/wizard/mail_compose_message.py
new file mode 100644
index 00000000000..82d67ea9b92
--- /dev/null
+++ b/addons/project/wizard/mail_compose_message.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2010-Today OpenERP SA ()
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
+#
+##############################################################################
+
+from osv import osv
+from osv import fields
+from tools.translate import _
+
+class mail_compose_message(osv.osv_memory):
+ _inherit = 'mail.compose.message'
+
+ def get_value(self, cr, uid, model, resource_id, context=None):
+ '''
+ To get values of the resource_id for the model
+ @param model: Object
+ @param resource_id: id of a record for which values to be read
+
+ @return: Returns a dictionary
+ '''
+ result = super(mail_compose_message, self).get_value(cr, uid, model, resource_id, context=context)
+ if model == 'project.task' and resource_id:
+ task_pool = self.pool.get('project.task')
+ task_data = task_pool.browse(cr, uid, resource_id, context=context)
+ partner = task_data.partner_id or task_data.project_id.partner_id
+ if task_data.project_id.warn_manager and (not task_data.project_id.user_id or task_data.project_id.user_id and not task_data.project_id.user_id.user_email) :
+ raise osv.except_osv(_('Error'), _("Please specify the Project Manager or email address of Project Manager."))
+ elif task_data.project_id.warn_customer and (not partner or not len(partner.address) or (partner and len(partner.address) and not partner.address[0].email)):
+ raise osv.except_osv(_('Error'), _("Please specify the Customer or email address of Customer."))
+
+ result.update({'email_from': task_data.user_id and task_data.user_id.user_email or False})
+ val = {
+ 'name': task_data.name,
+ 'user_id': task_data.user_id.name,
+ 'task_id': "%d/%d" % (task_data.project_id.id, task_data.id),
+ 'date_start': task_data.date_start,
+ 'date': task_data.date_end,
+ 'state': task_data.state
+ }
+ header = (task_data.project_id.warn_header or '') % val
+ footer = (task_data.project_id.warn_footer or '') % val
+ description = u'%s\n %s\n %s\n\n \n%s' % (header, task_data.description or '', footer, task_data.user_id and task_data.user_id.signature)
+ if partner and len(partner.address):
+ result.update({'email_to': result.get('email_to',False) and result.get('email_to') + ',' + partner.address[0].email})
+ result.update({
+ 'body_text': description or False,
+ 'email_to': task_data.project_id.user_id and task_data.project_id.user_id.user_email or False,
+ 'subject': _("Task '%s' Closed") % task_data.name,
+ 'model': model,
+ 'res_id': resource_id,
+ })
+
+ return result
+
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/project/wizard/project_task_close.py b/addons/project/wizard/project_task_close.py
deleted file mode 100644
index 627864820f6..00000000000
--- a/addons/project/wizard/project_task_close.py
+++ /dev/null
@@ -1,122 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2010 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-from osv import fields, osv
-import tools
-from tools.translate import _
-import re
-class project_task_close(osv.osv_memory):
- """
- Close Task
- """
- _name = "project.task.close"
- _description = "Project Close Task"
- _columns = {
- 'manager_warn': fields.boolean("Warn Manager", help="Warn Manager by Email"),
- 'partner_warn': fields.boolean("Warn Customer", help="Warn Customer by Email"),
- 'manager_email': fields.char('Manager Email', size=128, help="Email Address of Project's Manager"),
- 'partner_email': fields.char('Customer Email', size=128, help="Email Address of Customer"),
- 'description': fields.text('Description'),
- }
-
- def default_get(self, cr, uid, fields, context=None):
- """
- This function gets default values
- """
- if context is None:
- context = {}
- record_id = context and context.get('active_id', False) or False
- task_pool = self.pool.get('project.task')
-
- res = super(project_task_close, self).default_get(cr, uid, fields, context=context)
- task = task_pool.browse(cr, uid, record_id, context=context)
- project = task.project_id
- manager = project.user_id or False
- partner = task.partner_id or task.project_id.partner_id
-
- if 'description' in fields:
- res.update({'description': task.description or False})
- if 'manager_warn' in fields:
- res.update({'manager_warn': project.warn_manager or False})
- if 'partner_warn' in fields:
- res.update({'partner_warn': project.warn_customer or False})
- if 'manager_email' in fields:
- res.update({'manager_email': manager and manager.user_email or False})
- if partner and len(partner.address) and 'partner_email' in fields:
- res.update({'partner_email': partner.address[0].email})
- return res
-
- def send(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
-
- task_pool = self.pool.get('project.task')
- task_id = context.get('active_id', False)
- if not task_id:
- return {}
- task = task_pool.browse(cr, uid, task_id, context=context)
- for data in self.browse(cr, uid, ids, context=context):
- # Send Warn Message by Email to Manager and Customer
- if data.manager_warn and not data.manager_email:
- raise osv.except_osv(_('Error'), _("Please specify the email address of Project Manager."))
-
- elif data.partner_warn and not data.partner_email:
- raise osv.except_osv(_('Error'), _("Please specify the email address of Customer."))
-
- elif data.manager_warn or data.partner_warn:
- project = task.project_id
- subject = _("Task '%s' Closed") % task.name
- if task.user_id and task.user_id.user_email:
- from_adr = task.user_id.user_email
- signature = task.user_id.signature
- else:
- raise osv.except_osv(_('Error'), _("Couldn't send mail because your email address is not configured!"))
- val = {
- 'name': task.name,
- 'user_id': task.user_id.name,
- 'task_id': "%d/%d" % (project.id, task.id),
- 'date_start': task.date_start,
- 'date_end': task.date_end,
- 'state': task.state
- }
-
- to_adr = []
- header = footer = ''
- try:
- header_str = (project.warn_header or '')
- footer_str = (project.warn_footer or '')
- header = (re.sub(r'\%\W*\(', '%(', header_str)) % val
- footer = (re.sub(r'\%\W*\(', '%(', footer_str)) % val
- except:
- raise osv.except_osv(_('Error'), _("Invlaid automatic variables used in project header or foooter."))
- body = u'%s\n%s\n%s\n\n-- \n%s' % (header, data.description or '', footer, signature)
- if data.manager_warn and data.manager_email:
- to_adr.append(data.manager_email)
- if data.partner_warn and data.partner_email:
- to_adr.append(data.partner_email)
- mail_id = tools.email_send(from_adr, to_adr, subject, tools.ustr(body), email_bcc=[from_adr])
- if not mail_id:
- raise osv.except_osv(_('Error'), _("Couldn't send mail! Check the email ids and smtp configuration settings"))
- return {'type': 'ir.actions.act_window_close'}
-
-project_task_close()
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/project/wizard/project_task_close_view.xml b/addons/project/wizard/project_task_close_view.xml
deleted file mode 100644
index d6bed92824d..00000000000
--- a/addons/project/wizard/project_task_close_view.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
- Send Email
- project.task.close
- form
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Send Email
- ir.actions.act_window
- project.task.close
- form
- form
-
- new
-
-
-
-
diff --git a/addons/project_issue/__init__.py b/addons/project_issue/__init__.py
index 21efa34f6f4..f519698a60c 100644
--- a/addons/project_issue/__init__.py
+++ b/addons/project_issue/__init__.py
@@ -22,6 +22,5 @@
import project_issue
import report
-import wizard
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/project_issue/board_project_issue_view.xml b/addons/project_issue/board_project_issue_view.xml
index 3b6a6bcd0a3..cfe285134a7 100644
--- a/addons/project_issue/board_project_issue_view.xml
+++ b/addons/project_issue/board_project_issue_view.xml
@@ -123,7 +123,7 @@
project.issueformtree,form
- [('state','not in',('cancel','done')),'|',('assigned_to','=',uid),('user_id','=',uid)]
+ [('state','not in',('cancel','done')),('user_id','=',uid)]
@@ -142,7 +142,7 @@
project.issue.reportformgraph,tree,form,
- [('state','=','open'),'|',('assigned_to','=',uid),('user_id','=',uid)]
+ [('state','=','open'),('user_id','=',uid)]
diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py
index 61a45f01753..af756d34c5f 100644
--- a/addons/project_issue/project_issue.py
+++ b/addons/project_issue/project_issue.py
@@ -26,7 +26,9 @@ from tools.translate import _
import binascii
import time
import tools
+from crm import wizard
+wizard.mail_compose_message.SUPPORTED_MODELS.append('project.issue')
class project_issue_version(osv.osv):
_name = "project.issue.version"
@@ -44,8 +46,8 @@ class project_issue(crm.crm_case, osv.osv):
_name = "project.issue"
_description = "Project Issue"
_order = "priority, create_date desc"
- _inherit = ['mailgate.thread']
-
+ _inherit = ['mail.thread']
+
def case_open(self, cr, uid, ids, *args):
"""
@param self: The object pointer
@@ -56,7 +58,7 @@ class project_issue(crm.crm_case, osv.osv):
"""
res = super(project_issue, self).case_open(cr, uid, ids, *args)
- self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'assigned_to' : uid})
+ self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'), 'user_id' : uid})
for (id, name) in self.name_get(cr, uid, ids):
message = _("Issue '%s' has been opened.") % name
self.log(cr, uid, id, message)
@@ -156,7 +158,7 @@ class project_issue(crm.crm_case, osv.osv):
issues = []
issue_pool = self.pool.get('project.issue')
for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
- issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
+ issues += issue_pool.search(cr, uid, [('task_id','=',task.id)])
return issues
def _get_issue_work(self, cr, uid, ids, context=None):
@@ -174,8 +176,8 @@ class project_issue(crm.crm_case, osv.osv):
progress = 0.0
if issue.task_id:
progress = task_pool._hours_get(cr, uid, [issue.task_id.id], field_names, args, context=context)[issue.task_id.id]['progress']
- res[issue.id] = {'progress' : progress}
- return res
+ res[issue.id] = {'progress' : progress}
+ return res
_columns = {
'id': fields.integer('ID'),
@@ -189,7 +191,6 @@ class project_issue(crm.crm_case, osv.osv):
'section_id': fields.many2one('crm.case.section', 'Sales Team', \
select=True, help='Sales team to which Case belongs to.\
Define Responsible user and Email account for mail gateway.'),
- 'user_id': fields.related('project_id', 'user_id', type='many2one', relation='res.users', store=True, select=1, string='Responsible'),
'partner_id': fields.many2one('res.partner', 'Partner', select=1),
'partner_address_id': fields.many2one('res.partner.address', 'Partner Contact', \
domain="[('partner_id','=',partner_id)]"),
@@ -218,14 +219,14 @@ class project_issue(crm.crm_case, osv.osv):
multi='compute_day', type="float", store=True),
'day_close': fields.function(_compute_day, string='Days to Close', \
multi='compute_day', type="float", store=True),
- 'assigned_to': fields.many2one('res.users', 'Assigned to', required=False, select=1),
+ 'user_id': fields.many2one('res.users', 'Assigned to', required=False, select=1),
'working_hours_open': fields.function(_compute_day, string='Working Hours to Open the Issue', \
multi='compute_day', type="float", store=True),
'working_hours_close': fields.function(_compute_day, string='Working Hours to Close the Issue', \
multi='compute_day', type="float", store=True),
'inactivity_days': fields.function(_compute_day, string='Days since last action', \
multi='compute_day', type="integer", help="Difference in days between last action and current date"),
- 'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
+ 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
'date_action_last': fields.datetime('Last Action', readonly=1),
'date_action_next': fields.datetime('Next Action', readonly=1),
'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
@@ -243,19 +244,11 @@ class project_issue(crm.crm_case, osv.osv):
return False
def on_change_project(self, cr, uid, ids, project_id, context=None):
- result = {}
-
- if project_id:
- project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
- if project.user_id:
- result['value'] = {'user_id' : project.user_id.id}
-
- return result
+ return {}
_defaults = {
'active': 1,
- #'user_id': crm.crm_case._get_default_user,
'partner_id': crm.crm_case._get_default_partner,
'partner_address_id': crm.crm_case._get_default_partner_address,
'email_from': crm.crm_case._get_default_email,
@@ -265,7 +258,6 @@ class project_issue(crm.crm_case, osv.osv):
'priority': crm.AVAILABLE_PRIORITIES[2][0],
'project_id':_get_project,
'categ_id' : lambda *a: False,
- #'assigned_to' : lambda obj, cr, uid, context: uid,
}
def convert_issue_task(self, cr, uid, ids, context=None):
@@ -294,7 +286,7 @@ class project_issue(crm.crm_case, osv.osv):
'date': bug.date,
'project_id': bug.project_id.id,
'priority': bug.priority,
- 'user_id': bug.assigned_to.id,
+ 'user_id': bug.user_id.id,
'planned_hours': 0.0,
})
@@ -362,7 +354,7 @@ class project_issue(crm.crm_case, osv.osv):
if not task_id:
return {'value':{}}
task = self.pool.get('project.task').browse(cr, uid, task_id, context=context)
- return {'value':{'assigned_to': task.user_id.id,}}
+ return {'value':{'user_id': task.user_id.id,}}
def case_escalate(self, cr, uid, ids, *args):
"""Escalates case to top level
@@ -384,26 +376,17 @@ class project_issue(crm.crm_case, osv.osv):
else:
raise osv.except_osv(_('Warning !'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!'))
self.write(cr, uid, [case.id], data)
- self._history(cr, uid, cases, _('Escalate'))
+ self.message_append(cr, uid, cases, _('Escalate'))
return True
- def message_new(self, cr, uid, msg, context=None):
- """
- Automatically calls when new email message arrives
-
- @param self: The object pointer
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks
- """
- if context is None:
+ def message_new(self, cr, uid, msg, custom_values=None, context=None):
+ """Automatically called when new email message arrives"""
+ if context is None:
context = {}
- mailgate_pool = self.pool.get('email.server.tools')
-
subject = msg.get('subject') or _('No Title')
- body = msg.get('body')
+ body = msg.get('body_text')
msg_from = msg.get('from')
priority = msg.get('priority')
-
vals = {
'name': subject,
'email_from': msg_from,
@@ -411,37 +394,20 @@ class project_issue(crm.crm_case, osv.osv):
'description': body,
'user_id': False,
}
- if msg.get('priority', False):
+ if priority:
vals['priority'] = priority
-
- res = mailgate_pool.get_partner(cr, uid, msg.get('from'))
- if res:
- vals.update(res)
+ vals.update(self.message_partner_by_email(cr, uid, msg_from))
context.update({'state_to' : 'draft'})
- res = self.create(cr, uid, vals, context=context)
- self.convert_to_bug(cr, uid, [res], context=context)
- attachents = msg.get('attachments', [])
- for attactment in attachents or []:
- data_attach = {
- 'name': attactment,
- 'datas': binascii.b2a_base64(str(attachents.get(attactment))),
- 'datas_fname': attactment,
- 'description': 'Mail attachment',
- 'res_model': self._name,
- 'res_id': res,
- }
- self.pool.get('ir.attachment').create(cr, uid, data_attach)
+ if custom_values and isinstance(custom_values, dict):
+ vals.update(custom_values)
- return res
+ res_id = self.create(cr, uid, vals, context)
+ self.message_append_dict(cr, uid, [res_id], msg, context=context)
+ self.convert_to_bug(cr, uid, [res_id], context=context)
+ return res_id
- def message_update(self, cr, uid, ids, vals=None, msg="", default_act='pending', context=None):
- """
- @param self: The object pointer
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param ids: List of update mail’s IDs
- """
+ def message_update(self, cr, uid, ids, msg, vals=None, default_act='pending', context=None):
if vals is None:
vals = {}
@@ -450,7 +416,7 @@ class project_issue(crm.crm_case, osv.osv):
ids = [ids]
vals.update({
- 'description': msg['body']
+ 'description': msg['body_text']
})
if msg.get('priority', False):
vals['priority'] = msg.get('priority')
@@ -467,7 +433,7 @@ class project_issue(crm.crm_case, osv.osv):
record.write({'state' : 'open'})
vls = { }
- for line in msg['body'].split('\n'):
+ for line in msg['body_text'].split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower(), False):
@@ -476,20 +442,9 @@ class project_issue(crm.crm_case, osv.osv):
vals.update(vls)
res = self.write(cr, uid, ids, vals)
+ self.message_append_dict(cr, uid, ids, msg, context=context)
return res
- def msg_send(self, cr, uid, id, *args, **argv):
-
- """ Send The Message
- @param self: The object pointer
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param ids: List of email’s IDs
- @param *args: Return Tuple Value
- @param **args: Return Dictionary of Keyword Value
- """
- return True
-
def copy(self, cr, uid, id, default=None, context=None):
issue = self.read(cr, uid, id, ['name'], context=context)
if not default:
diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml
index 1154579960e..860dd73216b 100644
--- a/addons/project_issue/project_issue_view.xml
+++ b/addons/project_issue/project_issue_view.xml
@@ -55,8 +55,7 @@
-
-
+
@@ -97,15 +96,15 @@
-
+
-
-
+
+