diff --git a/addons/account/account.py b/addons/account/account.py index 3537c4f4c55..b87caf547ab 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -840,16 +840,11 @@ class account_journal(osv.osv): def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100): if not args: args = [] - if context is None: - context = {} - ids = [] - if context.get('journal_type', False): - args += [('type','=',context.get('journal_type'))] - if name: - ids = self.search(cr, user, [('code', 'ilike', name)]+ args, limit=limit, context=context) - if not ids: - ids = self.search(cr, user, [('name', 'ilike', name)]+ args, limit=limit, context=context)#fix it ilike should be replace with operator - + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = [('code', operator, name), ('name', operator, name)] + else: + domain = ['|', ('code', operator, name), ('name', operator, name)] + ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) return self.name_get(cr, user, ids, context=context) @@ -938,13 +933,11 @@ class account_fiscalyear(osv.osv): def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): if args is None: args = [] - if context is None: - context = {} - ids = [] - if name: - ids = self.search(cr, user, [('code', 'ilike', name)]+ args, limit=limit) - if not ids: - ids = self.search(cr, user, [('name', operator, name)]+ args, limit=limit) + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = [('code', operator, name), ('name', operator, name)] + else: + domain = ['|', ('code', operator, name), ('name', operator, name)] + ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) return self.name_get(cr, user, ids, context=context) @@ -1040,19 +1033,11 @@ class account_period(osv.osv): def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100): if args is None: args = [] - if context is None: - context = {} - ids = [] - if name: - ids = self.search(cr, user, - [('code', 'ilike', name)] + args, - limit=limit, - context=context) - if not ids: - ids = self.search(cr, user, - [('name', operator, name)] + args, - limit=limit, - context=context) + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = [('code', operator, name), ('name', operator, name)] + else: + domain = ['|', ('code', operator, name), ('name', operator, name)] + ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) return self.name_get(cr, user, ids, context=context) def write(self, cr, uid, ids, vals, context=None): @@ -1187,36 +1172,6 @@ class account_move(osv.osv): 'company_id': company_id, } - def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): - """ - Returns a list of tupples containing id, name, as internally it is called {def name_get} - result format: {[(id, name), (id, name), ...]} - - @param cr: A database cursor - @param user: ID of the user currently logged in - @param name: name to search - @param args: other arguments - @param operator: default operator is 'ilike', it can be changed - @param context: context arguments, like lang, time zone - @param limit: Returns first 'n' ids of complete result, default is 80. - - @return: Returns a list of tuples containing id and name - """ - - if not args: - args = [] - ids = [] - if name: - ids += self.search(cr, user, [('name','ilike',name)]+args, limit=limit, context=context) - - if not ids and name and type(name) == int: - ids += self.search(cr, user, [('id','=',name)]+args, limit=limit, context=context) - - if not ids: - ids += self.search(cr, user, args, limit=limit, context=context) - - return self.name_get(cr, user, ids, context=context) - def name_get(self, cursor, user, ids, context=None): if isinstance(ids, (int, long)): ids = [ids] @@ -1842,10 +1797,12 @@ class account_tax_code(osv.osv): def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80): if not args: args = [] - if context is None: - context = {} - ids = self.search(cr, user, ['|',('name',operator,name),('code',operator,name)] + args, limit=limit, context=context) - return self.name_get(cr, user, ids, context) + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = [('code', operator, name), ('name', operator, name)] + else: + domain = ['|', ('code', operator, name), ('name', operator, name)] + ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) + return self.name_get(cr, user, ids, context=context) def name_get(self, cr, uid, ids, context=None): if isinstance(ids, (int, long)): @@ -1974,15 +1931,11 @@ class account_tax(osv.osv): """ if not args: args = [] - if context is None: - context = {} - ids = [] - if name: - ids = self.search(cr, user, [('description', '=', name)] + args, limit=limit, context=context) - if not ids: - ids = self.search(cr, user, [('name', operator, name)] + args, limit=limit, context=context) + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = [('description', operator, name), ('name', operator, name)] else: - ids = self.search(cr, user, args, limit=limit, context=context or {}) + domain = ['|', ('description', operator, name), ('name', operator, name)] + ids = self.search(cr, user, expression.AND([domain, args]), limit=limit, context=context) return self.name_get(cr, user, ids, context=context) def write(self, cr, uid, ids, vals, context=None): diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index d8d9104426e..ce1a146c68f 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -672,25 +672,14 @@ class account_invoice(osv.osv): self.create_workflow(cr, uid, ids) return True - # ---------------------------------------- - # Mail related methods - # ---------------------------------------- - - def _get_formview_action(self, cr, uid, id, context=None): + def get_formview_id(self, cr, uid, id, context=None): """ Update form view id of action to open the invoice """ - action = super(account_invoice, self)._get_formview_action(cr, uid, id, context=context) obj = self.browse(cr, uid, id, context=context) if obj.type == 'in_invoice': model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_supplier_form') - action.update({ - 'views': [(view_id, 'form')], - }) else: model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'invoice_form') - action.update({ - 'views': [(view_id, 'form')], - }) - return action + return view_id # Workflow stuff ################# diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index 8a583b79383..02b046edfcf 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -165,7 +165,7 @@ class account_invoice_refund(osv.osv_memory): to_reconcile_ids = {} for line in movelines: if line.account_id.id == inv.account_id.id: - to_reconcile_ids[line.account_id.id] = [line.id] + to_reconcile_ids.setdefault(line.account_id.id, []).append(line.id) if line.reconcile_id: line.reconcile_id.unlink() inv_obj.signal_invoice_open(cr, uid, [refund.id]) diff --git a/addons/account_budget/account_budget.py b/addons/account_budget/account_budget.py index e6fc3a29668..753a5df79f4 100644 --- a/addons/account_budget/account_budget.py +++ b/addons/account_budget/account_budget.py @@ -162,7 +162,7 @@ class crossovered_budget_lines(osv.osv): elapsed = strToDate(date_to) - strToDate(date_to) if total.days: - theo_amt = float(elapsed.days / float(total.days)) * line.planned_amount + theo_amt = float((elapsed.days + 1) / float(total.days + 1)) * line.planned_amount else: theo_amt = line.planned_amount diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index cf756e121da..75854708055 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -88,7 +88,7 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({ var qdict = { current_layout : this.$el.find('.oe_dashboard').attr('data-layout') }; - var $dialog = instance.web.Dialog(this, { + var $dialog = new instance.web.Dialog(this, { title: _t("Edit Layout"), }, QWeb.render('DashBoard.layouts', qdict)).open(); $dialog.find('li').click(function() { diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index add0030d76c..6dff8ee2e8b 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -197,6 +197,10 @@ class calendar_attendee(osv.Model): @param email_from: email address for user sending the mail """ res = False + + if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False): + return res + mail_ids = [] data_pool = self.pool['ir.model.data'] mailmess_pool = self.pool['mail.message'] @@ -431,7 +435,7 @@ class calendar_alarm_manager(osv.AbstractModel): if cron and len(cron) == 1: cron = self.pool.get('ir.cron').browse(cr, uid, cron[0], context=context) else: - raise ("Cron for " + self._name + " not identified :( !") + _logger.exception("Cron for " + self._name + " can not be identified !") if cron.interval_type == "weeks": cron_interval = cron.interval_number * 7 * 24 * 60 * 60 @@ -445,7 +449,7 @@ class calendar_alarm_manager(osv.AbstractModel): cron_interval = cron.interval_number if not cron_interval: - raise ("Cron delay for " + self._name + " can not be calculated :( !") + _logger.exception("Cron delay can not be computed !") all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context) @@ -649,7 +653,7 @@ class calendar_event(osv.Model): _inherit = ["mail.thread", "ir.needaction_mixin"] def do_run_scheduler(self, cr, uid, id, context=None): - self.pool['calendar.alarm_manager'].do_run_scheduler(cr, uid, context=context) + self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context) def get_recurrent_date_by_event(self, cr, uid, event, context=None): """Get recurrent dates based on Rule string and all event where recurrent_id is child diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index eee42ba2280..6e70666ea11 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -79,6 +79,7 @@ class crm_lead(format_address, osv.osv): 'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1, }, } + _mail_mass_mailing = _('Leads / Opportunities') def get_empty_list_help(self, cr, uid, help, context=None): if context.get('default_type') == 'lead': @@ -980,15 +981,13 @@ class crm_lead(format_address, osv.osv): return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False for lead in self.browse(cr, SUPERUSER_ID, ids, context=context)] - def _get_formview_action(self, cr, uid, id, context=None): - action = super(crm_lead, self)._get_formview_action(cr, uid, id, context=context) + def get_formview_id(self, cr, uid, id, context=None): obj = self.browse(cr, uid, id, context=context) if obj.type == 'opportunity': model, view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'crm', 'crm_case_form_view_oppor') - action.update({ - 'views': [(view_id, 'form')], - }) - return action + else: + view_id = super(crm_lead, self).get_formview_id(cr, uid, id, model=model, context=context) + return view_id def message_get_suggested_recipients(self, cr, uid, ids, context=None): recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context) diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py index b7977660134..298754c8b8c 100644 --- a/addons/email_template/__openerp__.py +++ b/addons/email_template/__openerp__.py @@ -63,7 +63,7 @@ campaigns on any OpenERP document. 'wizard/mail_compose_message_view.xml', 'security/ir.model.access.csv' ], - 'demo': ['res_partner_demo.yml'], + 'demo': [], 'installable': True, 'auto_install': True, 'images': ['images/1_email_account.jpeg','images/2_email_template.jpeg','images/3_emails.jpeg'], diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 3f0fad1c8a0..dd9ed49d3ef 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -231,6 +231,11 @@ class email_template(osv.osv): 'email_from': fields.char('From', help="Sender address (placeholders may be used here). If not set, the default " "value will be the author's email alias if configured, or email address."), + 'use_default_to': fields.boolean( + 'Default recipients', + help="Default recipients of the record:\n" + "- partner (using id on a partner or the partner_id field) OR\n" + "- email (using email_from or email field)"), 'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"), 'partner_to': fields.char('To (Partners)', help="Comma-separated ids of recipient partners (placeholders may be used here)", @@ -386,6 +391,37 @@ class email_template(osv.osv): }) return {'value': result} + def generate_recipients_batch(self, cr, uid, results, template_id, res_ids, context=None): + """Generates the recipients of the template. Default values can ben generated + instead of the template values if requested by template or context. + Emails (email_to, email_cc) can be transformed into partners if requested + in the context. """ + if context is None: + context = {} + template = self.browse(cr, uid, template_id, context=context) + + if template.use_default_to or context.get('tpl_force_default_to'): + ctx = dict(context, thread_model=template.model) + default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx) + for res_id, recipients in default_recipients.iteritems(): + results[res_id].pop('partner_to', None) + results[res_id].update(recipients) + + for res_id, values in results.iteritems(): + partner_ids = values.get('partner_ids', list()) + if context and context.get('tpl_partners_only'): + mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', '')) + for mail in mails: + partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context) + partner_ids.append(partner_id) + partner_to = values.pop('partner_to', '') + if partner_to: + # placeholders could generate '', 3, 2 due to some empty field values + tpl_partner_ids = [pid for pid in partner_to.split(',') if pid] + partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) + results[res_id]['partner_ids'] = partner_ids + return results + def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): """Generates an email from the template for given the given model based on records given by res_ids. @@ -420,14 +456,18 @@ class email_template(osv.osv): context=context) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value + # compute recipients + results = self.generate_recipients_batch(cr, uid, results, template.id, template_res_ids, context=context) # update values for all res_ids for res_id in template_res_ids: values = results[res_id] + # body: add user signature, sanitize if 'body_html' in fields and template.user_signature: signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature values['body_html'] = tools.append_content_to_html(values['body_html'], signature) if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) + # technical settings values.update( mail_server_id=template.mail_server_id.id or False, auto_delete=template.auto_delete, @@ -484,17 +524,8 @@ class email_template(osv.osv): # create a mail_mail based on values, without attachments values = self.generate_email(cr, uid, template_id, res_id, context=context) if not values.get('email_from'): - raise osv.except_osv(_('Warning!'),_("Sender email is missing or empty after template rendering. Specify one to deliver your message")) - # process partner_to field that is a comma separated list of partner_ids -> recipient_ids - # NOTE: only usable if force_send is True, because otherwise the value is - # not stored on the mail_mail, and therefore lost -> fixed in v8 - values['recipient_ids'] = [] - partner_to = values.pop('partner_to', '') - if partner_to: - # placeholders could generate '', 3, 2 due to some empty field values - tpl_partner_ids = [pid for pid in partner_to.split(',') if pid] - values['recipient_ids'] += [(4, pid) for pid in self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)] - + raise osv.except_osv(_('Warning!'), _("Sender email is missing or empty after template rendering. Specify one to deliver your message")) + values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())] attachment_ids = values.pop('attachment_ids', []) attachments = values.pop('attachments', []) msg_id = mail_mail.create(cr, uid, values, context=context) diff --git a/addons/email_template/email_template_view.xml b/addons/email_template/email_template_view.xml index 0f54504cbf6..c48d93365f9 100644 --- a/addons/email_template/email_template_view.xml +++ b/addons/email_template/email_template_view.xml @@ -9,8 +9,10 @@
@@ -30,43 +32,28 @@ context="{'template_id':active_id}"/>
- - - - - - - - - - - - -

Dynamic placeholder generator

- - - - - -
-
-

Body

- + + + + + + + + + + + + + @@ -77,6 +64,21 @@ attrs="{'invisible':[('report_template','=',False)]}"/> + + + + + + + + +
diff --git a/addons/email_template/res_partner_demo.yml b/addons/email_template/res_partner_demo.yml deleted file mode 100644 index 8eaf48f9e87..00000000000 --- a/addons/email_template/res_partner_demo.yml +++ /dev/null @@ -1,9 +0,0 @@ -- - Set opt-out to True on all demo partners -- - !python {model: res.partner}: | - partner_ids = self.search(cr, uid, []) - # assume partners with an external ID come from demo data - ext_ids = self._get_external_ids(cr, uid, partner_ids) - ids_to_update = [k for (k,v) in ext_ids.iteritems() if v] - self.write(cr, uid, ids_to_update, {'opt_out': True}) \ No newline at end of file diff --git a/addons/email_template/tests/test_mail.py b/addons/email_template/tests/test_mail.py index 580421fe67e..8bc6d62017c 100644 --- a/addons/email_template/tests/test_mail.py +++ b/addons/email_template/tests/test_mail.py @@ -74,7 +74,7 @@ class test_message_compose(TestMail): # 1. Comment on pigs compose_id = mail_compose.create(cr, uid, - {'subject': 'Forget me subject', 'body': '

Dummy body

', 'post': True}, + {'subject': 'Forget me subject', 'body': '

Dummy body

'}, {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id, @@ -102,7 +102,7 @@ class test_message_compose(TestMail): 'default_template_id': email_template_id, 'active_ids': [self.group_pigs_id, self.group_bird_id] } - compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context) + compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context) compose = mail_compose.browse(cr, uid, compose_id, context) onchange_res = compose.onchange_template_id(email_template_id, 'comment', 'mail.group', self.group_pigs_id)['value'] onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] @@ -146,7 +146,7 @@ class test_message_compose(TestMail): 'default_partner_ids': [p_a_id], 'active_ids': [self.group_pigs_id, self.group_bird_id] } - compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context) + compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context) compose = mail_compose.browse(cr, uid, compose_id, context) onchange_res = compose.onchange_template_id(email_template_id, 'mass_mail', 'mail.group', self.group_pigs_id)['value'] onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])] @@ -172,12 +172,12 @@ class test_message_compose(TestMail): self.assertIn(_body_html1, message_pigs.body, 'mail.message body on Pigs incorrect') self.assertIn(_body_html2, message_bird.body, 'mail.message body on Bird incorrect') # Test: partner_ids: p_a_id (default) + 3 newly created partners - message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids] - message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids] - partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) - partner_ids.append(p_a_id) - self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids') - self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect') + # message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids] + # message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids] + # partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) + # partner_ids.append(p_a_id) + # self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids') + # self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect') # ---------------------------------------- # CASE4: test newly introduced partner_to field @@ -237,8 +237,8 @@ class test_message_compose(TestMail): email_template.send_mail(cr, uid, email_template_id, self.group_pigs_id, force_send=True, context=context) sent_emails = self._build_email_kwargs_list email_to_lst = [ - ['b@b.b', 'c@c.c'], ['"Followers of Pigs" '], - ['"Followers of Pigs" '], ['"Followers of Pigs" ']] + ['b@b.b', 'c@c.c'], ['Administrator '], + ['Raoul Grosbedon '], ['Bert Tartignole ']] self.assertEqual(len(sent_emails), 4, 'email_template: send_mail: 3 valid email recipients + email_to -> should send 4 emails') for email in sent_emails: self.assertIn(email['email_to'], email_to_lst, 'email_template: send_mail: wrong email_recipients') diff --git a/addons/email_template/wizard/email_template_preview.py b/addons/email_template/wizard/email_template_preview.py index 5fee415a75e..104437ec0d3 100644 --- a/addons/email_template/wizard/email_template_preview.py +++ b/addons/email_template/wizard/email_template_preview.py @@ -66,6 +66,7 @@ class email_template_preview(osv.osv_memory): _columns = { 'res_id': fields.selection(_get_records, 'Sample Document'), + 'partner_ids': fields.many2many('res.partner', string='Recipients'), } def on_change_res_id(self, cr, uid, ids, res_id, context=None): @@ -80,7 +81,7 @@ class email_template_preview(osv.osv_memory): # generate and get template values mail_values = email_template.generate_email(cr, uid, template_id, res_id, context=context) - vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to')) + vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to', 'partner_ids', 'attachment_ids')) vals['name'] = template.name return {'value': vals} diff --git a/addons/email_template/wizard/email_template_preview_view.xml b/addons/email_template/wizard/email_template_preview_view.xml index 0a74f7d813e..87c1e92fa60 100644 --- a/addons/email_template/wizard/email_template_preview_view.xml +++ b/addons/email_template/wizard/email_template_preview_view.xml @@ -8,14 +8,17 @@
-

Preview of

- Using sample document +

Preview of

+ Choose an example record: + - - + + + @@ -30,10 +34,11 @@ Template Preview email_template.preview - email_template.preview + email.template ir.actions.act_window form form + new {'template_id':active_id} diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index 71f99471afa..676f11b23e3 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -42,7 +42,8 @@ class mail_compose_message(osv.TransientModel): _inherit = 'mail.compose.message' def default_get(self, cr, uid, fields, context=None): - """ Override to pre-fill the data when having a template in single-email mode """ + """ Override to pre-fill the data when having a template in single-email mode + and not going through the view: the on_change is not called in that case. """ if context is None: context = {} res = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) @@ -50,19 +51,13 @@ class mail_compose_message(osv.TransientModel): res.update( self.onchange_template_id( cr, uid, [], context['default_template_id'], res.get('composition_mode'), - res.get('model'), res.get('res_id', context.get('active_id')), context=context + res.get('model'), res.get('res_id'), context=context )['value'] ) return res _columns = { 'template_id': fields.many2one('email.template', 'Use template', select=True), - 'partner_to': fields.char('To (Partner IDs)', - help="Comma-separated list of recipient partners ids (placeholders may be used here)"), - 'email_to': fields.char('To (Emails)', - help="Comma-separated recipient addresses (placeholders may be used here)",), - 'email_cc': fields.char('Cc (Emails)', - help="Carbon copy recipients (placeholders may be used here)"), } def send_mail(self, cr, uid, ids, context=None): @@ -92,14 +87,13 @@ class mail_compose_message(osv.TransientModel): """ - mass_mailing: we cannot render, so return the template values - normal mode: return rendered values """ if template_id and composition_mode == 'mass_mail': - fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] + fields = ['subject', 'body_html', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context) values = dict((field, template_values[field]) for field in fields if template_values.get(field)) elif template_id: values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send - values['attachment_ids'] = values.pop('attachment_ids', []) ir_attach_obj = self.pool.get('ir.attachment') for attach_fname, attach_datas in values.pop('attachments', []): data_attach = { @@ -110,7 +104,7 @@ class mail_compose_message(osv.TransientModel): 'res_id': 0, 'type': 'binary', # override default_type from context, possibly meant for another model! } - values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context)) + values.setdefault('attachment_ids', list()).append(ir_attach_obj.create(cr, uid, data_attach, context=context)) else: values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context) @@ -148,47 +142,29 @@ class mail_compose_message(osv.TransientModel): # Wizard validation and send #------------------------------------------------------ - def _get_or_create_partners_from_values(self, cr, uid, rendered_values, context=None): - """ Check for email_to, email_cc, partner_to """ - partner_ids = [] - mails = tools.email_split(rendered_values.pop('email_to', '')) + tools.email_split(rendered_values.pop('email_cc', '')) - for mail in mails: - partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context) - partner_ids.append(partner_id) - partner_to = rendered_values.pop('partner_to', '') - if partner_to: - # placeholders could generate '', 3, 2 due to some empty field values - tpl_partner_ids = [pid for pid in partner_to.split(',') if pid] - partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) - return partner_ids - def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ - # filter template values + if context is None: + context = {} if fields is None: fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'] - returned_fields = fields + ['attachments'] + returned_fields = fields + ['partner_ids', 'attachments'] values = dict.fromkeys(res_ids, False) - template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context) + ctx = dict(context, tpl_partners_only=True) + template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=ctx) for res_id in res_ids: res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field)) res_id_values['body'] = res_id_values.pop('body_html', '') - - # transform email_to, email_cc into partner_ids - ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_')) - partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx) - # legacy template behavior: void values do not erase existing values and the - # related key is removed from the values dict - if partner_ids: - res_id_values['partner_ids'] = list(partner_ids) - values[res_id] = res_id_values return values def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """ Override to handle templates. """ + # generate composer values + composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context) + # generate template-based values if wizard.template_id: template_values = self.generate_email_for_composer_batch( @@ -196,17 +172,18 @@ class mail_compose_message(osv.TransientModel): fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'], context=context) else: - template_values = dict.fromkeys(res_ids, dict()) - # generate composer values - composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context) + template_values = {} for res_id in res_ids: - # remove attachments from template values as they should not be rendered - template_values[res_id].pop('attachment_ids', None) - # remove some keys from composer that are readonly - composer_values[res_id].pop('email_to', None) - composer_values[res_id].pop('email_cc', None) - composer_values[res_id].pop('partner_to', None) + if template_values.get(res_id): + # recipients are managed by the template + composer_values[res_id].pop('partner_ids') + composer_values[res_id].pop('email_to') + composer_values[res_id].pop('email_cc') + # remove attachments from template values as they should not be rendered + template_values[res_id].pop('attachment_ids', None) + else: + template_values[res_id] = dict() # update template values by composer values template_values[res_id].update(composer_values[res_id]) return template_values diff --git a/addons/email_template/wizard/mail_compose_message_view.xml b/addons/email_template/wizard/mail_compose_message_view.xml index 44145114fff..2f755a625d6 100644 --- a/addons/email_template/wizard/mail_compose_message_view.xml +++ b/addons/email_template/wizard/mail_compose_message_view.xml @@ -7,22 +7,6 @@ mail.compose.message - -
Use template diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml index 428cc357047..0437dee031c 100644 --- a/addons/gamification/data/goal_base.xml +++ b/addons/gamification/data/goal_base.xml @@ -2,7 +2,7 @@ - + @@ -164,7 +164,7 @@ once personal never - + [('groups_id', 'in', ref('base.group_user'))] inprogress other @@ -174,7 +174,7 @@ once personal never - + [('groups_id', 'in', ref('base.user_root'))] inprogress other diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 3ab12ff66e7..295340a309b 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -22,11 +22,13 @@ from openerp import SUPERUSER_ID from openerp.osv import fields, osv from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF +from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from datetime import date, datetime, timedelta import calendar import logging +import functools _logger = logging.getLogger(__name__) # display top 3 in ranking, could be db variable @@ -115,6 +117,12 @@ class gamification_challenge(osv.Model): except ValueError: return False + def _get_challenger_users(self, cr, uid, domain, context=None): + ref = functools.partial(self.pool['ir.model.data'].xmlid_to_res_id, cr, uid) + user_domain = eval(domain, {'ref': ref}) + return self.pool['res.users'].search(cr, uid, user_domain, context=context) + + _order = 'end_date, start_date, name, id' _columns = { 'name': fields.char('Challenge Name', required=True, translate=True), @@ -131,9 +139,7 @@ class gamification_challenge(osv.Model): 'user_ids': fields.many2many('res.users', 'user_ids', string='Users', help="List of users participating to the challenge"), - 'autojoin_group_id': fields.many2one('res.groups', - string='Auto-subscription Group', - help='Group of users whose members will be automatically added to user_ids once the challenge is started'), + 'user_domain': fields.char('User domain', help="Alternative to a list of users"), 'period': fields.selection([ ('once', 'Non recurring'), @@ -213,12 +219,12 @@ class gamification_challenge(osv.Model): """Overwrite the create method to add the user of groups""" # add users when change the group auto-subscription - if vals.get('autojoin_group_id'): - new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) + if vals.get('user_domain'): + user_ids = self._get_challenger_users(cr, uid, vals.get('user_domain'), context=context) if not vals.get('user_ids'): vals['user_ids'] = [] - vals['user_ids'] += [(4, user.id) for user in new_group.users] + vals['user_ids'] += [(4, user_id) for user_id in user_ids] create_res = super(gamification_challenge, self).create(cr, uid, vals, context=context) @@ -234,23 +240,12 @@ class gamification_challenge(osv.Model): if isinstance(ids, (int,long)): ids = [ids] - # add users when change the group auto-subscription - if vals.get('autojoin_group_id'): - new_group = self.pool.get('res.groups').browse(cr, uid, vals['autojoin_group_id'], context=context) - - if not vals.get('user_ids'): - vals['user_ids'] = [] - vals['user_ids'] += [(4, user.id) for user in new_group.users] - if vals.get('state') == 'inprogress': - # starting a challenge - if not vals.get('autojoin_group_id'): - # starting challenge, add users in autojoin group - if not vals.get('user_ids'): - vals['user_ids'] = [] - for challenge in self.browse(cr, uid, ids, context=context): - if challenge.autojoin_group_id: - vals['user_ids'] += [(4, user.id) for user in challenge.autojoin_group_id.users] + for challenge in self.browse(cr, uid, ids, context=context): + user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context) + write_op = [(4, user_id) for user_id in user_ids] + self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context) + self.message_subscribe_users(cr, uid, [challenge.id], user_ids, context=context) self.generate_goals_from_challenge(cr, uid, ids, context=context) @@ -264,11 +259,6 @@ class gamification_challenge(osv.Model): write_res = super(gamification_challenge, self).write(cr, uid, ids, vals, context=context) - # subscribe new users to the challenge - if vals.get('user_ids'): - # done with browse after super if changes in groups - for challenge in self.browse(cr, uid, ids, context=context): - self.message_subscribe_users(cr, uid, [challenge.id], [user.id for user in challenge.user_ids], context=context) return write_res @@ -325,9 +315,16 @@ class gamification_challenge(osv.Model): goal_obj.update(cr, uid, goal_ids, context=context) for challenge in self.browse(cr, uid, ids, context=context): - if challenge.autojoin_group_id: - # check in case of new users in challenge, this happens if manager removed users in challenge manually - self.write(cr, uid, [challenge.id], {'user_ids': [(4, user.id) for user in challenge.autojoin_group_id.users]}, context=context) + # in case of new users matching the domain + old_user_ids = [user.id for user in challenge.user_ids] + new_user_ids = self._get_challenger_users(cr, uid, challenge.user_domain, context=context) + to_remove_ids = list(set(old_user_ids) - set(new_user_ids)) + to_add_ids = list(set(new_user_ids) - set(old_user_ids)) + + write_op = [(3, user_id) for user_id in to_remove_ids] + write_op += [(4, user_id) for user_id in to_add_ids] + self.write(cr, uid, [challenge.id], {'user_ids': write_op}, context=context) + self.generate_goals_from_challenge(cr, uid, [challenge.id], context=context) # goals closed but still opened at the last report date diff --git a/addons/gamification/models/goal.py b/addons/gamification/models/goal.py index 6ab4259cca2..af9a763236d 100644 --- a/addons/gamification/models/goal.py +++ b/addons/gamification/models/goal.py @@ -287,30 +287,33 @@ class gamification_goal(osv.Model): field_date_name = definition.field_date_id and definition.field_date_id.name or False if definition.computation_mode == 'count' and definition.batch_mode: - + # batch mode, trying to do as much as possible in one request general_domain = safe_eval(definition.domain) - # goal_distinct_values = {goal.id: safe_eval(definition.batch_user_expression, {'user': goal.user_id}) for goal in goals} field_name = definition.batch_distinctive_field.name - # general_domain.append((field_name, 'in', list(set(goal_distinct_values.keys())))) subqueries = {} for goal in goals: start_date = field_date_name and goal.start_date or False end_date = field_date_name and goal.end_date or False subqueries.setdefault((start_date, end_date), {}).update({goal.id:safe_eval(definition.batch_user_expression, {'user': goal.user_id})}) + # the global query should be split by time periods (especially for recurrent goals) for (start_date, end_date), query_goals in subqueries.items(): subquery_domain = list(general_domain) subquery_domain.append((field_name, 'in', list(set(query_goals.values())))) if start_date: subquery_domain.append((field_date_name, '>=', start_date)) if end_date: - subquery_domain.append((field_date_name, '>=', end_date)) - - user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context) + subquery_domain.append((field_date_name, '<=', end_date)) + if field_name == 'id': + # grouping on id does not work and is similar to search anyway + user_ids = obj.search(cr, uid, subquery_domain, context=context) + user_values = [{'id': user_id, 'id_count': 1} for user_id in user_ids] + else: + user_values = obj.read_group(cr, uid, subquery_domain, fields=[field_name], groupby=[field_name], context=context) + # user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...] for goal in [g for g in goals if g.id in query_goals.keys()]: for user_value in user_values: - # return format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...] queried_value = field_name in user_value and user_value[field_name] or False if isinstance(queried_value, tuple) and len(queried_value) == 2 and isinstance(queried_value[0], (int, long)): queried_value = queried_value[0] diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index 85cc18636f0..330311ba05e 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -31,43 +31,12 @@ class res_users_gamification_group(osv.Model): _name = 'res.users' _inherit = ['res.users'] - def write(self, cr, uid, ids, vals, context=None): - """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge""" - write_res = super(res_users_gamification_group, self).write(cr, uid, ids, vals, context=context) - if vals.get('groups_id'): - # 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 vals['groups_id'] if command[0] == 4] - user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] + def get_serialised_gamification_summary(self, cr, uid, excluded_categories=None, context=None): + return self._serialised_goals_summary(cr, uid, user_id=uid, excluded_categories=excluded_categories, context=context) - challenge_obj = self.pool.get('gamification.challenge') - challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', user_group_ids)], context=context) - if challenge_ids: - challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, user_id) for user_id in ids]}, context=context) - challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) - return write_res - - def create(self, cr, uid, vals, context=None): - """Overwrite to autosubscribe users if added to a group marked as autojoin, user will be added to challenge""" - write_res = super(res_users_gamification_group, self).create(cr, uid, vals, context=context) - if vals.get('groups_id'): - # 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 vals['groups_id'] if command[0] == 4] - user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] - - challenge_obj = self.pool.get('gamification.challenge') - challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', user_group_ids)], context=context) - if challenge_ids: - challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, write_res)]}, context=context) - challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) - return write_res - - # def get_goals_todo_info(self, cr, uid, context=None): - - def get_serialised_gamification_summary(self, cr, uid, context=None): - return self._serialised_goals_summary(cr, uid, user_id=uid, context=context) - - def _serialised_goals_summary(self, cr, uid, user_id, context=None): + def _serialised_goals_summary(self, cr, uid, user_id, excluded_categories=None, context=None): """Return a serialised list of goals assigned to the user, grouped by challenge + :excluded_categories: list of challenge categories to exclude in search [ { @@ -81,9 +50,11 @@ class res_users_gamification_group(osv.Model): """ all_goals_info = [] challenge_obj = self.pool.get('gamification.challenge') - + domain = [('user_ids', 'in', uid), ('state', '=', 'inprogress')] + if excluded_categories and isinstance(excluded_categories, list): + domain.append(('category', 'not in', excluded_categories)) user = self.browse(cr, uid, uid, context=context) - challenge_ids = challenge_obj.search(cr, uid, [('user_ids', 'in', uid), ('state', '=', 'inprogress')], context=context) + challenge_ids = challenge_obj.search(cr, uid, domain, context=context) for challenge in challenge_obj.browse(cr, uid, challenge_ids, context=context): # serialize goals info to be able to use it in javascript lines = challenge_obj._get_serialized_challenge_lines(cr, uid, challenge, user_id, restrict_top=MAX_VISIBILITY_RANKING, context=context) @@ -111,28 +82,3 @@ class res_users_gamification_group(osv.Model): } challenge_info.append(values) return challenge_info - - -class res_groups_gamification_group(osv.Model): - """ Update of res.groups class - - if adding users from a group, check gamification.challenge linked to - this group, and the user. This is done by overriding the write method. - """ - _name = 'res.groups' - _inherit = 'res.groups' - - # No need to overwrite create as very unlikely to be the value in the autojoin_group_id field - def write(self, cr, uid, ids, vals, context=None): - """Overwrite to autosubscribe users if add users to a group marked as autojoin, these will be added to the challenge""" - write_res = super(res_groups_gamification_group, self).write(cr, uid, ids, vals, context=context) - if vals.get('users'): - # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} - user_ids = [command[1] for command in vals['users'] if command[0] == 4] - user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]] - - challenge_obj = self.pool.get('gamification.challenge') - challenge_ids = challenge_obj.search(cr, SUPERUSER_ID, [('autojoin_group_id', 'in', ids)], context=context) - if challenge_ids: - challenge_obj.write(cr, SUPERUSER_ID, challenge_ids, {'user_ids': [(4, user_id) for user_id in user_ids]}, context=context) - challenge_obj.generate_goals_from_challenge(cr, SUPERUSER_ID, challenge_ids, context=context) - return write_res diff --git a/addons/gamification/tests/test_challenge.py b/addons/gamification/tests/test_challenge.py index 6b93da727ac..cdbca962e0f 100644 --- a/addons/gamification/tests/test_challenge.py +++ b/addons/gamification/tests/test_challenge.py @@ -57,6 +57,7 @@ class test_challenge(common.TransactionCase): 'groups_id': [(6, 0, [self.group_user_id])] }, {'no_reset_password': True}) + self.challenge_obj._update_all(cr, uid, [self.challenge_base_id], context=context) challenge = self.challenge_obj.browse(cr, uid, self.challenge_base_id, context=context) self.assertGreaterEqual(len(challenge.user_ids), len(user_ids)+1, "These are not droids you are looking for") diff --git a/addons/gamification/views/challenge.xml b/addons/gamification/views/challenge.xml index 0ce39303410..fbf4f4cf009 100644 --- a/addons/gamification/views/challenge.xml +++ b/addons/gamification/views/challenge.xml @@ -45,9 +45,9 @@

-
@@ -87,12 +87,12 @@ - + - +

Badges are granted when a challenge is finished. This is either at the end of a running period (eg: end of the month for a monthly challenge), at the end date of a challenge (if no periodicity is set) or when the challenge is manually closed.

@@ -100,7 +100,6 @@ - diff --git a/addons/gamification_sale_crm/sale_crm_goals.xml b/addons/gamification_sale_crm/sale_crm_goals.xml index 4c1f8e46149..fbc1c952bb8 100644 --- a/addons/gamification_sale_crm/sale_crm_goals.xml +++ b/addons/gamification_sale_crm/sale_crm_goals.xml @@ -130,7 +130,7 @@ Monthly Sales Targets monthly ranking - + [('groups_id', 'in', ref('base.group_sale_salesman'))] weekly @@ -138,7 +138,7 @@ Lead Acquisition monthly ranking - + [('groups_id', 'in', ref('base.group_sale_salesman'))] weekly diff --git a/addons/hr_attendance/__openerp__.py b/addons/hr_attendance/__openerp__.py index 631880ec73b..abdc1ad8cc1 100644 --- a/addons/hr_attendance/__openerp__.py +++ b/addons/hr_attendance/__openerp__.py @@ -33,7 +33,7 @@ actions(Sign in/Sign out) performed by them. """, 'author': 'OpenERP SA', 'images': ['images/hr_attendances.jpeg'], - 'depends': ['hr'], + 'depends': ['hr', 'report'], 'data': [ 'security/ir_rule.xml', 'security/ir.model.access.csv', @@ -43,6 +43,7 @@ actions(Sign in/Sign out) performed by them. 'wizard/hr_attendance_byweek_view.xml', 'wizard/hr_attendance_error_view.xml', 'res_config_view.xml', + 'views/report_attendanceerrors.xml', ], 'demo': ['hr_attendance_demo.xml'], 'test': [ @@ -51,10 +52,10 @@ actions(Sign in/Sign out) performed by them. ], 'installable': True, 'auto_install': False, - #web "js": ["static/src/js/attendance.js"], - 'qweb' : ["static/src/xml/attendance.xml"], - 'css' : ["static/src/css/slider.css"], + 'qweb': ["static/src/xml/attendance.xml"], + 'css': ["static/src/css/slider.css"], } + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_attendance/hr_attendance_report.xml b/addons/hr_attendance/hr_attendance_report.xml index d3e9448ead3..f98eb6c053b 100644 --- a/addons/hr_attendance/hr_attendance_report.xml +++ b/addons/hr_attendance/hr_attendance_report.xml @@ -1,8 +1,13 @@ - - - + diff --git a/addons/hr_attendance/report/attendance_errors.py b/addons/hr_attendance/report/attendance_errors.py index 811f4d8d906..e37a329ac60 100644 --- a/addons/hr_attendance/report/attendance_errors.py +++ b/addons/hr_attendance/report/attendance_errors.py @@ -21,9 +21,10 @@ import datetime import time - +from openerp.osv import osv from openerp.report import report_sxw + class attendance_print(report_sxw.rml_parse): def __init__(self, cr, uid, name, context): @@ -39,7 +40,6 @@ class attendance_print(report_sxw.rml_parse): emp_obj_list = self.pool.get('hr.employee').browse(self.cr, self.uid, emp_ids) return emp_obj_list - def _lst(self, employee_id, dt_from, dt_to, max, *args): self.cr.execute("select name as date, create_date, action, create_date-name as delay from hr_attendance where employee_id=%s and to_char(name,'YYYY-mm-dd')<=%s and to_char(name,'YYYY-mm-dd')>=%s and action IN (%s,%s) order by name", (employee_id, dt_to, dt_from, 'sign_in', 'sign_out')) res = self.cr.dictfetchall() @@ -75,7 +75,11 @@ class attendance_print(report_sxw.rml_parse): } return [result_dict] -report_sxw.report_sxw('report.hr.attendance.error', 'hr.employee', 'addons/hr_attendance/report/attendance_errors.rml', parser=attendance_print, header='internal') + +class report_hr_attendanceerrors(osv.AbstractModel): + _name = 'report.hr_attendance.report_attendanceerrors' + _inherit = 'report.abstract_report' + _template = 'hr_attendance.report_attendanceerrors' + _wrapped_report_class = attendance_print # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/hr_attendance/report/attendance_errors.rml b/addons/hr_attendance/report/attendance_errors.rml deleted file mode 100644 index 6806c02c422..00000000000 --- a/addons/hr_attendance/report/attendance_errors.rml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [[ repeatIn(get_employees(data['form']['emp_ids']),'employee') ]] - Attendance Errors - [[ employee.name ]] - - - - Operation - - - Date Signed - - - Date Recorded - - - Delay - - - Min Delay - - - -
- [[ repeatIn(lst(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay']), 'att') ]] - - - - [[ att['action'] ]] - - - [[ formatLang(att['date'],date_time=True) ]] - - - [[ formatLang(att['create_date'],date_time=True) ]] - - - [[ att['delay'] ]] - - - [[ att['delay2'] ]] - - - -
- - - - Total period:[[ repeatIn(total(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay']),'total') ]] - - - [[ total['total'] ]] - - - [[ total['total2'] ]] - - - - - - - (*) A positive delay means that the employee worked less than recorded. - (*) A negative delay means that the employee worked more than encoded. -
-
\ No newline at end of file diff --git a/addons/hr_attendance/views/report_attendanceerrors.xml b/addons/hr_attendance/views/report_attendanceerrors.xml new file mode 100644 index 00000000000..8ee00a60534 --- /dev/null +++ b/addons/hr_attendance/views/report_attendanceerrors.xml @@ -0,0 +1,45 @@ + + + + + + \ No newline at end of file diff --git a/addons/hr_attendance/wizard/hr_attendance_error.py b/addons/hr_attendance/wizard/hr_attendance_error.py index 82d75149428..58a165baff3 100644 --- a/addons/hr_attendance/wizard/hr_attendance_error.py +++ b/addons/hr_attendance/wizard/hr_attendance_error.py @@ -23,6 +23,7 @@ import time from openerp.osv import fields, osv from openerp.tools.translate import _ + class hr_attendance_error(osv.osv_memory): _name = 'hr.attendance.error' @@ -58,11 +59,8 @@ class hr_attendance_error(osv.osv_memory): 'model': 'hr.employee', 'form': data_error } - return { - 'type': 'ir.actions.report.xml', - 'report_name': 'hr.attendance.error', - 'datas': datas, - } - + return self.pool['report'].get_action( + cr, uid, [], 'hr_attendance.report_attendanceerrors', data=datas, context=context + ) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_expense/__openerp__.py b/addons/hr_expense/__openerp__.py index f149bc0c7c6..873da978165 100644 --- a/addons/hr_expense/__openerp__.py +++ b/addons/hr_expense/__openerp__.py @@ -46,7 +46,7 @@ This module also uses analytic accounting and is compatible with the invoice on 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', 'images': ['images/hr_expenses_analysis.jpeg', 'images/hr_expenses.jpeg'], - 'depends': ['hr', 'account_accountant'], + 'depends': ['hr', 'account_accountant', 'report'], 'data': [ 'security/ir.model.access.csv', 'hr_expense_data.xml', @@ -59,6 +59,7 @@ This module also uses analytic accounting and is compatible with the invoice on 'report/hr_expense_report_view.xml', 'board_hr_expense_view.xml', 'hr_expense_installer_view.xml', + 'views/report_expense.xml', ], 'demo': ['hr_expense_demo.xml'], 'test': [ @@ -69,4 +70,5 @@ This module also uses analytic accounting and is compatible with the invoice on 'auto_install': False, 'application': True, } + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_expense/hr_expense_report.xml b/addons/hr_expense/hr_expense_report.xml index 5defbfec330..ab3200e5a54 100644 --- a/addons/hr_expense/hr_expense_report.xml +++ b/addons/hr_expense/hr_expense_report.xml @@ -1,8 +1,13 @@ - - - + diff --git a/addons/hr_expense/report/__init__.py b/addons/hr_expense/report/__init__.py index ad0c48a0819..9ec287fb9c7 100644 --- a/addons/hr_expense/report/__init__.py +++ b/addons/hr_expense/report/__init__.py @@ -19,8 +19,6 @@ # ############################################################################## -import expense import hr_expense_report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/hr_expense/report/expense.rml b/addons/hr_expense/report/expense.rml deleted file mode 100644 index 3e07516c01b..00000000000 --- a/addons/hr_expense/report/expense.rml +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Date - - - Name - - - Ref. - - - Unit Price - - - Qty - - - Price - - - - - [[ repeatIn(objects,'o') ]] - - - - HR Expenses - - [[ o.name or '' ]] - - - - - - - - Employee - - - Date - - - Description - - - Validated By - - - - - - - [[ o.employee_id.name ]] - - - [[ formatLang(o.date,date=True) ]] - - - [[ o.name ]] - - - [[ o.user_valid.name ]] - - - - - - - - - - Date - - - Name - - - Ref. - - - Unit Price - - - Qty - - - Price - - - - - - -
- [[ repeatIn(o.line_ids,'line') ]] - - - - [[ formatLang(line.date_value,date=True) ]] - - - [[ line.name or '' ]] [[ line.description or '' ]] - - - [[ line.ref or '' ]] - - - [[ formatLang(line.unit_amount) ]] - - - [[ formatLang(line.unit_quantity) ]] - - - [[ formatLang(line.total_amount, currency_obj=o.currency_id) ]] - - - - - - - - - - [[ line.analytic_account and line.analytic_account.complete_name or removeParentNode('tr') ]] - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - Total: - - - [[ formatLang(o.amount, currency_obj=o.currency_id) ]] - - - - - - - [[ o.note or '' ]] - - - - Certified honest and conform, - (Date and signature) - - - - - - - This document must be dated and signed for reimbursement -
-
-
diff --git a/addons/hr_expense/test/expense_process.yml b/addons/hr_expense/test/expense_process.yml index e9339e24a95..b3e19714c74 100644 --- a/addons/hr_expense/test/expense_process.yml +++ b/addons/hr_expense/test/expense_process.yml @@ -32,11 +32,3 @@ !python {model: hr.expense.expense}: | duplicate_id = self.copy(cr, uid, ref('sep_expenses'), context=context) self.expense_canceled(cr, uid, [duplicate_id]) -- - I print a report of the expenses. -- - !python {model: hr.expense.expense}: | - data, format = self.print_report(cr, uid, [ref('hr_expense.sep_expenses')], 'hr.expense', {}, {}) - if openerp.tools.config['test_report_directory']: - import os - file(os.path.join(openerp.tools.config['test_report_directory'], 'hr_expense-report.'+format), 'wb+').write(data) diff --git a/addons/hr_expense/views/report_expense.xml b/addons/hr_expense/views/report_expense.xml new file mode 100644 index 00000000000..5bb2e1dd8e2 --- /dev/null +++ b/addons/hr_expense/views/report_expense.xml @@ -0,0 +1,89 @@ + + + + + + diff --git a/addons/hr_payroll/__openerp__.py b/addons/hr_payroll/__openerp__.py index dfa87622264..826845834c8 100644 --- a/addons/hr_payroll/__openerp__.py +++ b/addons/hr_payroll/__openerp__.py @@ -19,6 +19,7 @@ # along with this program. If not, see . # ############################################################################## + { 'name': 'Payroll', 'version': '1.0', @@ -37,14 +38,20 @@ Generic Payroll system. * Monthly Payroll Register * Integrated with Holiday Management """, - 'author':'OpenERP SA', - 'website':'http://www.openerp.com', - 'images': ['images/hr_company_contributions.jpeg','images/hr_salary_heads.jpeg','images/hr_salary_structure.jpeg','images/hr_employee_payslip.jpeg'], + 'author': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'images': [ + 'images/hr_company_contributions.jpeg', + 'images/hr_salary_heads.jpeg', + 'images/hr_salary_structure.jpeg', + 'images/hr_employee_payslip.jpeg' + ], 'depends': [ 'hr', 'hr_contract', 'hr_holidays', 'decimal_precision', + 'report', ], 'data': [ 'security/hr_security.xml', @@ -57,12 +64,12 @@ Generic Payroll system. 'security/ir.model.access.csv', 'wizard/hr_payroll_contribution_register_report.xml', 'res_config_view.xml', + 'views/report_contributionregister.xml', + 'views/report_payslip.xml', + 'views/report_payslipdetails.xml', ], 'test': [ 'test/payslip.yml', -# 'test/payment_advice.yml', -# 'test/payroll_register.yml', - # 'test/hr_payroll_report.yml', ], 'demo': ['hr_payroll_demo.xml'], 'installable': True, diff --git a/addons/hr_payroll/hr_payroll_report.xml b/addons/hr_payroll/hr_payroll_report.xml index a1b3587313a..86d5b3b8a66 100644 --- a/addons/hr_payroll/hr_payroll_report.xml +++ b/addons/hr_payroll/hr_payroll_report.xml @@ -1,31 +1,30 @@ - + + - - - - - + string="PaySlip Details" + report_type="qweb-pdf" + name="hr_payroll.report_payslipdetails" + file="hr_payroll.report_payslipdetails" + /> diff --git a/addons/hr_payroll/report/report_contribution_register.py b/addons/hr_payroll/report/report_contribution_register.py index 72500a9c6c9..27cc0eb6458 100644 --- a/addons/hr_payroll/report/report_contribution_register.py +++ b/addons/hr_payroll/report/report_contribution_register.py @@ -24,9 +24,10 @@ import time from datetime import datetime from dateutil import relativedelta - +from openerp.osv import osv from openerp.report import report_sxw + class contribution_register_report(report_sxw.rml_parse): def __init__(self, cr, uid, name, context): super(contribution_register_report, self).__init__(cr, uid, name, context) @@ -44,7 +45,6 @@ class contribution_register_report(report_sxw.rml_parse): return self.regi_total def _get_payslip_lines(self, obj): - payslip_obj = self.pool.get('hr.payslip') payslip_line = self.pool.get('hr.payslip.line') payslip_lines = [] res = [] @@ -69,6 +69,11 @@ class contribution_register_report(report_sxw.rml_parse): self.regi_total += line.total return res -report_sxw.report_sxw('report.contribution.register.lines', 'hr.contribution.register', 'hr_payroll/report/report_contribution_register.rml', parser=contribution_register_report) + +class wrapped_report_contribution_register(osv.AbstractModel): + _name = 'report.hr_payroll.report_contributionregister' + _inherit = 'report.abstract_report' + _template = 'hr_payroll.report_contributionregister' + _wrapped_report_class = contribution_register_report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_payroll/report/report_contribution_register.rml b/addons/hr_payroll/report/report_contribution_register.rml deleted file mode 100644 index 8a5897c6091..00000000000 --- a/addons/hr_payroll/report/report_contribution_register.rml +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [[repeatIn(objects,'o')]] - - - - PaySlip Lines by Contribution Register - - - - - - - - - - - - - - Register Name - - - Date From - - - Date To - - - - - - - [[ o.name or '']] - - - [[ data['form']['date_from'] or '']] - - - [[ data['form']['date_to'] or '' ]] - - - - - - - - - - - - - - PaySlip Name - - - Code - - - Name - - - Quantity/Rate - - - Amount - - - Total - - - - - - -
- [[repeatIn(get_payslip_lines(o),'r') ]] - - - - [[ r.get('payslip_name', False) ]][[ r.get('payslip_name', False) and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]] - - - [[ r['code'] ]] - - - [[ r['name'] ]] - - - [[ formatLang(r['quantity']) ]] - - - [[ formatLang(r['amount']) ]] - - - [[ formatLang(r['total'], currency_obj = o.company_id and o.company_id.currency_id)]] - - - -
- - - - - - - - - Total: - - - [[ formatLang(sum_total(), currency_obj = o.company_id and o.company_id.currency_id)]] - - - - - - -
-
- diff --git a/addons/hr_payroll/report/report_payslip.py b/addons/hr_payroll/report/report_payslip.py index 3e1dc2b1966..0c78ceec49a 100644 --- a/addons/hr_payroll/report/report_payslip.py +++ b/addons/hr_payroll/report/report_payslip.py @@ -21,8 +21,9 @@ # ############################################################################## +from openerp.osv import osv from openerp.report import report_sxw -from openerp.tools import amount_to_text_en + class payslip_report(report_sxw.rml_parse): @@ -37,12 +38,17 @@ class payslip_report(report_sxw.rml_parse): res = [] ids = [] for id in range(len(obj)): - if obj[id].appears_on_payslip == True: + if obj[id].appears_on_payslip is True: ids.append(obj[id].id) if ids: res = payslip_line.browse(self.cr, self.uid, ids) return res -report_sxw.report_sxw('report.payslip', 'hr.payslip', 'hr_payroll/report/report_payslip.rml', parser=payslip_report) + +class wrapped_report_payslip(osv.AbstractModel): + _name = 'report.hr_payroll.report_payslip' + _inherit = 'report.abstract_report' + _template = 'hr_payroll.report_payslip' + _wrapped_report_class = payslip_report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_payroll/report/report_payslip.rml b/addons/hr_payroll/report/report_payslip.rml deleted file mode 100644 index 990419ee5f1..00000000000 --- a/addons/hr_payroll/report/report_payslip.rml +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [[repeatIn(objects,'o')]] - - - - Pay Slip - - - - - [[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 display_address(o.employee_id.address_home_id)]] - - - - - - - Email - - - [[ o.employee_id.work_email or '' ]] - - - - Identification No - - - - [[ o.employee_id.identification_id or '' ]] - - - - - - - Reference - - - [[ o.number or '' ]] - - - Bank Account - - - [[ o.employee_id.otherid or '' ]] - - - - - - - Date From - - - [[ o.date_from or '']] - - - - Date To - - - - [[ o.date_to or '' ]] - - - - - - - - - - - Code - - - Name - - - Quantity/Rate - - - Amount - - - Total - - - -
- [[repeatIn(get_payslip_lines(o.line_ids),'p') ]] - - - - [[ p.code ]] - - - [[ p.name ]] - - - [[ formatLang(p.quantity) ]] - - - [[ formatLang(p.amount) ]] - - - [[ formatLang(p.total, currency_obj = o.company_id and o.company_id.currency_id)]] - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Authorized Signature - - - - - - -
-
- diff --git a/addons/hr_payroll/report/report_payslip_details.py b/addons/hr_payroll/report/report_payslip_details.py index 6b561b8d688..45d35373455 100644 --- a/addons/hr_payroll/report/report_payslip_details.py +++ b/addons/hr_payroll/report/report_payslip_details.py @@ -1,5 +1,4 @@ #-*- coding:utf-8 -*- - ############################################################################## # # OpenERP, Open Source Management Solution @@ -21,8 +20,9 @@ # ############################################################################## +from openerp.osv import osv from openerp.report import report_sxw -from openerp.tools import amount_to_text_en + class payslip_details_report(report_sxw.rml_parse): @@ -113,6 +113,11 @@ class payslip_details_report(report_sxw.rml_parse): }) return res -report_sxw.report_sxw('report.paylip.details', 'hr.payslip', 'hr_payroll/report/report_payslip_details.rml', parser=payslip_details_report) + +class wrapped_report_payslipdetails(osv.AbstractModel): + _name = 'report.hr_payroll.report_payslipdetails' + _inherit = 'report.abstract_report' + _template = 'hr_payroll.report_payslipdetails' + _wrapped_report_class = payslip_details_report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_payroll/report/report_payslip_details.rml b/addons/hr_payroll/report/report_payslip_details.rml deleted file mode 100644 index 4ace0ad2f82..00000000000 --- a/addons/hr_payroll/report/report_payslip_details.rml +++ /dev/null @@ -1,426 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [[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 display_address(o.employee_id.address_home_id)]] - - - - - - - Email - - - [[ o.employee_id.work_email or '' ]] - - - - Identification No - - - - [[ o.employee_id.identification_id or '' ]] - - - - - - - Reference - - - [[ o.number or '' ]] - - - Bank Account - - - [[ o.employee_id.otherid or '' ]] - - - - - - - Date From - - - [[ o.date_from or '']] - - - - Date To - - - - [[ o.date_to or '' ]] - - - - - - - - - - - - - - Details by Salary Rule Category: - - - - - - - Code - - - Salary Rule Category - - - Total - - - - - - -
- [[repeatIn(get_details_by_rule_category(o.details_by_salary_rule_category),'h') ]] - - - - - [[ 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'], currency_obj = o.company_id and o.company_id.currency_id)]] [[ h['level']==0 and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font') ]] - - - -
- - - - - - - Payslip Lines by Contribution Register: - - - - - - - Register Name - - - Code - - - Name - - - Quantity/Rate - - - Amount - - - Total - - - -
- [[repeatIn(get_lines_by_contribution_register(o.details_by_salary_rule_category),'r') ]] - - - - [[ r.get('register_name', False) ]][[ h.get('register_name', False) and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]] - - - [[ r['code'] ]] - - - [[ r['name'] ]] - - - [[ formatLang(r['quantity']) ]] - - - [[ formatLang(r['amount']) ]] - - - [[ formatLang(r['total'], currency_obj = o.company_id and o.company_id.currency_id)]][[ r.get('register_name', False) and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font')]] - - - -
- - - - - - - - - - - - - - - - - - Authorized Signature - - - - - - -
-
- diff --git a/addons/hr_payroll/test/hr_payroll_report.yml b/addons/hr_payroll/test/hr_payroll_report.yml deleted file mode 100644 index ab4b0fa97e1..00000000000 --- a/addons/hr_payroll/test/hr_payroll_report.yml +++ /dev/null @@ -1,12 +0,0 @@ -- - In order to test the PDF reports defined on HR Payroll, we will print Employees' Salary Structure -- - Print HR Payslip -- - !python {model: hr.payslip}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('hr_payroll.hr_payslip_salaryslipofbonamyforjune0')], 'payslip.pdf', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'hr_payroll-payslip_report.'+format), 'wb+').write(data) diff --git a/addons/hr_payroll/test/payslip.yml b/addons/hr_payroll/test/payslip.yml index f78ad4a374d..b4029146b6d 100644 --- a/addons/hr_payroll/test/payslip.yml +++ b/addons/hr_payroll/test/payslip.yml @@ -107,8 +107,29 @@ date_from: '2011-09-30' date_to: '2011-09-01' - - I print the report. + I print the payslip report - - !python {model: payslip.lines.contribution.register}: | - self.print_report(cr, uid, [ref('payslip_lines_contribution_register0')], context={'active_ids': [ref('hr_houserent_register')]}) - + !python {model: hr.payslip}: | + import os + import openerp.report + from openerp import tools + data, format = openerp.report.render_report(cr, uid, [ref('hr_payslip_0')], 'hr_payroll.report_payslip', {}, {}) + if tools.config['test_report_directory']: + file(os.path.join(tools.config['test_report_directory'], 'hr_payroll-payslip.'+format), 'wb+').write(data) +- + I print the payslip details report +- + !python {model: hr.payslip}: | + import os + import openerp.report + from openerp import tools + data, format = openerp.report.render_report(cr, uid, [ref('hr_payslip_0')], 'hr_payroll.report_payslipdetails', {}, {}) + if tools.config['test_report_directory']: + file(os.path.join(tools.config['test_report_directory'], 'hr_payroll-payslipdetails.'+format), 'wb+').write(data) +- + I print the contribution register report +- + !python {model: hr.contribution.register}: | + ctx={'model': 'hr.contribution.register', 'active_ids': [ref('hr_houserent_register')]} + from openerp.tools import test_reports + test_reports.try_report_action(cr, uid, 'action_payslip_lines_contribution_register', context=ctx, our_module='hr_payroll') diff --git a/addons/hr_payroll/views/report_contributionregister.xml b/addons/hr_payroll/views/report_contributionregister.xml new file mode 100644 index 00000000000..814a6025268 --- /dev/null +++ b/addons/hr_payroll/views/report_contributionregister.xml @@ -0,0 +1,68 @@ + + + + + + diff --git a/addons/hr_payroll/views/report_payslip.xml b/addons/hr_payroll/views/report_payslip.xml new file mode 100644 index 00000000000..5ac9dfa557e --- /dev/null +++ b/addons/hr_payroll/views/report_payslip.xml @@ -0,0 +1,74 @@ + + + + + + diff --git a/addons/hr_payroll/views/report_payslipdetails.xml b/addons/hr_payroll/views/report_payslipdetails.xml new file mode 100644 index 00000000000..acd0fbc2610 --- /dev/null +++ b/addons/hr_payroll/views/report_payslipdetails.xml @@ -0,0 +1,99 @@ + + + + + + diff --git a/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py b/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py index ff078b09a67..20e3c0413aa 100644 --- a/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py +++ b/addons/hr_payroll/wizard/hr_payroll_contribution_register_report.py @@ -22,9 +22,9 @@ import time from datetime import datetime from dateutil import relativedelta - from openerp.osv import fields, osv + class payslip_lines_contribution_register(osv.osv_memory): _name = 'payslip.lines.contribution.register' _description = 'PaySlip Lines by Contribution Registers' @@ -44,11 +44,8 @@ class payslip_lines_contribution_register(osv.osv_memory): 'model': 'hr.contribution.register', 'form': self.read(cr, uid, ids, [], context=context)[0] } - return { - 'type': 'ir.actions.report.xml', - 'report_name': 'contribution.register.lines', - 'datas': datas, - } - + return self.pool['report'].get_action( + cr, uid, [], 'hr_payroll.report_contributionregister', data=datas, context=context + ) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index 0f943a46cde..753b0bac183 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -80,6 +80,7 @@ class hr_applicant(osv.Model): _description = "Applicant" _order = "id desc" _inherit = ['mail.thread', 'ir.needaction_mixin'] + _track = { 'stage_id': { # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages @@ -87,6 +88,7 @@ class hr_applicant(osv.Model): 'hr_recruitment.mt_applicant_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1, }, } + _mail_mass_mailing = _('Applicants') def _get_default_department_id(self, cr, uid, context=None): """ Gives default department by checking if present in the context """ diff --git a/addons/hr_timesheet_invoice/__openerp__.py b/addons/hr_timesheet_invoice/__openerp__.py index d15dd3b0e3b..c2857ba4ceb 100644 --- a/addons/hr_timesheet_invoice/__openerp__.py +++ b/addons/hr_timesheet_invoice/__openerp__.py @@ -35,7 +35,7 @@ reports.""", 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', 'images': ['images/hr_bill_task_work.jpeg','images/hr_type_of_invoicing.jpeg'], - 'depends': ['account', 'hr_timesheet'], + 'depends': ['account', 'hr_timesheet', 'report'], 'data': [ 'security/ir.model.access.csv', 'hr_timesheet_invoice_data.xml', @@ -47,6 +47,7 @@ reports.""", 'wizard/hr_timesheet_analytic_profit_view.xml', 'wizard/hr_timesheet_invoice_create_view.xml', 'wizard/hr_timesheet_invoice_create_final_view.xml', + 'views/report_analyticprofit.xml', ], 'demo': ['hr_timesheet_invoice_demo.xml'], 'test': ['test/test_hr_timesheet_invoice.yml', diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml index 473820d5123..3c6df8ef378 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml @@ -2,13 +2,12 @@ - + name="hr_timesheet_invoice.report_analyticprofit" + file="hr_timesheet_invoice.report_analyticprofit" + report_type="qweb-pdf" + string="Timesheet Profit" + /> diff --git a/addons/hr_timesheet_invoice/report/account_analytic_profit.py b/addons/hr_timesheet_invoice/report/account_analytic_profit.py index bfaf585aff5..3f7108fb665 100644 --- a/addons/hr_timesheet_invoice/report/account_analytic_profit.py +++ b/addons/hr_timesheet_invoice/report/account_analytic_profit.py @@ -20,6 +20,8 @@ ############################################################################## from openerp.report import report_sxw +from openerp.osv import osv + class account_analytic_profit(report_sxw.rml_parse): def __init__(self, cr, uid, name, context): @@ -30,6 +32,7 @@ class account_analytic_profit(report_sxw.rml_parse): 'journal_ids': self._journal_ids, 'line': self._line, }) + def _user_ids(self, lines): user_obj = self.pool['res.users'] ids=list(set([b.user_id.id for b in lines])) @@ -116,6 +119,11 @@ class account_analytic_profit(report_sxw.rml_parse): ]) return line_obj.browse(self.cr, self.uid, ids) -report_sxw.report_sxw('report.account.analytic.profit', 'account.analytic.line', 'addons/hr_timesheet_invoice/report/account_analytic_profit.rml', parser=account_analytic_profit) + +class report_account_analytic_profit(osv.AbstractModel): + _name = 'report.hr_timesheet_invoice.report_analyticprofit' + _inherit = 'report.abstract_report' + _template = 'hr_timesheet_invoice.report_analyticprofit' + _wrapped_report_class = account_analytic_profit # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_timesheet_invoice/report/account_analytic_profit.rml b/addons/hr_timesheet_invoice/report/account_analytic_profit.rml deleted file mode 100644 index 57ca806061a..00000000000 --- a/addons/hr_timesheet_invoice/report/account_analytic_profit.rml +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Invoice rate by user - - - - - - - - - - - - - - - - - - Period from startdate - - - Period to enddate - - - Currency - - - - - - - [[ formatLang(data['form']['date_from'],date=True) ]] - - - [[ formatLang (data['form']['date_to'] ,date=True)]] - - - [[ company.currency_id.name ]] - - - - - - - - - - - - - User or Journal Name - - - Units - - - Theorical - - - Income - - - Cost - - - Profit - - - Eff. - - - - - - - - - - Totals: - - - [[ reduce(lambda x, y: x+y['unit_amount'], line(data['form'], data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]] - - - - - - - - [[ reduce(lambda x, y: x+y['amount'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]] - - - [[ reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]] - - - [[ reduce(lambda x, y: x+y['profit'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]] - - - [[ reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) and round(reduce(lambda x, y: x+y['amount'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)/reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)* -100, 2)]] % - - - - -
- [[ repeatIn(user_ids(lines(data['form'])), 'e') ]] - - - - [[ e.name ]] - - - [[ repeatIn(journal_ids(data['form'], [e.id]), 'j') ]] - - - [[ reduce(lambda x, y: x+y['unit_amount'], line(data['form'], [j.id], [e.id]), 0) ]] - - - [[ reduce(lambda x, y: x+y['amount_th'], line(data['form'], [j.id], [e.id]), 0) ]] - - - [[ reduce(lambda x, y: x+y['amount'], line(data['form'], [j.id], [e.id]), 0) ]] - - - [[ reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) ]] - - - [[ reduce(lambda x, y: x+y['profit'], line(data['form'], [j.id], [e.id]), 0) ]] - - - [[reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) and '%d' % (reduce(lambda x, y: x+y['amount'], line(data['form'], [j.id], [e.id]), 0) / reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) * 100.0, 2)]] % - - - - - - - [[ j.name ]] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- [[ repeatIn(line(data['form'], [j.id],[e.id]), 'l') ]] - - - - [[ l['name'] ]] - - - [[ l['unit_amount'] ]] - - - [[ l['amount_th'] ]] - - - [[ l['amount'] ]] - - - [[ l['cost'] ]] - - - [[ l['profit'] ]] - - - [[ l['eff'] ]] % - - - - - - -
- - - -
- - - -
-
diff --git a/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml b/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml index 39c3161f244..e2fa5b344a9 100644 --- a/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml +++ b/addons/hr_timesheet_invoice/test/hr_timesheet_invoice_report.yml @@ -6,6 +6,6 @@ import openerp.report from openerp import tools data_dict = {'model': 'ir.ui.menu', 'form': {'date_from': time.strftime('%Y-%m-01'), 'employee_ids': [[6,0,[ref('hr.employee_fp'), ref('hr.employee_qdp'),ref('hr.employee_al')]]], 'journal_ids': [[6,0,[ref('hr_timesheet.analytic_journal')]]], 'date_to': time.strftime('%Y-%m-%d')}} - data, format = openerp.report.render_report(cr, uid, [], 'account.analytic.profit', data_dict, {}) + data, format = openerp.report.render_report(cr, uid, [], 'hr_timesheet_invoice.report_analyticprofit', data_dict, {}) if tools.config['test_report_directory']: file(os.path.join(tools.config['test_report_directory'], 'hr_timesheet_invoice-account_analytic_profit_report.'+format), 'wb+').write(data) diff --git a/addons/hr_timesheet_invoice/views/report_analyticprofit.xml b/addons/hr_timesheet_invoice/views/report_analyticprofit.xml new file mode 100644 index 00000000000..c067a9b1d2d --- /dev/null +++ b/addons/hr_timesheet_invoice/views/report_analyticprofit.xml @@ -0,0 +1,84 @@ + + + + + + diff --git a/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py b/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py index 2675556e8ac..a78bc1c5bf5 100644 --- a/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py +++ b/addons/hr_timesheet_invoice/wizard/hr_timesheet_analytic_profit.py @@ -23,6 +23,7 @@ import datetime from openerp.osv import fields, osv from openerp.tools.translate import _ + class account_analytic_profit(osv.osv_memory): _name = 'hr.timesheet.analytic.profit' _description = 'Print Timesheet Profit' @@ -60,15 +61,12 @@ class account_analytic_profit(osv.osv_memory): data['form']['journal_ids'] = [(6, 0, data['form']['journal_ids'])] # Improve me => Change the rml/sxw so that it can support withou [0][2] data['form']['employee_ids'] = [(6, 0, data['form']['employee_ids'])] datas = { - 'ids': [], - 'model': 'account.analytic.line', - 'form': data['form'] - } - return { - 'type': 'ir.actions.report.xml', - 'report_name': 'account.analytic.profit', - 'datas': datas, - } - + 'ids': [], + 'model': 'account.analytic.line', + 'form': data['form'] + } + return self.pool['report'].get_action( + cr, uid, [], 'hr_timesheet_invoice.report_analyticprofit', data=datas, context=context + ) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index a33eaaea15d..12903d02236 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -126,7 +126,7 @@ class mail_mail(osv.Model): _logger.exception("Failed processing mail queue") return res - def _postprocess_sent_message(self, cr, uid, mail, context=None): + def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True): """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. @@ -145,9 +145,8 @@ class mail_mail(osv.Model): #------------------------------------------------------ def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None): - """ Generate URLs for links in mails: - - partner is an user and has read access to the document: direct link to document with model, res_id - """ + """Generate URLs for links in mails: partner has access (is user): + link to action_mail_redirect action that will redirect to doc or Inbox """ if partner and partner.user_ids: base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') # the parameters to encode for the query and fragment part of url @@ -167,11 +166,10 @@ class mail_mail(osv.Model): return None def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): - """ If subject is void and record_name defined: ' posted on ' + """If subject is void, set the subject as 'Re: ' or + 'Re: ' :param boolean force: force the subject replacement - :param browse_record mail: mail.mail browse_record - :param browse_record partner: specific recipient partner """ if (force or not mail.subject) and mail.record_name: return 'Re: %s' % (mail.record_name) @@ -180,12 +178,8 @@ class mail_mail(osv.Model): return mail.subject def send_get_mail_body(self, cr, uid, mail, partner=None, context=None): - """ Return a specific ir_email body. The main purpose of this method - is to be inherited to add custom content depending on some module. - - :param browse_record mail: mail.mail browse_record - :param browse_record partner: specific recipient partner - """ + """Return a specific ir_email body. The main purpose of this method + is to be inherited to add custom content depending on some module.""" body = mail.body_html # generate footer @@ -194,34 +188,34 @@ class mail_mail(osv.Model): body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div') return body - def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): - """ Return a dictionary for specific email values, depending on a - partner, or generic to the whole recipients given by mail.email_to. - - :param browse_record mail: mail.mail browse_record - :param browse_record partner: specific recipient partner - """ - body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) - subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context) - body_alternative = tools.html2plaintext(body) - - # generate email_to, heuristic: - # 1. if 'partner' is specified and there is a related document: Followers of 'Doc' - # 2. if 'partner' is specified, but no related document: Partner Name - # 3; fallback on mail.email_to that we split to have an email addresses list - if partner and mail.record_name: + def send_get_mail_to(self, cr, uid, mail, partner=None, context=None): + """Forge the email_to with the following heuristic: + - if 'partner' and mail is a notification on a document: followers (Followers of 'Doc' ) + - elif 'partner', no notificatoin or no doc: recipient specific (Partner Name ) + - else fallback on mail.email_to splitting """ + if partner and mail.notification and mail.record_name: sanitized_record_name = re.sub(r'[^\w+.]+', '-', mail.record_name) email_to = [_('"Followers of %s" <%s>') % (sanitized_record_name, partner.email)] elif partner: email_to = ['%s <%s>' % (partner.name, partner.email)] else: email_to = tools.email_split(mail.email_to) + return email_to + def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): + """Return a dictionary for specific email values, depending on a + partner, or generic to the whole recipients given by mail.email_to. + + :param browse_record mail: mail.mail browse_record + :param browse_record partner: specific recipient partner + """ + body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) + body_alternative = tools.html2plaintext(body) return { 'body': body, 'body_alternative': body_alternative, - 'subject': subject, - 'email_to': email_to, + 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context), + 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context), } def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None): @@ -240,7 +234,7 @@ class mail_mail(osv.Model): :return: True """ ir_mail_server = self.pool.get('ir.mail_server') - + for mail in self.browse(cr, SUPERUSER_ID, ids, context=context): try: # handle attachments @@ -284,7 +278,7 @@ class mail_mail(osv.Model): 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}) mail_sent = True @@ -294,11 +288,11 @@ class mail_mail(osv.Model): # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1 - if mail_sent: - self._postprocess_sent_message(cr, uid, mail, context=context) + self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent) except Exception as e: _logger.exception('failed sending mail.mail %s', mail.id) mail.write({'state': 'exception'}) + self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False) if raise_exception: if isinstance(e, AssertionError): # get the args of the original error, wrap into a value and throw a MailDeliveryException @@ -307,6 +301,6 @@ class mail_mail(osv.Model): raise MailDeliveryException(_("Mail Delivery Failed"), value) raise - if auto_commit == True: + if auto_commit is True: cr.commit() return True diff --git a/addons/mail/mail_mail_view.xml b/addons/mail/mail_mail_view.xml index cf3dd1e8233..e259d28a2b7 100644 --- a/addons/mail/mail_mail_view.xml +++ b/addons/mail/mail_mail_view.xml @@ -36,6 +36,7 @@
+ diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 4c05d7a6d1f..5b716a14ef2 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -81,22 +81,6 @@ class mail_message(osv.Model): context = dict(context, default_type=None) return super(mail_message, self).default_get(cr, uid, fields, context=context) - 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_record_name(self, cr, uid, ids, name, arg, context=None): - """ Return the related document name, using name_get. It is done using - SUPERUSER_ID, to be sure to have the record name correctly stored. """ - # TDE note: regroup by model/ids, to have less queries to perform - result = dict.fromkeys(ids, False) - for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context): - if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool: - continue - result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1] - return result - def _get_to_read(self, cr, uid, ids, name, arg, context=None): """ Compute if the message is unread by the current user. """ res = dict((id, False) for id in ids) @@ -135,16 +119,6 @@ class mail_message(osv.Model): inversed because we search unread message on a read column. """ return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])] - 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 = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '') - res.append((message.id, self._shorten_name(name.lstrip(' :')))) - return res - _columns = { 'type': fields.selection([ ('email', 'Email'), @@ -172,9 +146,7 @@ class mail_message(osv.Model): '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='char', - store=True, string='Message Record Name', - help="Name get of the related document."), + 'record_name': fields.char('Message Record Name', help="Name get of the related document."), 'notification_ids': fields.one2many('mail.notification', 'message_id', string='Notifications', auto_join=True, help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'), @@ -783,6 +755,13 @@ class mail_message(osv.Model): _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \ (self._description, operation)) + def _get_record_name(self, cr, uid, values, context=None): + """ Return the related document name, using name_get. It is done using + SUPERUSER_ID, to be sure to have the record name correctly stored. """ + if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool: + return False + return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1] + def _get_reply_to(self, cr, uid, values, context=None): """ Return a specific reply_to: alias of the document through message_get_reply_to or take the email_from @@ -841,8 +820,11 @@ class mail_message(osv.Model): values['message_id'] = self._get_message_id(cr, uid, values, context=context) if 'reply_to' not in values: values['reply_to'] = self._get_reply_to(cr, uid, values, context=context) + if 'record_name' not in values and 'default_record_name' not in context: + values['record_name'] = self._get_record_name(cr, uid, values, context=context) newid = super(mail_message, self).create(cr, uid, values, context) + self._notify(cr, uid, newid, context=context, force_send=context.get('mail_notify_force_send', True), user_signature=context.get('mail_notify_user_signature', True)) @@ -887,78 +869,6 @@ class mail_message(osv.Model): # Messaging API #------------------------------------------------------ - # TDE note: this code is not used currently, will be improved in a future merge, when quoted context - # will be added to email send for notifications. Currently only WIP. - MAIL_TEMPLATE = """
- % if message: - ${display_message(message)} - % endif - % for ctx_msg in context_messages: - ${display_message(ctx_msg)} - % endfor - % if add_expandable: - ${display_expandable()} - % endif - ${display_message(header_message)} -
- - <%def name="display_message(message)"> -
- Subject: ${message.subject}
- Body: ${message.body} -
- - - <%def name="display_expandable()"> -
This is an expandable.
- - """ - - def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False): - """ - 1. message.parent_id = False: new thread, no quote_context - 2. get the lasts messages in the thread before message - 3. get the message header - 4. add an expandable between them - - :param dict quote_context: options for quoting - :return string: html quote - """ - add_expandable = False - - message = self.browse(cr, uid, id, context=context) - if not message.parent_id: - return '' - context_ids = self.search(cr, uid, [ - ('parent_id', '=', message.parent_id.id), - ('id', '<', message.id), - ], limit=limit, context=context) - - if len(context_ids) >= limit: - add_expandable = True - context_ids = context_ids[0:-1] - - context_ids.append(message.parent_id.id) - context_messages = self.browse(cr, uid, context_ids, context=context) - header_message = context_messages.pop() - - try: - if not add_original: - message = False - result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message, - context_messages=context_messages, - header_message=header_message, - add_expandable=add_expandable, - # context kw would clash with mako internals - ctx=context, - format_exceptions=True) - result = result.strip() - return result - except Exception: - _logger.exception("failed to render mako template for quoting message") - return '' - return result - def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True): """ Add the related record followers to the destination partner_ids if is not a private message. Call mail_notification.notify to manage the email sending @@ -975,9 +885,11 @@ class mail_message(osv.Model): cr, SUPERUSER_ID, [ ('res_model', '=', message.model), ('res_id', '=', message.res_id), - ('subtype_ids', 'in', message.subtype_id.id) ], context=context) - partners_to_notify |= set(fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)) + partners_to_notify |= set( + fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context) + if message.subtype_id.id in [st.id for st in fo.subtype_ids] + ) # remove me from notified partners, unless the message is written on my own wall if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id: partners_to_notify |= set([message.author_id.id]) @@ -1006,25 +918,3 @@ class mail_message(osv.Model): 'partner_id': partner.id, 'read': True, }, context=context) - - #------------------------------------------------------ - # Tools - #------------------------------------------------------ - - def check_partners_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, - } - } diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 73588207d6a..112dff6bac6 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -31,6 +31,7 @@ except ImportError: from lxml import etree import logging import pytz +import socket import time import xmlrpclib from email.message import Message @@ -96,6 +97,9 @@ class mail_thread(osv.AbstractModel): # :param function lambda: returns whether the tracking should record using this subtype _track = {} + # Mass mailing feature + _mail_mass_mailing = False + def get_empty_list_help(self, cr, uid, help, context=None): """ Override of BaseModel.get_empty_list_help() to generate an help message that adds alias information. """ @@ -584,23 +588,6 @@ class mail_thread(osv.AbstractModel): model_obj.check_access_rights(cr, uid, check_operation) model_obj.check_access_rule(cr, uid, mids, check_operation, context=context) - def _get_formview_action(self, cr, uid, id, model=None, context=None): - """ Return an action to open the document. This method is meant to be - overridden in addons that want to give specific view ids for example. - - :param int id: id of the document to open - :param string model: specific model that overrides self._name - """ - return { - 'type': 'ir.actions.act_window', - 'res_model': model or self._name, - 'view_type': 'form', - 'view_mode': 'form', - 'views': [(False, 'form')], - 'target': 'current', - 'res_id': id, - } - def _get_inbox_action_xml_id(self, cr, uid, context=None): """ When redirecting towards the Inbox, choose which action xml_id has to be fetched. This method is meant to be inherited, at least in portal @@ -643,10 +630,7 @@ class mail_thread(osv.AbstractModel): if model_obj.check_access_rights(cr, uid, 'read', raise_exception=False): try: model_obj.check_access_rule(cr, uid, [res_id], 'read', context=context) - if not hasattr(model_obj, '_get_formview_action'): - action = self.pool.get('mail.thread')._get_formview_action(cr, uid, res_id, model=model, context=context) - else: - action = model_obj._get_formview_action(cr, uid, res_id, context=context) + action = model_obj.get_formview_action(cr, uid, res_id, context=context) except (osv.except_osv, orm.except_orm): pass action.update({ @@ -661,15 +645,31 @@ class mail_thread(osv.AbstractModel): # Email specific #------------------------------------------------------ + def message_get_default_recipients(self, cr, uid, ids, context=None): + if context and context.get('thread_model') and context['thread_model'] in self.pool and context['thread_model'] != self._name: + sub_ctx = dict(context) + sub_ctx.pop('thread_model') + return self.pool[context['thread_model']].message_get_default_recipients(cr, uid, ids, context=sub_ctx) + res = {} + for record in self.browse(cr, SUPERUSER_ID, ids, context=context): + recipient_ids, email_to, email_cc = set(), False, False + if 'partner_id' in self._all_columns and record.partner_id: + recipient_ids.add(record.partner_id.id) + elif 'email_from' in self._all_columns and record.email_from: + email_to = record.email_from + elif 'email' in self._all_columns: + email_to = record.email + res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc} + return res + def message_get_reply_to(self, cr, uid, ids, context=None): """ Returns the preferred reply-to email address that is basically the alias of the document, if it exists. """ if not self._inherits.get('mail.alias'): return [False for id in ids] - return ["%s@%s" % (record['alias_name'], record['alias_domain']) - if record.get('alias_domain') and record.get('alias_name') - else False - for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)] + return ["%s@%s" % (record.alias_name, record.alias_domain) + if record.alias_domain and record.alias_name else False + for record in self.browse(cr, SUPERUSER_ID, ids, context=context)] #------------------------------------------------------ # Mail gateway @@ -880,25 +880,30 @@ class mail_thread(osv.AbstractModel): # 2. message is a reply to an existign thread (6.1 compatibility) ref_match = thread_references and tools.reference_re.search(thread_references) if ref_match: - thread_id = int(ref_match.group(1)) - model = ref_match.group(2) or fallback_model - if thread_id and model in self.pool: - model_obj = self.pool[model] - compat_mail_msg_ids = mail_msg_obj.search( - cr, uid, [ - ('message_id', '=', False), - ('model', '=', model), - ('res_id', '=', thread_id), - ], context=context) - if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'): - _logger.info( - 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s', - email_from, email_to, message_id, model, thread_id, custom_values, uid) - route = self.message_route_verify( - cr, uid, message, message_dict, - (model, thread_id, custom_values, uid, None), - update_author=True, assert_model=True, create_fallback=True, context=context) - return route and [route] or [] + reply_thread_id = int(ref_match.group(1)) + reply_model = ref_match.group(2) or fallback_model + reply_hostname = ref_match.group(3) + local_hostname = socket.gethostname() + # do not match forwarded emails from another OpenERP system (thread_id collision!) + if local_hostname == reply_hostname: + thread_id, model = reply_thread_id, reply_model + if thread_id and model in self.pool: + model_obj = self.pool[model] + compat_mail_msg_ids = mail_msg_obj.search( + cr, uid, [ + ('message_id', '=', False), + ('model', '=', model), + ('res_id', '=', thread_id), + ], context=context) + if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'): + _logger.info( + 'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s', + email_from, email_to, message_id, model, thread_id, custom_values, uid) + route = self.message_route_verify( + cr, uid, message, message_dict, + (model, thread_id, custom_values, uid, None), + update_author=True, assert_model=True, create_fallback=True, context=context) + return route and [route] or [] # 2. Reply to a private message if in_reply_to: diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py index 8da2441f3bc..8b8f0d3c145 100644 --- a/addons/mail/res_partner.py +++ b/addons/mail/res_partner.py @@ -28,6 +28,7 @@ class res_partner_mail(osv.Model): _name = "res.partner" _inherit = ['res.partner', 'mail.thread'] _mail_flat_thread = False + _mail_mass_mailing = _('Customers') _columns = { 'notification_email_send': fields.selection([ @@ -53,4 +54,5 @@ class res_partner_mail(osv.Model): self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile')) return recipients -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + def message_get_default_recipients(self, cr, uid, ids, context=None): + return dict((id, {'partner_ids': [id], 'email_to': False, 'email_cc': False}) for id in ids) diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 17dd4b57cd0..99a7751e15e 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -507,18 +507,15 @@ openerp.mail = function (session) { } $.when(recipient_done).done(function (partner_ids) { var context = { - 'default_composition_mode': default_composition_mode, 'default_parent_id': self.id, 'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''), 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), 'default_partner_ids': partner_ids, + 'default_is_log': self.is_log, 'mail_post_autofollow': true, 'mail_post_autofollow_partner_ids': partner_ids, 'is_private': self.is_private }; - if (self.is_log) { - _.extend(context, {'mail_compose_log': true}); - } if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) { context.default_model = self.context.default_model; context.default_res_id = self.context.default_res_id; diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index c7f1056c59a..bea80b729a4 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -210,24 +210,6 @@ class test_mail(TestMail): self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs') self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs') - def test_10_message_quote_context(self): - """ Tests designed for message_post. """ - cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs - - msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'}) - msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id}) - - result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3) - self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.') - self.assertIn('Second answer', result, 'Answer should be in quote.') - self.assertIn('Third answer', result, 'Answer should be in quote.') - self.assertIn('expandable', result, 'Expandable should be present.') - self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.') - self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.') - def test_11_notification_url(self): """ Tests designed to test the URL added in notification emails. """ cr, uid, group_pigs = self.cr, self.uid, self.group_pigs @@ -674,7 +656,6 @@ class test_mail(TestMail): 'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])] }, context={ 'default_composition_mode': 'reply', - 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id }) @@ -699,11 +680,10 @@ class test_mail(TestMail): # -------------------------------------------------- # Do: Compose in mass_mail_mode on pigs and bird - compose_id = mail_compose.create(cr, user_raoul.id, - { + compose_id = mail_compose.create( + cr, user_raoul.id, { 'subject': _subject, 'body': '${object.description}', - 'post': True, 'partner_ids': [(4, p_c_id), (4, p_d_id)], }, context={ 'default_composition_mode': 'mass_mail', @@ -718,6 +698,13 @@ class test_mail(TestMail): 'default_res_id': -1, 'active_ids': [self.group_pigs_id, group_bird_id] }) + # check mail_mail + mail_mail_ids = self.mail_mail.search(cr, uid, [('subject', '=', _subject)]) + for mail_mail in self.mail_mail.browse(cr, uid, mail_mail_ids): + self.assertEqual(set([p.id for p in mail_mail.recipient_ids]), set([p_c_id, p_d_id]), + 'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients') + + # check logged messages group_pigs.refresh() group_bird.refresh() message1 = group_pigs.message_ids[0] @@ -733,14 +720,14 @@ class test_mail(TestMail): 'compose wizard: message_post: mail.message in mass mail subject incorrect') self.assertEqual(message1.body, '

%s

' % group_pigs.description, 'compose wizard: message_post: mail.message in mass mail body incorrect') - self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]), - 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') + # self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]), + # 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') self.assertEqual(message2.subject, _subject, 'compose wizard: message_post: mail.message in mass mail subject incorrect') self.assertEqual(message2.body, '

%s

' % group_bird.description, 'compose wizard: message_post: mail.message in mass mail body incorrect') - self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]), - 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') + # self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]), + # 'compose wizard: message_post: mail.message in mass mail incorrect notified partners') # Test: mail.group followers: author not added as follower in mass mail mode pigs_pids = [p.id for p in group_pigs.message_follower_ids] @@ -757,7 +744,6 @@ class test_mail(TestMail): { 'subject': _subject, 'body': '${object.description}', - 'post': True, 'partner_ids': [(4, p_c_id), (4, p_d_id)], }, context={ 'default_composition_mode': 'mass_mail', diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index cb735ec098e..3fbf3d40405 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -21,6 +21,7 @@ from openerp.addons.mail.tests.common import TestMail from openerp.tools import mute_logger +import socket MAIL_TEMPLATE = """Return-Path: To: {to} @@ -400,13 +401,15 @@ class TestMailgateway(TestMail): to='noone@example.com', subject='spam', extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>' % frog_group.id, msg_id='<1.1.JavaMail.new@agrolait.com>') - # There are 6.1 messages, activate compat mode + + # When 6.1 messages are present, compat mode is available + # Create a fake 6.1 message tmp_msg_id = self.mail_message.create(cr, uid, {'message_id': False, 'model': 'mail.group', 'res_id': frog_group.id}) # Do: compat mode accepts partial-matching emails frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other5@gmail.com', msg_id='<1.2.JavaMail.new@agrolait.com>', to='noone@example.com>', subject='spam', - extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>' % frog_group.id) + extra='In-Reply-To: <12321321-openerp-%d-mail.group@%s>' % (frog_group.id, socket.gethostname())) self.mail_message.unlink(cr, uid, [tmp_msg_id]) # Test: no group 'Re: news' created, still only 1 Frogs group self.assertEqual(len(frog_groups), 0, @@ -418,6 +421,17 @@ class TestMailgateway(TestMail): # Test: one new message self.assertEqual(len(frog_group.message_ids), 4, 'message_process: group should contain 4 messages after reply') + # 6.1 compat mode should not work if hostname does not match! + tmp_msg_id = self.mail_message.create(cr, uid, {'message_id': False, 'model': 'mail.group', 'res_id': frog_group.id}) + self.assertRaises(ValueError, + format_and_process, + MAIL_TEMPLATE, email_from='other5@gmail.com', + msg_id='<1.3.JavaMail.new@agrolait.com>', + to='noone@example.com>', subject='spam', + extra='In-Reply-To: <12321321-openerp-%d-mail.group@neighbor.com>' % frog_group.id) + self.mail_message.unlink(cr, uid, [tmp_msg_id]) + + # Do: due to some issue, same email goes back into the mailgateway frog_groups = format_and_process(MAIL_TEMPLATE, email_from='other4@gmail.com', msg_id='<1198923581.41972151344608186760.JavaMail.diff1@agrolait.com>', @@ -445,7 +459,7 @@ class TestMailgateway(TestMail): # Do: post a new message, with a known partner -> duplicate emails -> partner format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ', - to='erroneous@example.com>', subject='Re: news (2)', + subject='Re: news (2)', msg_id='<1198923581.41972151344608186760.JavaMail.new1@agrolait.com>', extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>') frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')]) @@ -456,10 +470,9 @@ class TestMailgateway(TestMail): # Do: post a new message, with a known partner -> duplicate emails -> user frog_group.message_unsubscribe([extra_partner_id]) - raoul_email = self.user_raoul.email self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'}) format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ', - to='erroneous@example.com>', subject='Re: news (3)', + to='groups@example.com', subject='Re: news (3)', msg_id='<1198923581.41972151344608186760.JavaMail.new2@agrolait.com>', extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>') frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')]) @@ -474,7 +487,7 @@ class TestMailgateway(TestMail): raoul_email = self.user_raoul.email self.res_users.write(cr, uid, self.user_raoul_id, {'email': 'test_raoul@email.com'}) format_and_process(MAIL_TEMPLATE, email_from='Lombrik Lubrik ', - to='erroneous@example.com>', subject='Re: news (3)', + to='groups@example.com', subject='Re: news (3)', msg_id='<1198923581.41972151344608186760.JavaMail.new3@agrolait.com>', extra='In-Reply-To: <1198923581.41972151344608186799.JavaMail.diff1@agrolait.com>') frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'Frogs')]) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index e5ff2b51fcd..e14a8f3dfe5 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -38,10 +38,7 @@ class mail_compose_message(osv.TransientModel): at model and view levels to provide specific features. 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`` + - 'comment': 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. @@ -50,6 +47,7 @@ class mail_compose_message(osv.TransientModel): _inherit = 'mail.message' _description = 'Email composition wizard' _log_access = True + _batch_size = 500 def default_get(self, cr, uid, fields, context=None): """ Handle composition mode. Some details about context keys: @@ -68,28 +66,22 @@ class mail_compose_message(osv.TransientModel): if context is None: context = {} 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') + + # v6.1 compatibility mode + result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode')) + result['model'] = result.get('model', context.get('active_model')) + result['res_id'] = result.get('res_id', context.get('active_id')) + result['parent_id'] = result.get('parent_id', context.get('message_id')) + + # default values according to composition mode - NOTE: reply is deprecated, fall back on comment + if result['composition_mode'] == 'reply': + result['composition_mode'] = 'comment' + vals = {} if 'active_domain' in context: # not context.get() because we want to keep global [] domains - result['use_active_domain'] = True - result['active_domain'] = '%s' % context.get('active_domain') - elif not result.get('active_domain'): - result['active_domain'] = '' - # get default values according to the composition mode - 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} - else: - vals = {'model': model, 'res_id': res_id} - if composition_mode: - vals['composition_mode'] = composition_mode + vals['use_active_domain'] = True + vals['active_domain'] = '%s' % context.get('active_domain') + if result['composition_mode'] == 'comment': + vals.update(self.get_record_data(cr, uid, result, context=context)) for field in vals: if field in fields: @@ -102,13 +94,15 @@ class mail_compose_message(osv.TransientModel): # but when creating the mail.message to create the mail.compose.message # access rights issues may rise # We therefore directly change the model and res_id - if result.get('model') == 'res.users' and result.get('res_id') == uid: + if result['model'] == 'res.users' and result['res_id'] == uid: result['model'] = 'res.partner' result['res_id'] = self.pool.get('res.users').browse(cr, uid, uid).partner_id.id 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')] + return [('comment', 'Post on a document'), + ('mass_mail', 'Email Mass Mailing'), + ('mass_post', 'Post on Multiple Documents')] _columns = { 'composition_mode': fields.selection( @@ -116,19 +110,19 @@ class mail_compose_message(osv.TransientModel): string='Composition mode'), 'partner_ids': fields.many2many('res.partner', 'mail_compose_message_res_partner_rel', - 'wizard_id', 'partner_id', 'Additional contacts'), + 'wizard_id', 'partner_id', 'Additional Contacts'), 'use_active_domain': fields.boolean('Use active domain'), 'active_domain': fields.char('Active domain', readonly=True), - 'post': fields.boolean('Post a copy in the document', - help='Post a copy of the message on the document communication history.'), - 'notify': fields.boolean('Notify followers', - help='Notify followers of the document'), - 'same_thread': fields.boolean('Replies in the document', - help='Replies to the messages will go into the selected document.'), 'attachment_ids': fields.many2many('ir.attachment', 'mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id', 'Attachments'), - 'filter_id': fields.many2one('ir.filters', 'Filters'), + 'is_log': fields.boolean('Log an Internal Note', + help='Whether the message is an internal note (comment mode only)'), + # mass mode options + 'notify': fields.boolean('Notify followers', + help='Notify followers of the document (mass post only)'), + 'same_thread': fields.boolean('Replies in the document', + help='Replies to the messages will go into the selected document (mass mail only)'), } #TODO change same_thread to False in trunk (Require view update) _defaults = { @@ -136,8 +130,6 @@ class mail_compose_message(osv.TransientModel): 'body': lambda self, cr, uid, ctx={}: '', 'subject': lambda self, cr, uid, ctx={}: False, 'partner_ids': lambda self, cr, uid, ctx={}: [], - 'post': False, - 'notify': False, 'same_thread': True, } @@ -169,61 +161,36 @@ class mail_compose_message(osv.TransientModel): not want that feature in the wizard. """ return - def get_record_data(self, cr, uid, model, res_id, context=None): + def get_record_data(self, cr, uid, values, 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``. - - :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 - """ - doc_name_get = self.pool[model].name_get(cr, uid, [res_id], context=context) - record_name = False - if doc_name_get: - record_name = doc_name_get[0][1] - values = { - 'model': model, - 'res_id': res_id, - 'record_name': record_name, - } - if record_name: - values['subject'] = 'Re: %s' % record_name - return values - - 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 recipients). - - :param int message_id: id of the mail.message to which the user - is replying. - """ - if not message_id: - return {} + wizard when sending an email related a previous email (parent_id) or + a document (model, res_id). This is based on previously computed default + values. """ if context is None: context = {} - message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context) + result, subject = {}, False + if values.get('parent_id'): + parent = self.pool.get('mail.message').browse(cr, uid, values.get('parent_id'), context=context) + result['record_name'] = parent.record_name, + subject = tools.ustr(parent.subject or parent.record_name or '') + if not values.get('model'): + result['model'] = parent.model + if not values.get('res_id'): + result['res_id'] = parent.res_id + partner_ids = values.get('partner_ids', list()) + [partner.id for partner in parent.partner_ids] + if context.get('is_private') and parent.author_id: # check message is private then add author also in partner list. + partner_ids += [parent.author_id.id] + result['partner_ids'] = partner_ids + elif values.get('model') and values.get('res_id'): + doc_name_get = self.pool[values.get('model')].name_get(cr, uid, [values.get('res_id')], context=context) + result['record_name'] = doc_name_get and doc_name_get[0][1] or '' + subject = tools.ustr(result['record_name']) - # create subject re_prefix = _('Re:') - reply_subject = tools.ustr(message_data.subject or message_data.record_name or '') - if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject: - reply_subject = "%s %s" % (re_prefix, reply_subject) - # get partner_ids from original message - partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else [] - partner_ids += context.get('default_partner_ids', []) - if context.get('is_private',False) and message_data.author_id : #check message is private then add author also in partner list. - partner_ids += [message_data.author_id.id] - # update the result - result = { - 'record_name': message_data.record_name, - 'model': message_data.model, - 'res_id': message_data.res_id, - 'parent_id': message_data.id, - 'subject': reply_subject, - 'partner_ids': partner_ids, - } + if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)): + subject = "%s %s" % (re_prefix, subject) + result['subject'] = subject + return result #------------------------------------------------------ @@ -235,53 +202,42 @@ class mail_compose_message(osv.TransientModel): email(s), rendering any template patterns on the fly if needed. """ if context is None: context = {} + # clean the context (hint: mass mailing sets some default values that # could be wrongly interpreted by mail_mail) context.pop('default_email_to', None) context.pop('default_partner_ids', None) - active_ids = context.get('active_ids') - is_log = context.get('mail_compose_log', False) - for wizard in self.browse(cr, uid, ids, context=context): - mass_mail_mode = wizard.composition_mode == 'mass_mail' + mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post') active_model_pool = self.pool[wizard.model if wizard.model else 'mail.thread'] if not hasattr(active_model_pool, 'message_post'): context['thread_model'] = wizard.model active_model_pool = self.pool['mail.thread'] # wizard works in batch mode: [res_id] or active_ids or active_domain - if mass_mail_mode and wizard.use_active_domain and wizard.model: + if mass_mode and wizard.use_active_domain and wizard.model: res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context) - elif mass_mail_mode and wizard.model and active_ids: - res_ids = active_ids + elif mass_mode and wizard.model and context.get('active_ids'): + res_ids = context['active_ids'] else: res_ids = [wizard.res_id] - all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context) - - for res_id, mail_values in all_mail_values.iteritems(): - if mass_mail_mode and not wizard.post: - m2m_attachment_ids = self.pool['mail.thread']._message_preprocess_attachments( - cr, uid, mail_values.pop('attachments', []), - mail_values.pop('attachment_ids', []), - 'mail.message', 0, - context=context) - mail_values['attachment_ids'] = m2m_attachment_ids - if not mail_values.get('reply_to'): - mail_values['reply_to'] = mail_values['email_from'] - self.pool.get('mail.mail').create(cr, uid, mail_values, context=context) - else: - subtype = 'mail.mt_comment' - if is_log: # log a note: subtype is False - subtype = False - elif mass_mail_mode: # mass mail: is a log pushed to recipients unless specified, author not added - if not wizard.notify: + sliced_res_ids = [res_ids[i:i + self._batch_size] for i in range(0, len(res_ids), self._batch_size)] + for res_ids in sliced_res_ids: + all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context) + for res_id, mail_values in all_mail_values.iteritems(): + if wizard.composition_mode == 'mass_mail': + self.pool['mail.mail'].create(cr, uid, mail_values, context=context) + else: + subtype = 'mail.mt_comment' + if context.get('mail_compose_log') or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False subtype = False - context = dict(context, - mail_notify_force_send=False, # do not send emails directly but use the queue instead - mail_create_nosubscribe=True) # add context key to avoid subscribing the author - active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values) + if wizard.composition_mode == 'mass_post': + context = dict(context, + mail_notify_force_send=False, # do not send emails directly but use the queue instead + mail_create_nosubscribe=True) # add context key to avoid subscribing the author + active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values) return {'type': 'ir.actions.act_window_close'} @@ -289,6 +245,7 @@ class mail_compose_message(osv.TransientModel): """Generate the values that will be used by send_mail to create mail_messages or mail_mails. """ results = dict.fromkeys(res_ids, False) + rendered_values, default_recipients = {}, {} mass_mail_mode = wizard.composition_mode == 'mass_mail' # render all template-based value at once @@ -303,40 +260,46 @@ class mail_compose_message(osv.TransientModel): 'parent_id': wizard.parent_id and wizard.parent_id.id, 'partner_ids': [partner.id for partner in wizard.partner_ids], 'attachment_ids': [attach.id for attach in wizard.attachment_ids], + 'author_id': wizard.author_id.id, + 'email_from': wizard.email_from, + 'record_name': wizard.record_name, } # mass mailing: rendering override wizard static values if mass_mail_mode and wizard.model: + # always keep a copy, reset record name (avoid browsing records) + mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False) + # auto deletion of mail_mail + if 'mail_auto_delete' in context: + mail_values['auto_delete'] = context.get('mail_auto_delete') + # rendered values using template email_dict = rendered_values[res_id] mail_values['partner_ids'] += email_dict.pop('partner_ids', []) + mail_values.update(email_dict) + if wizard.same_thread: + mail_values.pop('reply_to') + elif not mail_values.get('reply_to'): + mail_values['reply_to'] = mail_values['email_from'] + # mail_mail values: body -> body_html, partner_ids -> recipient_ids + mail_values['body_html'] = mail_values.get('body', '') + mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])] + # process attachments: should not be encoded before being processed by message_post / mail_mail create - attachments = [] - if email_dict.get('attachments'): - for name, enc_cont in email_dict.pop('attachments'): - attachments.append((name, base64.b64decode(enc_cont))) - mail_values['attachments'] = attachments + mail_values['attachments'] = [(name, base64.b64decode(enc_cont)) for name, enc_cont in email_dict.pop('attachments', list())] attachment_ids = [] for attach_id in mail_values.pop('attachment_ids'): new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context) attachment_ids.append(new_attach_id) - mail_values['attachment_ids'] = attachment_ids - # email_from: mass mailing only can specify another email_from - if email_dict.get('email_from'): - mail_values['email_from'] = email_dict.pop('email_from') - # replies redirection: mass mailing only - if wizard.same_thread and wizard.post: - email_dict.pop('reply_to', None) - else: - mail_values['reply_to'] = email_dict.pop('reply_to', None) - mail_values.update(email_dict) - # mass mailing without post: mail_mail values - if mass_mail_mode and not wizard.post: - if 'mail_auto_delete' in context: - mail_values['auto_delete'] = context.get('mail_auto_delete') - mail_values['body_html'] = mail_values.get('body', '') - mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])] + mail_values['attachment_ids'] = self.pool['mail.thread']._message_preprocess_attachments( + cr, uid, mail_values.pop('attachments', []), + attachment_ids, 'mail.message', 0, context=context) + results[res_id] = mail_values return results + #------------------------------------------------------ + # Template rendering + #------------------------------------------------------ + def render_message_batch(self, cr, uid, wizard, res_ids, context=None): """Generate template-based values of wizard, for the document records given by res_ids. This method is meant to be inherited by email_template that @@ -346,6 +309,10 @@ class mail_compose_message(osv.TransientModel): once, and render it multiple times. This is useful for mass mailing where template rendering represent a significant part of the process. + Default recipients are also computed, based on mail_thread method + message_get_default_recipients. This allows to ensure a mass mailing has + always some recipients specified. + :param browse wizard: current mail.compose.message browse record :param list res_ids: list of record ids @@ -357,6 +324,9 @@ class mail_compose_message(osv.TransientModel): emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context=context) replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context=context) + ctx = dict(context, thread_model=wizard.model) + default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx) + results = dict.fromkeys(res_ids, False) for res_id in res_ids: results[res_id] = { @@ -365,6 +335,7 @@ class mail_compose_message(osv.TransientModel): 'email_from': emails_from[res_id], 'reply_to': replies_to[res_id], } + results[res_id].update(default_recipients.get(res_id, dict())) return results def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False): diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 23931649402..5b6bfc50439 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -11,6 +11,7 @@ + @@ -28,29 +29,27 @@ - -
+ + + + + + + + + + + + + + email.template.kanban + email.template + + + + + +
+
+ i + +
+
+

+ +

+
+ +
+
+
+
+
+
+
+
+
+ + + Templates + email.template + form + kanban,tree,form + { + 'form_view_ref': 'mass_mailing.email_template_form_minimal', + 'default_use_default_to': True, +} + + + + + + + diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml new file mode 100644 index 00000000000..7892feacacc --- /dev/null +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -0,0 +1,626 @@ + + + + + + + + + + + + + + mail.mass_mailing.contact.search + mail.mass_mailing.contact + + + + + + + + + + + + + + + + + mail.mass_mailing.contact.tree + mail.mass_mailing.contact + 10 + + + + + + + + + + + + Mailing List Subscribers + mail.mass_mailing.contact + form + tree + {'search_default_not_opt_out': 1} + + + + Recipients + mail.mass_mailing.contact + form + tree + {'search_default_list_id': active_id, 'search_default_not_opt_out': 1} + +

+ Click to create a recipient. +

+
+
+ + + + + + mail.mass_mailing.list.search + mail.mass_mailing.list + + + + + + + + + mail.mass_mailing.list.tree + mail.mass_mailing.list + 10 + + + + + + + + + + mail.mass_mailing.list.form + mail.mass_mailing.list + +
+ +
+ +
+
+
+
+
+
+
+ + + Contact Lists + mail.mass_mailing.list + form + tree,form + +

+ Click here to create a new mailing list. +

+ Mailing lists allows you to to manage customers and + contacts easily and to send to mailings in a single click. +

+
+ + + + + + mail.mass_mailing.search + mail.mass_mailing + + + + + + + + + + + + + + mail.mass_mailing.tree + mail.mass_mailing + 10 + + + + + + + + + + + + + + mail.mass_mailing.form + mail.mass_mailing + +
+
+
+
+

+ + emails are in queue and will be sent soon. +

+
+ +
+ + + + + +
+
+ +
+ + + + + + + + + + +
+
+ + Related Mailing(s) + + + + + + + + + +
@@ -178,7 +178,7 @@
-