]]>
- '%(object.user_id.email) or ''}
-
-You can view the invoice document, download it and pay online using the following link:
- ${ctx.get('edi_web_url_view') or 'n/a'}
-
-% if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'):
-<%
-comp_name = quote(object.company_id.name)
-inv_number = quote(object.number)
-paypal_account = quote(object.company_id.paypal_account)
-inv_amount = quote(str(object.amount_total))
-cur_name = quote(object.currency_id.name)
-paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Invoice%%20%s"\
- "&invoice=%s&amount=%s¤cy_code=%s&button_subtype=services&no_note=1&bn=OpenERP_Invoice_PayNow_%s" % \
- (paypal_account,comp_name,inv_number,inv_number,inv_amount,cur_name,cur_name)
-%>
-It is also possible to directly pay with Paypal:
- ${paypal_url}
-% endif
-
-If you have any question, do not hesitate to contact us.
-
-
-Thank you for choosing ${object.company_id.name}!
-
-
---
-${object.user_id.name} ${object.user_id.email and '<%s>'%(object.user_id.email) or ''}
-${object.company_id.name}
-% if object.company_id.street:
-${object.company_id.street or ''}
-% endif
-% if object.company_id.street2:
-${object.company_id.street2}
-% endif
-% if object.company_id.city or object.company_id.zip:
-${object.company_id.zip or ''} ${object.company_id.city or ''}
-% endif
-% if object.company_id.country_id:
-${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}
-% endif
-% if object.company_id.phone:
-Phone: ${object.company_id.phone}
-% endif
-% if object.company_id.website:
-${object.company_id.website or ''}
-% endif
- ]]>
diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot
index 174002a8d8d..0d5439719e2 100644
--- a/addons/account/i18n/account.pot
+++ b/addons/account/i18n/account.pot
@@ -9538,7 +9538,7 @@ msgid "Refund"
msgstr ""
#. module: account
-#: model:email.template,body_text:account.email_template_edi_invoice
+#: model:email.template,body:account.email_template_edi_invoice
msgid "\n"
"Hello${object.address_invoice_id.name and ' ' or ''}${object.address_invoice_id.name or ''},\n"
"\n"
diff --git a/addons/account/res_config.py b/addons/account/res_config.py
index 658f87962ff..9674cd0e65c 100644
--- a/addons/account/res_config.py
+++ b/addons/account/res_config.py
@@ -145,42 +145,45 @@ class account_config_settings(osv.osv_memory):
def onchange_company_id(self, cr, uid, ids, company_id):
# update related fields
- company = self.pool.get('res.company').browse(cr, uid, company_id)
- has_chart_of_accounts = company_id not in self.pool.get('account.installer').get_unconfigured_cmp(cr, uid)
- fiscalyear_count = self.pool.get('account.fiscalyear').search_count(cr, uid,
- [('date_start', '<=', time.strftime('%Y-%m-%d')), ('date_stop', '>=', time.strftime('%Y-%m-%d')),
- ('company_id', '=', company_id)])
- values = {
- 'expects_chart_of_accounts': company.expects_chart_of_accounts,
- 'currency_id': company.currency_id.id,
- 'paypal_account': company.paypal_account,
- 'company_footer': company.rml_footer2,
- 'has_chart_of_accounts': has_chart_of_accounts,
- 'has_fiscal_year': bool(fiscalyear_count),
- 'chart_template_id': False,
- 'tax_calculation_rounding_method': company.tax_calculation_rounding_method,
- }
- # update journals and sequences
- for journal_type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'):
- for suffix in ('_journal_id', '_sequence_prefix', '_sequence_next'):
- values[journal_type + suffix] = False
- journal_obj = self.pool.get('account.journal')
- journal_ids = journal_obj.search(cr, uid, [('company_id', '=', company_id)])
- for journal in journal_obj.browse(cr, uid, journal_ids):
- if journal.type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'):
- values.update({
- journal.type + '_journal_id': journal.id,
- journal.type + '_sequence_prefix': journal.sequence_id.prefix,
- journal.type + '_sequence_next': journal.sequence_id.number_next,
- })
- # update taxes
- ir_values = self.pool.get('ir.values')
- taxes_id = ir_values.get_default(cr, uid, 'product.product', 'taxes_id', company_id=company_id)
- supplier_taxes_id = ir_values.get_default(cr, uid, 'product.product', 'supplier_taxes_id', company_id=company_id)
- values.update({
- 'default_sale_tax': isinstance(taxes_id, list) and taxes_id[0] or taxes_id,
- 'default_purchase_tax': isinstance(supplier_taxes_id, list) and supplier_taxes_id[0] or supplier_taxes_id,
- })
+ values = {}
+ values['currency_id'] = False
+ if company_id:
+ company = self.pool.get('res.company').browse(cr, uid, company_id)
+ has_chart_of_accounts = company_id not in self.pool.get('account.installer').get_unconfigured_cmp(cr, uid)
+ fiscalyear_count = self.pool.get('account.fiscalyear').search_count(cr, uid,
+ [('date_start', '<=', time.strftime('%Y-%m-%d')), ('date_stop', '>=', time.strftime('%Y-%m-%d')),
+ ('company_id', '=', company_id)])
+ values = {
+ 'expects_chart_of_accounts': company.expects_chart_of_accounts,
+ 'currency_id': company.currency_id.id,
+ 'paypal_account': company.paypal_account,
+ 'company_footer': company.rml_footer2,
+ 'has_chart_of_accounts': has_chart_of_accounts,
+ 'has_fiscal_year': bool(fiscalyear_count),
+ 'chart_template_id': False,
+ 'tax_calculation_rounding_method': company.tax_calculation_rounding_method,
+ }
+ # update journals and sequences
+ for journal_type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'):
+ for suffix in ('_journal_id', '_sequence_prefix', '_sequence_next'):
+ values[journal_type + suffix] = False
+ journal_obj = self.pool.get('account.journal')
+ journal_ids = journal_obj.search(cr, uid, [('company_id', '=', company_id)])
+ for journal in journal_obj.browse(cr, uid, journal_ids):
+ if journal.type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'):
+ values.update({
+ journal.type + '_journal_id': journal.id,
+ journal.type + '_sequence_prefix': journal.sequence_id.prefix,
+ journal.type + '_sequence_next': journal.sequence_id.number_next,
+ })
+ # update taxes
+ ir_values = self.pool.get('ir.values')
+ taxes_id = ir_values.get_default(cr, uid, 'product.product', 'taxes_id', company_id=company_id)
+ supplier_taxes_id = ir_values.get_default(cr, uid, 'product.product', 'supplier_taxes_id', company_id=company_id)
+ values.update({
+ 'default_sale_tax': isinstance(taxes_id, list) and taxes_id[0] or taxes_id,
+ 'default_purchase_tax': isinstance(supplier_taxes_id, list) and supplier_taxes_id[0] or supplier_taxes_id,
+ })
return {'value': values}
def onchange_chart_template_id(self, cr, uid, ids, chart_template_id, context=None):
diff --git a/addons/account/wizard/account_report_common.py b/addons/account/wizard/account_report_common.py
index 02da9a6ab1f..9acc09e9026 100644
--- a/addons/account/wizard/account_report_common.py
+++ b/addons/account/wizard/account_report_common.py
@@ -30,9 +30,11 @@ class account_common_report(osv.osv_memory):
_description = "Account Common Report"
def onchange_chart_id(self, cr, uid, ids, chart_account_id=False, context=None):
+ res = {}
if chart_account_id:
company_id = self.pool.get('account.account').browse(cr, uid, chart_account_id, context=context).company_id.id
- return {'value': {'company_id': company_id}}
+ res['value'] = {'company_id': company_id}
+ return res
_columns = {
'chart_account_id': fields.many2one('account.account', 'Chart of Account', help='Select Charts of Accounts', required=True, domain = [('parent_id','=',False)]),
diff --git a/addons/account_accountant/account_accountant_data.xml b/addons/account_accountant/account_accountant_data.xml
index 383b53cf49e..5b563017c62 100644
--- a/addons/account_accountant/account_accountant_data.xml
+++ b/addons/account_accountant/account_accountant_data.xml
@@ -21,13 +21,14 @@
-
-
-
- Module Accounting and Finance has been installed.
- With OpenERP's accounting, you can get an instant access to all your financial data, setup your analytic accounting, forecast your taxes, control your budgets, easily create and send invoices, record bank statements, etc.
+
+ mail.group
+
+ notification
+ Accounting and Finance application installed!
+ With OpenERP's accounting, you get instant access to your financial data, and can setup analytic accounting, forecast taxes, control budgets, easily create and send invoices, record bank statements, etc.
-The accounting features are fully integrated with others OpenERP applications to automate all your processes: creation of customer invoices, control of supplier invoices, point-of-sale integration, automated follow-ups, etc.
-
+The accounting features are fully integrated with other OpenERP applications to automate all your processes: creation of customer invoices, control of supplier invoices, point-of-sale integration, automated follow-ups, etc.
+
diff --git a/addons/account_followup/wizard/account_followup_print.py b/addons/account_followup/wizard/account_followup_print.py
index d151d2171a4..1e84681b1ea 100644
--- a/addons/account_followup/wizard/account_followup_print.py
+++ b/addons/account_followup/wizard/account_followup_print.py
@@ -213,8 +213,6 @@ class account_followup_print_all(osv.osv_memory):
mod_obj = self.pool.get('ir.model.data')
move_obj = self.pool.get('account.move.line')
user_obj = self.pool.get('res.users')
- line_obj = self.pool.get('account_followup.stat')
- mail_message = self.pool.get('mail.message')
if context is None:
context = {}
@@ -235,13 +233,7 @@ class account_followup_print_all(osv.osv_memory):
total_amt += line.debit - line.credit
dest = False
if partner:
- if partner.type=='contact':
- if adr.email:
- dest = [partner.email]
- if (not dest) and partner.type=='default':
- if partner.email:
- dest = [partner.email]
- src = tools.config.options['email_from']
+ dest = [partner.email]
if not data.partner_lang:
body = data.email_body
else:
@@ -281,7 +273,12 @@ class account_followup_print_all(osv.osv_memory):
msg = ''
if dest:
try:
- mail_message.schedule_with_attach(cr, uid, src, dest, sub, body, context=context)
+ vals = {'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': '
%s
' % body,
+ 'email_to': dest,
+ 'email_from': data_user.email or tools.config.options['email_from']}
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
msg_sent += partner.name + '\n'
except Exception, e:
raise osv.except_osv('Error !', e )
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index 223705af85e..f75cda8cdde 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -1293,17 +1293,17 @@ class account_voucher(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s created." % self._document_type[obj.type or False]
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
def post_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s '%s' is posted." % (self._document_type[obj.type or False], obj.move_id.name)
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
def reconcile_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s reconciled." % self._document_type[obj.type or False]
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
account_voucher()
diff --git a/addons/account_voucher/account_voucher_data.xml b/addons/account_voucher/account_voucher_data.xml
index 8be2851a434..656d2c2cd91 100644
--- a/addons/account_voucher/account_voucher_data.xml
+++ b/addons/account_voucher/account_voucher_data.xml
@@ -2,15 +2,16 @@
-
-
-
- Module eInvoicing & Payments has been installed.
- OpenERP's electronic invoicing allows to ease and fasten the creation of invoices and collection of customer payments. Invoices are created in a few clicks and your customers receive them by email. They can pay online and/or import them in their own system.
+
+ mail.group
+
+ notification
+ eInvoicing & Payments application installed!
+ OpenERP's electronic invoicing accelerates the creation of invoices and collection of customer payments. Invoices are created in a few clicks and your customers receive them by email. They can pay online and/or import them in their own system.
-You can track customer payments easily and automate the reminders. You get an overview of the discussion with your customers on each invoice to ensure a full traceability.
+You can track customer payments easily and automate follow-ups. You get an overview of the discussion with your customers on each invoice for easier traceability.
-If you want to use advanced accounting features, you should install the "Accounting and Finance" module.
-
+For advanced accounting features, you should install the "Accounting and Finance" module.
+
diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py
index 7eeb9266e47..d90e2006a71 100644
--- a/addons/analytic/analytic.py
+++ b/addons/analytic/analytic.py
@@ -297,7 +297,7 @@ class account_analytic_account(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
- self.message_append_note(cr, uid, [obj.id], body=_("Contract for %s has been created.") % (obj.partner_id.name), context=context)
+ self.message_post(cr, uid, [obj.id], body=_("Contract for %s has been created.") % (obj.partner_id.name), context=context)
account_analytic_account()
diff --git a/addons/auth_reset_password/auth_reset_password.xml b/addons/auth_reset_password/auth_reset_password.xml
index 26ea8878c45..b7d7e0baef6 100644
--- a/addons/auth_reset_password/auth_reset_password.xml
+++ b/addons/auth_reset_password/auth_reset_password.xml
@@ -9,16 +9,13 @@
]]>Password reset
- A password reset was requested the OpenERP account linked to this email on ${object._auth_reset_password_host()}
-You may change your password following this link:
+
You may change your password following this link,
+or by copy-pasting the following URL in your browser: ${object._auth_reset_password_link()}
-${object._auth_reset_password_link()}
-
-If you don't have asked for password reset, you can safely ignore this email.
-
-]]>
+
Note: If you did not ask for a password reset, you can safely ignore this email.
]]>
diff --git a/addons/base_action_rule/base_action_rule.py b/addons/base_action_rule/base_action_rule.py
index b6eacda63c0..7e9bbacab13 100644
--- a/addons/base_action_rule/base_action_rule.py
+++ b/addons/base_action_rule/base_action_rule.py
@@ -302,33 +302,27 @@ the rule to mark CC(mail to any other person defined in actions)."),
return self.format_body(body % data)
def email_send(self, cr, uid, obj, emails, body, emailfrom=None, context=None):
- """ send email
- @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 email: pass the emails
- @param emailfrom: Pass name the email From else False
- @param context: A standard dictionary for contextual values """
-
if not emailfrom:
- emailfrom = tools.config.get('email_from', False)
-
- if context is None:
- context = {}
-
- mail_message = self.pool.get('mail.message')
+ emailfrom = tools.config.get('email_from')
body = self.format_mail(obj, body)
- if not emailfrom:
- if hasattr(obj, 'user_id') and obj.user_id and obj.user_id.email:
- emailfrom = obj.user_id.email
-
- name = '[%d] %s' % (obj.id, tools.ustr(obj.name))
+ if not emailfrom and hasattr(obj, 'user_id') and obj.user_id and obj.user_id.email:
+ emailfrom = obj.user_id.email
emailfrom = tools.ustr(emailfrom)
reply_to = emailfrom
if not emailfrom:
raise osv.except_osv(_('Error!'),
- _("No email ID found for your company address."))
- return mail_message.schedule_with_attach(cr, uid, emailfrom, emails, name, body, model='base.action.rule', reply_to=reply_to, res_id=obj.id)
+ _("Missing default email address or missing email on responsible user"))
+ return self.pool.get('mail.mail').create(cr, uid,
+ { 'email_from': emailfrom,
+ 'email_to': emails.join(','),
+ 'reply_to': reply_to,
+ 'state': 'outgoing',
+ 'subject': '[%d] %s' % (obj.id, tools.ustr(obj.name)),
+ 'body_html': '
%s
' % body,
+ 'res_id': obj.id,
+ 'model': obj._table_name,
+ 'auto_delete': True
+ }, context=context)
def do_check(self, cr, uid, action, obj, context=None):
@@ -438,11 +432,8 @@ the rule to mark CC(mail to any other person defined in actions)."),
if len(emails) and action.act_mail_body:
emails = list(set(emails))
email_from = safe_eval(action.act_email_from, {}, locals_for_emails)
-
- def to_email(text):
- return re.findall(r'([^ ,<@]+@[^> ,]+)', text or '')
- emails = to_email(','.join(filter(None, emails)))
- email_froms = to_email(email_from)
+ emails = tools.email_split(','.join(filter(None, emails)))
+ email_froms = tools.email_split(email_from)
if email_froms:
self.email_send(cr, uid, obj, emails, action.act_mail_body, emailfrom=email_froms[0])
return True
diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py
index be3244f8a5b..a6d5f6859af 100644
--- a/addons/base_calendar/base_calendar.py
+++ b/addons/base_calendar/base_calendar.py
@@ -471,18 +471,10 @@ property or property parameter."),
def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
"""
Send mail for event invitation to event attendees.
- @param cr: the current row, from the database cursor,
- @param uid: the current user’s ID for security checks,
- @param ids: List of attendee’s IDs.
@param email_from: Email address for user sending the mail
- @param context: A standard dictionary for contextual values
@return: True
"""
- if context is None:
- context = {}
-
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
- mail_message = self.pool.get('mail.message')
for att in self.browse(cr, uid, ids, context=context):
sign = att.sent_by_uid and att.sent_by_uid.signature or ''
sign = ' '.join(sign and sign.split('\n') or [])
@@ -508,17 +500,18 @@ property or property parameter."),
}
body = html_invitation % body_vals
if mail_to and email_from:
- attach = self.get_ics_file(cr, uid, res_obj, context=context)
- mail_message.schedule_with_attach(cr, uid,
- email_from,
- mail_to,
- sub,
- body,
- attachments=attach and {'invitation.ics': attach} or None,
- content_subtype='html',
- reply_to=email_from,
- context=context
- )
+ ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
+ vals = {'email_from': email_from,
+ 'email_to': mail_to,
+ 'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': body,
+ 'auto_delete': True}
+ if ics_file:
+ vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
+ 'datas_fname': 'invitation.ics',
+ 'datas': str(ics_file).encode('base64')})]
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
return True
def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
@@ -812,7 +805,6 @@ class calendar_alarm(osv.osv):
"""
if context is None:
context = {}
- mail_message = self.pool.get('mail.message')
current_datetime = datetime.now()
alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
@@ -849,36 +841,10 @@ class calendar_alarm(osv.osv):
else:
re_dates = [alarm.trigger_date]
- for r_date in re_dates:
- ref = alarm.model_id.model + ',' + str(alarm.res_id)
-
- # search for alreay sent requests
- #if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
- #continue
-
- # Deactivated because of the removing of res.request
- # TODO: when cleaning calendar module, re-add this in a new mechanism
- #if alarm.action == 'display':
- #value = {
- #'name': alarm.name,
- #'act_from': alarm.user_id.id,
- #'act_to': alarm.user_id.id,
- #'body': alarm.description,
- #'trigger_date': r_date,
- #'ref_doc1': ref
- #}
- #request_id = request_obj.create(cr, uid, value)
- #request_ids = [request_id]
- #for attendee in res_obj.attendee_ids:
- #if attendee.user_id:
- #value['act_to'] = attendee.user_id.id
- #request_id = request_obj.create(cr, uid, value)
- #request_ids.append(request_id)
- #request_obj.request_send(cr, uid, request_ids)
-
+ if re_dates:
if alarm.action == 'email':
- sub = '[Openobject Reminder] %s' % (alarm.name)
- body = """
+ sub = '[OpenERP Reminder] %s' % (alarm.name)
+ body = """
""" % (alarm.name, alarm.trigger_date, alarm.description, \
alarm.user_id.name, alarm.user_id.signature)
mail_to = [alarm.user_id.email]
for att in alarm.attendee_ids:
mail_to.append(att.user_id.email)
if mail_to:
- mail_message.schedule_with_attach(cr, uid,
- tools.config.get('email_from', False),
- mail_to,
- sub,
- body,
- context=context
- )
+ vals = {
+ 'state': 'outgoing',
+ 'subject': sub,
+ 'body_html': body,
+ 'email_to': mail_to,
+ 'email_from': tools.config.get('email_from', mail_to),
+ }
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
if next_trigger_date:
update_vals.update({'trigger_date': next_trigger_date})
else:
@@ -1616,36 +1583,6 @@ class calendar_todo(osv.osv):
calendar_todo()
-class ir_attachment(osv.osv):
- _name = 'ir.attachment'
- _inherit = 'ir.attachment'
-
- def search_count(self, cr, user, args, context=None):
- new_args = []
- for domain_item in args:
- if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
- new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
- else:
- new_args.append(domain_item)
- return super(ir_attachment, self).search_count(cr, user, new_args, context)
-
- def create(self, cr, uid, vals, context=None):
- if context:
- id = context.get('default_res_id', False)
- context.update({'default_res_id' : base_calendar_id2real_id(id)})
- return super(ir_attachment, self).create(cr, uid, vals, context=context)
-
- def search(self, cr, uid, args, offset=0, limit=None, order=None,
- context=None, count=False):
- new_args = []
- for domain_item in args:
- if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
- new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
- else:
- new_args.append(domain_item)
- return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
- limit=limit, order=order, context=context, count=False)
-ir_attachment()
class ir_values(osv.osv):
_inherit = 'ir.values'
diff --git a/addons/base_calendar/crm_meeting.py b/addons/base_calendar/crm_meeting.py
index cb96ff2a4d0..e1c66148ae7 100644
--- a/addons/base_calendar/crm_meeting.py
+++ b/addons/base_calendar/crm_meeting.py
@@ -43,7 +43,7 @@ class crm_meeting(base_state, osv.Model):
_name = 'crm.meeting'
_description = "Meeting"
_order = "id desc"
- _inherit = ["calendar.event", 'ir.needaction_mixin', "mail.thread"]
+ _inherit = ["calendar.event", "mail.thread", 'ir.needaction_mixin']
_columns = {
# base_state required fields
'create_date': fields.datetime('Creation Date', readonly=True),
@@ -70,13 +70,17 @@ class crm_meeting(base_state, osv.Model):
# OpenChatter
# ----------------------------------------
+ # shows events of the day for this user
+ def needaction_domain_get(self, cr, uid, domain=[], context={}):
+ return [('date','<=',time.strftime('%Y-%M-%D 23:59:59')), ('date_deadline','>=', time.strftime('%Y-%M-%D 00:00:00')), ('user_id','=',uid)]
+
def case_get_note_msg_prefix(self, cr, uid, id, context=None):
return 'Meeting'
def case_open_send_note(self, cr, uid, ids, context=None):
- return self.message_append_note(cr, uid, ids, body=_("Meeting has been confirmed."), context=context)
+ return self.message_post(cr, uid, ids, body=_("Meeting confirmed."), context=context)
def case_close_send_note(self, cr, uid, ids, context=None):
- return self.message_append_note(cr, uid, ids, body=_("Meeting has been done."), context=context)
+ return self.message_post(cr, uid, ids, body=_("Meeting completed."), context=context)
diff --git a/addons/base_calendar/crm_meeting_view.xml b/addons/base_calendar/crm_meeting_view.xml
index 6009251a460..928f8d94d5c 100644
--- a/addons/base_calendar/crm_meeting_view.xml
+++ b/addons/base_calendar/crm_meeting_view.xml
@@ -170,7 +170,8 @@
@@ -182,17 +183,16 @@
+ />
+ type="object" />
+ type="object" />
@@ -200,16 +200,16 @@
+ string="Uncertain" />
+ string="Accept" />
+ string="Decline" />
@@ -244,13 +244,13 @@
CRM - Meetings Treecrm.meeting
-
+
-
+
@@ -287,9 +287,9 @@
-
+
-
+
diff --git a/addons/base_status/base_stage.py b/addons/base_status/base_stage.py
index 775de51394c..529e70fdb49 100644
--- a/addons/base_status/base_stage.py
+++ b/addons/base_status/base_stage.py
@@ -297,55 +297,31 @@ class base_stage(object):
destination=False)
def remind_user(self, cr, uid, ids, context=None, attach=False, destination=True):
- mail_message = self.pool.get('mail.message')
- for case in self.browse(cr, uid, ids, context=context):
- if not destination and not case.email_from:
- return False
- if not case.user_id.email:
- return False
- if destination and case.section_id.user_id:
- case_email = case.section_id.user_id.email
- else:
- case_email = case.user_id.email
-
- src = case_email
- dest = case.user_id.email or ""
- body = case.description or ""
- for message in case.message_ids:
- if message.email_from and message.body_text:
- body = message.body_text
- break
-
- if not destination:
- src, dest = dest, case.email_from
- if body and case.user_id.signature:
- if body:
- body += '\n\n%s' % (case.user_id.signature)
- else:
- body = '\n\n%s' % (case.user_id.signature)
-
- body = self.format_body(body)
-
- attach_to_send = {}
-
- if attach:
- attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
- attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
- attach_to_send = dict(map(lambda x: (x['datas_fname'], base64.decodestring(x['datas'])), attach_to_send))
-
- # Send an email
- subject = "Reminder: [%s] %s" % (str(case.id), case.name, )
- mail_message.schedule_with_attach(cr, uid,
- src,
- [dest],
- subject,
- body,
- model=self._name,
- reply_to=case.section_id.reply_to,
- res_id=case.id,
- attachments=attach_to_send,
- context=context
- )
+ if 'message_post' in self:
+ for case in self.browse(cr, uid, ids, context=context):
+ if destination:
+ recipient_id = case.user_id.partner_id.id
+ else:
+ if not case.email_from:
+ return False
+ recipient_id = self.pool.get('res.partner').find_or_create(cr, uid, case.email_from, context=context)
+
+ body = case.description or ""
+ for message in case.message_ids:
+ if message.type == 'email' and message.body:
+ body = message.body
+ break
+ body = self.format_body(body)
+ attach_to_send = {}
+ if attach:
+ attach_ids = self.pool.get('ir.attachment').search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', case.id)])
+ attach_to_send = self.pool.get('ir.attachment').read(cr, uid, attach_ids, ['datas_fname', 'datas'])
+ attach_to_send = dict(map(lambda x: (x['datas_fname'], x['datas'].decode('base64')), attach_to_send))
+
+ subject = "Reminder: [%s] %s" % (case.id, case.name)
+ self.message_post(cr, uid, case.id, body=body,
+ subject=subject, attachments=attach_to_send,
+ partner_ids=[recipient_id], context=context)
return True
def _check(self, cr, uid, ids=False, context=None):
@@ -360,17 +336,6 @@ class base_stage(object):
def format_mail(self, obj, body):
return self.pool.get('base.action.rule').format_mail(obj, body)
- def message_thread_followers(self, cr, uid, ids, context=None):
- res = {}
- for case in self.browse(cr, uid, ids, context=context):
- l=[]
- if case.email_cc:
- l.append(case.email_cc)
- if case.user_id and case.user_id.email:
- l.append(case.user_id.email)
- res[case.id] = l
- return res
-
# ******************************
# Notifications
# ******************************
@@ -395,31 +360,31 @@ class base_stage(object):
def case_open_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been opened.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_close_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been closed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_cancel_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been canceled.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_pending_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s is now pending.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_reset_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been renewed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
@@ -428,5 +393,5 @@ class base_stage(object):
msg = '%s has been escalated to %s.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name)
else:
msg = '%s has been escalated.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
diff --git a/addons/base_status/base_state.py b/addons/base_status/base_state.py
index 8fa2f92e22a..cebf5537485 100644
--- a/addons/base_status/base_state.py
+++ b/addons/base_status/base_state.py
@@ -179,13 +179,13 @@ class base_state(object):
# Notifications
# ******************************
- def case_get_note_msg_prefix(self, cr, uid, id, context=None):
- return ''
-
+ def case_get_note_msg_prefix(self, cr, uid, id, context=None):
+ return ''
+
def case_open_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been opened.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_escalate_send_note(self, cr, uid, ids, new_section=None, context=None):
@@ -194,29 +194,29 @@ class base_state(object):
msg = '%s has been escalated to %s.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context), new_section.name)
else:
msg = '%s has been escalated.' % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], 'System Notification', msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_close_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been closed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_cancel_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been canceled.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_pending_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s is now pending.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
def case_reset_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been renewed.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=msg, context=context)
+ self.message_post(cr, uid, [id], body=msg, context=context)
return True
diff --git a/addons/base_vat/i18n/es_MX.po b/addons/base_vat/i18n/es_MX.po
index 57d269be72d..cd511c528b0 100644
--- a/addons/base_vat/i18n/es_MX.po
+++ b/addons/base_vat/i18n/es_MX.po
@@ -1,70 +1,54 @@
-# Translation of OpenERP Server.
-# This file contains the translation of the following modules:
-# * base_vat
+# Spanish (Mexico) translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
#
msgid ""
msgstr ""
-"Project-Id-Version: OpenERP Server 6.0dev\n"
-"Report-Msgid-Bugs-To: support@openerp.com\n"
-"POT-Creation-Date: 2011-01-11 11:14+0000\n"
-"PO-Revision-Date: 2010-12-25 18:58+0000\n"
-"Last-Translator: Jordi Esteve (www.zikzakmedia.com) "
-"\n"
-"Language-Team: \n"
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-07 00:31+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Spanish (Mexico) \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:10+0000\n"
-"X-Generator: Launchpad (build 13830)\n"
+"X-Launchpad-Export-Date: 2012-09-08 04:54+0000\n"
+"X-Generator: Launchpad (build 15914)\n"
#. module: base_vat
-#: code:addons/base_vat/base_vat.py:87
+#: code:addons/base_vat/base_vat.py:141
#, python-format
msgid ""
-"The Vat does not seems to be correct. You should have entered something like "
-"this %s"
-msgstr ""
-"El CIF/NIF parece que no sea correcto. Debería haber introducido algo como "
-"esto %s"
+"This VAT number does not seem to be valid.\n"
+"Note: the expected format is %s"
+msgstr "El RFC no es válido. El formato esperado es %s"
#. module: base_vat
-#: model:ir.module.module,description:base_vat.module_meta_information
-msgid ""
-"\n"
-" Enable the VAT Number for the partner. Check the validity of that VAT "
-"Number.\n"
-"\n"
-" This module follows the methods stated at http://sima-pc.com/nif.php "
-"for\n"
-" checking the validity of VAT Number assigned to partners in European "
-"countries.\n"
-" "
+#: sql_constraint:res.company:0
+msgid "The company name must be unique !"
msgstr ""
-"\n"
-" Permite la validación del CIF/NIF de las empresas. Comprueba si el "
-"CIF/NIF es un número válido.\n"
-"\n"
-" Este módulo usa los métodos especificados en http://sima-pc.com/nif.php "
-"para\n"
-" la validación del CIF/NIF asignado a las empresas de los países "
-"europeos.\n"
-" "
-
-#. module: base_vat
-#: model:ir.module.module,shortdesc:base_vat.module_meta_information
-msgid "Base VAT - To check VAT number validity"
-msgstr "Base CIF/NIF - Para comprobar la validez de los CIF/NIF"
#. module: base_vat
#: constraint:res.partner:0
-msgid "Error ! You can not create recursive associated members."
-msgstr "¡Error! No puede crear miembros asociados recursivos."
+msgid "Error ! You cannot create recursive associated members."
+msgstr ""
#. module: base_vat
-#: code:addons/base_vat/base_vat.py:88
-#, python-format
-msgid "The VAT is invalid, It should begin with the country code"
-msgstr "El CIF/NIF no es válido, debería empezar con el código del país"
+#: field:res.company,vat_check_vies:0
+msgid "VIES VAT Check"
+msgstr ""
+
+#. module: base_vat
+#: model:ir.model,name:base_vat.model_res_company
+msgid "Companies"
+msgstr ""
+
+#. module: base_vat
+#: constraint:res.company:0
+msgid "Error! You can not create recursive companies."
+msgstr ""
#. module: base_vat
#: help:res.partner,vat_subjected:0
@@ -72,27 +56,20 @@ msgid ""
"Check this box if the partner is subjected to the VAT. It will be used for "
"the VAT legal statement."
msgstr ""
-"Marque esta opción si la empresa está sujeta al IVA. Será utilizado para la "
-"declaración legal del IVA."
#. module: base_vat
#: model:ir.model,name:base_vat.model_res_partner
msgid "Partner"
-msgstr "Empresa"
+msgstr ""
+
+#. module: base_vat
+#: help:res.company,vat_check_vies:0
+msgid ""
+"If checked, Partners VAT numbers will be fully validated against EU's VIES "
+"service rather than via a simple format validation (checksum)."
+msgstr ""
#. module: base_vat
#: field:res.partner,vat_subjected:0
msgid "VAT Legal Statement"
-msgstr "Sujeto a IVA"
-
-#~ msgid "Invalid XML for View Architecture!"
-#~ msgstr "¡XML inválido para la definición de la vista!"
-
-#~ msgid ""
-#~ "Enable the VAT Number for the partner. Check the validity of that VAT Number."
-#~ msgstr ""
-#~ "Activa el IVA (Impuesto Valor Añadido) para la empresa. Comprueba la validez "
-#~ "del CIF/NIF."
-
-#~ msgid "VAT"
-#~ msgstr "IVA"
+msgstr ""
diff --git a/addons/board/__openerp__.py b/addons/board/__openerp__.py
index f07878717fd..2690012f3b4 100644
--- a/addons/board/__openerp__.py
+++ b/addons/board/__openerp__.py
@@ -28,9 +28,7 @@
Lets the user create a custom dashboard.
========================================
-This module also creates the Administration Dashboard.
-
-The user can also publish notes.
+Allows users to create custom dashboard.
""",
'author': 'OpenERP SA',
'depends': ['base'],
diff --git a/addons/board/i18n/nb.po b/addons/board/i18n/nb.po
new file mode 100644
index 00000000000..2d759af27d1
--- /dev/null
+++ b/addons/board/i18n/nb.po
@@ -0,0 +1,348 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-06 14:01+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-07 04:58+0000\n"
+"X-Generator: Launchpad (build 15914)\n"
+
+#. module: board
+#: view:res.log.report:0
+msgid " Year "
+msgstr " År "
+
+#. module: board
+#: model:ir.model,name:board.model_board_menu_create
+msgid "Menu Create"
+msgstr "Meny laget"
+
+#. module: board
+#: view:board.menu.create:0
+msgid "Menu Information"
+msgstr "Meny informasjon"
+
+#. module: board
+#: view:res.users:0
+msgid "Latest Connections"
+msgstr "Siste Tilkoblinger"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log created in last month"
+msgstr "Logg opprettet i forrige måned"
+
+#. module: board
+#: view:board.board:0
+#: model:ir.actions.act_window,name:board.open_board_administration_form
+msgid "Administration Dashboard"
+msgstr "Administrasjon kontrollpanel"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Group By..."
+msgstr "Grupper etter ..."
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log created in current year"
+msgstr "Logg opprettet i gjeldende år."
+
+#. module: board
+#: model:ir.model,name:board.model_board_board
+msgid "Board"
+msgstr "Brett"
+
+#. module: board
+#: field:board.menu.create,menu_name:0
+msgid "Menu Name"
+msgstr "Menynavn"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.board_weekly_res_log_report_action
+#: view:res.log.report:0
+msgid "Weekly Global Activity"
+msgstr "Ukentlig global aktivitet"
+
+#. module: board
+#: field:board.board.line,name:0
+msgid "Title"
+msgstr "Tittel"
+
+#. module: board
+#: field:res.log.report,nbr:0
+msgid "# of Entries"
+msgstr "# av oppføringer"
+
+#. module: board
+#: view:res.log.report:0
+#: field:res.log.report,month:0
+msgid "Month"
+msgstr "Måned"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log created in current month"
+msgstr "Logg opprettet i gjeldende måned."
+
+#. module: board
+#: model:ir.actions.act_window,name:board.board_monthly_res_log_report_action
+#: view:res.log.report:0
+msgid "Monthly Activity per Document"
+msgstr "Månedlig aktivitet per dokument"
+
+#. module: board
+#: view:board.board:0
+msgid "Configuration Overview"
+msgstr "Konfigurasjonsoversikt"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_view_board_list_form
+#: model:ir.ui.menu,name:board.menu_view_board_form
+msgid "Dashboard Definition"
+msgstr "Kontrollpanel Definisjon"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "March"
+msgstr "Mars"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "August"
+msgstr "August"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_user_connection_tree
+msgid "User Connections"
+msgstr "Brukertilkoblinger"
+
+#. module: board
+#: field:res.log.report,creation_date:0
+msgid "Creation Date"
+msgstr "Opprettelsesdato"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Log Analysis"
+msgstr "Logg analyse"
+
+#. module: board
+#: field:res.log.report,res_model:0
+msgid "Object"
+msgstr "Objekt"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "June"
+msgstr "Juni"
+
+#. module: board
+#: field:board.board,line_ids:0
+msgid "Action Views"
+msgstr "Handling Visninger"
+
+#. module: board
+#: model:ir.model,name:board.model_res_log_report
+msgid "Log Report"
+msgstr "Logg rapport"
+
+#. module: board
+#: code:addons/board/wizard/board_menu_create.py:46
+#, python-format
+msgid "Please Insert Dashboard View(s) !"
+msgstr "Vennligst Sett Kontrollpanel Vinsning (er)!"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "July"
+msgstr "juli"
+
+#. module: board
+#: view:res.log.report:0
+#: field:res.log.report,day:0
+msgid "Day"
+msgstr "Dag"
+
+#. module: board
+#: view:board.menu.create:0
+msgid "Create Menu For Dashboard"
+msgstr "Opprett meny for Kontrollpanel."
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "February"
+msgstr "Februar"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "October"
+msgstr "Oktober"
+
+#. module: board
+#: model:ir.model,name:board.model_board_board_line
+msgid "Board Line"
+msgstr "bord Linje"
+
+#. module: board
+#: field:board.menu.create,menu_parent_id:0
+msgid "Parent Menu"
+msgstr "Overordnet meny"
+
+#. module: board
+#: view:res.log.report:0
+msgid " Month-1 "
+msgstr " Måned-1 "
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "January"
+msgstr "Januar"
+
+#. module: board
+#: view:board.board:0
+msgid "Users"
+msgstr "Brukere"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "November"
+msgstr "November"
+
+#. module: board
+#: help:board.board.line,sequence:0
+msgid ""
+"Gives the sequence order when displaying a list of "
+"board lines."
+msgstr "Gir rekkefølgen av når du viser en liste over bord linjer."
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "April"
+msgstr "April"
+
+#. module: board
+#: view:board.board:0
+#: field:board.board,name:0
+#: field:board.board.line,board_id:0
+#: model:ir.ui.menu,name:board.menu_dasboard
+msgid "Dashboard"
+msgstr "Kontrollpanel"
+
+#. module: board
+#: code:addons/board/wizard/board_menu_create.py:45
+#, python-format
+msgid "User Error!"
+msgstr "Bruker feil!"
+
+#. module: board
+#: field:board.board.line,action_id:0
+msgid "Action"
+msgstr "Handling"
+
+#. module: board
+#: field:board.board.line,position:0
+msgid "Position"
+msgstr "Posisjon"
+
+#. module: board
+#: view:res.log.report:0
+msgid "Model"
+msgstr "Modell"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.board_homepage_action
+msgid "Home Page"
+msgstr "Hjemmeside"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_latest_activities_tree
+msgid "Latest Activities"
+msgstr "Senest aktiveter"
+
+#. module: board
+#: selection:board.board.line,position:0
+msgid "Left"
+msgstr "Venstre"
+
+#. module: board
+#: field:board.board,view_id:0
+msgid "Board View"
+msgstr "Bord visning"
+
+#. module: board
+#: selection:board.board.line,position:0
+msgid "Right"
+msgstr "Høyre"
+
+#. module: board
+#: field:board.board.line,width:0
+msgid "Width"
+msgstr "Bredde"
+
+#. module: board
+#: view:res.log.report:0
+msgid " Month "
+msgstr " Måned "
+
+#. module: board
+#: field:board.board.line,sequence:0
+msgid "Sequence"
+msgstr "Sekvens"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "September"
+msgstr "September"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "December"
+msgstr "Desember"
+
+#. module: board
+#: view:board.board:0
+#: view:board.menu.create:0
+msgid "Create Menu"
+msgstr "Opprett meny"
+
+#. module: board
+#: field:board.board.line,height:0
+msgid "Height"
+msgstr "Høyde"
+
+#. module: board
+#: model:ir.actions.act_window,name:board.action_board_menu_create
+msgid "Create Board Menu"
+msgstr "Opprett bord meny"
+
+#. module: board
+#: selection:res.log.report,month:0
+msgid "May"
+msgstr "Mai"
+
+#. module: board
+#: view:res.log.report:0
+#: field:res.log.report,name:0
+msgid "Year"
+msgstr "År"
+
+#. module: board
+#: view:board.menu.create:0
+msgid "Cancel"
+msgstr "Kanseller"
+
+#. module: board
+#: view:board.board:0
+msgid "Dashboard View"
+msgstr "Kontrollpanel visning"
diff --git a/addons/board/static/src/css/Makefile b/addons/board/static/src/css/Makefile
new file mode 100644
index 00000000000..0cb5ae70f4a
--- /dev/null
+++ b/addons/board/static/src/css/Makefile
@@ -0,0 +1,3 @@
+dashboard.css: dashboard.sass
+ sass --trace -t expanded dashboard.sass dashboard.css
+
diff --git a/addons/board/static/src/css/dashboard.css b/addons/board/static/src/css/dashboard.css
index 6907b4271a0..38e224daf42 100644
--- a/addons/board/static/src/css/dashboard.css
+++ b/addons/board/static/src/css/dashboard.css
@@ -5,6 +5,11 @@
.openerp .oe_dashboard {
width: 100%;
}
+.openerp .oe_dashboard .oe_dashboard_layout_selector ul, .openerp .oe_dashboard .oe_dashboard_layout_selector li {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
.openerp .oe_dashboard .oe_action {
margin: 0 8px 8px 0;
background-color: white;
diff --git a/addons/board/static/src/css/dashboard.sass b/addons/board/static/src/css/dashboard.sass
index 4bacc7bd892..1bfc01a3809 100644
--- a/addons/board/static/src/css/dashboard.sass
+++ b/addons/board/static/src/css/dashboard.sass
@@ -14,6 +14,11 @@
margin: 0 4px 6px 0
.oe_dashboard
width: 100%
+ .oe_dashboard_layout_selector
+ ul, li
+ margin: 0
+ padding: 0
+ list-style-type: none
.oe_action
margin: 0 8px 8px 0
background-color: white
diff --git a/addons/board/static/src/xml/board.xml b/addons/board/static/src/xml/board.xml
index 3cd7090f1e0..b2363cafd53 100644
--- a/addons/board/static/src/xml/board.xml
+++ b/addons/board/static/src/xml/board.xml
@@ -36,7 +36,7 @@
-
+
Choose dashboard layout
diff --git a/addons/crm/board_crm_view.xml b/addons/crm/board_crm_view.xml
index 16ed1e4c5ad..d80183b80c9 100644
--- a/addons/crm/board_crm_view.xml
+++ b/addons/crm/board_crm_view.xml
@@ -2,22 +2,6 @@
-
- My Opportunities
- crm.lead
- tree
- [('user_id','=',uid),('type', '=', 'opportunity'),('state','not in',('cancel','done'))]
-
-
-
-
- New Leads
- crm.lead
- tree
- [('user_id','=',uid),('state','=','draft'),('type','=','lead')]
-
-
-
Opportunities By Stage - Graphcrm.lead.report
@@ -70,11 +54,9 @@
-
-
-
- Module CRM has been installed
- From the top menu Sales, you can: trace leads and opportunities, get accurate forecast on your sales pipeline, plan meetings and phonecalls, get realtime statistics and efficiently organize the communication with your prospects.
+
+ mail.group
+
+ notification
+ CRM application installed!
+ From the top Sales menu you can track leads and opportunities, get accurate forecast on your sales pipeline, plan meetings and phonecalls, get realtime statistics and efficiently organize the communication with your prospects.
+To manage quotations and sale orders, install the "Sales Management" application.
+
-To manage quotations and sale orders, install the module "Sales Management".
-
-
sales{'type':'lead'}
-
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 25a47ecbadc..ddfdb05ca66 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -19,11 +19,9 @@
#
##############################################################################
-import binascii
from base_status.base_stage import base_stage
import crm
from datetime import datetime
-from mail.mail_message import to_email
from osv import fields, osv
import time
import tools
@@ -40,8 +38,7 @@ class crm_lead(base_stage, osv.osv):
_name = "crm.lead"
_description = "Lead/Opportunity"
_order = "priority,date_action,id desc"
- _inherit = ['ir.needaction_mixin', 'mail.thread']
- _mail_compose_message = True
+ _inherit = ['mail.thread','ir.needaction_mixin']
def _get_default_section_id(self, cr, uid, context=None):
""" Gives default section by checking if present in the context """
@@ -90,8 +87,8 @@ class crm_lead(base_stage, osv.osv):
search_domain = []
section_id = self._resolve_section_id_from_context(cr, uid, context=context)
if section_id:
- search_domain += ['|', '&', ('section_ids', '=', section_id), ('fold', '=', False)]
- search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)]
+ search_domain += ['|', ('section_ids', '=', section_id)]
+ search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)]
# retrieve type from the context (if set: choose 'type' or 'both')
type = self._resolve_type_from_context(cr, uid, context=context)
if type:
@@ -101,7 +98,12 @@ class crm_lead(base_stage, osv.osv):
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
# restore order of the search
result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
- return result
+
+ fold = {}
+ for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
+ fold[stage.id] = stage.fold or False
+
+ return result, fold
_group_by_full = {
'stage_id': _read_group_stage_ids
@@ -175,16 +177,6 @@ class crm_lead(base_stage, osv.osv):
else:
return [('id', '=', '0')]
- def _get_email_subject(self, cr, uid, ids, fields, args, context=None):
- res = {}
- for obj in self.browse(cr, uid, ids, context=context):
- res[obj.id] = ''
- for msg in obj.message_ids:
- if msg.email_from:
- res[obj.id] = msg.subject
- break
- return res
-
_columns = {
'partner_id': fields.many2one('res.partner', 'Partner', ondelete='set null',
select=True, help="Optional linked partner, usually after conversion of the lead"),
@@ -213,7 +205,7 @@ class crm_lead(base_stage, osv.osv):
'priority': fields.selection(crm.AVAILABLE_PRIORITIES, 'Priority', select=True),
'date_closed': fields.datetime('Closed', readonly=True),
'stage_id': fields.many2one('crm.case.stage', 'Stage',
- domain="['&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
+ domain="['&', ('fold', '=', False), '&', '|', ('section_ids', '=', section_id), ('case_default', '=', True), '|', ('type', '=', type), ('type', '=', 'both')]"),
'user_id': fields.many2one('res.users', 'Salesperson'),
'referred': fields.char('Referred By', size=64),
'date_open': fields.datetime('Opened', readonly=True),
@@ -228,7 +220,6 @@ class crm_lead(base_stage, osv.osv):
When the case is over, the state is set to \'Done\'.\
If the case needs to be reviewed then the state is \
set to \'Pending\'.'),
- 'subjects': fields.function(_get_email_subject, fnct_search=_history_search, string='Subject of Email', type='char', size=64),
# Only used for type opportunity
'probability': fields.float('Success Rate (%)',group_operator="avg"),
@@ -449,7 +440,7 @@ class crm_lead(base_stage, osv.osv):
oldest_id = opportunity_ids[0]
return self.browse(cr, uid, oldest_id, context=context)
- def _mail_body_text(self, cr, uid, lead, fields, title=False, context=None):
+ def _mail_body(self, cr, uid, lead, fields, title=False, context=None):
body = []
if title:
body.append("%s\n" % (title))
@@ -486,11 +477,11 @@ class crm_lead(base_stage, osv.osv):
for opportunity in opportunities:
subject.append(opportunity.name)
title = "%s : %s" % (merge_message, opportunity.name)
- details.append(self._mail_body_text(cr, uid, opportunity, fields, title=title, context=context))
+ details.append(self._mail_body(cr, uid, opportunity, fields, title=title, context=context))
subject = subject[0] + ", ".join(subject[1:])
details = "\n\n".join(details)
- return self.message_append_note(cr, uid, [opportunity_id], subject=subject, body=details)
+ return self.message_post(cr, uid, [opportunity_id], body=details, subject=subject, context=context)
def _merge_opportunity_history(self, cr, uid, opportunity_id, opportunities, context=None):
message = self.pool.get('mail.message')
@@ -546,7 +537,7 @@ class crm_lead(base_stage, osv.osv):
oldest = self._merge_find_oldest(cr, uid, ids, context=context)
if ctx_opportunities :
first_opportunity = ctx_opportunities[0]
- tail_opportunities = opportunities_list
+ tail_opportunities = opportunities_list + ctx_opportunities[1:]
else:
first_opportunity = opportunities_list[0]
tail_opportunities = opportunities_list[1:]
@@ -606,19 +597,13 @@ class crm_lead(base_stage, osv.osv):
for lead in self.browse(cr, uid, ids, context=context):
if lead.state in ('done', 'cancel'):
continue
- if user_ids or section_id:
- self.allocate_salesman(cr, uid, [lead.id], user_ids, section_id, context=context)
-
vals = self._convert_opportunity_data(cr, uid, lead, customer, section_id, context=context)
self.write(cr, uid, [lead.id], vals, context=context)
-
self.convert_opportunity_send_note(cr, uid, lead, context=context)
- #TOCHECK: why need to change partner details in all messages of lead ?
- if lead.partner_id:
- msg_ids = [ x.id for x in lead.message_ids]
- mail_message.write(cr, uid, msg_ids, {
- 'partner_id': lead.partner_id.id
- }, context=context)
+
+ if user_ids or section_id:
+ self.allocate_salesman(cr, uid, ids, user_ids, section_id, context=context)
+
return True
def _lead_create_contact(self, cr, uid, lead, name, is_company, parent_id=False, context=None):
@@ -630,7 +615,7 @@ class crm_lead(base_stage, osv.osv):
'parent_id': parent_id,
'phone': lead.phone,
'mobile': lead.mobile,
- 'email': lead.email_from and to_email(lead.email_from)[0],
+ 'email': lead.email_from and tools.email_split(lead.email_from)[0],
'fax': lead.fax,
'title': lead.title and lead.title.id or False,
'function': lead.function,
@@ -650,7 +635,7 @@ class crm_lead(base_stage, osv.osv):
partner_id = False
if lead.partner_name and lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
- self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
+ partner_id = self._lead_create_contact(cr, uid, lead, lead.contact_name, False, partner_id, context=context)
elif lead.partner_name and not lead.contact_name:
partner_id = self._lead_create_contact(cr, uid, lead, lead.partner_name, True, context=context)
elif not lead.partner_name and lead.contact_name:
@@ -678,32 +663,16 @@ class crm_lead(base_stage, osv.osv):
if context is None:
context = {}
partner_ids = {}
+ force_partner_id = partner_id
for lead in self.browse(cr, uid, ids, context=context):
if action == 'create':
if not partner_id:
partner_id = self._create_lead_partner(cr, uid, lead, context)
+ partner_id = force_partner_id or self._create_lead_partner(cr, uid, lead, context=context)
self._lead_set_partner(cr, uid, lead, partner_id, context=context)
partner_ids[lead.id] = partner_id
return partner_ids
- def _send_mail_to_salesman(self, cr, uid, lead, context=None):
- """
- Send mail to salesman with updated Lead details.
- @ lead: browse record of 'crm.lead' object.
- """
- #TOFIX: mail template should be used here instead of fix subject, body text.
- message = self.pool.get('mail.message')
- email_to = lead.user_id and lead.user_id.email
- if not email_to:
- return False
-
- email_from = lead.section_id and lead.section_id.user_id and lead.section_id.user_id.email or email_to
- partner = lead.partner_id and lead.partner_id.name or lead.partner_name
- subject = "lead %s converted into opportunity" % lead.name
- body = "Info \n Id : %s \n Subject: %s \n Partner: %s \n Description : %s " % (lead.id, lead.name, lead.partner_id.name, lead.description)
- return message.schedule_with_attach(cr, uid, email_from, [email_to], subject, body)
-
-
def allocate_salesman(self, cr, uid, ids, user_ids, team_id=False, context=None):
index = 0
for lead_id in ids:
@@ -821,14 +790,13 @@ class crm_lead(base_stage, osv.osv):
if custom_values is None: custom_values = {}
custom_values.update({
'name': msg.get('subject') or _("No Subject"),
- 'description': msg.get('body_text'),
+ 'description': msg.get('body'),
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
})
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
custom_values['priority'] = msg.get('priority')
- custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
@@ -841,18 +809,18 @@ class crm_lead(base_stage, osv.osv):
if update_vals is None: update_vals = {}
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
- vals['priority'] = msg.get('priority')
+ update_vals['priority'] = msg.get('priority')
maps = {
'cost':'planned_cost',
'revenue': 'planned_revenue',
'probability':'probability',
}
- for line in msg.get('body_text', '').split('\n'):
+ for line in msg.get('body', '').split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower()):
key = maps.get(res.group(1).lower())
- vals[key] = res.group(2).lower()
+ update_vals[key] = res.group(2).lower()
return super(crm_lead, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
@@ -860,15 +828,10 @@ class crm_lead(base_stage, osv.osv):
# OpenChatter methods and notifications
# ----------------------------------------
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'user_id' to the monitored fields """
- res = super(crm_lead, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['user_id']
-
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
stage_name = self.pool.get('crm.case.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
- return self.message_append_note(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), context=context)
+ return self.message_post(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), context=context)
def case_get_note_msg_prefix(self, cr, uid, lead, context=None):
if isinstance(lead, (int, long)):
@@ -878,33 +841,33 @@ class crm_lead(base_stage, osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for id in ids:
message = _("%s has been created.")% (self.case_get_note_msg_prefix(cr, uid, id, context=context))
- self.message_append_note(cr, uid, [id], body=message, context=context)
+ self.message_post(cr, uid, [id], body=message, context=context)
return True
def case_mark_lost_send_note(self, cr, uid, ids, context=None):
message = _("Opportunity has been lost.")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def case_mark_won_send_note(self, cr, uid, ids, context=None):
message = _("Opportunity has been won.")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def schedule_phonecall_send_note(self, cr, uid, ids, phonecall_id, action, context=None):
phonecall = self.pool.get('crm.phonecall').browse(cr, uid, [phonecall_id], context=context)[0]
if action == 'log': prefix = 'Logged'
else: prefix = 'Scheduled'
message = _("%s a call for the %s.") % (prefix, phonecall.date)
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def _lead_set_partner_send_note(self, cr, uid, ids, context=None):
for lead in self.browse(cr, uid, ids, context=context):
message = _("%s partner is now set to %s." % (self.case_get_note_msg_prefix(cr, uid, lead, context=context), lead.partner_id.name))
- lead.message_append_note(body=message)
+ lead.message_post(body=message)
return True
def convert_opportunity_send_note(self, cr, uid, lead, context=None):
message = _("Lead has been converted to an opportunity.")
- lead.message_append_note(body=message)
+ lead.message_post(body=message)
return True
crm_lead()
diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml
index 04f91933f66..f734a839ff4 100644
--- a/addons/crm/crm_lead_data.xml
+++ b/addons/crm/crm_lead_data.xml
@@ -55,7 +55,7 @@
Dead
-
+ cancel
diff --git a/addons/crm/crm_lead_demo.xml b/addons/crm/crm_lead_demo.xml
index 6118c08c0e5..b3c00256b06 100644
--- a/addons/crm/crm_lead_demo.xml
+++ b/addons/crm/crm_lead_demo.xml
@@ -552,86 +552,67 @@ Andrew
Plan to buy a Laptopcrm.lead
- html
- <![CDATA[Email0 inquiry]]><div><font size="2">Hello,</font></div><div><font size="2"><br></font></div><div><font size="2">I am interested in your company's product and I plan to buy a new laptop having latest technologies and affordable price.</font></div><div><font size="2">Can you please send me product catalogue?</font></div>
+ <![CDATA[Email0 inquiry]]><div><font size="2">Hello,</font></div><div><font size="2"><br></font></div><div><font size="2">I am interested in your company's product and I plan to buy a new laptop having latest technologies and affordable price.</font></div><div><font size="2">Can you please send me product catalogue?</font></div>email
- received
- Re: Plan to buy a Laptopcrm.lead
-
- plain
- Dear Customer,
+ comment
+ Dear Customer,
Thanks for showing interest in our products.
We have attached the catalogue,
We would like to know your interests, Let us know if we can call you for more details.
Thanks
- email
-
- sent
+
+ Re: Plan to buy a Laptopcrm.lead
- html
- <div>Thanks for the information,</div><div>I will visit the store soon.</div>
+ comment
+ <div>Thanks for the information,</div><div>I will visit the store soon.</div>
- email
-
- received
+ Re: Plan to buy a Laptopcrm.lead
- html
- <font color="#1f1f1f">Can you tell me if the store is open at 9:00 PM?</b></font>
+ comment
+ <font color="#1f1f1f">Can you tell me if the store is open at 9:00 PM?</b></font>
- email
- received
-
+ Re: Plan to buy a Laptopcrm.lead
- plain
- Yes, its open till 10:00 PM, you are welcome!
+ Yes, its open till 10:00 PM, you are welcome!email
- sent
-
+
-
Inquirycrm.lead
- plain
- Hello,
+ Hello,
I am Jason from Le Club SARL,
I am intertested to attend Training organized in your company,
Can you send details,email
- received
-
-
Need Detailscrm.lead
- plain
- Want to know features and benifits to use the new software.
+ Want to know features and benifits to use the new software.comment
-
-
Leads
crm.lead
-
+
@@ -257,12 +257,11 @@
-
+
-
-
+
@@ -296,19 +295,12 @@
-
+
-
-
-
-
-
-
- Sign in / Sign out
- hr.sign.in.out
- form
- tree,form
-
- new
- Sign in / Sign out. In some companies, staff have to sign in when they arrive at work and sign out again at the end of the day. If each employee has been linked to a system user, then they can encode their time with this action button.
-
-
-
-
-
- hr.sign.in.out.ask.form
- hr.sign.in.out.ask
-
-
' % body,
+ 'email_to': child.work_email,
+ 'email_from': evaluation.employee_id.work_email}
+ self.pool.get('mail.mail').create(cr, uid, vals, context=context)
self.write(cr, uid, ids, {'state':'wait'}, context=context)
return True
diff --git a/addons/hr_evaluation/hr_evaluation_data.xml b/addons/hr_evaluation/hr_evaluation_data.xml
index cf632a795cc..2cf656a72bb 100644
--- a/addons/hr_evaluation/hr_evaluation_data.xml
+++ b/addons/hr_evaluation/hr_evaluation_data.xml
@@ -14,12 +14,14 @@
100
-
-
-
- Module Employee Appraisals has been installed
- Create evaluations for your subordinates or manager. You can define a plan with several surveys, where you organize evaluation surveys related to the hierarchy levels. Evaluations filled by employees can exported as pdf files.
-
+
+ mail.group
+
+ notification
+ Employee Appraisals application installed!
+ Manage employee reviews: you can define an appraisal campaign with several steps, with specific evaluation surveys according to hierarchy levels.
+Evaluations filled by employees may be exported as pdf files.
+ Self Appraisal
diff --git a/addons/hr_evaluation/hr_evaluation_view.xml b/addons/hr_evaluation/hr_evaluation_view.xml
index 12c5dc0af6d..8744567c597 100644
--- a/addons/hr_evaluation/hr_evaluation_view.xml
+++ b/addons/hr_evaluation/hr_evaluation_view.xml
@@ -128,14 +128,12 @@
hr.employee
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -267,16 +265,27 @@
-
-
-
-
-
+
+
+
+
+
-
+
@@ -389,21 +398,23 @@
+ id="evaluation_reminders" name="Appraisal Reminders"
+ res_model="mail.compose.message"
+ src_model="hr.evaluation.interview"
+ view_type="form" view_mode="form"
+ target="new" multi="True"
+ key2="client_action_multi"
+ context="{'default_composition_mode': 'mass_mail',
+ 'default_body_text': 'Hello,\n\nKindly post your response for the survey interview.\n\nThanks',
+ 'default_subject': 'Reminder to fill up Survey'}"/>
+ context="{'search_default_user_to_review_id': [active_id], 'default_user_to_review_id': active_id}"
+ id="act_hr_employee_2_hr__evaluation_interview"
+ name="Appraisal Interviews"
+ res_model="hr.evaluation.interview"
+ src_model="hr.employee"/>
diff --git a/addons/hr_evaluation/wizard/hr_evaluation_mail.py b/addons/hr_evaluation/wizard/hr_evaluation_mail.py
deleted file mode 100644
index 35c54a4cca3..00000000000
--- a/addons/hr_evaluation/wizard/hr_evaluation_mail.py
+++ /dev/null
@@ -1,42 +0,0 @@
-##############################################################################
-#
-# 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
-
-class hr_evaluation_reminder(osv.osv_memory):
- _name = "hr.evaluation.reminder"
- _description = "Sends Reminders to employees to fill the evaluations"
- _columns = {
- 'evaluation_id': fields.many2one('hr.evaluation.interview', 'Interview', required=True)
- }
-
- def send_mail(self, cr, uid, ids, context=None):
- mail_message = self.pool.get('mail.message')
- hr_evaluation_interview_obj = self.pool.get('hr.evaluation.interview')
- evaluation_data = self.read(cr, uid, ids, context=context)[0]
- current_interview = hr_evaluation_interview_obj.browse(cr, uid, evaluation_data.get('evaluation_id'))
- if current_interview.state == "waiting_answer" and current_interview.user_to_review_id.work_email :
- msg = " Hello %s, \n\n Kindly post your response for '%s' survey interview. \n\n Thanks," %(current_interview.user_to_review_id.name, current_interview.survey_id.title)
- mail_message.schedule_with_attach(cr, uid, tools.config['email_from'], [current_interview.user_to_review_id.work_email],\
- 'Reminder to fill up Survey', msg, context=context)
- return {'type': 'ir.actions.act_window_close'}
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_evaluation/wizard/hr_evaluation_mail_view.xml b/addons/hr_evaluation/wizard/hr_evaluation_mail_view.xml
deleted file mode 100644
index 89ae99bb6c3..00000000000
--- a/addons/hr_evaluation/wizard/hr_evaluation_mail_view.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
- hr.evaluation.send.mail
- hr.evaluation.reminder
-
-
-
-
-
-
-
-
-
-
-
- Evaluation Send Mail
- hr.evaluation.reminder
- ir.actions.act_window
- form
- tree,form
-
- {'record_id':active_id}
- new
-
-
-
-
diff --git a/addons/hr_evaluation/wizard/mail_compose_message.py b/addons/hr_evaluation/wizard/mail_compose_message.py
deleted file mode 100644
index 7e4ba326947..00000000000
--- a/addons/hr_evaluation/wizard/mail_compose_message.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# -*- 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
-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
- '''
- if context is None:
- context = {}
- result = super(mail_compose_message, self).get_value(cr, uid, model, resource_id, context=context)
- if model == 'hr.evaluation.interview' and resource_id:
- model_pool = self.pool.get(model)
- record_data = model_pool.browse(cr, uid, resource_id, context)
- if record_data.state == "waiting_answer":
- msg = _("Hello %s, \n\n Kindly post your response for '%s' survey interview. \n\n Thanks,") %(record_data.user_to_review_id.name, record_data.survey_id.title)
- result.update({
- 'email_to': record_data.user_to_review_id.work_email or False,
- 'subject': _("Reminder to fill up Survey"),
- 'body_text': msg,
- })
- return result
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py
index 9c148ad9019..63fca88b148 100644
--- a/addons/hr_expense/__openerp__.py
+++ b/addons/hr_expense/__openerp__.py
@@ -21,14 +21,14 @@
{
- 'name': 'Expenses Management',
+ 'name': 'Expense Management',
'version': '1.0',
'category': 'Human Resources',
'sequence': 30,
'summary': 'Expenses Validation, Invoicing',
'description': """
-This module aims to manage employee's expenses.
-===============================================
+This module aims to manage employee expenses.
+=============================================
The whole workflow is implemented:
----------------------------------
diff --git a/addons/hr_expense/hr_expense_data.xml b/addons/hr_expense/hr_expense_data.xml
index 6e41f3d4e05..2a9f3ae0a56 100644
--- a/addons/hr_expense/hr_expense_data.xml
+++ b/addons/hr_expense/hr_expense_data.xml
@@ -2,20 +2,20 @@
-
-
-
- Module Expenses Management has been installed
- Manage your employee's expenses, with validations by employee manager and accountant, creation and payment of invoices.
+
+ mail.group
+
+ notification
+ Expense Management application installed!
+ Manage your employees' expenses, after due validation by their manager and the accountant, then generate and pay the corresponding invoices.
-This module also uses the analytic accounting and is compatible with the invoice on timesheet module so that you will be able to automatically re-invoice your customer's expenses if your work by project.
-
+This feature is also linked to analytic accounting and compatible with timesheet invoices, so you will be able to automatically re-invoice project-related expenses to your customers.
+
Expenses
-
diff --git a/addons/hr_holidays/__openerp__.py b/addons/hr_holidays/__openerp__.py
index d5a2728c763..070ae0344b0 100644
--- a/addons/hr_holidays/__openerp__.py
+++ b/addons/hr_holidays/__openerp__.py
@@ -21,7 +21,7 @@
{
- 'name': 'Leaves Management',
+ 'name': 'Leave Management',
'version': '1.5',
'author': 'OpenERP SA',
'category': 'Human Resources',
@@ -29,8 +29,8 @@
'summary': 'Holidays, Allocation and Leave Requests',
'website': 'http://www.openerp.com',
'description': """
-This module allows you to manage leaves and leave's requests.
-=============================================================
+This module allows you to manage leaves and leave requests.
+===========================================================
Implements a dashboard for human resource management that includes:
-------------------------------------------------------------------
diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py
index f687e91c3f0..759170224bd 100644
--- a/addons/hr_holidays/hr_holidays.py
+++ b/addons/hr_holidays/hr_holidays.py
@@ -95,7 +95,7 @@ class hr_holidays(osv.osv):
_name = "hr.holidays"
_description = "Leave"
_order = "type desc, date_from asc"
- _inherit = ['ir.needaction_mixin', 'mail.thread']
+ _inherit = [ 'mail.thread','ir.needaction_mixin']
def _employee_get(self, cr, uid, context=None):
ids = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
@@ -344,62 +344,53 @@ class hr_holidays(osv.osv):
if leaves_rest < record.number_of_days_temp:
raise osv.except_osv(_('Warning!'), _('There are not enough %s allocated for employee %s; please create an allocation request for this leave type.') % (record.holiday_status_id.name, record.employee_id.name))
return True
-
+
# -----------------------------
# OpenChatter and notifications
# -----------------------------
-
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- result = super(hr_holidays, self).get_needaction_user_ids(cr, uid, ids, context=context)
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.state == 'confirm' and obj.holiday_type == 'employee' and obj.employee_id.parent_id:
- result[obj.id] = [obj.employee_id.parent_id.user_id.id]
- elif obj.state == 'validate1':
- # get group_hr_manager: everyone will be warned of second validation
- res = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_hr_manager') or False
- obj_id = res and res[1] or False
- if obj_id:
- hr_manager_group = self.pool.get('res.groups').read(cr, uid, [obj_id], ['users'], context=context)[0]
- result[obj.id] = hr_manager_group['users']
- return result
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'user_id' and 'manager' to the monitored fields """
- res = super(hr_holidays, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['user_id']
-
+ def needaction_domain_get(self, cr, uid, ids, context=None):
+ # to be tested, otherwise convert into employee_id in ...
+ emp_obj = self.pool.get('hr.employee')
+ empids = emp_obj.search(cr, uid, [('parent_id.user_id','=',uid)], context=context)
+ dom = [
+ '&', ('state','=','confirm'),('employee_id', 'in', empids)
+ ]
+ # if this user is a hr.manager, he should do second validations
+ if self.pool.get('res.users').has_group(cr, uid, 'base.group_hr_manager'):
+ dom = ['|'] + dom + [ ('state','=','validate1') ]
+ return dom
+
def create_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
- self.message_append_note(cr, uid, ids, _('System notification'),
- _("The request has been created and is waiting confirmation."), type='notification', context=context)
+ self.message_post(cr, uid, ids,
+ _("The request has been created and is waiting confirmation."), context=context)
return True
def holidays_confirm_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids):
- self.message_append_note(cr, uid, [obj.id], _('System notification'),
- _("The request has been submitted and is waiting for validation by the manager."), type='notification')
+ self.message_post(cr, uid, [obj.id],
+ _("The request has been submitted and is waiting for validation by the manager."), context=context)
def holidays_first_validate_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
- self.message_append_note(cr, uid, [obj.id], _('System notification'),
- _("The request has been approved. A second validation is necessary and is now pending."), type='notification', context=context)
+ self.message_post(cr, uid, [obj.id],
+ _("The request has been approved. A second validation is necessary and is now pending."), context=context)
def holidays_validate_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids):
if obj.double_validation:
- self.message_append_note(cr, uid, [obj.id], _('System notification'),
- _("The request has been double validated. The validation process is now over."), type='notification', context=context)
+ self.message_post(cr, uid, [obj.id],
+ _("The request has been double validated. The validation process is now over."), context=context)
else:
- self.message_append_note(cr, uid, [obj.id], _('System notification'),
- _("The request has been approved. The validation process is now over."), type='notification', context=context)
-
+ self.message_post(cr, uid, [obj.id],
+ _("The request has been approved. The validation process is now over."), context=context)
def holidays_refuse_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids):
- self.message_append_note(cr, uid, [obj.id], _('System notification'),
- _("The request has been refused. The validation process is now over."), type='notification', context=context)
-
-hr_holidays()
+ self.message_post(cr, uid, [obj.id],
+ _("The request has been refused. The validation process is now over."), context=context)
+
class resource_calendar_leaves(osv.osv):
_inherit = "resource.calendar.leaves"
diff --git a/addons/hr_holidays/hr_holidays_data.xml b/addons/hr_holidays/hr_holidays_data.xml
index 9db0f2e987a..1e534093eb3 100644
--- a/addons/hr_holidays/hr_holidays_data.xml
+++ b/addons/hr_holidays/hr_holidays_data.xml
@@ -13,12 +13,15 @@
100
-
-
-
- Module Leaves Management has been installed
- Manage employee leaves from the top menu "Human Resources". Employees can create leave requests that are validated by their manager and/or HR people. Once validated, they are visible in the employee's calendar. HR people can define leave types and allocate off-days for employees.
-
+
+ mail.group
+
+ notification
+ Leave Management application installed!
+ Manage employee leaves from the top menu "Human Resources". Employees can create leave requests that are validated by their manager and/or HR officers.
+Once validated, they are visible in the employee's calendar. HR officers can define leave types and allocate leaves to employees and employee categories.
+
+
diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml
index 650445b2ade..5a2e0603b88 100644
--- a/addons/hr_holidays/hr_holidays_view.xml
+++ b/addons/hr_holidays/hr_holidays_view.xml
@@ -44,7 +44,6 @@
-
@@ -170,8 +169,7 @@
hr.holidays.allocation.treehr.holidays
-
-
+
@@ -218,8 +216,7 @@
hr.holidays20
-
-
+
@@ -241,8 +238,7 @@
hr.holidays.treehr.holidays
-
-
+
@@ -517,13 +513,17 @@
hr.employee
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/hr_payroll_account/hr_payroll_account.py b/addons/hr_payroll_account/hr_payroll_account.py
index 7156735636b..184f9a06867 100644
--- a/addons/hr_payroll_account/hr_payroll_account.py
+++ b/addons/hr_payroll_account/hr_payroll_account.py
@@ -218,6 +218,17 @@ class hr_payslip_run(osv.osv):
'journal_id': fields.many2one('account.journal', 'Expense Journal', states={'draft': [('readonly', False)]}, readonly=True, required=True),
}
+ def _get_default_journal(self, cr, uid, context=None):
+ model_data = self.pool.get('ir.model.data')
+ res = model_data.search(cr, uid, [('name', '=', 'expenses_journal')])
+ if res:
+ return model_data.browse(cr, uid, res[0]).res_id
+ return False
+
+ _defaults = {
+ 'journal_id': _get_default_journal,
+ }
+
hr_payslip_run()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hr_payroll_account/i18n/nb.po b/addons/hr_payroll_account/i18n/nb.po
new file mode 100644
index 00000000000..d1c0bf47a00
--- /dev/null
+++ b/addons/hr_payroll_account/i18n/nb.po
@@ -0,0 +1,141 @@
+# Norwegian Bokmal translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-07 19:55+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Norwegian Bokmal \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-08 04:54+0000\n"
+"X-Generator: Launchpad (build 15914)\n"
+
+#. module: hr_payroll_account
+#: field:hr.payslip,move_id:0
+msgid "Accounting Entry"
+msgstr "Regnskapsmessig oppføring"
+
+#. module: hr_payroll_account
+#: field:hr.salary.rule,account_tax_id:0
+msgid "Tax Code"
+msgstr "Skattekode"
+
+#. module: hr_payroll_account
+#: field:hr.payslip,journal_id:0
+#: field:hr.payslip.run,journal_id:0
+msgid "Expense Journal"
+msgstr "Bekostning Journal"
+
+#. module: hr_payroll_account
+#: code:addons/hr_payroll_account/hr_payroll_account.py:157
+#: code:addons/hr_payroll_account/hr_payroll_account.py:173
+#, python-format
+msgid "Adjustment Entry"
+msgstr "Justering oppføring"
+
+#. module: hr_payroll_account
+#: field:hr.contract,analytic_account_id:0
+#: field:hr.salary.rule,analytic_account_id:0
+msgid "Analytic Account"
+msgstr "Analytisk konto"
+
+#. module: hr_payroll_account
+#: model:ir.model,name:hr_payroll_account.model_hr_salary_rule
+msgid "hr.salary.rule"
+msgstr "hr.salary.rule"
+
+#. module: hr_payroll_account
+#: model:ir.model,name:hr_payroll_account.model_hr_payslip_run
+msgid "Payslip Batches"
+msgstr "Lønnsslipp Batcher."
+
+#. module: hr_payroll_account
+#: field:hr.contract,journal_id:0
+msgid "Salary Journal"
+msgstr "lønn Journal"
+
+#. module: hr_payroll_account
+#: model:ir.model,name:hr_payroll_account.model_hr_payslip
+msgid "Pay Slip"
+msgstr "Lønnslipp"
+
+#. module: hr_payroll_account
+#: constraint:hr.payslip:0
+msgid "Payslip 'Date From' must be before 'Date To'."
+msgstr "Lønnslipp \" Dato fra \" må være før \" Dato Til \""
+
+#. module: hr_payroll_account
+#: help:hr.payslip,period_id:0
+msgid "Keep empty to use the period of the validation(Payslip) date."
+msgstr "Hold tom for å bruke tid på validering (lønnsslipp) dato."
+
+#. module: hr_payroll_account
+#: code:addons/hr_payroll_account/hr_payroll_account.py:171
+#, python-format
+msgid ""
+"The Expense Journal \"%s\" has not properly configured the Debit Account!"
+msgstr "Bekostning Journal \"% s\" har ikke riktig konfigurert Debet konto!"
+
+#. module: hr_payroll_account
+#: code:addons/hr_payroll_account/hr_payroll_account.py:155
+#, python-format
+msgid ""
+"The Expense Journal \"%s\" has not properly configured the Credit Account!"
+msgstr ""
+"Bekostning Journal \"% s\" har ikke riktig konfigurert kreditt-konto!"
+
+#. module: hr_payroll_account
+#: field:hr.salary.rule,account_debit:0
+msgid "Debit Account"
+msgstr "Debetkonto"
+
+#. module: hr_payroll_account
+#: code:addons/hr_payroll_account/hr_payroll_account.py:102
+#, python-format
+msgid "Payslip of %s"
+msgstr "Lønnslipp av %s."
+
+#. module: hr_payroll_account
+#: model:ir.model,name:hr_payroll_account.model_hr_contract
+msgid "Contract"
+msgstr "Kontrakt"
+
+#. module: hr_payroll_account
+#: constraint:hr.contract:0
+msgid "Error! contract start-date must be lower then contract end-date."
+msgstr "Feil! Kontrakten startdato må være lavere enn kontrakt sluttdato."
+
+#. module: hr_payroll_account
+#: field:hr.payslip,period_id:0
+msgid "Force Period"
+msgstr "Kraft Periode."
+
+#. module: hr_payroll_account
+#: field:hr.salary.rule,account_credit:0
+msgid "Credit Account"
+msgstr "Kredittkonto"
+
+#. module: hr_payroll_account
+#: model:ir.model,name:hr_payroll_account.model_hr_payslip_employees
+msgid "Generate payslips for all selected employees"
+msgstr "Generer lønnsslipper for alle utvalgte ansatte."
+
+#. module: hr_payroll_account
+#: code:addons/hr_payroll_account/hr_payroll_account.py:155
+#: code:addons/hr_payroll_account/hr_payroll_account.py:171
+#, python-format
+msgid "Configuration Error!"
+msgstr "Konfigurasjonsfeil!"
+
+#. module: hr_payroll_account
+#: view:hr.contract:0
+#: view:hr.salary.rule:0
+msgid "Accounting"
+msgstr "Regnskap"
diff --git a/addons/hr_payroll_account/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll_account/wizard/hr_payroll_payslips_by_employees.py
index a00ff97ef6b..7e3f4bbbe2b 100644
--- a/addons/hr_payroll_account/wizard/hr_payroll_payslips_by_employees.py
+++ b/addons/hr_payroll_account/wizard/hr_payroll_payslips_by_employees.py
@@ -31,8 +31,9 @@ class hr_payslip_employees(osv.osv_memory):
context = {}
if context and context.get('active_id', False):
run_data = run_pool.read(cr, uid, context['active_id'], ['journal_id'])
- journal_id = run_data.get('journal_id', False)[0]
- context.update({'journal_id': journal_id})
+ journal_id = run_data.get('journal_id', False)
+ journal_id = journal_id and journal_id[0] or False
+ if journal_id: context.update({'journal_id': journal_id})
return super(hr_payslip_employees, self).compute_sheet(cr, uid, ids, context=context)
hr_payslip_employees()
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index 6b4839b7b2b..2748834ae9a 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -90,8 +90,7 @@ class hr_applicant(base_stage, osv.Model):
_name = "hr.applicant"
_description = "Applicant"
_order = "id desc"
- _inherit = ['ir.needaction_mixin', 'mail.thread']
- _mail_compose_message = True
+ _inherit = ['mail.thread', 'ir.needaction_mixin']
def _get_default_department_id(self, cr, uid, context=None):
""" Gives default department by checking if present in the context """
@@ -132,13 +131,17 @@ class hr_applicant(base_stage, osv.Model):
department_id = self._resolve_department_id_from_context(cr, uid, context=context)
search_domain = []
if department_id:
- search_domain += ['|', '&', ('department_id', '=', department_id), ('fold', '=', False)]
- search_domain += ['|', ('id', 'in', ids), '&', ('department_id', '=', False), ('fold', '=', False)]
+ search_domain += ['|', ('department_id', '=', department_id)]
+ search_domain += ['|', ('id', 'in', ids), ('department_id', '=', False)]
stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
# restore order of the search
result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
- return result
+
+ fold = {}
+ for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
+ fold[stage.id] = stage.fold or False
+ return result, fold
def _compute_day(self, cr, uid, ids, fields, args, context=None):
"""
@@ -161,13 +164,11 @@ class hr_applicant(base_stage, osv.Model):
date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
date_open = datetime.strptime(issue.date_open, "%Y-%m-%d %H:%M:%S")
ans = date_open - date_create
- date_until = issue.date_open
elif field in ['day_close']:
if issue.date_closed:
date_create = datetime.strptime(issue.create_date, "%Y-%m-%d %H:%M:%S")
date_close = datetime.strptime(issue.date_closed, "%Y-%m-%d %H:%M:%S")
- date_until = issue.date_closed
ans = date_close - date_create
if ans:
duration = float(ans.days)
@@ -185,7 +186,7 @@ class hr_applicant(base_stage, osv.Model):
'create_date': fields.datetime('Creation Date', readonly=True, select=True),
'write_date': fields.datetime('Update Date', readonly=True),
'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage',
- domain="['|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
+ domain="['&', ('fold', '=', False), '|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
'state': fields.related('stage_id', 'state', type="selection", store=True,
selection=AVAILABLE_STATES, string="State", readonly=True,
help='The state is set to \'Draft\', when a case is created.\
@@ -328,14 +329,13 @@ class hr_applicant(base_stage, osv.Model):
if custom_values is None: custom_values = {}
custom_values.update({
'name': msg.get('subject') or _("No Subject"),
- 'description': msg.get('body_text'),
+ 'description': msg.get('body'),
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
})
if msg.get('priority'):
custom_values['priority'] = msg.get('priority')
- custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context))
return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
@@ -360,7 +360,7 @@ class hr_applicant(base_stage, osv.Model):
'revenue': 'planned_revenue',
'probability': 'probability',
}
- for line in msg.get('body_text', '').split('\n'):
+ for line in msg.get('body', '').split('\n'):
line = line.strip()
res = tools.misc.command_re.match(line)
if res and maps.get(res.group(1).lower(), False):
@@ -457,23 +457,18 @@ class hr_applicant(base_stage, osv.Model):
# OpenChatter methods and notifications
# -------------------------------------------------------
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'user_id' to the monitored fields """
- res = super(hr_applicant, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['user_id']
-
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
if not stage_id: return True
stage_name = self.pool.get('hr.recruitment.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
- return self.message_append_note(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), context=context)
+ return self.message_post(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), context=context)
def case_get_note_msg_prefix(self, cr, uid, id, context=None):
return 'Applicant'
def case_open_send_note(self, cr, uid, ids, context=None):
message = _("Applicant has been set in progress.")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def case_close_send_note(self, cr, uid, ids, context=None):
if context is None:
@@ -481,23 +476,23 @@ class hr_applicant(base_stage, osv.Model):
for applicant in self.browse(cr, uid, ids, context=context):
if applicant.emp_id:
message = _("Applicant has been hired and created as an employee.")
- self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
+ self.message_post(cr, uid, [applicant.id], body=message, context=context)
else:
message = _("Applicant has been hired.")
- self.message_append_note(cr, uid, [applicant.id], body=message, context=context)
+ self.message_post(cr, uid, [applicant.id], body=message, context=context)
return True
def case_cancel_send_note(self, cr, uid, ids, context=None):
msg = 'Applicant refused.'
- return self.message_append_note(cr, uid, ids, body=msg, context=context)
+ return self.message_post(cr, uid, ids, body=msg, context=context)
def case_reset_send_note(self, cr, uid, ids, context=None):
message =_("Applicant has been set as new.")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
def create_send_note(self, cr, uid, ids, context=None):
message = _("Applicant has been created.")
- return self.message_append_note(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=message, context=context)
class hr_job(osv.osv):
diff --git a/addons/hr_recruitment/hr_recruitment_data.xml b/addons/hr_recruitment/hr_recruitment_data.xml
index d90f5ab596a..84289da7bae 100644
--- a/addons/hr_recruitment/hr_recruitment_data.xml
+++ b/addons/hr_recruitment/hr_recruitment_data.xml
@@ -2,14 +2,15 @@
-
-
-
- Module Recruitment Process has been installed
- Manage job positions and the recruitment process in your company. This module is integrated with the module Survey to help you to define interviews for different jobs.
+
+ mail.group
+
+ notification
+ Recruitment Process application installed!
+ Manage job positions and your company's recruitment process. This application is integrated with the Survey application to help you define interviews for different jobs.
-You can automatically create application records from an email gateway, that you can configure in the Human Resources Settings.
-
+You can automatically receive job application though an email gateway, see the Human Resources settings.
+
diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml
index 9fa8907e448..d00fbc632be 100644
--- a/addons/hr_recruitment/hr_recruitment_view.xml
+++ b/addons/hr_recruitment/hr_recruitment_view.xml
@@ -76,8 +76,8 @@
Applicantshr.applicant
-
-
+
+
@@ -95,7 +95,7 @@
-
+
@@ -107,7 +107,7 @@
+ [[ e1[0] in ["Basic","Gross"] and e1[0] ]]
+ [[ e1[0] not in ["Basic","Gross"] and e1[0] ]]
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[1]!='' and formatLang(e1[1])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[1]!='' and formatLang(e1[1])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[2]!='' and formatLang(e1[2])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[2]!='' and formatLang(e1[2])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[3]!='' and formatLang(e1[3])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[3]!='' and formatLang(e1[3])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[4]!='' and formatLang(e1[4])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[4]!='' and formatLang(e1[4])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[5]!='' and formatLang(e1[5])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[5]!='' and formatLang(e1[5])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[6]!='' and formatLang(e1[6])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[6]!='' and formatLang(e1[6])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[7]!='' and formatLang(e1[7])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[7]!='' and formatLang(e1[7])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[8]!='' and formatLang(e1[8])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[8]!='' and formatLang(e1[8])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[9]!='' and formatLang(e1[9])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[9]!='' and formatLang(e1[9])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[10]!='' and formatLang(e1[10])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[10]!='' and formatLang(e1[10])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[11]!='' and formatLang(e1[11])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[11]!='' and formatLang(e1[11])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e1[0] in ["Basic","Gross"] and e1[12]!='' and formatLang(e1[12])) or removeParentNode('font') ]]
+ [[ (e1[0] not in ["Basic","Gross"] and e1[12]!='' and formatLang(e1[12])) or removeParentNode('font') ]]
+
+
+ [[ e2[0] in ["Net"] and e2[0] ]]
+ [[ e2[0] not in ["Net"] and e2[0] ]]
+
+
+
+ [[ (e2[0] in ["Net"] and e2[1]!='' and formatLang(e2[1])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[1]!='' and formatLang(e2[1])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[2]!='' and formatLang(e2[2])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[2]!='' and formatLang(e2[2])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[3]!='' and formatLang(e2[3])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[3]!='' and formatLang(e2[3])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[4]!='' and formatLang(e2[4])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[4]!='' and formatLang(e2[4])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[5]!='' and formatLang(e2[5])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[5]!='' and formatLang(e2[5])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[6]!='' and formatLang(e2[6])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[6]!='' and formatLang(e2[6])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[7]!='' and formatLang(e2[7])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[7]!='' and formatLang(e2[7])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[8]!='' and formatLang(e2[8])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[8]!='' and formatLang(e2[8])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[9]!='' and formatLang(e2[9])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[9]!='' and formatLang(e2[9])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[10]!='' and formatLang(e2[10])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[10]!='' and formatLang(e2[10])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[11]!='' and formatLang(e2[11])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[11]!='' and formatLang(e2[11])) or removeParentNode('font') ]]
+
+
+
+
+ [[ (e2[0] in ["Net"] and e2[12]!='' and formatLang(e2[12])) or removeParentNode('font') ]]
+ [[ (e2[0] not in ["Net"] and e2[12]!='' and formatLang(e2[12])) or removeParentNode('font') ]]
+
+
+ Payment Advice from [[ o.name ]] A/C no. [[ o.company_id.bank_ids and o.company_id.bank_ids[0].acc_number ]] form period [[ get_month(o.date)['from_name'] ]] to [[ get_month(o.date)['to_name'] ]]
+
+ Total : [[ o.line_ids==[] and removeParentNode('para') ]]
+
+
+
+ [[ o.line_ids==[] and removeParentNode('para') ]][[ formatLang(get_bysal_total()) ]] [[ (company.currency_id and company.currency_id.symbol) or '' ]]
+
+
+
+
+
+
+
+
+ [[ o.neft !=True and removeParentNode('blockTable') ]]
+
+
+
+
+
+
+
+
+
+
+
+
+ Total : [[ o.line_ids==[] and removeParentNode('para') ]]
+
+
+
+ [[ o.line_ids==[] and removeParentNode('para') ]][[ formatLang(get_bysal_total()) ]] [[ (company.currency_id and company.currency_id.symbol) or '' ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Yours Sincerely
+
+
+
+
+
+
+
+
+
+ For [[ company.name ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorized Signature
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_in_hr_payroll/report/report_payslip_details.py b/addons/l10n_in_hr_payroll/report/report_payslip_details.py
new file mode 100644
index 00000000000..2732d5d09ae
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/report/report_payslip_details.py
@@ -0,0 +1,35 @@
+#-*- coding:utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 OpenERP SA (). All Rights Reserved
+#
+# 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 report import report_sxw
+from hr_payroll import report
+
+class payslip_details_report_in(report.report_payslip_details.payslip_details_report):
+
+ def __init__(self, cr, uid, name, context):
+ super(payslip_details_report_in, self).__init__(cr, uid, name, context)
+ self.localcontext.update({
+ 'get_details_by_rule_category': self.get_details_by_rule_category,
+ })
+
+report_sxw.report_sxw('report.paylip.details.in', 'hr.payslip', 'l10n_in_hr_payroll/report/report_payslip_details.rml', parser=payslip_details_report_in)
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/report/report_payslip_details.rml b/addons/l10n_in_hr_payroll/report/report_payslip_details.rml
new file mode 100644
index 00000000000..a7ab3db3185
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/report/report_payslip_details.rml
@@ -0,0 +1,372 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [[repeatIn(objects,'o')]]
+
+
+
+ Pay Slip Details
+
+
+
+
+ [[o.credit_note==False and removeParentNode('para')]]
+ Credit
+
+ Note
+
+ ([[o.name or removeParentNode('para')]])
+
+
+
+ Name
+
+
+ [[o.employee_id.name]]
+
+
+ Designation
+
+
+ [[ o.employee_id.job_id.name or '' ]]
+
+
+
+
+
+
+
+ Address
+
+
+
+ [[o.employee_id.address_home_id and o.employee_id.address_home_id.name or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.street or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.street2 or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.zip or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.city or '' ]],[[o.employee_id.address_home_id and o.employee_id.address_home_id.state_id and o.employee_id.address_home_id.state_id.name or '' ]] [[o.employee_id.address_home_id and o.employee_id.address_home_id.country_id and o.employee_id.address_home_id.country_id.name or '' ]]
+
+
+ [[ h['code'] ]]
+ [[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]
+
+
+
+ [[ '..'*h['level'] ]][[ h['rule_category'] ]][[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font') ]]
+
+
+ [[ formatLang(h['total']) ]] [[o.company_id and o.company_id.currency_id.symbol or '']] [[ h['level']==0 and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font') ]]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Authorized Signature
+
+
+
+
+
+
+
+
+
diff --git a/addons/l10n_in_hr_payroll/security/ir.model.access.csv b/addons/l10n_in_hr_payroll/security/ir.model.access.csv
new file mode 100644
index 00000000000..7ca0834449b
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_hr_payroll_advice_user","hr.payroll.advice","model_hr_payroll_advice","base.group_hr_user",1,1,1,1
+"access_hr_payroll_advice_line_user","hr.payroll.advice.line","model_hr_payroll_advice_line","base.group_hr_user",1,1,1,1
+"access_hr_payroll_advice_report_user","payment.advice.report","model_payment_advice_report","base.group_hr_manager",1,1,1,1
+"access_hr_payroll_payslip_report_user","payslip.report","model_payslip_report","base.group_hr_manager",1,1,1,1
diff --git a/addons/l10n_in_hr_payroll/static/src/img/icon.png b/addons/l10n_in_hr_payroll/static/src/img/icon.png
new file mode 100644
index 00000000000..ddaf38f8b6d
Binary files /dev/null and b/addons/l10n_in_hr_payroll/static/src/img/icon.png differ
diff --git a/addons/l10n_in_hr_payroll/test/payment_advice.yml b/addons/l10n_in_hr_payroll/test/payment_advice.yml
new file mode 100644
index 00000000000..20b45830c5e
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/test/payment_advice.yml
@@ -0,0 +1,41 @@
+-
+ In order to test Payment Advice I create a new Payment Advice
+-
+ I create a new Payment Advice with NEFT Transaction Enable
+-
+ !record {model: hr.payroll.advice, id: payment_advice_1}:
+ name: 'NEFT Advice'
+ bank_id: base.res_bank_1
+ line_ids:
+ - employee_id: hr.employee_fp
+ name: '90125452552'
+ ifsc_code: 'abn45215145'
+ bysal: 25000.00
+ - employee_id: hr.employee_al
+ name: '00014521111232'
+ ifsc_code: 'sbi45452145'
+ bysal: 20000.00
+-
+ I check that the Payment Advice is in "Draft"
+-
+ !assert {model: hr.payroll.advice, id: payment_advice_1}:
+ - state == 'draft'
+-
+ Now I confirm Payment Advice
+-
+ !python {model: hr.payroll.advice}: |
+ self.confirm_sheet(cr, uid, [ref('payment_advice_1')])
+-
+ I check that the Payment Advice state is "Confirmed"
+-
+ !python {model: hr.payroll.advice}: |
+ advice = self.browse(cr, uid, ref("payment_advice_1"))
+ assert (advice.state == 'confirm')
+-
+ In order to test the PDF report defined on a Payment Advice, we will print a Print Advice Report when NEFT is checked
+-
+ !python {model: hr.payroll.advice}: |
+ import netsvc, tools, os
+ (data, format) = netsvc.LocalService('report.payroll.advice').create(cr, uid, [ref('l10n_in_hr_payroll.payment_advice_1')], {})
+ if tools.config['test_report_directory']:
+ file(os.path.join(tools.config['test_report_directory'], 'l10n_in_hr_payroll_summary report'+format), 'wb+').write(data)
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/test/payment_advice_batch.yml b/addons/l10n_in_hr_payroll/test/payment_advice_batch.yml
new file mode 100644
index 00000000000..d1a1436b50b
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/test/payment_advice_batch.yml
@@ -0,0 +1,61 @@
+-
+ In order to test Payment Advice Creation from Payslip Bacth I create New Payslip Batch
+-
+ I Create a bank record
+-
+ !record {model: res.partner.bank, id: res_partner_bank_0}:
+ acc_number: '3025632343043'
+ partner_id: base.res_partner_21
+ state: bank
+ bank: base.res_bank_1
+-
+ I create a new employee “Rahul”
+-
+ !record {model: hr.employee, id: hr_employee_rahul0}:
+ country_id: base.in
+ department_id: hr.dep_rd
+ name: Rahul
+ bank_account_id: res_partner_bank_0
+-
+ I want to generate a payslip from Payslip Batch.
+-
+ !record {model: hr.payslip.run, id: hr_payslip_run_forAdvice}:
+ name: Payslip Batch
+-
+ I create record for generating the Payslip for Payslip Batch.
+-
+ !record {model: hr.payslip.employees, id: hr_payslip_employees}:
+ employee_ids:
+ - hr_employee_rahul0
+-
+ I generate the payslip by clicking on Generate button wizard.
+-
+ !python {model: hr.payslip.employees}: |
+ self.compute_sheet(cr, uid, [ref('hr_payslip_employees')], context={'active_id': ref('hr_payslip_run_forAdvice')})
+-
+ I check that the Payslip Batch is in "Draft"
+-
+ !assert {model: hr.payslip.run, id: hr_payslip_run_forAdvice}:
+ - state == 'draft'
+-
+ Now I close Payslip Batch
+-
+ !python {model: hr.payslip.run}: |
+ self.close_payslip_run(cr, uid, [ref('hr_payslip_run_forAdvice')])
+-
+ I check that the Payslip Batch is "Close"
+-
+ !python {model: hr.payslip.run}: |
+ batch = self.browse(cr, uid, ref("hr_payslip_run_forAdvice"))
+ assert (batch.state == 'close')
+-
+ I create Advice from Payslip Batch using Create Advice button
+-
+ !python {model: hr.payslip.run}: |
+ self.create_advice(cr, uid, [ref('hr_payslip_run_forAdvice')])
+-
+ I check for Advice is created from Payslip Batch
+-
+ !python {model: hr.payroll.advice}: |
+ advice_ids = self.search(cr, uid, [('batch_id','=',ref('hr_payslip_run_forAdvice'))])
+ assert advice_ids,"Advice is not created from Payslip Batch."
\ No newline at end of file
diff --git a/addons/project_mailgate/__init__.py b/addons/l10n_in_hr_payroll/wizard/__init__.py
similarity index 94%
rename from addons/project_mailgate/__init__.py
rename to addons/l10n_in_hr_payroll/wizard/__init__.py
index 0f82ebd1b41..36635aec3c2 100644
--- a/addons/project_mailgate/__init__.py
+++ b/addons/l10n_in_hr_payroll/wizard/__init__.py
@@ -19,6 +19,7 @@
#
##############################################################################
-import project_mailgate
+import hr_salary_employee_bymonth
+import hr_yearly_salary_detail
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth.py b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth.py
new file mode 100644
index 00000000000..25cc615a1df
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth.py
@@ -0,0 +1,71 @@
+#-*- coding:utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 OpenERP SA (). All Rights Reserved
+#
+# 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
+
+from osv import osv, fields
+
+class hr_salary_employee_bymonth(osv.osv_memory):
+
+ _name = 'hr.salary.employee.month'
+ _description = 'Hr Salary Employee By Month Report'
+ _columns = {
+ 'start_date': fields.date('Start Date', required=True),
+ 'end_date': fields.date('End Date', required=True),
+ 'employee_ids': fields.many2many('hr.employee', 'payroll_year_rel', 'payroll_year_id', 'employee_id', 'Employees', required=True),
+ 'category_id': fields.many2one('hr.salary.rule.category', 'Category', required=True),
+ }
+
+ def _get_default_category(self, cr, uid, context=None):
+ category_ids = self.pool.get('hr.salary.rule.category').search(cr, uid, [('code', '=', 'NET')], context=context)
+ return category_ids and category_ids[0] or False
+
+ _defaults = {
+ 'start_date': lambda *a: time.strftime('%Y-01-01'),
+ 'end_date': lambda *a: time.strftime('%Y-%m-%d'),
+ 'category_id': _get_default_category
+ }
+
+ def print_report(self, cr, uid, ids, context=None):
+ """
+ To get the date and print the report
+ @param self: The object pointer.
+ @param cr: A database cursor
+ @param uid: ID of the user currently logged in
+ @param context: A standard dictionary
+ @return: return report
+ """
+ if context is None:
+ context = {}
+ datas = {'ids': context.get('active_ids', [])}
+
+ res = self.read(cr, uid, ids, context=context)
+ res = res and res[0] or {}
+ datas.update({'form': res})
+ return {
+ 'type': 'ir.actions.report.xml',
+ 'report_name': 'salary.employee.bymonth',
+ 'datas': datas,
+ }
+
+hr_salary_employee_bymonth()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
\ No newline at end of file
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth_view.xml b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth_view.xml
new file mode 100644
index 00000000000..e05519b08db
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_salary_employee_bymonth_view.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ Hr monthly Employee Salary
+ hr.salary.employee.month
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Yearly Salary by Head
+ ir.actions.act_window
+ hr.salary.employee.month
+ form
+ form
+ new
+
+
+
+
+
+
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail.py b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail.py
new file mode 100644
index 00000000000..16bfd63bdc9
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail.py
@@ -0,0 +1,66 @@
+#-*- coding:utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2011 OpenERP SA (). All Rights Reserved
+#
+# 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
+
+from osv import fields, osv
+
+class yearly_salary_detail(osv.osv_memory):
+
+ _name ='yearly.salary.detail'
+ _description = 'Hr Salary Employee By Category Report'
+ _columns = {
+ 'employee_ids': fields.many2many('hr.employee', 'payroll_emp_rel', 'payroll_id', 'employee_id', 'Employees', required=True),
+ 'date_from': fields.date('Start Date', required=True),
+ 'date_to': fields.date('End Date', required=True),
+ }
+
+ _defaults = {
+ 'date_from': lambda *a: time.strftime('%Y-01-01'),
+ 'date_to': lambda *a: time.strftime('%Y-%m-%d'),
+
+ }
+
+ def print_report(self, cr, uid, ids, context=None):
+ """
+ To get the date and print the report
+ @param self: The object pointer.
+ @param cr: A database cursor
+ @param uid: ID of the user currently logged in
+ @param context: A standard dictionary
+ @return: return report
+ """
+ if context is None:
+ context = {}
+ datas = {'ids': context.get('active_ids', [])}
+
+ res = self.read(cr, uid, ids, context=context)
+ res = res and res[0] or {}
+ datas.update({'form': res})
+ return {
+ 'type': 'ir.actions.report.xml',
+ 'report_name': 'salary.detail.byyear',
+ 'datas': datas,
+ }
+
+yearly_salary_detail()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail_view.xml b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail_view.xml
new file mode 100644
index 00000000000..ec70e8b41e0
--- /dev/null
+++ b/addons/l10n_in_hr_payroll/wizard/hr_yearly_salary_detail_view.xml
@@ -0,0 +1,44 @@
+
+
+
+
+ Employee Yearly Salary
+ yearly.salary.detail
+ form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Yearly Salary by Employee
+ ir.actions.act_window
+ yearly.salary.detail
+ form
+ form
+ new
+
+
+
+
+
+
diff --git a/addons/l10n_syscohada/l10n_syscohada_data.xml b/addons/l10n_syscohada/l10n_syscohada_data.xml
index feeea3a2314..d90988e00e3 100644
--- a/addons/l10n_syscohada/l10n_syscohada_data.xml
+++ b/addons/l10n_syscohada/l10n_syscohada_data.xml
@@ -1816,15 +1816,15 @@
#
-->
-
+ XOFCFA14
-
+ 655.957
-
+
-
-
-
- Welcome to OpenERP!
- Your homepage is a summary of messages you received and key information about documents you follow.
+
+ mail.group
+
+ notification
+ Welcome to OpenERP!
+ Your homepage is a summary of messages you received and key information about documents you follow.
The top menu bar contains all applications you installed. You can use this <i>Settings</i> menu to install more applications, activate others features or give access to new users.
-To setup your preferences (name, email signature, avatar), click on the top right corner.
-
-
+To setup your preferences (name, email signature, avatar), click on the top right corner.
+
diff --git a/addons/mail/doc/mail_openchatter_howto.rst b/addons/mail/doc/mail_openchatter_howto.rst
index 789acdd87eb..e4c86add7bc 100644
--- a/addons/mail/doc/mail_openchatter_howto.rst
+++ b/addons/mail/doc/mail_openchatter_howto.rst
@@ -57,7 +57,7 @@ Use the thread viewer widget inside your form view by using the mail_thread widg
Send notifications
+++++++++++++++++++
-When sending a notification is required in your workflow or business logic, use the ``message_append_note`` method. This method is a shortcut to the ``message_append`` method that takes all ``mail.message`` fields as arguments. This latter method calls ``message_create`` that
+When sending a notification is required in your workflow or business logic, use the ``message_post`` method. This method is a shortcut to the ``message_append`` method that takes all ``mail.message`` fields as arguments. This latter method calls ``message_create`` that
- creates the message
- parses the body to find users you want to push the message to (finding and parsing ``@login`` in the message body)
@@ -74,7 +74,7 @@ You should therefore not worry about subscriptions or anything else than sending
return res
def do_something_send_note(self, cr, uid, ids, context=None):
- self.message_append_note(cr, uid, ids, _('My subject'),
+ self.message_post(cr, uid, ids, _('My subject'),
_("has received a notification and is happy for it."), context=context)
Notifications guidelines
@@ -147,7 +147,7 @@ The best way to direct the messages that will be displayed in the OpenChatter wi
# add: search in the current task project messages
'&', '&', ('res_id', '=', my_task.project_id.id), ('model', '=', 'project.project'),
# ... containing the task name
- '|', ('body_text', 'like', '%s' % (my_task.name)), ('body_html', 'like', '%s' % (my_task.name))
+ '|', ('body', 'like', '%s' % (my_task.name)), ('body_html', 'like', '%s' % (my_task.name))
] + domain, limit=limit, offset=offset, context=context)
# if asked: add ancestor ids to have complete threads
if (ascent): msg_ids = self._message_add_ancestor_ids(cr, uid, ids, msg_ids, root_ids, context=context)
diff --git a/addons/mail/doc/mail_state.rst b/addons/mail/doc/mail_state.rst
index 527a31ba835..fc2772bf094 100644
--- a/addons/mail/doc/mail_state.rst
+++ b/addons/mail/doc/mail_state.rst
@@ -1,15 +1,15 @@
.. _mail_state:
-message_state
+message_unread
=============
-``message_state`` is a boolean field that states whether the document
+``message_unread`` is a boolean field that states whether the document
has unread messages. In previous versions, some documents were going
back to ``pending`` state when receiving an email through the mail
gateway. Now the state related to messages differs from the state or
stage of the document itself.
-message_state and need action mechanism
+message_unread and need action mechanism
+++++++++++++++++++++++++++++++++++++++
The ``mail`` module introduces a default behavior for the need_action
@@ -23,6 +23,6 @@ mechanism [REF].
"""
result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
for obj in self.browse(cr, uid, ids, context=context):
- if obj.message_state == False and obj.user_id:
+ if obj.message_unread == False and obj.user_id:
result[obj.id].append(obj.user_id.id)
return result
diff --git a/addons/mail/ir_needaction.py b/addons/mail/ir_needaction.py
deleted file mode 100644
index 8193ac4f385..00000000000
--- a/addons/mail/ir_needaction.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2012-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
-#
-##############################################################################
-
-from osv import osv
-
-class ir_needaction_mixin(osv.Model):
- """ Update of ir.needaction_mixin class
- - override the get_needaction_user_ids method to define the default
- mail gateway need_action: when the object is unread, the object
- responsible has an action to perform.
- """
- _name = 'ir.needaction_mixin'
- _inherit = ['ir.needaction_mixin']
-
- def get_needaction_user_ids(self, cr, uid, ids, context=None):
- """ Returns the user_ids that have to perform an action. It the
- document mail state is unread (False), return object.user_id.id
- as need_action uid.
- :return: dict { record_id: [user_ids], }
- """
- result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context)
- for obj in self.browse(cr, uid, ids, context=context):
- if obj.message_state == False and obj.user_id:
- result[obj.id].append(obj.user_id.id)
- return result
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index fcdc78d5f0a..2aba685c4fa 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -21,43 +21,113 @@
from osv import osv
from osv import fields
+import tools
class mail_followers(osv.Model):
""" mail_followers holds the data related to the follow mechanism inside
- OpenERP. Users can choose to follow documents (records) of any kind that
- inherits from mail.thread. Following documents allow to receive
- notifications for new messages.
- A subscription is characterized by:
- :param: res_model: model of the followed objects
- :param: res_id: ID of resource (may be 0 for every objects)
- :param: user_id: user_id of the follower
+ OpenERP. Partners can choose to follow documents (records) of any kind
+ that inherits from mail.thread. Following documents allow to receive
+ notifications for new messages.
+ A subscription is characterized by:
+ :param: res_model: model of the followed objects
+ :param: res_id: ID of resource (may be 0 for every objects)
"""
_name = 'mail.followers'
- _rec_name = 'id'
+ _rec_name = 'partner_id'
_log_access = False
- _order = 'res_model asc'
- _description = 'Mail Document Followers'
+ _description = 'Document Followers'
_columns = {
'res_model': fields.char('Related Document Model', size=128,
required=True, select=1,
help='Model of the followed resource'),
'res_id': fields.integer('Related Document ID', select=1,
help='Id of the followed resource'),
- 'user_id': fields.many2one('res.users', string='Related User',
+ 'partner_id': fields.many2one('res.partner', string='Related Partner',
ondelete='cascade', required=True, select=1),
}
+
class mail_notification(osv.Model):
- """ mail_notification is a relational table modeling messages pushed to users.
- """
+ """ Class holding notifications pushed to partners. Followers and partners
+ added in 'contacts to notify' receive notifications. """
_name = 'mail.notification'
- _rec_name = 'id'
+ _rec_name = 'partner_id'
_log_access = False
- _order = 'message_id desc'
- _description = 'Mail notification'
+ _description = 'Notifications'
+
_columns = {
- 'user_id': fields.many2one('res.users', string='User',
- ondelete='cascade', required=True, select=1),
+ 'partner_id': fields.many2one('res.partner', string='Contact',
+ ondelete='cascade', required=True),
+ 'read': fields.boolean('Read'),
'message_id': fields.many2one('mail.message', string='Message',
- ondelete='cascade', required=True, select=1),
+ ondelete='cascade', required=True),
}
+
+ _defaults = {
+ 'read': False,
+ }
+
+ def init(self, cr):
+ cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_message_id',))
+ if not cr.fetchone():
+ cr.execute('CREATE INDEX mail_notification_partner_id_read_message_id ON mail_notification (partner_id, read, message_id)')
+
+ def create(self, cr, uid, vals, context=None):
+ """ Override of create to check that we can not create a notification
+ for a message the user can not read. """
+ if self.pool.get('mail.message').check_access_rights(cr, uid, 'read'):
+ return super(mail_notification, self).create(cr, uid, vals, context=context)
+ return False
+
+ def set_message_read(self, cr, uid, msg_id, context=None):
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
+ return self.write(cr, uid, notif_ids, {'read': True}, context=context)
+
+ def notify(self, cr, uid, partner_ids, msg_id, context=None):
+ """ Send by email the notification depending on the user preferences """
+ context = context or {}
+ # mail_noemail (do not send email) or no partner_ids: do not send, return
+ if context.get('mail_noemail') or not partner_ids:
+ return True
+
+ mail_mail = self.pool.get('mail.mail')
+ msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
+
+ # add signature
+ body_html = msg.body
+ signature = msg.author_id and msg.author_id.user_ids[0].signature or ''
+ if signature:
+ body_html = tools.append_content_to_html(body_html, signature)
+
+ mail_values = {
+ 'mail_message_id': msg.id,
+ 'email_to': [],
+ 'auto_delete': True,
+ 'body_html': body_html,
+ 'state': 'outgoing',
+ }
+
+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+ # Do not send an email to the writer
+ if partner.user_ids and partner.user_ids[0].id == uid:
+ continue
+ # Do not send to partners without email address defined
+ if not partner.email:
+ continue
+ # Partner does not want to receive any emails
+ if partner.notification_email_send == 'none':
+ continue
+ # Partner wants to receive only emails and comments
+ if partner.notification_email_send == 'comment' and msg.type not in ('email', 'comment'):
+ continue
+ # Partner wants to receive only emails
+ if partner.notification_email_send == 'email' and msg.type != 'email':
+ continue
+ if partner.email not in mail_values['email_to']:
+ mail_values['email_to'].append(partner.email)
+ if mail_values['email_to']:
+ mail_values['email_to'] = ', '.join(mail_values['email_to'])
+ email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
+ mail_mail.send(cr, uid, [email_notif_id], context=context)
+ return True
diff --git a/addons/mail/mail_followers_view.xml b/addons/mail/mail_followers_view.xml
index 62e93430675..36f08b3814a 100644
--- a/addons/mail/mail_followers_view.xml
+++ b/addons/mail/mail_followers_view.xml
@@ -6,13 +6,12 @@
mail.followers.treemail.followers
- tree10
-
+
@@ -24,7 +23,7 @@
10
-
+
@@ -38,7 +37,7 @@
- Pushed notif
+ Notificationsmail.notificationformtree,form
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 6a14fef83aa..fdfeafd4afe 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -28,13 +28,8 @@ from osv import fields
from tools.translate import _
class mail_group(osv.Model):
- """
- A mail_group is a collection of users sharing messages in a discussion
- group. Group users are users that follow the mail group, using the
- subscription/follow mechanism of OpenSocial. A mail group has nothing
- in common with res.users.group.
- """
-
+ """ A mail_group is a collection of users sharing messages in a discussion
+ group. The group mechanics are based on the followers. """
_description = 'Discussion group'
_name = 'mail.group'
_inherit = ['mail.thread']
@@ -45,31 +40,16 @@ class mail_group(osv.Model):
for obj in self.browse(cr, uid, ids, context=context):
result[obj.id] = tools.image_get_resized_images(obj.image)
return result
-
+
def _set_image(self, cr, uid, id, name, value, args, context=None):
return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context)
-
- def _get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None):
- result = {}
- for id in ids:
- lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
- result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context)
- return result
-
- def _get_default_image(self, cr, uid, context=None):
- image_path = openerp.modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
- return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
-
+
_columns = {
'description': fields.text('Description'),
'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"),
- 'responsible_id': fields.many2one('res.users', string='Responsible',
- ondelete='set null', required=True, select=1,
- help="Responsible of the group that has all rights on the record."),
- 'public': fields.selection([('public', 'Public'), ('private', 'Private'), ('groups', 'Selected Group Only')],
- string='Privacy', required=True,
- help='This group is visible by non members. '\
- 'Invisible groups can add members through the invite button.'),
+ 'public': fields.selection([('public','Public'),('private','Private'),('groups','Selected Group Only')], 'Privacy', required=True,
+ help='This group is visible by non members. \
+ Invisible groups can add members through the invite button.'),
'group_public_id': fields.many2one('res.groups', string='Authorized Group'),
'group_ids': fields.many2many('res.groups', rel='mail_group_res_group_rel',
id1='mail_group_id', id2='groups_id', string='Auto Subscription',
@@ -79,7 +59,7 @@ class mail_group(osv.Model):
'image': fields.binary("Photo",
help="This field holds the image used as photo for the "\
"user. The image is base64 encoded, and PIL-supported. "\
- "It is limited to a 12024x1024 px image."),
+ "It is limited to a 1024x1024 px image."),
'image_medium': fields.function(_get_image, fnct_inv=_set_image,
string="Medium-sized photo", type="binary", multi="_get_image",
store = {
@@ -96,17 +76,19 @@ class mail_group(osv.Model):
help="Small-sized photo of the group. It is automatically "\
"resized as a 50x50px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required."),
- 'last_month_msg_nbr': fields.function(_get_last_month_msg_nbr, type='integer',
- string='Messages count for last month'),
- 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade",
- help="The email address associated with this group. New emails received will automatically "
- "create new topics."),
+ 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
+ help="The email address associated with this group. New emails received will automatically "
+ "create new topics."),
}
def _get_default_employee_group(self, cr, uid, context=None):
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
return ref and ref[1] or False
+ def _get_default_image(self, cr, uid, context=None):
+ image_path = openerp.modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
+ return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
+
def _get_menu_parent(self, cr, uid, context=None):
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'mail_group_root')
return ref and ref[1] or False
@@ -114,29 +96,23 @@ class mail_group(osv.Model):
_defaults = {
'public': 'groups',
'group_public_id': _get_default_employee_group,
- 'responsible_id': (lambda s, cr, uid, ctx: uid),
'image': _get_default_image,
'parent_id': _get_menu_parent,
- 'alias_domain': False, # always hide alias during creation
+ 'alias_domain': False, # always hide alias during creation
}
- def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None):
- # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
- user_group_ids = [command[1] for command in group_ids_command if command[0] == 4]
- user_group_ids += [id for command in group_ids_command if command[0] == 6 for id in command[2]]
- # retrieve the user member of those groups
- user_ids = []
- res_groups_obj = self.pool.get('res.groups')
- for group in res_groups_obj.browse(cr, uid, user_group_ids, context=context):
- user_ids += [user.id for user in group.users]
- # subscribe the users
- return self.message_subscribe(cr, uid, ids, user_ids, context=context)
+ def _subscribe_users(self, cr, uid, ids, context=None):
+ for mail_group in self.browse(cr, uid, ids, context=context):
+ partner_ids = []
+ for group in mail_group.group_ids:
+ partner_ids += [user.partner_id.id for user in group.users]
+ self.message_subscribe(cr, uid, ids, partner_ids, context=context)
def create(self, cr, uid, vals, context=None):
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
vals.pop('alias_name', None) # prevent errors during copy()
- alias_id = mail_alias.create_unique_alias(cr, uid,
+ alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': "group+"+vals['name']},
@@ -148,23 +124,22 @@ class mail_group(osv.Model):
# Create client action for this group and link the menu to it
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'action_mail_group_feeds')
if ref:
- search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search_wall')
+ search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search')
params = {
'search_view_id': search_ref and search_ref[1] or False,
- 'domain': [('model','=','mail.group'),('res_id','=',mail_group_id)],
- 'res_model': 'mail.group',
- 'res_id': mail_group_id,
- 'thread_level': 2
+ 'domain': [('model','=','mail.group'), ('res_id','=',mail_group_id)],
+ 'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id},
+ 'res_model': 'mail.message',
+ 'thread_level': 1,
}
cobj = self.pool.get('ir.actions.client')
newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context)
self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
-
- if vals.get('group_ids'):
- self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context)
+ if vals.get('group_ids'):
+ self._subscribe_users(cr, uid, [mail_group_id], context=context)
return mail_group_id
def unlink(self, cr, uid, ids, context=None):
@@ -176,21 +151,17 @@ class mail_group(osv.Model):
return res
def write(self, cr, uid, ids, vals, context=None):
+ result = super(mail_group, self).write(cr, uid, ids, vals, context=context)
if vals.get('group_ids'):
- self._subscribe_user_with_group_m2m_command(cr, uid, ids, vals.get('group_ids'), context=context)
- return super(mail_group, self).write(cr, uid, ids, vals, context=context)
+ self._subscribe_users(cr, uid, ids, vals.get('group_ids'), context=context)
+ return result
- def action_group_join(self, cr, uid, ids, context=None):
- return self.message_subscribe(cr, uid, ids, context=context)
+ def action_follow(self, cr, uid, ids, context=None):
+ """ Wrapper because message_subscribe_users take a user_ids=None
+ that receive the context without the wrapper. """
+ return self.message_subscribe_users(cr, uid, ids, context=context)
- def action_group_leave(self, cr, uid, ids, context=None):
- return self.message_unsubscribe(cr, uid, ids, context=context)
-
- # ----------------------------------------
- # OpenChatter methods and notifications
- # ----------------------------------------
-
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'responsible_id' to the monitored fields """
- res = super(mail_group, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['responsible_id']
+ def action_unfollow(self, cr, uid, ids, context=None):
+ """ Wrapper because message_unsubscribe_users take a user_ids=None
+ that receive the context without the wrapper. """
+ return self.message_unsubscribe_users(cr, uid, ids, context=context)
diff --git a/addons/mail/mail_group_menu.py b/addons/mail/mail_group_menu.py
index 8625285d640..25b85bb7b22 100644
--- a/addons/mail/mail_group_menu.py
+++ b/addons/mail/mail_group_menu.py
@@ -40,11 +40,12 @@ class ir_ui_menu(osv.osv):
""" Override to take off menu entries (mail.group) the user is not
following. """
ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0, limit=None, order=order, context=context, count=False)
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
follower_obj = self.pool.get('mail.followers')
for menu in self.browse(cr, uid, ids, context=context):
if menu.mail_group_id:
sub_ids = follower_obj.search(cr, uid, [
- ('user_id', '=', uid), ('res_model', '=', 'mail.group'),
+ ('partner_id', '=', partner_id), ('res_model', '=', 'mail.group'),
('res_id', '=', menu.mail_group_id.id)
], context=context)
if not sub_ids:
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index 2b5e9b340af..31beba80ecc 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -1,10 +1,11 @@
+
Discussion Groupmail.wall
-
+ mail.message
@@ -14,8 +15,9 @@
-
+
+
@@ -103,7 +97,6 @@
-
@@ -116,7 +109,6 @@
-
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
new file mode 100644
index 00000000000..7b593c5b998
--- /dev/null
+++ b/addons/mail/mail_mail.py
@@ -0,0 +1,204 @@
+# -*- 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 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 ast
+import base64
+import logging
+import tools
+
+from osv import osv
+from osv import fields
+from tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+class mail_mail(osv.Model):
+ """ Model holding RFC2822 email messages to send. This model also provides
+ facilities to queue and send new email messages. """
+ _name = 'mail.mail'
+ _description = 'Outgoing Mails'
+ _inherits = {'mail.message': 'mail_message_id'}
+ _order = 'id desc'
+
+ _columns = {
+ 'mail_message_id': fields.many2one('mail.message', 'Message', required=True, ondelete='cascade'),
+ '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'),
+ ], 'Status', readonly=True),
+ 'auto_delete': fields.boolean('Auto Delete',
+ help="Permanently delete this email after sending it, to save space"),
+ 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
+ 'email_from': fields.char('From', help='Message sender, taken from user preferences.'),
+ 'email_to': fields.text('To', help='Message recipients'),
+ 'email_cc': fields.char('Cc', help='Carbon copy message recipients'),
+ 'reply_to': fields.char('Reply-To', help='Preferred response address for the message'),
+ 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
+
+ # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
+ # and during unlink() we will cascade delete the parent and its attachments
+ 'notification': fields.boolean('Is Notification')
+ }
+
+ def _get_default_from(self, cr, uid, context=None):
+ cur = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+ if not cur.alias_domain:
+ raise osv.except_osv(_('Invalid Action!'), _('Unable to send email, set an alias domain in your server settings.'))
+ return cur.alias_name + '@' + cur.alias_domain
+
+ _defaults = {
+ 'state': 'outgoing',
+ 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx),
+ }
+
+ def create(self, cr, uid, values, context=None):
+ if 'notification' not in values and values.get('mail_message_id'):
+ values['notification'] = True
+ return super(mail_mail,self).create(cr, uid, values, context=context)
+
+ def unlink(self, cr, uid, ids, context=None):
+ # cascade-delete the parent message for all mails that are not created for a notification
+ ids_to_cascade = self.search(cr, uid, [('notification','=',False),('id','in',ids)])
+ parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
+ res = super(mail_mail,self).unlink(cr, uid, ids, context=context)
+ self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
+ return res
+
+ def mark_outgoing(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
+
+ def cancel(self, cr, uid, ids, context=None):
+ return self.write(cr, uid, ids, {'state':'cancel'}, context=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'), ('type', '=', 'email')]
+ 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 _postprocess_sent_message(self, cr, uid, mail, context=None):
+ """Perform any post-processing necessary after sending ``mail``
+ successfully, including deleting it completely along with its
+ attachment if the ``auto_delete`` flag of the mail was set.
+ Overridden by subclasses for extra post-processing behaviors.
+
+ :param browse_record mail: the mail that was just sent
+ :return: True
+ """
+ if mail.auto_delete:
+ mail.unlink()
+ return True
+
+ def _send_get_mail_subject(self, cr, uid, mail, force=False, context=None):
+ """ if void and related document: ' posted on '
+ :param mail: mail.mail browse_record """
+ if force or (not mail.subject and mail.model and mail.res_id):
+ return '%s posted on %s' % (mail.author_id.name, mail.record_name)
+ return mail.subject
+
+ 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 mail is output in the server logs.
+
+ :param bool auto_commit: whether to force a commit of the mail status
+ after sending each mail (meant only for scheduler processing);
+ should never be True during normal transactions (default: False)
+ :return: True
+ """
+ ir_mail_server = self.pool.get('ir.mail_server')
+ for mail in self.browse(cr, uid, ids, context=context):
+ try:
+ body = mail.body_html
+ subject = self._send_get_mail_subject(cr, uid, mail, context=context)
+
+ # handle attachments
+ attachments = []
+ for attach in mail.attachment_ids:
+ attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
+
+ # use only sanitized html and set its plaintexted version as alternative
+ body_alternative = tools.html2plaintext(body)
+ content_subtype_alternative = 'plain'
+
+ # build an RFC2822 email.message.Message object and send it without queuing
+ msg = ir_mail_server.build_email(
+ email_from = mail.email_from,
+ email_to = tools.email_split(mail.email_to),
+ subject = subject,
+ body = body,
+ body_alternative = body_alternative,
+ email_cc = tools.email_split(mail.email_cc),
+ reply_to = mail.reply_to,
+ attachments = attachments,
+ message_id = mail.message_id,
+ references = mail.references,
+ object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
+ subtype = 'html',
+ subtype_alternative = content_subtype_alternative)
+ res = ir_mail_server.send_email(cr, uid, msg,
+ mail_server_id=mail.mail_server_id.id, context=context)
+ if res:
+ mail.write({'state':'sent', 'message_id': res})
+ else:
+ mail.write({'state':'exception'})
+ mail.refresh()
+ if mail.state == 'sent':
+ self._postprocess_sent_message(cr, uid, mail, context=context)
+ except Exception:
+ _logger.exception('failed sending mail.mail %s', mail.id)
+ mail.write({'state':'exception'})
+
+ if auto_commit == True:
+ cr.commit()
+ return True
diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml
new file mode 100644
index 00000000000..72d41e33a84
--- /dev/null
+++ b/addons/mail/mail_mail_view.xml
@@ -0,0 +1,127 @@
+
+
+
+
+ mail.mail.form
+ mail.mail
+
+
+
+
+
+
+ mail.mail.tree
+ mail.mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mail.mail.search
+ mail.mail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Emails
+ mail.mail
+ form
+ tree,form
+ {'search_default_outgoing': 1, 'search_default_type_email': 1}
+
+
+
+
+
+
+
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 7801a9b42d9..e4862ca9fb9 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -19,25 +19,13 @@
#
##############################################################################
-import ast
-import base64
-import dateutil.parser
-import email
import logging
-import re
-import time
-import datetime
-from email.header import decode_header
-from email.message import Message
-
-from openerp import SUPERUSER_ID
-from osv import osv
-from osv import fields
-import pytz
-from tools import DEFAULT_SERVER_DATETIME_FORMAT
-from tools.translate import _
import tools
+from email.header import decode_header
+from operator import itemgetter
+from osv import osv, fields
+
_logger = logging.getLogger(__name__)
""" Some tools for parsing / creating email fields """
@@ -47,633 +35,309 @@ def decode(text):
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
-def mail_tools_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(osv.Model):
+ """ Messages model: system notification (replacing res.log notifications),
+ comments (OpenChatter discussion) and incoming emails. """
+ _name = 'mail.message'
+ _description = 'Message'
+ _inherit = ['ir.needaction_mixin']
+ _order = 'id desc'
-# TODO: remove that after cleaning
-def to_email(text):
- return mail_tools_to_email(text)
+ _message_read_limit = 10
+ _message_record_name_length = 18
-class mail_message_common(osv.TransientModel):
- """ 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.
- All internal logic should be in another model while this
- model holds the basics of a message. For example, a wizard for writing
- emails should inherit from this class and not from mail.message."""
+ def _shorten_name(self, name):
+ if len(name) <= (self._message_record_name_length + 3):
+ return name
+ return name[:self._message_record_name_length] + '...'
- def get_body(self, cr, uid, ids, name, arg, context=None):
- """ get correct body version: body_html for html messages, and
- body_text for plain text messages
- """
- result = dict.fromkeys(ids, '')
- for message in self.browse(cr, uid, ids, context=context):
- if message.content_subtype == 'html':
- result[message.id] = message.body_html
- else:
- result[message.id] = message.body_text
- return result
-
- def search_body(self, cr, uid, obj, name, args, context=None):
- # will receive:
- # - obj: mail.message object
- # - name: 'body'
- # - args: [('body', 'ilike', 'blah')]
- return ['|', '&', ('content_subtype', '=', 'html'), ('body_html', args[0][1], args[0][2]), ('body_text', args[0][1], args[0][2])]
-
- def get_record_name(self, cr, uid, ids, name, arg, context=None):
+ def _get_record_name(self, cr, uid, ids, name, arg, context=None):
+ """ Return the related document name, using get_name. """
result = dict.fromkeys(ids, '')
for message in self.browse(cr, uid, ids, context=context):
if not message.model or not message.res_id:
continue
- result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1]
+ result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
return result
+ def _get_unread(self, cr, uid, ids, name, arg, context=None):
+ """ Compute if the message is unread by the current user. """
+ res = dict((id, {'unread': False}) for id in ids)
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ notif_obj = self.pool.get('mail.notification')
+ notif_ids = notif_obj.search(cr, uid, [
+ ('partner_id', 'in', [partner_id]),
+ ('message_id', 'in', ids),
+ ('read', '=', False)
+ ], context=context)
+ for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
+ res[notif.message_id.id]['unread'] = True
+ return res
+
+ def _search_unread(self, cr, uid, obj, name, domain, context=None):
+ """ Search for messages unread by the current user. Condition is
+ inversed because we search unread message on a read column. """
+ if domain[0][2]:
+ read_cond = '(read = false or read is null)'
+ else:
+ read_cond = 'read = true'
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ cr.execute("SELECT message_id FROM mail_notification "\
+ "WHERE partner_id = %%s AND %s" % read_cond,
+ (partner_id,))
+ return [('id', 'in', [r[0] for r in cr.fetchall()])]
+
def name_get(self, cr, uid, ids, context=None):
# name_get may receive int id instead of an id list
if isinstance(ids, (int, long)):
ids = [ids]
res = []
for message in self.browse(cr, uid, ids, context=context):
- name = ''
- if message.subject:
- name = '%s: ' % (message.subject)
- if message.body_text:
- name = '%s%s ' % (name, message.body_text[0:20])
- if message.date:
- name = '%s(%s)' % (name, message.date)
- res.append((message.id, name))
+ name = '%s: %s' % (message.subject or '', message.body or '')
+ res.append((message.id, self._shorten_name(name.lstrip(' :'))))
return res
- _name = 'mail.message.common'
- _rec_name = 'subject'
- _columns = {
- 'subject': fields.char('Subject', size=512),
- 'model': fields.char('Related Document Model', size=128, select=1),
- 'res_id': fields.integer('Related Document ID', select=1),
- 'record_name': fields.function(get_record_name, type='string',
- string='Message Record Name',
- help="Name get of the related document."),
- '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),
- 'content_subtype': fields.char('Message content subtype', size=32,
- oldname="subtype", readonly=1,
- help="Type of message, usually 'html' or 'plain', used to select "\
- "plain-text or rich-text contents accordingly"),
- 'body_text': fields.text('Text Contents', help="Plain-text version of the message"),
- 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"),
- 'body': fields.function(get_body, fnct_search = search_body, type='text',
- string='Message Content', store=True,
- help="Content of the message. This content equals the body_text field "\
- "for plain-test messages, and body_html for rich-text/HTML "\
- "messages. This allows having one field if we want to access "\
- "the content matching the message content_subtype."),
- 'parent_id': fields.many2one('mail.message.common', 'Parent Message',
- select=True, ondelete='set null',
- help="Parent message, used for displaying as threads with hierarchy"),
- }
-
- _defaults = {
- 'content_subtype': 'plain',
- 'date': (lambda *a: fields.datetime.now()),
- }
-
-class mail_message(osv.Model):
- """Model holding messages: system notification (replacing res.log
- notifications), comments (for OpenChatter feature) and
- RFC2822 email messages. This model also provides facilities to
- parse, queue and send new email messages. Type of messages
- are differentiated using the 'type' column. """
-
- _name = 'mail.message'
- _inherit = 'mail.message.common'
- _description = 'Mail Message (email, comment, notification)'
- _order = 'date desc'
-
- def open_document(self, cr, uid, ids, context=None):
- """ Open the message related document. Note that only the document of
- ids[0] will be opened.
- TODO: how to determine the action to use ?
- """
- action_data = False
- if not ids:
- return action_data
- msg = self.browse(cr, uid, ids[0], context=context)
- ir_act_window = self.pool.get('ir.actions.act_window')
- action_ids = ir_act_window.search(cr, uid, [('res_model', '=', msg.model)], context=context)
- if action_ids:
- action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
- action_data.update({
- 'domain' : "[('id', '=', %d)]" % (msg.res_id),
- 'nodestroy': True,
- 'context': {}
- })
- return action_data
-
- def open_attachment(self, cr, uid, ids, context=None):
- """ Open the message related attachments.
- TODO: how to determine the action to use ?
- """
- action_data = False
- if not ids:
- return action_data
- action_pool = self.pool.get('ir.actions.act_window')
- messages = self.browse(cr, uid, ids, context=context)
- att_ids = [x.id for message in messages for x in message.attachment_ids]
- action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')], context=context)
- 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
-
_columns = {
'type': fields.selection([
- ('email', 'email'),
+ ('email', 'Email'),
('comment', 'Comment'),
('notification', 'System notification'),
], 'Type',
help="Message type: email for email message, notification for system "\
- "message, comment for other messages such as user replies"),
- 'partner_id': fields.many2one('res.partner', 'Related partner',
- help="Deprecated field. Use partner_ids instead."),
- 'partner_ids': fields.many2many('res.partner',
- 'mail_message_res_partner_rel',
- 'message_id', 'partner_id', 'Destination partners',
- help="When sending emails through the social network composition wizard"\
- "you may choose to send a copy of the mail to partners."),
- 'user_id': fields.many2one('res.users', 'Related User', readonly=1),
+ "message, comment for other messages such as user replies"),
+ 'author_id': fields.many2one('res.partner', 'Author', required=True),
+ 'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
'message_id', 'attachment_id', 'Attachments'),
- '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'),
- ], 'Status', readonly=True),
- 'auto_delete': fields.boolean('Auto Delete',
- help="Permanently delete this email after sending it, to save space"),
- 'original': fields.binary('Original', readonly=1,
- help="Original version of the message, as it was sent on the network"),
- 'parent_id': fields.many2one('mail.message', 'Parent Message',
- select=True, ondelete='set null',
- help="Parent message, used for displaying as threads with hierarchy"),
+ 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
+ 'model': fields.char('Related Document Model', size=128, select=1),
+ 'res_id': fields.integer('Related Document ID', select=1),
+ 'record_name': fields.function(_get_record_name, type='string',
+ string='Message Record Name',
+ help="Name get of the related document."),
+ 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
+ 'subject': fields.char('Subject'),
+ 'date': fields.datetime('Date'),
+ 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
+ 'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
+ 'unread': fields.function(_get_unread, fnct_search=_search_unread,
+ type='boolean', string='Unread',
+ help='Functional field to search for unread messages linked to uid'),
}
-
+
+ def _needaction_domain_get(self, cr, uid, context=None):
+ if self._needaction:
+ return [('unread', '=', True)]
+ return []
+
+ def _get_default_author(self, cr, uid, context=None):
+ return self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+
_defaults = {
'type': 'email',
- 'state': 'received',
+ 'date': lambda *a: fields.datetime.now(),
+ 'author_id': lambda self, cr, uid, ctx={}: self._get_default_author(cr, uid, ctx),
+ 'body': '',
}
-
+
+ #------------------------------------------------------
+ # Message loading for web interface
+ #------------------------------------------------------
+
+ def _message_dict_get(self, cr, uid, msg, context=None):
+ """ Return a dict representation of the message browse record. """
+ attachment_ids = self.pool.get('ir.attachment').name_get(cr, uid, [x.id for x in msg.attachment_ids], context=context)
+ author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
+ author_user_id = self.pool.get('res.users').name_get(cr, uid, [msg.author_id.user_ids[0].id], context=context)[0]
+ partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
+ return {
+ 'id': msg.id,
+ 'type': msg.type,
+ 'attachment_ids': attachment_ids,
+ 'body': msg.body,
+ 'model': msg.model,
+ 'res_id': msg.res_id,
+ 'record_name': msg.record_name,
+ 'subject': msg.subject,
+ 'date': msg.date,
+ 'author_id': author_id,
+ 'author_user_id': author_user_id,
+ 'partner_ids': partner_ids,
+ 'child_ids': [],
+ }
+
+ def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
+ """ Given a tree with several roots of following structure :
+ [ {'id': 1, 'child_ids': [
+ {'id': 11, 'child_ids': [...] },],
+ {...} ]
+ Flatten it to have a maximum number of levels, 0 being flat and
+ sort messages in a level according to a key of the messages.
+ Perform the flattening at leafs if above the maximum depth, then get
+ back in the tree.
+ :param context: ``sort_key``: key for sorting (id by default)
+ :param context: ``sort_reverse``: reverser order for sorting (True by default)
+ """
+ def _flatten(msg_dict):
+ """ from {'id': x, 'child_ids': [{child1}, {child2}]}
+ get [{'id': x, 'child_ids': []}, {child1}, {child2}]
+ """
+ child_ids = msg_dict.pop('child_ids', [])
+ msg_dict['child_ids'] = []
+ return [msg_dict] + child_ids
+ # return sorted([msg_dict] + child_ids, key=itemgetter('id'), reverse=True)
+ context = context or {}
+ # Depth-first flattening
+ for message in messages:
+ if message.get('type') == 'expandable':
+ continue
+ message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level + 1, level, context=context)
+ # Flatten if above maximum depth
+ if current_level < level:
+ return_list = messages
+ else:
+ return_list = []
+ for message in messages:
+ for flat_message in _flatten(message):
+ return_list.append(flat_message)
+ return sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True))
+
+ def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
+ """ If IDs are provided, fetch these records. Otherwise use the domain
+ to fetch the matching records.
+ After having fetched the records provided by IDs, it will fetch the
+ parents to have well-formed threads.
+ :return list: list of trees of messages
+ """
+ limit = limit or self._message_read_limit
+ context = context or {}
+ if not ids:
+ ids = self.search(cr, uid, domain, context=context, limit=limit)
+ messages = self.browse(cr, uid, ids, context=context)
+
+ result = []
+ tree = {} # key: ID, value: record
+ for msg in messages:
+ if len(result) < (limit - 1):
+ record = self._message_dict_get(cr, uid, msg, context=context)
+ if thread_level and msg.parent_id:
+ while msg.parent_id:
+ if msg.parent_id.id in tree:
+ record_parent = tree[msg.parent_id.id]
+ else:
+ record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
+ if msg.parent_id.parent_id:
+ tree[msg.parent_id.id] = record_parent
+ if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
+ record_parent['child_ids'].append(record)
+ record = record_parent
+ msg = msg.parent_id
+ if msg.id not in tree:
+ result.append(record)
+ tree[msg.id] = record
+ else:
+ result.append({
+ 'type': 'expandable',
+ 'domain': [('id', '<=', msg.id)] + domain,
+ 'context': context,
+ 'thread_level': thread_level, # should be improve accodting to level of records
+ 'id': -1,
+ })
+ break
+
+ # Flatten the result
+ if thread_level > 0:
+ result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
+ return result
+
#------------------------------------------------------
# Email api
#------------------------------------------------------
-
+
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 check(self, cr, uid, ids, mode, context=None, values=None):
- """Restricts the access to a mail.message, according to referred model
- """
- if not ids:
- return
- res_ids = {}
+ def check_access_rule(self, cr, uid, ids, operation, context=None):
+ """ mail.message access rule check
+ - message received (a notification exists) -> ok
+ - check rules of related document if exists
+ - fallback on normal mail.message check """
if isinstance(ids, (int, long)):
ids = [ids]
- cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
- for rmod, rid in cr.fetchall():
+
+ # check messages for which you have a notification
+ partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
+ not_obj = self.pool.get('mail.notification')
+ not_ids = not_obj.search(cr, uid, [
+ ('partner_id', '=', partner_id),
+ ('message_id', 'in', ids),
+ ], context=context)
+ notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, uid, not_ids, context=context)
+ if notification.message_id.id in ids]
+
+ # check messages linked to an existing document
+ model_record_ids = {}
+ document_ids = []
+ cr.execute('SELECT DISTINCT id, model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
+ for id, rmod, rid in cr.fetchall():
if not (rmod and rid):
continue
- res_ids.setdefault(rmod,set()).add(rid)
- if values:
- if 'res_model' in values and 'res_id' in values:
- res_ids.setdefault(values['res_model'],set()).add(values['res_id'])
+ document_ids.append(id)
+ model_record_ids.setdefault(rmod, set()).add(rid)
+ for model, mids in model_record_ids.items():
+ model_obj = self.pool.get(model)
+ mids = model_obj.exists(cr, uid, mids)
+ model_obj.check_access_rights(cr, uid, operation)
+ model_obj.check_access_rule(cr, uid, mids, operation, context=context)
+
+ # fall back on classic operation for other ids
+ other_ids = set(ids).difference(set(notified_ids), set(document_ids))
+ super(mail_message, self).check_access_rule(cr, uid, other_ids, operation, context=None)
- ima_obj = self.pool.get('ir.model.access')
- for model, mids in res_ids.items():
- # ignore mail messages that are not attached to a resource anymore when checking access rights
- # (resource was deleted but message was not)
- mids = self.pool.get(model).exists(cr, uid, mids)
- ima_obj.check(cr, uid, model, mode)
- self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
-
def create(self, cr, uid, values, context=None):
- self.check(cr, uid, [], mode='create', context=context, values=values)
- return super(mail_message, self).create(cr, uid, values, context)
+ if not values.get('message_id') and values.get('res_id') and values.get('model'):
+ values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s' % values)
+ newid = super(mail_message, self).create(cr, uid, values, context)
+ self.notify(cr, uid, newid, context=context)
+ return newid
- def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
- self.check(cr, uid, ids, 'read', context=context)
- return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
+ def unlink(self, cr, uid, ids, context=None):
+ # cascade-delete attachments that are directly attached to the message (should only happen
+ # for mail.messages that act as parent for a standalone mail.mail record.
+ attachments_to_delete = []
+ for mail in self.browse(cr, uid, ids, context=context):
+ for attach in mail.attachment_ids:
+ if attach.res_model == 'mail.message' and attach.res_id == mail.id:
+ attachments_to_delete.append(attach.id)
+ if attachments_to_delete:
+ self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
+ return super(mail_message,self).unlink(cr, uid, ids, context=context)
+
+ def notify(self, cr, uid, newid, context=None):
+ """ Add the related record followers to the destination partner_ids.
+ Call mail_notification.notify to manage the email sending
+ """
+ message = self.browse(cr, uid, newid, context=context)
+ partners_to_notify = set([])
+ # add all partner_ids of the message
+ if message.partner_ids:
+ partners_to_notify |= set(partner.id for partner in message.partner_ids)
+ # add all followers and set add them in partner_ids
+ if message.model and message.res_id:
+ record = self.pool.get(message.model).browse(cr, uid, message.res_id, context=context)
+ extra_notified = set(partner.id for partner in record.message_follower_ids)
+ missing_notified = extra_notified - partners_to_notify
+ if missing_notified:
+ message.write({'partner_ids': [(4, p_id) for p_id in missing_notified]})
+ partners_to_notify |= extra_notified
+ self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
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 = {}
- self.check(cr, uid, [id], 'read', context=context)
- default.update(message_id=False, original=False, headers=False)
- return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
-
- def write(self, cr, uid, ids, vals, context=None):
- self.check(cr, uid, ids, 'write', context=context, values=vals)
- return super(mail_message, self).write(cr, uid, ids, vals, context)
-
- def unlink(self, cr, uid, ids, context=None):
- self.check(cr, uid, ids, 'unlink', context=context)
- return super(mail_message, self).unlink(cr, uid, ids, context)
-
- def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email',
- email_cc=None, email_bcc=None, reply_to=False, partner_ids=None, attachments=None,
- message_id=False, references=False, res_id=False, content_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 ``content_subtype``
- (by default, plaintext). If html content_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 partner_ids: destination partner_ids
- :param string content_subtype: optional mime content_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 = {}
- if partner_ids is None:
- partner_ids = []
- 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': fields.datetime.now(),
- 'user_id': uid,
- 'model': model,
- 'res_id': res_id,
- 'type': type,
- 'body_text': body if content_subtype != 'html' else False,
- 'body_html': body if content_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 '',
- 'partner_ids': partner_ids,
- 'reply_to': reply_to,
- 'message_id': message_id,
- 'references': references,
- 'content_subtype': content_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 attachment in attachments:
- fname, fcontent = attachment
- attachment_data = {
- 'name': fname,
- 'datas_fname': fname,
- 'datas': fcontent and fcontent.encode('base64'),
- '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=context)
-
- def cancel(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state':'cancel'}, context=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'), ('type', '=', 'email')]
- 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, context=None):
- """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...
- },
- 'content_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)
-
- msg_fields = msg_txt.keys()
- msg['id'] = message_id
- msg['message-id'] = message_id
-
- if 'Subject' in msg_fields:
- msg['subject'] = decode(msg_txt.get('Subject'))
-
- if 'Content-Type' in msg_fields:
- msg['content-type'] = msg_txt.get('Content-Type')
-
- if 'From' in msg_fields:
- msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
-
- if 'To' in msg_fields:
- msg['to'] = decode(msg_txt.get('To'))
-
- if 'Delivered-To' in msg_fields:
- msg['to'] = decode(msg_txt.get('Delivered-To'))
-
- if 'CC' in msg_fields:
- msg['cc'] = decode(msg_txt.get('CC'))
-
- if 'Cc' in msg_fields:
- msg['cc'] = decode(msg_txt.get('Cc'))
-
- if 'Reply-To' in msg_fields:
- msg['reply'] = decode(msg_txt.get('Reply-To'))
-
- if 'Date' in msg_fields:
- date_hdr = decode(msg_txt.get('Date'))
- # convert from email timezone to server timezone
- date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone()))
- date_server_datetime_str = date_server_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
- msg['date'] = date_server_datetime_str
-
- if 'Content-Transfer-Encoding' in msg_fields:
- msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
-
- if 'References' in msg_fields:
- msg['references'] = msg_txt.get('References')
-
- if 'In-Reply-To' in msg_fields:
- msg['in-reply-to'] = msg_txt.get('In-Reply-To')
-
- msg['headers'] = {}
- msg['content_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['content_subtype'] = 'html'
- if body:
- 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['content_subtype'] = 'alternative'
- else:
- msg['content_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.append((filename, content))
- content = tools.ustr(content, encoding)
- if part.get_content_subtype() == 'html':
- msg['body_html'] = content
- msg['content_subtype'] = 'html' # html version prevails
- body = tools.ustr(tools.html2plaintext(content))
- body = body.replace('
', '')
- elif part.get_content_subtype() == 'plain':
- body = content
- elif part.get_content_maintype() in ('application', 'image'):
- if filename :
- attachments.append((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['content_subtype'] or 'plain'
- return msg
-
- def _postprocess_sent_message(self, cr, uid, message, context=None):
- """Perform any post-processing necessary after sending ``message``
- successfully, including deleting it completely along with its
- attachment if the ``auto_delete`` flag of the message was set.
- Overridden by subclasses for extra post-processing behaviors.
-
- :param browse_record message: the message that was just sent
- :return: True
- """
- if message.auto_delete:
- self.pool.get('ir.attachment').unlink(cr, uid,
- [x.id for x in message.attachment_ids
- if x.res_model == self._name and x.res_id == message.id],
- context=context)
- message.unlink()
- return True
-
- 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
- """
- 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)))
-
- body = message.body_html if message.content_subtype == 'html' else message.body_text
- body_alternative = None
- content_subtype_alternative = None
- if message.content_subtype == 'html' and message.body_text:
- # we have a plain text alternative prepared, pass it to
- # build_message instead of letting it build one
- body_alternative = message.body_text
- content_subtype_alternative = 'plain'
-
- # handle destination_partners
- partner_ids_email_to = ''
- for partner in message.partner_ids:
- partner_ids_email_to += '%s ' % (partner.email or '')
- message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '')
-
- # build an RFC2822 email.message.Message object and send it
- # without queuing
- msg = ir_mail_server.build_email(
- email_from=message.email_from,
- email_to=mail_tools_to_email(message_email_to),
- subject=message.subject,
- body=body,
- body_alternative=body_alternative,
- email_cc=mail_tools_to_email(message.email_cc),
- email_bcc=mail_tools_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.content_subtype,
- subtype_alternative=content_subtype_alternative,
- headers=message.headers and ast.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, 'email_to': message_email_to})
- else:
- message.write({'state':'exception', 'email_to': message_email_to})
- message.refresh()
- if message.state == 'sent':
- self._postprocess_sent_message(cr, uid, message, context=context)
- except Exception:
- _logger.exception('failed sending mail.message %s', message.id)
- message.write({'state':'exception'})
-
- if auto_commit == True:
- cr.commit()
- return True
-
-
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+ default.update(message_id=False, headers=False)
+ return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index 0aa5261d7e7..914937a34f3 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -2,15 +2,15 @@
-
- mail.message.tree.short
+
+ mail.message.treemail.message20
-
+
@@ -18,8 +18,8 @@
-
- mail.message.form.short
+
+ mail.message.formmail.message20
@@ -28,10 +28,9 @@
-
+
-
@@ -40,45 +39,22 @@
-
-
-
-
-
-
-
-
+
-
-
- mail.message.search.short
- mail.message
- 20
-
-
-
-
-
-
-
-
-
-
- mail.message.search.wall
+
+ mail.message.searchmail.message25
-
+
@@ -88,159 +64,17 @@
-
-
-
+
-
- mail.message.form
- mail.message
-
-
\n')
+ _mail_bodyalt2 = 'Pigs rules\nAdmin'
+ _attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
+
+ # CASE1: post comment, body and subject specified
+ msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment')
+ message = self.mail_message.browse(cr, uid, msg_id)
+ sent_email = self._build_email_kwargs
+ # Test: notifications have been deleted
+ self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id)]), 'mail.mail notifications should have been auto-deleted!')
+ # Test: mail_message: subject is _subject, body is _body1 (no formatting done)
+ self.assertEqual(message.subject, _subject, 'mail.message subject incorrect')
+ self.assertEqual(message.body, _body1, 'mail.message body incorrect')
+ # Test: sent_email: email send by server: correct subject, body; body_alternative
+ self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
+ self.assertEqual(sent_email['body'], _mail_body1, 'sent_email body incorrect')
+ self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1, 'sent_email body_alternative is incorrect')
+ # Test: mail_message: partner_ids = group followers
+ message_pids = set([partner.id for partner in message.partner_ids])
+ test_pids = set([p_a_id, p_b_id, p_c_id])
+ self.assertEqual(test_pids, message_pids, 'mail.message partners incorrect')
+ # Test: notification linked to this message = group followers = partner_ids
+ notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
+ notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
+ self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
+ # Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer)
+ self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
+
+ # CASE2: post an email with attachments, parent_id, partner_ids
+ # TESTS: automatic subject, signature in body_html, attachments propagation
+ msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email',
+ partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments)
+ message = self.mail_message.browse(cr, uid, msg_id2)
+ sent_email = self._build_email_kwargs
+ self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id2)]), 'mail.mail notifications should have been auto-deleted!')
+
+ # Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id
+ self.assertEqual(message.subject, False, 'mail.message subject incorrect')
+ self.assertEqual(message.body, html_sanitize(_body2), 'mail.message body incorrect')
+ self.assertEqual(message.parent_id.id, msg_id, 'mail.message parent_id incorrect')
+ # Test: sent_email: email send by server: correct subject, body, body_alternative
+ self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
+ self.assertEqual(sent_email['body'], _mail_body2, 'sent_email body incorrect')
+ self.assertEqual(sent_email['body_alternative'], _mail_bodyalt2, 'sent_email body_alternative incorrect')
+ # Test: mail_message: partner_ids = group followers
+ message_pids = set([partner.id for partner in message.partner_ids])
+ test_pids = set([p_a_id, p_b_id, p_c_id, p_d_id])
+ self.assertEqual(message_pids, test_pids, 'mail.message partners incorrect')
+ # Test: notifications linked to this message = group followers = partner_ids
+ notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
+ notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
+ self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
+ # Test: sent_email: email_to should contain b@b, c@c, not a@a (writer)
+ self.assertEqual(set(sent_email['email_to']), set(['b@b', 'c@c']), 'sent_email email_to incorrect')
+ # Test: attachments
+ for attach in message.attachment_ids:
+ self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
+ self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
+ self.assertIn((attach.name, attach.datas.decode('base64')), _attachments,
+ 'mail.message attachment name / data incorrect')
+
+ def test_21_message_compose_wizard(self):
+ """ Tests designed for the mail.compose.message wizard. """
+ cr, uid = self.cr, self.uid
+ mail_compose = self.registry('mail.compose.message')
+ self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
+ user_admin = self.res_users.browse(cr, uid, uid)
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ group_bird_id = self.mail_group.create(cr, uid, {'name': 'Bird', 'description': 'Bird resistance'})
+ group_bird = self.mail_group.browse(cr, uid, group_bird_id)
+
+ # Mail data
+ _subject = 'Pigs'
+ _body_text = 'Pigs rules'
+ _msg_reply = 'Re: Pigs'
+ _msg_body = '
Pigs rules
'
+ _attachments = [
+ {'name': 'First', 'datas_fname': 'first.txt', 'datas': 'My first attachment'.encode('base64')},
+ {'name': 'Second', 'datas_fname': 'second.txt', 'datas': 'My second attachment'.encode('base64')}
+ ]
+ _attachments_test = [('first.txt', 'My first attachment'), ('second.txt', 'My second attachment')]
+
+ # Create partners
+ # 0 - Admin
+ p_a_id = user_admin.partner_id.id
+ # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
+ p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
+ # 2 - Carine Poilvache, with email, should never receive emails
+ p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
+ # 3 - Dédé Grosbedon, without email, to test email verification; should receive emails for every message
+ p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'notification_email_send': 'all'})
+
+ # Subscribe #1
+ group_pigs.message_subscribe([p_b_id])
+
+ # ----------------------------------------
+ # CASE1: comment on group_pigs
+ # ----------------------------------------
+
+ # 1. Comment group_pigs with body_text and subject
+ compose_id = mail_compose.create(cr, uid,
+ {'subject': _subject, 'body_text': _body_text, 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
+ {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id})
+ compose = mail_compose.browse(cr, uid, compose_id)
+ # Test: mail.compose.message: composition_mode, model, res_id
+ self.assertEqual(compose.composition_mode, 'comment', 'mail.compose.message incorrect composition_mode')
+ self.assertEqual(compose.model, 'mail.group', 'mail.compose.message incorrect model')
+ self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
+
+ # 2. Post the comment, get created message
+ mail_compose.send_mail(cr, uid, [compose_id])
+ group_pigs.refresh()
+ message = group_pigs.message_ids[0]
+ # Test: mail.message: subject, body inside pre
+ self.assertEqual(message.subject, False, 'mail.message incorrect subject')
+ self.assertEqual(message.body, _msg_body, 'mail.message incorrect body')
+ # Test: mail.message: partner_ids = entries in mail.notification: group_pigs fans (a, b) + mail.compose.message partner_ids (c, d)
+ msg_pids = [partner.id for partner in message.partner_ids]
+ test_pids = [p_a_id, p_b_id, p_c_id, p_d_id]
+ notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
+ self.assertEqual(len(notif_ids), 4, 'mail.message: too much notifications created')
+ self.assertEqual(set(msg_pids), set(test_pids), 'mail.message partner_ids incorrect')
+
+ # ----------------------------------------
+ # CASE2: reply to last comment with attachments
+ # ----------------------------------------
+
+ # 1. Update last comment subject, reply with attachments
+ message.write({'subject': _subject})
+ compose_id = mail_compose.create(cr, uid,
+ {'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]},
+ {'default_composition_mode': 'reply', 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id})
+ compose = mail_compose.browse(cr, uid, compose_id)
+ # Test: model, res_id, parent_id, content_subtype
+ self.assertEqual(compose.model, 'mail.group', 'mail.compose.message incorrect model')
+ self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
+ self.assertEqual(compose.parent_id.id, message.id, 'mail.compose.message incorrect parent_id')
+ self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
+
+ # 2. Post the comment, get created message
+ mail_compose.send_mail(cr, uid, [compose_id])
+ group_pigs.refresh()
+ message = group_pigs.message_ids[0]
+ # Test: mail.message: subject as Re:.., body in html
+ self.assertEqual(message.subject, _msg_reply, 'mail.message incorrect subject')
+ self.assertIn('Administrator wrote:
Pigs rules
', message.body, 'mail.message body is incorrect')
+ # Test: mail.message: attachments
+ for attach in message.attachment_ids:
+ self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
+ self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
+ self.assertIn((attach.name, attach.datas.decode('base64')), _attachments_test,
+ 'mail.message attachment name / data incorrect')
+
+ # ----------------------------------------
+ # CASE3: mass_mail on Pigs and Bird
+ # ----------------------------------------
+
+ # 1. mass_mail on pigs and bird
+ compose_id = mail_compose.create(cr, uid,
+ {'subject': _subject, 'body': '${object.description}'},
+ {'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': -1,
+ 'active_ids': [self.group_pigs_id, group_bird_id]})
+ compose = mail_compose.browse(cr, uid, compose_id)
+ # Test: content_subtype is html
+ self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message content_subtype incorrect')
+
+ # 2. Post the comment, get created message for each group
+ mail_compose.send_mail(cr, uid, [compose_id],
+ context={'default_res_id': -1, 'active_ids': [self.group_pigs_id, group_bird_id]})
+ group_pigs.refresh()
+ group_bird.refresh()
+ message1 = group_pigs.message_ids[0]
+ message2 = group_bird.message_ids[0]
+ # Test: Pigs and Bird did receive their message
+ test_msg_ids = self.mail_message.search(cr, uid, [], limit=2)
+ self.assertIn(message1.id, test_msg_ids, 'Pigs did not receive its mass mailing message')
+ self.assertIn(message2.id, test_msg_ids, 'Bird did not receive its mass mailing message')
+ # Test: mail.message: subject, body
+ self.assertEqual(message1.subject, _subject, 'mail.message subject incorrect')
+ self.assertEqual(message1.body, group_pigs.description, 'mail.message body incorrect')
+ self.assertEqual(message2.subject, _subject, 'mail.message subject incorrect')
+ self.assertEqual(message2.body, group_bird.description, 'mail.message body incorrect')
+
+ def test_30_message_read(self):
+ """ Tests designed for message_read. """
+ # TDE NOTE: this test is not finished, as the message_read method is not fully specified.
+ # It will be updated as soon as we have fixed specs !
+ cr, uid = self.cr, self.uid
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ def _compare_structures(struct1, struct2, n=0):
+ # print '%scompare structure' % ('\t' * n)
+ self.assertEqual(len(struct1), len(struct2), 'message_read structure number of childs incorrect')
+ for x in range(len(struct1)):
+ # print '%s' % ('\t' * n), struct1[x]['id'], struct2[x]['id'], struct1[x].get('subject') or ''
+ self.assertEqual(struct1[x]['id'], struct2[x]['id'], 'message_read failure %s' % struct1[x].get('subject'))
+ _compare_structures(struct1[x]['child_ids'], struct2[x]['child_ids'], n + 1)
+ # print '%send compare' % ('\t' * n)
+
+ # ----------------------------------------
+ # CASE1: Flattening test
+ # ----------------------------------------
+
+ # Create dummy message structure
+ import copy
+ tree = [{'id': 2, 'child_ids': [
+ {'id': 6, 'child_ids': [
+ {'id': 8, 'child_ids': []},
+ ]},
+ ]},
+ {'id': 1, 'child_ids':[
+ {'id': 7, 'child_ids': [
+ {'id': 9, 'child_ids': []},
+ ]},
+ {'id': 4, 'child_ids': [
+ {'id': 10, 'child_ids': []},
+ {'id': 5, 'child_ids': []},
+ ]},
+ {'id': 3, 'child_ids': []},
+ ]},
+ ]
+ # Test: completely flat
+ new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 0)
+ self.assertEqual(len(new_tree), 10, 'message_read_tree_flatten wrong in flat')
+ # Test: 1 thread level
+ tree_test = [{'id': 2, 'child_ids': [
+ {'id': 8, 'child_ids': []}, {'id': 6, 'child_ids': []},
+ ]},
+ {'id': 1, 'child_ids': [
+ {'id': 10, 'child_ids': []}, {'id': 9, 'child_ids': []},
+ {'id': 7, 'child_ids': []}, {'id': 5, 'child_ids': []},
+ {'id': 4, 'child_ids': []}, {'id': 3, 'child_ids': []},
+ ]},
+ ]
+ new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 1)
+ _compare_structures(new_tree, tree_test)
+ # Test: 2 thread levels
+ new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 2)
+ _compare_structures(new_tree, tree)
+
+ # ----------------------------------------
+ # CASE2: message_read test
+ # ----------------------------------------
+
+ # 1. Add a few messages to pigs group
+ msgid1 = group_pigs.message_post(body='1', subject='1', parent_id=False)
+ msgid2 = group_pigs.message_post(body='2', subject='1-1', parent_id=msgid1)
+ msgid3 = group_pigs.message_post(body='3', subject='1-2', parent_id=msgid1)
+ msgid4 = group_pigs.message_post(body='4', subject='2', parent_id=False)
+ msgid5 = group_pigs.message_post(body='5', subject='1-1-1', parent_id=msgid2)
+ msgid6 = group_pigs.message_post(body='6', subject='2-1', parent_id=msgid4)
+
+ # Test: read all messages flat
+ tree_test = [{'id': msgid6, 'child_ids': []}, {'id': msgid5, 'child_ids': []},
+ {'id': msgid4, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
+ {'id': msgid2, 'child_ids': []}, {'id': msgid1, 'child_ids': []}]
+ tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=0, limit=10)
+ _compare_structures(tree, tree_test)
+ # Test: read with 1 level of thread
+ tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
+ {'id': msgid1, 'child_ids': [
+ {'id': msgid5, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
+ {'id': msgid2, 'child_ids': []},
+ ]},
+ ]
+ tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=1, limit=10)
+ _compare_structures(tree, tree_test)
+ # Test: read with 2 levels of thread
+ tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
+ {'id': msgid1, 'child_ids': [
+ {'id': msgid3, 'child_ids': []},
+ {'id': msgid2, 'child_ids': [{'id': msgid5, 'child_ids': []}, ]},
+ ]},
+ ]
+ tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=2, limit=10)
+ _compare_structures(tree, tree_test)
+
+ # 2. Test expandables
+ # TDE FIXME: add those tests when expandables are specified and implemented
+
+ def test_40_needaction(self):
+ """ Tests for mail.message needaction. """
+ cr, uid = self.cr, self.uid
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ user_admin = self.res_users.browse(cr, uid, uid)
+
+ # Demo values: check unread notification = needaction on mail.message
+ notif_ids = self.mail_notification.search(cr, uid, [
+ ('partner_id', '=', user_admin.partner_id.id),
+ ('read', '=', False)
+ ])
+ na_count = self.mail_message._needaction_count(cr, uid, domain=[])
+ self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
+
+ # Post 4 message on group_pigs
+ for dummy in range(4):
+ group_pigs.message_post(body='My Body')
+
+ # Check there are 4 new needaction on mail.message
+ notif_ids = self.mail_notification.search(cr, uid, [
+ ('partner_id', '=', user_admin.partner_id.id),
+ ('read', '=', False)
+ ])
+ na_count = self.mail_message._needaction_count(cr, uid, domain=[])
+ self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
+
+ # Check there are 4 needaction on mail.message with particular domain
+ na_count = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
+ self.assertEqual(na_count, 4, 'posted message count does not match needaction count')
+
+ def test_50_thread_parent_resolution(self):
+ """Verify parent/child relationships are correctly established when processing incoming mails"""
+ cr, uid = self.cr, self.uid
+ group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
+ msg1 = group_pigs.message_post(body='My Body', subject='1')
+ msg2 = group_pigs.message_post(body='My Body', subject='2')
+ msg1, msg2 = self.mail_message.browse(cr, uid, [msg1, msg2])
+ self.assertTrue(msg1.message_id, "New message should have a proper message_id")
+
+ # Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
+ # 1. In-Reply-To header
+ reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs , other@gmail.com', subject='Re: 1',
+ extra='In-Reply-To: %s' % msg1.message_id)
+ self.mail_thread.message_process(cr, uid, None, reply_msg)
+ # 2. References header
+ reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs , other@gmail.com', subject='Re: Re: 1',
+ extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
+ self.mail_thread.message_process(cr, uid, None, reply_msg2)
+ # 3. Subject contains [] + model passed to message+process -> only attached to group, not to mail
+ reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs , other@gmail.com',
+ extra='', subject='Re: [%s] 1' % self.group_pigs_id)
+ self.mail_thread.message_process(cr, uid, 'mail.group', reply_msg3)
+ group_pigs.refresh()
+ msg1.refresh()
+ self.assertEqual(5, len(group_pigs.message_ids), 'group should contain 5 messages')
+ self.assertEqual(2, len(msg1.child_ids), 'msg1 should have 2 children now')
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index fe87b7584e0..3153ccc2069 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -19,417 +19,289 @@
#
##############################################################################
-import ast
+import base64
import re
-
import tools
+
from osv import osv
from osv import fields
from tools.safe_eval import safe_eval as eval
from tools.translate import _
-from ..mail_message import to_email
-
# main mako-like expression pattern
EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
class mail_compose_message(osv.TransientModel):
- """Generic Email composition wizard. This wizard is meant to be inherited
- at model and view level to provide specific wizard features.
+ """ Generic message composition wizard. You may inherit from this wizard
+ at model and view levels to provide specific 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 to a previous message mode and pre-populated with the original
- quote. If set to 'comment', it means you are writing a new message to
- be attached to a document. 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.
- * 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'``.
+ The behavior of the wizard depends on the composition_mode field:
+ - 'reply': reply to a previous message. The wizard is pre-populated
+ via ``get_message_data``.
+ - 'comment': new post on a record. The wizard is pre-populated via
+ ``get_record_data``
+ - 'mass_mail': wizard in mass mailing mode where the mail details can
+ contain template placeholders that will be merged with actual data
+ before being sent to each recipient.
"""
_name = 'mail.compose.message'
- _inherit = 'mail.message.common'
+ _inherit = 'mail.message'
_description = 'Email composition wizard'
+ _log_access = True
def default_get(self, cr, uid, fields, context=None):
- """ Overridden to provide specific defaults depending on the context
- parameters.
-
- Composition mode
- - comment: default mode; active_model, active_id = model and ID of a
- document we are commenting,
- - reply: active_id = ID of a mail.message to which we are replying.
- From this message we can find the related model and res_id,
- - mass_mailing mode: active_model, active_id = model and ID of a
- document we are commenting,
-
- :param dict context: several context values will modify the behavior
- of the wizard, cfr. the class description.
+ """ Handle composition mode. Some details about context keys:
+ - comment: default mode, model and ID of a record the user comments
+ - default_model or active_model
+ - default_res_id or active_id
+ - reply: active_id of a message the user replies to
+ - default_parent_id or message_id or active_id: ID of the
+ mail.message we reply to
+ - message.res_model or default_model
+ - message.res_id or default_res_id
+ - mass_mail: model and IDs of records the user mass-mails
+ - active_ids: record IDs
+ - default_model or active_model
"""
if context is None:
context = {}
- compose_mode = context.get('mail.compose.message.mode', 'comment')
- active_model = context.get('active_model')
- active_id = context.get('active_id')
result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
+ # get some important values from context
+ composition_mode = context.get('default_composition_mode', context.get('mail.compose.message.mode'))
+ model = context.get('default_model', context.get('active_model'))
+ res_id = context.get('default_res_id', context.get('active_id'))
+ message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id')))
+ active_ids = context.get('active_ids')
+
# get default values according to the composition mode
- vals = {}
- if compose_mode in ['reply']:
- vals = self.get_message_data(cr, uid, int(context['active_id']), context=context)
- elif compose_mode in ['comment', 'mass_mail'] and active_model and active_id:
- vals = self.get_value(cr, uid, active_model, active_id, context)
+ if composition_mode == 'reply':
+ vals = self.get_message_data(cr, uid, message_id, context=context)
+ elif composition_mode == 'comment' and model and res_id:
+ vals = self.get_record_data(cr, uid, model, res_id, context=context)
+ elif composition_mode == 'mass_mail' and model and active_ids:
+ vals = {'model': model, 'res_id': res_id, 'content_subtype': 'html'}
+ else:
+ vals = {'model': model, 'res_id': res_id}
+ if composition_mode:
+ vals['composition_mode'] = composition_mode
+
for field in vals:
if field in fields:
result[field] = vals[field]
-
- # link to model and record if not done yet
- if not result.get('model') and active_model:
- result['model'] = active_model
- if not result.get('res_id') and active_id:
- result['res_id'] = active_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=context)
- result['email_from'] = current_user.email or False
return result
+ def _get_composition_mode_selection(self, cr, uid, context=None):
+ return [('comment', 'Comment a document'), ('reply', 'Reply to a message'), ('mass_mail', 'Mass mailing')]
+
_columns = {
- 'dest_partner_ids': fields.many2many('res.partner',
- 'email_message_send_partner_rel',
- 'wizard_id', 'partner_id', 'Destination partners',
- help="When sending emails through the social network composition wizard"\
- "you may choose to send a copy of the mail to partners."),
- '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"),
+ 'composition_mode': fields.selection(
+ lambda s, *a, **k: s._get_composition_mode_selection(*a, **k),
+ string='Composition mode'),
+ 'partner_ids': fields.many2many('res.partner',
+ 'mail_compose_message_res_partner_rel',
+ 'wizard_id', 'partner_id', 'Additional contacts'),
+ 'attachment_ids': fields.many2many('ir.attachment',
+ 'mail_compose_message_ir_attachments_rel',
+ 'wizard_id', 'attachment_id', 'Attachments'),
'filter_id': fields.many2one('ir.filters', 'Filters'),
+ 'body_text': fields.text('Plain-text Contents'),
+ 'content_subtype': fields.char('Message content subtype', size=32, readonly=1,
+ help="Type of message, usually 'html' or 'plain', used to select "\
+ "plain-text or rich-text contents accordingly"),
}
- def get_value(self, cr, uid, model, res_id, context=None):
+ _defaults = {
+ 'composition_mode': 'comment',
+ 'content_subtype': lambda self, cr, uid, ctx={}: 'plain',
+ 'body_text': lambda self, cr, uid, ctx={}: False,
+ 'body': lambda self, cr, uid, ctx={}: '',
+ 'subject': lambda self, cr, uid, ctx={}: False,
+ }
+
+ def notify(self, cr, uid, newid, context=None):
+ """ Override specific notify method of mail.message, because we do
+ not want that feature in the wizard. """
+ return
+
+ def get_record_data(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.
+ :param int res_id: id of the document record this mail is related to
"""
- result = {}
- user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- result.update({
- 'model': model,
- 'res_id': res_id,
- 'email_from': user.email or tools.config.get('email_from', False),
- 'body_html': False,
- 'body_text': False,
- 'subject': False,
- 'dest_partner_ids': [],
- })
- return result
-
- def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None):
- """ email_mode (values: True or False). This onchange on the email mode
- allows to have some specific behavior when going in email mode, or
- when going out of email mode.
- Basically, dest_partner_ids is reset when going out of email
- mode.
- This method can be overridden for models that want to have their
- specific behavior.
- Note that currently, this onchange is used in mail.js and called
- manually on the form instantiated in the Chatter.
- """
- if not value:
- return {'value': {'dest_partner_ids': []}}
- return {'value': {}}
-
- def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None):
- """ onchange_formatting (values: True or False). This onchange on the
- formatting allows to have some specific behavior when going in
- formatting mode, or when going out of formatting.
- Basically, subject is reset when going out of formatting mode.
- This method can be overridden for models that want to have their
- specific behavior.
- Note that currently, this onchange is used in mail.js and called
- manually on the form instantiated in the Chatter.
- """
- if not value:
- return {'value': {'subject': False}}
- return {'value': {}}
+ return {'model': model, 'res_id': res_id}
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). It should not be
- called unless ``context['mail.compose.message.mode'] == 'reply'``.
+ of the initial message, and the correct recipients).
: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.
"""
+ if not message_id:
+ return {}
if context is None:
context = {}
- result = {}
- if not message_id:
- return result
+ message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context)
- current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
- message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context)
- # Form the subject
- re_prefix = _("Re:")
+ # create subject
+ re_prefix = _('Re:')
reply_subject = tools.ustr(message_data.subject or '')
if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)):
reply_subject = "%s %s" % (re_prefix, reply_subject)
- # Form the bodies (text and html). We use the plain text version of the
- # original mail, by default, as it is easier to quote than the HTML
- # version. TODO: make it possible to switch to HTML on the fly
- 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'))}
- body_text = message_data.body_text or ''
- body_html = message_data.body_html or ''
- quoted_body_text = '> %s' % tools.ustr(body_text.replace('\n', "\n> ") or '')
- quoted_body_html = '
%s%s %s %s' % (sent_date, sender, quoted_body_html, current_user.signature)
- # form dest_partner_ids
- dest_partner_ids = [partner.id for partner in message_data.partner_ids]
- # Update header and references
- reply_headers = {}
- reply_references = message_data.references and tools.ustr(message_data.references) or False
- reply_message_id = message_data.message_id or False
- if reply_message_id:
- reply_references = (reply_references or '') + " " + message_data.message_id
- reply_headers['In-Reply-To'] = message_data.message_id
+ # create the reply in the body
+ reply_body = _('
On %(date)s, %(sender_name)s wrote:
%(body)s
') % {
+ 'date': message_data.date if message_data.date else '',
+ 'sender_name': message_data.author_id.name,
+ 'body': message_data.body,
+ }
+ # get partner_ids from original message
+ partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else []
+
# update the result
- result.update({
- 'body_text': reply_body_text,
- 'body_html': reply_body_html,
+ result = {
+ 'model': message_data.model,
+ 'res_id': message_data.res_id,
+ 'parent_id': message_data.id,
+ 'body': reply_body,
'subject': reply_subject,
- 'attachment_ids': [],
- 'dest_partner_ids': dest_partner_ids,
- 'model': message_data.model or False,
- 'res_id': message_data.res_id or False,
- 'email_from': current_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': reply_message_id,
- 'references': reply_references,
- 'headers': reply_headers,
- })
+ 'partner_ids': partner_ids,
+ 'content_subtype': 'html',
+ }
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.
+ def toggle_content_subtype(self, cr, uid, ids, context=None):
+ """ hit toggle formatting mode button: calls onchange_formatting to
+ emulate an on_change, then writes the value to update the form. """
+ for record in self.browse(cr, uid, ids, context=context):
+ content_st_new_value = 'plain' if record.content_subtype == 'html' else 'html'
+ onchange_res = self.onchange_content_subtype(cr, uid, ids, content_st_new_value, record.model, record.res_id, context=context)
+ self.write(cr, uid, [record.id], onchange_res['value'], context=context)
+ return True
- :param dict context: several context values will modify the behavior
- of the wizard, cfr. the class description.
- '''
+ def onchange_content_subtype(self, cr, uid, ids, value, model, res_id, context=None):
+ """ onchange_content_subtype (values: 'plain' or 'html'). This onchange
+ on the subtype allows to have some specific behavior when switching
+ between text or html mode.
+ This method can be overridden for models that want to have their
+ specific behavior. """
+ return {'value': {'content_subtype': value}}
+
+ def _verify_partner_email(self, cr, uid, partner_ids, context=None):
+ """ Verify that selected partner_ids have an email_address defined.
+ Otherwise throw a warning. """
+ partner_wo_email_lst = []
+ for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
+ if not partner.email:
+ partner_wo_email_lst.append(partner)
+ if not partner_wo_email_lst:
+ return {}
+ warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
+ for partner in partner_wo_email_lst:
+ warning_msg += '\n- %s' % (partner.name)
+ return {'warning': {
+ 'title': _('Partners email addresses not found'),
+ 'message': warning_msg,
+ }
+ }
+
+ def onchange_partner_ids(self, cr, uid, ids, value, context=None):
+ """ onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The
+ basic purpose of this method is to check that destination partners
+ effectively have email addresses. Otherwise a warning is thrown.
+ """
+ res = {'value': {}}
+ if not value or not value[0] or not value[0][0] == 6:
+ return
+ res.update(self._verify_partner_email(cr, uid, value[0][2], context=context))
+ return res
+
+ def unlink(self, cr, uid, ids, context=None):
+ # Cascade delete all attachments, as they are owned by the composition wizard
+ for wizard in self.read(cr, uid, ids, ['attachment_ids'], context=context):
+ self.pool.get('ir.attachment').unlink(cr, uid, wizard['attachment_ids'], context=context)
+ return super(mail_compose_message, self).unlink(cr, uid, ids, context=context)
+
+ def dummy(self, cr, uid, ids, context=None):
+ """ TDE: defined to have buttons that do basically nothing. It is
+ currently impossible to have buttons that do nothing special
+ in views (if type not specified, considered as 'object'). """
+ return True
+
+ #------------------------------------------------------
+ # Wizard validation and send
+ #------------------------------------------------------
+
+ def send_mail(self, cr, uid, ids, context=None):
+ """ Process the wizard content and proceed with sending the related
+ email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
- # composition wizard options
- email_mode = context.get('email_mode')
- formatting = context.get('formatting')
- mass_mail_mode = context.get('mail.compose.message.mode') == 'mass_mail'
+ active_ids = context.get('active_ids')
- mail_message_obj = self.pool.get('mail.message')
- for mail_wiz in self.browse(cr, uid, ids, context=context):
- # attachments
- attachment = {}
- for attach in mail_wiz.attachment_ids:
- attachment[attach.datas_fname] = attach.datas and attach.datas.decode('base64')
+ for wizard in self.browse(cr, uid, ids, context=context):
+ mass_mail_mode = wizard.composition_mode == 'mass_mail'
+ active_model_pool = self.pool.get(wizard.model if wizard.model else 'mail.thread')
- # default values, according to the wizard options
- subject = mail_wiz.subject if formatting else False
- content_subtype = 'html' if formatting else 'plain'
- type = 'email' if email_mode else 'comment'
- state = 'outgoing' if email_mode else False
- partner_ids = [partner.id for partner in mail_wiz.dest_partner_ids]
- references = None
- headers = {}
- body = mail_wiz.body_html if content_subtype == 'html' else mail_wiz.body_text
-
- # get model, active_ids, and check if model is openchatter-enabled
- if mass_mail_mode and context.get('active_ids') and context.get('active_model'):
- active_ids = context['active_ids']
- active_model = context['active_model']
- elif mass_mail_mode:
- active_model = mail_wiz.model
- active_model_pool = self.pool.get(active_model)
- active_ids = active_model_pool.search(cr, uid, ast.literal_eval(mail_wiz.filter_id.domain), context=ast.literal_eval(mail_wiz.filter_id.context))
- else:
- active_model = mail_wiz.model
- active_ids = [mail_wiz.res_id]
- active_model_pool = self.pool.get(active_model)
- mail_thread_enabled = hasattr(active_model_pool, 'message_append')
-
- if context.get('mail.compose.message.mode') == 'mass_mail':
- # Mass mailing: must render the template patterns
- for active_id in active_ids:
- rendered_subject = self.render_template(cr, uid, subject, active_model, active_id)
- rendered_body_html = self.render_template(cr, uid, mail_wiz.body_html, active_model, active_id)
- rendered_body_text = self.render_template(cr, uid, mail_wiz.body_text, active_model, active_id)
- email_from = self.render_template(cr, uid, mail_wiz.email_from, active_model, active_id)
- email_to = self.render_template(cr, uid, mail_wiz.email_to, active_model, active_id)
- email_cc = self.render_template(cr, uid, mail_wiz.email_cc, active_model, active_id)
- email_bcc = self.render_template(cr, uid, mail_wiz.email_bcc, active_model, active_id)
- reply_to = self.render_template(cr, uid, mail_wiz.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.
- if mail_thread_enabled:
- active_model_pool.message_append(cr, uid, [active_id], rendered_subject, rendered_body_text, rendered_body_html,
- type=type, content_subtype=content_subtype, state=state, partner_ids=partner_ids,
- email_from=email_from, email_to=email_to, email_cc=email_cc, email_bcc=email_bcc,
- reply_to=reply_to, references=references, attachments=attachment, headers=headers, context=context)
- else:
- mail_message_obj.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body_text,
- model=mail_wiz.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
- attachments=attachment, references=references, res_id=active_id, partner_ids=partner_ids,
- content_subtype=mail_wiz.content_subtype, headers=headers, context=context)
- else:
- # normal mode - no mass-mailing
- if mail_thread_enabled:
- msg_ids = active_model_pool.message_append(cr, uid, active_ids, subject, mail_wiz.body_text, mail_wiz.body_html,
- type=type, content_subtype=content_subtype, state=state, partner_ids=partner_ids,
- email_from=mail_wiz.email_from, email_to=mail_wiz.email_to, email_cc=mail_wiz.email_cc, email_bcc=mail_wiz.email_bcc,
- reply_to=mail_wiz.reply_to, references=references, attachments=attachment, headers=headers, context=context)
- else:
- msg_ids = [mail_message_obj.schedule_with_attach(cr, uid, mail_wiz.email_from, to_email(mail_wiz.email_to), subject, mail_wiz.body_text,
- type=type, model=mail_wiz.model, email_cc=to_email(mail_wiz.email_cc), email_bcc=to_email(mail_wiz.email_bcc), reply_to=mail_wiz.reply_to,
- attachments=attachment, references=references, res_id=int(mail_wiz.res_id), partner_ids=partner_ids,
- content_subtype=mail_wiz.content_subtype, headers=headers, context=context)]
- # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small)
- if type == 'email':
- mail_message_obj.send(cr, uid, msg_ids, context=context)
+ # wizard works in batch mode: [res_id] or active_ids
+ res_ids = active_ids if mass_mail_mode and wizard.model and active_ids else [wizard.res_id]
+ for res_id in res_ids:
+ # default values, according to the wizard options
+ post_values = {
+ 'subject': wizard.subject if wizard.content_subtype == 'html' else False,
+ 'body': wizard.body if wizard.content_subtype == 'html' else '
%s
' % tools.ustr(wizard.body_text),
+ 'partner_ids': [(4, partner.id) for partner in wizard.partner_ids],
+ 'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],
+ }
+ # mass mailing: render and override default values
+ if mass_mail_mode and wizard.model:
+ email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
+ new_partner_ids = email_dict.pop('partner_ids', [])
+ post_values['partner_ids'] += [(4, partner_id) for partner_id in new_partner_ids]
+ new_attachments = email_dict.pop('attachments', [])
+ post_values['attachments'] += new_attachments
+ post_values.update(email_dict)
+ # post the message
+ active_model_pool.message_post(cr, uid, [res_id], type='comment', context=context, **post_values)
return {'type': 'ir.actions.act_window_close'}
+ def render_message(self, cr, uid, wizard, res_id, context=None):
+ """ Generate an email from the template for given (wizard.model, res_id)
+ pair. This method is meant to be inherited by email_template that
+ will produce a more complete dictionary. """
+ return {
+ 'subject': self.render_template(cr, uid, wizard.subject, wizard.model, res_id, context),
+ 'body': self.render_template(cr, uid, wizard.body, wizard.model, res_id, context),
+ }
+
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:
+ """ 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.
+ :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)
+ 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
+ })
+ return result and tools.ustr(result) or ''
return template and EXPRESSION_PATTERN.sub(merge, template)
-
-
-class mail_compose_message_extended(osv.TransientModel):
- """ Extension of 'mail.compose.message' to support default field values related
- to CRM-like models that follow the following conventions:
-
- 1. The model object must have an attribute '_mail_compose_message' equal to True.
-
- 2. The model should define the following fields:
- - 'name' as subject of the message (required);
- - 'email_from' as destination email address (required);
- - 'email_cc' as cc email addresses (required);
- - 'section_id.reply_to' as reply-to address (optional).
- """
- _inherit = 'mail.compose.message'
-
- def get_value(self, cr, uid, model, res_id, context=None):
- """ Overrides the default implementation to provide more default field values
- related to the corresponding CRM case.
- """
- result = super(mail_compose_message_extended, self).get_value(cr, uid, model, res_id, context=context)
- model_obj = self.pool.get(model)
- if getattr(model_obj, '_mail_compose_message', False) and res_id:
- data = model_obj.browse(cr, uid , res_id, context)
- result.update({
- 'email_to': data.email_from or False,
- 'email_cc': tools.ustr(data.email_cc or ''),
- 'subject': data.name or False,
- })
- if hasattr(data, 'section_id'):
- result['reply_to'] = data.section_id and data.section_id.reply_to or False
- return result
-
- def onchange_email_mode(self, cr, uid, ids, value, model, res_id, context=None):
- """ Overrides the default implementation to provide default values for
- dest_partner_ids. This method checks that a partner maching the
- ``email_from`` of the record exists. It it does not exist, it
- creates a new partner. The found or created partner is then added
- in dest_partner_ids.
- Partner check/creation valid inly if the value is True, and if
- the model has the ``_mail_compose_message`` attribute.
- """
- result = super(mail_compose_message_extended, self).onchange_email_mode(cr, uid, ids, value, model, res_id, context=context)
- model_obj = self.pool.get(model)
- if not value or not (getattr(model_obj, '_mail_compose_message', False) and res_id):
- return result
- data = model_obj.browse(cr, uid , res_id, context=context)
- partner_obj = self.pool.get('res.partner')
- partner_ids = partner_obj.search(cr, uid, [('email', '=', data.email_from)], context=context)
- if partner_ids:
- partner_id = partner_ids[0]
- else:
- partner_id = partner_obj.name_create(cr, uid, data.email_from, context=context)[0]
- result['value'].update({
- 'dest_partner_ids': [partner_id],
- 'email_cc': tools.ustr(data.email_cc or ''),
- })
- if hasattr(data, 'section_id'):
- result['value']['reply_to'] = data.section_id and data.section_id.reply_to or False
- return result
-
- def onchange_formatting(self, cr, uid, ids, value, model, res_id, context=None):
- """ Overrides the default implementation to provide default values for
- the subject.
- Subject re-creation valid only if the value is True, and if the
- model has the ``_mail_compose_message`` attribute.
- """
- result = super(mail_compose_message_extended, self).onchange_formatting(cr, uid, ids, value, model, res_id, context=context)
- model_obj = self.pool.get(model)
- if not value or not (getattr(model_obj, '_mail_compose_message', False) and res_id):
- return result
- data = model_obj.browse(cr, uid , res_id, context=context)
- result['value'].update({
- 'subject': data.name or False,
- })
- return result
-
-# 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
index 73fb03cdc28..70b50c154ac 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -6,73 +6,87 @@
mail.compose.message
@@ -97,6 +111,6 @@
target="new"
key2="client_action_multi"
id="base.action_partner_mass_mail"
- context="{'mail.compose.message.mode':'mass_mail'}"/>
+ context="{'default_composition_mode': 'mass_mail'}"/>
diff --git a/addons/marketing_campaign/marketing_campaign_demo.xml b/addons/marketing_campaign/marketing_campaign_demo.xml
index 54897cd111a..92443fe987d 100644
--- a/addons/marketing_campaign/marketing_campaign_demo.xml
+++ b/addons/marketing_campaign/marketing_campaign_demo.xml
@@ -3,29 +3,29 @@
- Template for New Partner
+ welcome new partnerinfo@openerp.com
- Welcome in OpenERP Partner Channel!
+ Welcome to the OpenERP Partner Channel!${object.email or ''}
- Hello, We are very happy to send Welcome message.
+ Hello, you will receive your welcome pack via email shortly.
- Template for Silver Partner
+ congrats silver partnerinfo@openerp.com
- Congratulation! You become now our Silver Partner.
+ Congratulations! You are now a Silver Partner!${object.email or ''}
- Hello, We are happy to announce that you now become our Silver Partner.
+ Hi, we are delighted to welcome you among our Silver Partners as of today!
- Template for Gold Partner
+ congrats gold partnerinfo@openerp.com
- Congratulation! You become our Gold Partner.
+ Congratulations! You are now one of our Gold Partners!${object.email or ''}
- Hello, We are happy to announce that you become our Gold Partner.
+ Hi, we are delighted to let you know that you have entered the select circle of our Gold Partners
@@ -64,7 +64,7 @@
-
+
diff --git a/addons/marketing_campaign/test/marketing_campaign.yml b/addons/marketing_campaign/test/marketing_campaign.yml
index 04063a6fbc1..3d540b4ccb7 100644
--- a/addons/marketing_campaign/test/marketing_campaign.yml
+++ b/addons/marketing_campaign/test/marketing_campaign.yml
@@ -42,7 +42,7 @@
record = self.browse(cr, uid, ids[0])
assert record.state == 'todo' or record.state == 'done' , 'Marketing Workitem shoud be in draft state.'
-
- I check follow-up detail of first activity.
+ I process follow-up of first activity.
-
!python {model: marketing.campaign.workitem}: |
ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
@@ -50,12 +50,6 @@
assert ids, 'Follow-up item is not created for first activity.'
work_item_id = self.browse(cr ,uid ,ids[0] ,context)
assert work_item_id.res_name, 'Resource Name is not defined.'
--
- I process follow-up of first activity.
--
- !python {model: marketing.campaign.workitem}: |
- ids = self.search(cr, uid, [('segment_id', '=', ref('marketing_campaign_segment0')),
- ('campaign_id', '=', ref('marketing_campaign_openerppartnerchannel')), ('activity_id', '=', ref('marketing_campaign_activity_0'))])
self.process(cr, uid, ids)
record = self.browse(cr, uid, ids)[0]
assert record.state == "done", "Follow-up item should be closed after process."
diff --git a/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml b/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
index 0946ff6e129..141981a21d6 100644
--- a/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
+++ b/addons/marketing_campaign_crm_demo/marketing_campaign_demo.xml
@@ -40,7 +40,10 @@
- Hello,Thanks for generous interest you have shown in the openERP.Regards,OpenERP Team,
+ Hello,
+
Thanks for the genuine interest you have shown in OpenERP.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>For OpenERP OnDemand Free Trial 2010
@@ -50,11 +53,11 @@
- Hello,We have very good offer that might suit you.
- We propose you to subscribe to the OpenERP Discovery Day on May 2010.
- If any further information required kindly revert back.
- We really appreciate your co-operation on this.
- Regards,OpenERP Team,
+ Hello,
+
We have very good offer that might suit you.
+ We suggest you subscribe to the OpenERP Discovery Day on May 2010.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>For OpenERP Discovery Day on May 2010
@@ -65,10 +68,10 @@
- Hello,Thanks for showing intrest and for subscribing to the OpenERP Discovery Day.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
+ Hello,
+
Thanks for showing interest and for subscribing to the OpenERP Discovery Day.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>For OpenERP Discovery Day
@@ -79,10 +82,10 @@
- Hello,Thanks for showing intrest and buying the OpenERP book.
+ Hello,
+
Thanks for showing interest and buying the OpenERP book.
If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
+
Regards,OpenERP Team,
]]>For OpenERP book
@@ -92,12 +95,12 @@
- Hello, We have very good offer that might suit you.
- For our gold partners,We are arranging free technical training on june,2010.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For technical training to Gold partners
+ Hello,
+
We have very good offer that might suit you.
+ For our gold partners,We are arranging free technical training on june,2010.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ technical training to gold partners
@@ -107,12 +110,12 @@
- Hello, We have very good offer that might suit you.
- For our silver partners,We are paid technical training on june,2010.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For training to Silver partners
+ Hello,
+
We have very good offer that might suit you.
+ For our silver partners,We are paid technical training on june,2010.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ training to silver partners
@@ -122,12 +125,12 @@
- Hello, We have very good offer that might suit you.
- For our silver partners,We are offering Gold partnership.
- If any further information required kindly revert back.
- I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For gold partnership to silver partners
+ Hello,
+
We have very good offer that might suit you.
+ For our silver partners, we are offering Gold partnership.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ gold partnership to silver partners
@@ -137,9 +140,12 @@
- Hello, Thanks for showing intrest and for subscribing to technical training.If any further information required kindly revert back.I really appreciate your co-operation on this.
- Regards,OpenERP Team,
- For subscribing to technical training
+ Hello,
+
Thanks for showing interest and for subscribing to technical training.
+ If any further information required kindly revert back.I really appreciate your co-operation on this.
+
If any further information is required, do not hesitate to reply to this message.
+
Regards,OpenERP Team,
]]>
+ subscribing to technical training
diff --git a/addons/mrp/board_manufacturing_view.xml b/addons/mrp/board_manufacturing_view.xml
index e7668517a70..cae1fea07dc 100644
--- a/addons/mrp/board_manufacturing_view.xml
+++ b/addons/mrp/board_manufacturing_view.xml
@@ -9,7 +9,6 @@
-
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index 948c204ed6a..fcb420a475e 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -368,7 +368,7 @@ class mrp_bom(osv.osv):
prod_obj = self.pool.get('product.product')
for obj in self.browse(cr, uid, ids, context=context):
for prod in prod_obj.browse(cr, uid, [obj.product_id], context=context):
- self.message_append_note(cr, uid, [obj.id], body=_("Bill of Material has been created for %s product.") % (prod.id.name_template), context=context)
+ self.message_post(cr, uid, [obj.id], body=_("Bill of Material has been created for %s product.") % (prod.id.name_template), context=context)
return True
mrp_bom()
@@ -386,7 +386,7 @@ class mrp_production(osv.osv):
_name = 'mrp.production'
_description = 'Manufacturing Order'
_date_name = 'date_planned'
- _inherit = ['ir.needaction_mixin', 'mail.thread']
+ _inherit = ['mail.thread', 'ir.needaction_mixin']
def _production_calc(self, cr, uid, ids, prop, unknow_none, context=None):
""" Calculates total hours and total no. of cycles for a production order.
@@ -1025,33 +1025,28 @@ class mrp_production(osv.osv):
# OpenChatter methods and notifications
# ---------------------------------------------------
- def message_get_monitored_follower_fields(self, cr, uid, ids, context=None):
- """ Add 'user_id' to the monitored fields """
- res = super(mrp_production, self).message_get_monitored_follower_fields(cr, uid, ids, context=context)
- return res + ['user_id']
-
def create_send_note(self, cr, uid, ids, context=None):
- self.message_append_note(cr, uid, ids, body=_("Manufacturing order has been created."), context=context)
+ self.message_post(cr, uid, ids, body=_("Manufacturing order has been created."), context=context)
return True
def action_cancel_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order has been canceled.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_ready_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order is ready to produce.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_in_production_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order is in production.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_done_send_note(self, cr, uid, ids, context=None):
message = _("Manufacturing order has been done.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def action_confirm_send_note(self, cr, uid, ids, context=None):
@@ -1061,7 +1056,7 @@ class mrp_production(osv.osv):
obj_datetime = fields.DT.datetime.strptime(obj.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
obj_date_str = fields.datetime.context_timestamp(cr, uid, obj_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
message = _("Manufacturing order has been confirmed and is scheduled for the %s.") % (obj_date_str)
- self.message_append_note(cr, uid, [obj.id], body=message, context=context)
+ self.message_post(cr, uid, [obj.id], body=message, context=context)
return True
diff --git a/addons/mrp/mrp_data.xml b/addons/mrp/mrp_data.xml
index e5d5380f96a..0bbf2d3b64c 100644
--- a/addons/mrp/mrp_data.xml
+++ b/addons/mrp/mrp_data.xml
@@ -2,14 +2,16 @@
-
-
-
- Module MRP has been installed
- Manage your manufacturing process in OpenERP by defining bill of materials (BoM), routings and work centers. This module supports complete integration and planification of stockable goods, consumable, and services.
+
+ mail.group
+
+ notification
+ MRP application installed!
+ Manage your manufacturing process with OpenERP by defining your bills of materials (BoM), routings and work centers.
+This application supports complete integration and production scheduling for stockable goods, consumables, and services.
-From the Manufacturing Settings, you can choose to compute production schedules periodically or just-in-time.
-
+From the Manufacturing Settings, you can choose to compute production schedules periodically or just-in-time.
+
Production order
diff --git a/addons/mrp/mrp_demo.xml b/addons/mrp/mrp_demo.xml
index d0f05a2aaff..952a5e0e3de 100644
--- a/addons/mrp/mrp_demo.xml
+++ b/addons/mrp/mrp_demo.xml
@@ -49,7 +49,7 @@
0.05
-
+
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index 8811fcf5611..6d2a486514c 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -552,8 +552,8 @@
mrp.production.treemrp.production
-
-
+
+
diff --git a/addons/mrp/procurement.py b/addons/mrp/procurement.py
index 363f98e35d1..7f417dc18b1 100644
--- a/addons/mrp/procurement.py
+++ b/addons/mrp/procurement.py
@@ -43,7 +43,7 @@ class procurement_order(osv.osv):
cr.execute('update procurement_order set message=%s where id=%s', (_('No BoM defined for this product !'), procurement.id))
for (id, name) in self.name_get(cr, uid, procurement.id):
message = _("Procurement '%s' has an exception: 'No BoM defined for this product !'") % name
- self.message_append_note(cr, uid, [procurement.id], body=message, context=context)
+ self.message_post(cr, uid, [procurement.id], body=message, context=context)
return False
return True
diff --git a/addons/mrp_operations/mrp_operations.py b/addons/mrp_operations/mrp_operations.py
index 8692dab5c87..ee2547bc7df 100644
--- a/addons/mrp_operations/mrp_operations.py
+++ b/addons/mrp_operations/mrp_operations.py
@@ -224,7 +224,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been created for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_start_send_note(self, cr, uid, ids, context=None):
@@ -232,7 +232,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been started for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_done_send_note(self, cr, uid, ids, context=None):
@@ -240,7 +240,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been done for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_pending_send_note(self, cr, uid, ids, context=None):
@@ -248,7 +248,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order is pending for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
def action_cancel_send_note(self, cr, uid, ids, context=None):
@@ -256,7 +256,7 @@ class mrp_production_workcenter_line(osv.osv):
for workorder in self.browse(cr, uid, ids):
for prod in prod_obj.browse(cr, uid, [workorder.production_id]):
message = _("Work order has been cancelled for production order %s.") % (prod.id.name)
- self.message_append_note(cr, uid, [workorder.id], body=message, context=context)
+ self.message_post(cr, uid, [workorder.id], body=message, context=context)
return True
mrp_production_workcenter_line()
diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py
index f41abb58c08..c1ac8f809ce 100644
--- a/addons/mrp_repair/mrp_repair.py
+++ b/addons/mrp_repair/mrp_repair.py
@@ -571,40 +571,40 @@ class mrp_repair(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _("Repair Order for %s has been created." % (repair.product_id.name))
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_start_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _("Repair Order for %s has been started." % (repair.product_id.name))
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_toinvoiced_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _("Draft Invoice of %s %s waiting for validation.") % (repair.invoice_id.amount_total, repair.invoice_id.currency_id.symbol)
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_confirm_send_note(self, cr, uid, ids, context=None):
for repair in self.browse(cr, uid, ids, context):
message = _( "Repair Order for %s has been accepted." % (repair.product_id.name))
- self.message_append_note(cr, uid, [repair.id], body=message, context=context)
+ self.message_post(cr, uid, [repair.id], body=message, context=context)
return True
def set_cancel_send_note(self, cr, uid, ids, context=None):
message = _("Repair has been cancelled.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_ready_send_note(self, cr, uid, ids, context=None):
message = _("Repair Order is now ready to repair.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_done_send_note(self, cr, uid, ids, context=None):
message = _("Repair Order is closed.")
- self.message_append_note(cr, uid, ids, body=message, context=context)
+ self.message_post(cr, uid, ids, body=message, context=context)
return True
mrp_repair()
diff --git a/addons/note/note.py b/addons/note/note.py
index bce54566e64..abc7dca297e 100644
--- a/addons/note/note.py
+++ b/addons/note/note.py
@@ -64,11 +64,15 @@ class note_note(osv.osv):
stage_obj = self.pool.get('note.stage')
# only show stage groups not folded and owned by user
- search_domain = [('fold', '=', False),('user_id', '=', uid)]
+ search_domain = [('user_id', '=', uid)]
stage_ids = stage_obj._search(cr, uid, search_domain, order=self._order, access_rights_uid=access_rights_uid, context=context)
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
- return result
+
+ fold = {}
+ for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
+ fold[stage.id] = stage.fold or False
+ return result, fold
_columns = {
'name': fields.function(_get_note_first_line, fnct_inv=_set_note_first_line, string='Note Summary', type="text"),
diff --git a/addons/note/note_demo.xml b/addons/note/note_demo.xml
index e322f362ef5..e6ab25198a6 100644
--- a/addons/note/note_demo.xml
+++ b/addons/note/note_demo.xml
@@ -58,7 +58,7 @@
@@ -93,7 +93,7 @@
@@ -104,7 +104,7 @@
@@ -115,7 +115,7 @@
@@ -127,7 +127,7 @@
@@ -139,7 +139,7 @@
diff --git a/addons/pad_project/i18n/tr.po b/addons/pad_project/i18n/tr.po
new file mode 100644
index 00000000000..a19889de838
--- /dev/null
+++ b/addons/pad_project/i18n/tr.po
@@ -0,0 +1,38 @@
+# Turkish translation for openobject-addons
+# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012
+# This file is distributed under the same license as the openobject-addons package.
+# FIRST AUTHOR , 2012.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: openobject-addons\n"
+"Report-Msgid-Bugs-To: FULL NAME \n"
+"POT-Creation-Date: 2012-02-08 00:36+0000\n"
+"PO-Revision-Date: 2012-09-07 20:06+0000\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: Turkish
\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Launchpad-Export-Date: 2012-09-08 04:54+0000\n"
+"X-Generator: Launchpad (build 15914)\n"
+
+#. module: pad_project
+#: constraint:project.task:0
+msgid "Error ! Task end-date must be greater then task start-date"
+msgstr "Hata! görev bitiş tarihi başlangıç tarihinden sonra olmalı"
+
+#. module: pad_project
+#: model:ir.model,name:pad_project.model_project_task
+msgid "Task"
+msgstr "Görev"
+
+#. module: pad_project
+#: view:project.task:0
+msgid "Pad"
+msgstr ""
+
+#. module: pad_project
+#: constraint:project.task:0
+msgid "Error ! You cannot create recursive tasks."
+msgstr "Hata ! Yinelenen görevler oluşturamazsınız."
diff --git a/addons/plugin/plugin_handler.py b/addons/plugin/plugin_handler.py
index e497f902dc2..9c582a8e3bf 100644
--- a/addons/plugin/plugin_handler.py
+++ b/addons/plugin/plugin_handler.py
@@ -44,7 +44,7 @@ class plugin_handler(osv.osv_memory):
res_id = 0
url = ""
name = ""
- msg = mail_message_obj.parse_message(email)
+ msg = self.pool.get('mail.thread').parse_message(cr, uid, email)
references = [msg.get('message-id')]
refs = msg.get('references',False)
if refs:
@@ -91,7 +91,7 @@ class plugin_handler(osv.osv_memory):
"""
mail_message = self.pool.get('mail.message')
model_obj = self.pool.get(model)
- msg = mail_message.parse_message(email)
+ msg = self.pool.get('mail.thread').parse_message(cr, uid, email)
message_id = msg.get('message-id')
mail_ids = mail_message.search(cr, uid, [('message_id','=',message_id),('res_id','=',res_id),('model','=',model)])
@@ -101,16 +101,15 @@ class plugin_handler(osv.osv_memory):
notify = "Email already pushed"
elif res_id == 0:
if model == 'res.partner':
- notify = 'User the button Partner to create a new partner'
+ notify = 'User the Partner button to create a new partner'
else:
res_id = model_obj.message_new(cr, uid, msg)
- notify = "Mail succefully pushed, a new %s has been created " % model
+ notify = "Mail succesfully pushed, a new %s has been created " % model
else:
if model == 'res.partner':
model_obj = self.pool.get('mail.thread')
- res = self.pool.get(model).browse(cr, uid, [res_id])
- model_obj.message_append_dict(cr, uid, res, msg)
- notify = "Mail succefully pushed"
+ model_obj.message_post(cr, uid, [res_id], body=msg)
+ notify = "Mail succesfully pushed"
url = self._make_url(cr, uid, res_id, model)
return (model, res_id, url, notify)
@@ -133,7 +132,7 @@ class plugin_handler(osv.osv_memory):
return ('res.partner', partner_id, url)
# Specific to outlook rfc822 is not available so we split in arguments headerd,body,attachemnts
- def push_message_outlook(self, cr, uid, model, headers,res_id=0 ,body_text=False, body_html=False, attachments=False):
+ def push_message_outlook(self, cr, uid, model, headers,res_id=0 ,body=False, body_html=False, attachments=False):
# ----------------------------------------
# solution 1
# construct a fake rfc822 from the separated arguement
@@ -147,7 +146,7 @@ class plugin_handler(osv.osv_memory):
mail_message = self.pool.get('mail.message')
ir_attachment_obj = self.pool.get('ir.attachment')
attach_ids = []
- msg = mail_message.parse_message(headers)
+ msg = self.pool.get('mail.thread').parse_message(cr, uid, headers)
message_id = msg.get('message-id')
push_mail = self.push_message(cr, uid, model, headers, res_id)
res_id = push_mail[1]
@@ -161,6 +160,6 @@ class plugin_handler(osv.osv_memory):
attach_ids.append(ir_attachment_obj.create(cr, uid, vals))
mail_ids = mail_message.search(cr, uid, [('message_id','=',message_id),('res_id','=',res_id),('model','=',model)])
if mail_ids:
- ids = mail_message.write(cr, uid,mail_ids[0],{ 'attachment_ids': [(6, 0, attach_ids)],'body_text':body_text,'body_html':body_html})
+ ids = mail_message.write(cr, uid,mail_ids[0],{ 'attachment_ids': [(6, 0, attach_ids)],'body':body,'body_html':body_html})
url = self._make_url(cr, uid, res_id, model)
return (model, res_id, url)
diff --git a/addons/plugin_outlook/plugin_outlook.xml b/addons/plugin_outlook/plugin_outlook.xml
index c2c035f532d..e99fbf72b6d 100644
--- a/addons/plugin_outlook/plugin_outlook.xml
+++ b/addons/plugin_outlook/plugin_outlook.xml
@@ -19,7 +19,7 @@
Click on the link above to download the installer for either 32 or 64 bits, and execute it.
2. From the Thunderbird menubar: Tools > Add-ons -> Screwdriver/Wrench Icon -> Install add-on from file...
3. Select the plug-in (the file named openerp_plugin.xpi).
diff --git a/addons/point_of_sale/point_of_sale_data.xml b/addons/point_of_sale/point_of_sale_data.xml
index dda0b78e952..35a2516da02 100644
--- a/addons/point_of_sale/point_of_sale_data.xml
+++ b/addons/point_of_sale/point_of_sale_data.xml
@@ -24,14 +24,15 @@
-
-
-
- Module Point of Sale has been installed
- Encode sale orders, register payments, compute money to return, create invoices, and manage refunds of former sales through a specific, web-based, touch-screen user interface.
+
+ mail.group
+
+ notification
+ Point of Sale application installed!
+ Record sale orders, register payments, compute change to return, create invoices, and manage refunds through a specific web touch-screen interface.
-If you install the PoS proxy, you will be able to interface OpenERP with retail materials; barcode scanners, printers, cash registers, weighing machine, credit card payments.
-
+If you install the PoS proxy you will be able to interface OpenERP with retail hardware: barcode scanners, printers, cash registers, weighing machines, credit card payment terminals.
+
1.00
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index bfe2ef7f9dc..9ac7d4405d0 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -20,6 +20,12 @@
user-select: none;
}
+.point-of-sale ul, .point-of-sale li {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
.point-of-sale .shadow-top{
position: absolute;
top:0;
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index ab5f99a149e..bbe278758d5 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -4,7 +4,7 @@
-
+
diff --git a/addons/portal/portal_demo.xml b/addons/portal/portal_demo.xml
index a07709cd0d5..1ace02f3d7d 100644
--- a/addons/portal/portal_demo.xml
+++ b/addons/portal/portal_demo.xml
@@ -22,8 +22,7 @@
Our first company's blogpost !mail.group
- html
- sit amet, consectetur adipiscing elit. Pellentesque et quam sapien, in sagittis tellus.
Praesent vel massa sed massa consequat egestas in tristique orci. Praesent iaculis libero et neque vehicula iaculis. Vivamus placerat tincidunt orci ac ornare. Proin ut dolor fringilla velit ultricies consequat. Maecenas sit amet ipsum non leo interdum imperdiet. Donec sapien mi.
@@ -32,27 +31,25 @@ Fusce tempus elit volutpat mi auctor adipiscing. Nam congue luctus suscipit. Sed
Nulla turpis leo, rhoncus ut egestas sit amet, consectetur vitae urna. Mauris in dolor in sapien tempus vehicula.]]>comment
-
+ mail.group
- html
-
+ comment
-
+ mail.group
- html
-
+ comment
-
+
diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py
index 0260ce338fc..85f6566eba6 100644
--- a/addons/portal/wizard/portal_wizard.py
+++ b/addons/portal/wizard/portal_wizard.py
@@ -171,7 +171,7 @@ class wizard(osv.osv_memory):
'url': wiz.portal_id.url or _("(missing url)"),
'db': cr.dbname,
}
- mail_message_obj = self.pool.get('mail.message')
+ mail_mail_obj = self.pool.get('mail.mail')
dest_uids = user_obj.search(cr, ROOT_UID, login_cond)
dest_users = user_obj.browse(cr, ROOT_UID, dest_uids)
for dest_user in dest_users:
@@ -184,10 +184,12 @@ class wizard(osv.osv_memory):
email_to = dest_user.email
subject = _(WELCOME_EMAIL_SUBJECT) % data
body = _(WELCOME_EMAIL_BODY) % data
- res = mail_message_obj.schedule_with_attach(cr, uid, email_from , [email_to], subject, body, context=context)
- if not res:
- _logger.warning(
- 'Failed to send email from %s to %s', email_from, email_to)
+ mail_id = mail_mail_obj.create(cr, uid, {
+ 'email_from': email_from ,
+ 'email_to': email_to,
+ 'subject': subject,
+ 'state': 'outgoing',
+ 'body_html': '