[MERGE] merge from trunk

bzr revid: ged@openerp.com-20140417065356-x7o3jg5bo5430zth
This commit is contained in:
Gery Debongnie 2014-04-17 08:53:56 +02:00
commit 0c24df7074
154 changed files with 3735 additions and 4369 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,10 @@
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/><h1><field name="name" required="1"/></h1>
<label for="model_id"/><field name="model_id" required="1" on_change="onchange_model_id(model_id)" class="oe_inline"/>
<field name="model" invisible="1"/>
<group>
<field name="model_id" required="1" on_change="onchange_model_id(model_id)"/>
<field name="model" invisible="1"/>
</group>
</div>
<div class="oe_right oe_button_box" name="buttons">
<field name="ref_ir_act_window" invisible="1"/>
@ -30,43 +32,28 @@
context="{'template_id':active_id}"/>
</div>
<notebook>
<page string="Mailing Template">
<group>
<group>
<field name="email_from"
placeholder="Override author's email"/>
<field name="email_to"
placeholder="Comma-separated recipient addresses"/>
<field name="partner_to"
placeholder="Comma-separated ids of recipient partners"/>
<field name="email_cc"
placeholder="Comma-separated carbon copy recipients addresses"/>
<field name="reply_to"
placeholder="Preferred reply address"/>
<field name="subject"
placeholder="Subject (placeholders may be used here)"/>
<field name="user_signature" string="Author Signature (mass mail only)"/>
</group>
<group class="oe_edit_only">
<h3 colspan="2">Dynamic placeholder generator</h3>
<field name="model_object_field"
domain="[('model_id','=',model_id),('ttype','!=','one2many'),('ttype','!=','many2many')]"
on_change="onchange_sub_model_object_value_field(model_object_field)"/>
<field name="sub_object" readonly="1"/>
<field name="sub_model_object_field"
domain="[('model_id','=',sub_object),('ttype','!=','one2many'),('ttype','!=','many2many')]"
attrs="{'readonly':[('sub_object','=',False)],'required':[('sub_object','!=',False)]}"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field)"/>
<field name="null_value"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field,null_value)"/>
<field name="copyvalue"/>
</group>
</group>
<h3>Body</h3>
<field name="body_html" width="250" height="450"
placeholder="Rich-text/HTML content of the message (placeholders may be used here)"/>
<page string="Content">
<label for="subject"/>
<h2 style="display: inline-block;"><field name="subject" placeholder="Subject (placeholders may be used here)"/></h2>
<field name="body_html"/>
<field name="attachment_ids" widget="many2many_binary"/>
</page>
<page string="Email Configuration">
<group>
<field name="email_from"
placeholder="Override author's email"/>
<field name="use_default_to"/>
<field name="email_to" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated recipient addresses"/>
<field name="partner_to" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated ids of recipient partners"/>
<field name="email_cc" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated carbon copy recipients addresses"/>
<field name="reply_to"
placeholder="Preferred reply address"/>
<field name="user_signature" string="Author Signature (mass mail only)"/>
</group>
</page>
<page string="Advanced Settings">
<group>
<field name="lang"/>
@ -77,6 +64,21 @@
attrs="{'invisible':[('report_template','=',False)]}"/>
</group>
</page>
<page string="Dynamic Placeholder Generator">
<group>
<field name="model_object_field"
domain="[('model_id','=',model_id),('ttype','!=','one2many'),('ttype','!=','many2many')]"
on_change="onchange_sub_model_object_value_field(model_object_field)"/>
<field name="sub_object" readonly="1"/>
<field name="sub_model_object_field"
domain="[('model_id','=',sub_object),('ttype','!=','one2many'),('ttype','!=','many2many')]"
attrs="{'readonly':[('sub_object','=',False)],'required':[('sub_object','!=',False)]}"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field)"/>
<field name="null_value"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field,null_value)"/>
<field name="copyvalue"/>
</group>
</page>
</notebook>
</sheet>
</form>

View File

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

View File

@ -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': '<p>Dummy body</p>', 'post': True},
{'subject': 'Forget me subject', 'body': '<p>Dummy body</p>'},
{'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" <admin@yourcompany.example.com>'],
['"Followers of Pigs" <raoul@raoul.fr>'], ['"Followers of Pigs" <bert@bert.fr>']]
['b@b.b', 'c@c.c'], ['Administrator <admin@yourcompany.example.com>'],
['Raoul Grosbedon <raoul@raoul.fr>'], ['Bert Tartignole <bert@bert.fr>']]
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')

View File

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

View File

@ -8,14 +8,17 @@
<field name="arch" type="xml">
<form string="Email Preview" version="7.0">
<field name="model_id" invisible="1"/>
<h2 style="color: #7c7bad;">Preview of <field name="name" readonly="1" nolabel="1" class="oe_inline"/></h2>
Using sample document <field name="res_id" on_change="on_change_res_id(res_id, context)" class="oe_inline"/>
<h3>Preview of <field name="name" readonly="1" nolabel="1" class="oe_inline"/></h3>
Choose an example <field name="model_id" class="oe_inline" readonly="1"/> record:
<field name="res_id" on_change="on_change_res_id(res_id, context)" class="oe_inline"
style="margin-left: 8px;"/>
<group>
<field name="subject" readonly="1"/>
<field name="email_from" readonly="1"
attrs="{'invisible':[('email_from','=',False)]}"/>
<field name="email_to" readonly="1"/>
<field name="partner_to" readonly="1"/>
<field name="partner_ids" widget="many2many_tags" readonly="1"/>
<field name="email_to" readonly="1"
attrs="{'invisible':[('email_to','=',False)]}"/>
<field name="email_cc" readonly="1"
attrs="{'invisible':[('email_cc','=',False)]}"/>
<field name="reply_to" readonly="1"
@ -23,6 +26,7 @@
</group>
<field name="body_html" widget="html" readonly="1"
nolabel="1" options='{"safe": True}'/>
<field name="attachment_ids" widget="many2many_binary" radonly="1"/>
</form>
</field>
</record>
@ -30,10 +34,11 @@
<record id="wizard_email_template_preview" model="ir.actions.act_window">
<field name="name">Template Preview</field>
<field name="res_model">email_template.preview</field>
<field name="src_model">email_template.preview</field>
<field name="src_model">email.template</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="email_template_preview_form"/>
<field name="auto_refresh" eval="1" />
<field name="target">new</field>
<field name="context">{'template_id':active_id}</field>

View File

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

View File

@ -7,22 +7,6 @@
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='subject']" position="after">
<label string="Template Recipients" for="partner_to"
groups="base.group_no_one"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<div groups="base.group_no_one" name="template_recipients"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}">
<group class="oe_grey">
<!-- <label string="Partners" for="partner_to"/> -->
<field name="partner_to" readonly="1"/>
<!-- <label string="Email To" for="email_to"/> -->
<field name="email_to" readonly="1"/>
<!-- <label string="Email CC" for="email_cc"/> -->
<field name="email_cc" readonly="1"/>
</group>
</div>
</xpath>
<xpath expr="//footer" position="inside">
<group class="oe_right oe_form" col="1">
<div>Use template

View File

@ -2,7 +2,7 @@
<openerp>
<!-- Mail template is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="0">
<data noupdate="1">
<!--Email template -->
<record id="email_template_goal_reminder" model="email.template">
@ -164,7 +164,7 @@
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="autojoin_group_id" eval="ref('base.group_user')" />
<field name="user_domain">[('groups_id', 'in', ref('base.group_user'))]</field>
<field name="state">inprogress</field>
<field name="category">other</field>
</record>
@ -174,7 +174,7 @@
<field name="period">once</field>
<field name="visibility_mode">personal</field>
<field name="report_message_frequency">never</field>
<field name="user_ids" eval="[(4, ref('base.user_root'))]" />
<field name="user_domain">[('groups_id', 'in', ref('base.user_root'))]</field>
<field name="state">inprogress</field>
<field name="category">other</field>
</record>

View File

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

View File

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

View File

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

View File

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

View File

@ -45,9 +45,9 @@
<h1>
<field name="name" placeholder="e.g. Monthly Sales Objectives"/>
</h1>
<label for="user_ids" class="oe_edit_only" string="Assign Challenge To"/>
<label for="user_domain" class="oe_edit_only" string="Assign Challenge To"/>
<div>
<field name="user_ids" widget="many2many_tags" />
<field name="user_domain" widget="char_domain" options="{'model': 'res.users'}" />
</div>
</div>
@ -87,12 +87,12 @@
</page>
<page string="Reward">
<group>
<field name="reward_id"/>
<field name="reward_id" attrs="{'required': [('reward_realtime','=', True)]}" />
<field name="reward_first_id" />
<field name="reward_second_id" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_third_id" attrs="{'invisible': ['|',('reward_first_id','=', False),('reward_second_id','=', False)]}" />
<field name="reward_failure" attrs="{'invisible': [('reward_first_id','=', False)]}" />
<field name="reward_realtime" attrs="{'readonly': [('reward_id','=', False)], 'required': [('reward_id','!=', False)]}" />
<field name="reward_realtime" />
</group>
<div class="oe_grey">
<p>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.</p>
@ -100,7 +100,6 @@
</page>
<page string="Advanced Options">
<group string="Subscriptions">
<field name="autojoin_group_id" />
<field name="invited_user_ids" widget="many2many_tags" />
</group>
<group string="Notification Messages">

View File

@ -130,7 +130,7 @@
<field name="name">Monthly Sales Targets</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="autojoin_group_id" eval="ref('base.group_sale_salesman')" />
<field name="user_domain">[('groups_id', 'in', ref('base.group_sale_salesman'))]</field>
<field name="report_message_frequency">weekly</field>
</record>
@ -138,7 +138,7 @@
<field name="name">Lead Acquisition</field>
<field name="period">monthly</field>
<field name="visibility_mode">ranking</field>
<field name="autojoin_group_id" eval="ref('base.group_sale_salesman')" />
<field name="user_domain">[('groups_id', 'in', ref('base.group_sale_salesman'))]</field>
<field name="report_message_frequency">weekly</field>
</record>

View File

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

View File

@ -1,8 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<report auto="False" id="attendance_error_report" keyword="client_print_multi" menu="False" model="hr.employee" multi="True" name="report.hr.timesheet.attendance.error" rml="hr_attendance/report/attendance_errors.rml" string="Attendance Error Report"/>
<report
id="action_report_hrattendanceerror"
model="hr.employee"
string="Attendance Error Report"
report_type="qweb-pdf"
name="hr_attendance.report_attendanceerrors"
file="hr_attendance.report_attendanceerrors"
/>
</data>
</openerp>

View File

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

View File

@ -1,137 +0,0 @@
<?xml version="1.0"?>
<document filename="Attendance Errors.pdf">
<template title="Attendance Errors" author="OpenERP S.A.(sales@openerp.com)" allowSplitting="20">
<pageTemplate id="first">
<frame id="first" x1="28.0" y1="26.0" width="536" height="784"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="Standard_Outline">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table1">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
</blockTableStyle>
<blockTableStyle id="Table2">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="4,-1" stop="4,-1"/>
</blockTableStyle>
<blockTableStyle id="Table3">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="0,0" stop="0,0"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="1,0" stop="1,0"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="2,0" stop="2,0"/>
</blockTableStyle>
<initialize>
<paraStyle name="all" alignment="justify"/>
</initialize>
<paraStyle name="Standard" fontName="Helvetica"/>
<paraStyle name="Text body" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Heading" fontName="Helvetica" fontSize="14.0" leading="17" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="List" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Table Contents" fontName="Helvetica"/>
<paraStyle name="Caption" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Index" fontName="Helvetica"/>
<paraStyle name="terp_header" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="LEFT" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Centre" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details_Centre" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_Centre_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_8" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_9" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9_Bold" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Footer" fontName="Helvetica"/>
<paraStyle name="Table Heading" fontName="Helvetica" alignment="CENTER"/>
<paraStyle name="Horizontal Line" fontName="Helvetica" fontSize="6.0" leading="8" spaceBefore="0.0" spaceAfter="14.0"/>
<paraStyle name="Heading 9" fontName="Helvetica-Bold" fontSize="75%" leading="NaN" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General_Centre" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General_Right" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details_Right" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_Right_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Right" fontName="Helvetica-Bold" fontSize="15.0" leading="19" alignment="LEFT" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_address" fontName="Helvetica" fontSize="10.0" leading="13" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Right_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<images/>
</stylesheet>
<story>
<para style="terp_default_8">[[ repeatIn(get_employees(data['form']['emp_ids']),'employee') ]]</para>
<para style="terp_header_Centre">Attendance Errors</para>
<para style="terp_tblheader_Details">[[ employee.name ]]</para>
<blockTable colWidths="107.0,107.0,107.0,107.0,107.0" style="Table1">
<tr>
<td>
<para style="terp_tblheader_Details">Operation</para>
</td>
<td>
<para style="terp_tblheader_Details_Centre">Date Signed</para>
</td>
<td>
<para style="terp_tblheader_Details_Centre">Date Recorded</para>
</td>
<td>
<para style="terp_tblheader_Details_Centre">Delay</para>
</td>
<td>
<para style="terp_tblheader_Details_Centre">Min Delay</para>
</td>
</tr>
</blockTable>
<section>
<para style="terp_default_8">[[ repeatIn(lst(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay']), 'att') ]]</para>
<blockTable colWidths="107.0,107.0,107.0,107.0,107.0" style="Table2">
<tr>
<td>
<para style="terp_default_9">[[ att['action'] ]]</para>
</td>
<td>
<para style="terp_default_Centre_9">[[ formatLang(att['date'],date_time=True) ]]</para>
</td>
<td>
<para style="terp_default_Centre_9">[[ formatLang(att['create_date'],date_time=True) ]] </para>
</td>
<td>
<para style="terp_default_Centre_9">[[ att['delay'] ]]</para>
</td>
<td>
<para style="terp_default_Centre_9">[[ att['delay2'] ]]</para>
</td>
</tr>
</blockTable>
</section>
<blockTable colWidths="322.0,108.0,107.0" style="Table3">
<tr>
<td>
<para style="terp_default_Bold_9">Total period:[[ repeatIn(total(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay']),'total') ]]</para>
</td>
<td>
<para style="terp_default_Centre_9_Bold">[[ total['total'] ]]</para>
</td>
<td>
<para style="terp_default_Centre_9_Bold">[[ total['total2'] ]]</para>
</td>
</tr>
</blockTable>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<para style="terp_default_9">(*) A positive delay means that the employee worked less than recorded.</para>
<para style="terp_default_9">(*) A negative delay means that the employee worked more than encoded.</para>
</story>
</document>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_attendanceerrors">
<t t-call="report.html_container">
<t t-foreach="get_employees(data['form']['emp_ids'])" t-as="employee">
<t t-call="report.internal_layout">
<div class="page">
<div class="oe_structure"/>
<h2>Attendance Errors: <span t-esc="employee.name"/></h2>
<table class="table table-condensed mt32">
<thead>
<tr>
<th>Operation</th>
<th>Date Signed</th>
<th>Date Recorded</th>
<th>Delay</th>
<th>Min Delay</th>
</tr>
</thead>
<tbody>
<tr t-foreach="lst(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay'])" t-as="att">
<td><span t-esc="att['action']"/></td>
<td><span t-esc="formatLang(att['date'],date_time=True)"/></td>
<td><span t-esc="formatLang(att['create_date'],date_time=True)"/></td>
<td><span t-esc="att['delay']"/></td>
<td><span t-esc="att['delay2']"/></td>
</tr>
<tr class="border-black" t-foreach="total(employee.id,data['form']['init_date'], data['form']['end_date'], data['form']['max_delay'])" t-as="total">
<td colspan="3"><strong>Total period</strong></td>
<td><strong t-esc="total['total']"/></td>
<td><strong t-esc="total['total2']"/></td>
</tr>
</tbody>
</table>
<p>(*) A positive delay means that the employee worked less than recorded.<br/>
(*) A negative delay means that the employee worked more than encoded.</p>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

View File

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

View File

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

View File

@ -1,8 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<report auto="False" id="hr_expenses" model="hr.expense.expense" name="hr.expense" rml="hr_expense/report/expense.rml" string="HR expenses"/>
<report
id="action_report_hr_expense"
string="HR Expense"
model="hr.expense.expense"
report_type="qweb-pdf"
name="hr_expense.report_expense"
file="hr_expense.report_expense"
/>
</data>
</openerp>

View File

@ -19,8 +19,6 @@
#
##############################################################################
import expense
import hr_expense_report
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,302 +0,0 @@
<?xml version="1.0"?>
<document filename="test.pdf">
<template title="Expenses" author="OpenERP S.A. (sales@openerp.com)" allowSplitting="20">
<pageTemplate id="first">
<frame id="first" x1="16.0" y1="57.0" width="522" height="728"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="Standard_Outline">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table_employee_ref_header">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#e6e6e6" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_employee_ref_content">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#e6e6e6" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_hr_expense_line_header">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="5,-1" stop="5,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_expense_line">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="5,-1" stop="5,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="5,-1" stop="5,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_Final_total">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEABOVE" colorName="#ffffff" start="0,0" stop="0,0"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="1,0" stop="1,0"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="2,0" stop="2,0"/>
</blockTableStyle>
<initialize>
<paraStyle name="all" alignment="justify"/>
</initialize>
<paraStyle name="Standard" fontName="Helvetica"/>
<paraStyle name="Text body" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Heading" fontName="Helvetica" fontSize="14.0" leading="17" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="List" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Table Contents" fontName="Helvetica"/>
<paraStyle name="Table Heading" fontName="Helvetica" alignment="CENTER"/>
<paraStyle name="Caption" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Index" fontName="Helvetica"/>
<paraStyle name="Footer" fontName="Helvetica"/>
<paraStyle name="Horizontal Line" fontName="Helvetica" fontSize="6.0" leading="8" spaceBefore="0.0" spaceAfter="14.0"/>
<paraStyle name="terp_header" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="LEFT" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="Heading 9" fontName="Helvetica-Bold" fontSize="75%" leading="NaN" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_8" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_General_Centre" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General_Right" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details_Centre" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details_Right" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_Right_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Right" fontName="Helvetica-Bold" fontSize="15.0" leading="19" alignment="LEFT" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_header_Centre" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="CENTER" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_address" fontName="Helvetica" fontSize="10.0" leading="13" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_centre_9" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Right_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_1" fontName="Helvetica" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9_bold_right" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_italic_8" fontName="Helvetica-Oblique" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<images/>
</stylesheet>
<story>
<pto>
<pto_header>
<blockTable colWidths="60.0,200.0,50.0,70.0,70.0,85.0" style="Table_hr_expense_line_header">
<tr>
<td>
<para style="terp_tblheader_Details">Date</para>
</td>
<td>
<para style="terp_tblheader_Details">Name</para>
</td>
<td>
<para style="terp_tblheader_Details">Ref.</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Unit Price</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Qty</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Price</para>
</td>
</tr>
</blockTable>
</pto_header>
<para style="terp_default_8">[[ repeatIn(objects,'o') ]]</para>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<para style="terp_header_Centre">HR Expenses</para>
<para style="terp_tblheader_General_Centre">
<font face="Helvetica" size="9.0">[[ o.name or '' ]]</font>
</para>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<blockTable colWidths="130.5,130.5,130.5,130.5" style="Table_employee_ref_header">
<tr>
<td>
<para style="terp_tblheader_General_Centre">Employee</para>
</td>
<td>
<para style="terp_tblheader_General_Centre">Date</para>
</td>
<td>
<para style="terp_tblheader_General_Centre">Description</para>
</td>
<td>
<para style="terp_tblheader_General_Centre">Validated By</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="130.5,130.5,130.5,130.5" style="Table_employee_ref_content">
<tr>
<td>
<para style="terp_default_Centre_8">[[ o.employee_id.name ]] </para>
</td>
<td>
<para style="terp_default_Centre_8">[[ formatLang(o.date,date=True) ]]</para>
</td>
<td>
<para style="terp_default_Centre_8">[[ o.name ]]</para>
</td>
<td>
<para style="terp_default_Centre_8">[[ o.user_valid.name ]]</para>
</td>
</tr>
</blockTable>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<blockTable colWidths="60.0,200.0,50.0,70.0,70.0,85.0" style="Table_hr_expense_line_header">
<tr>
<td>
<para style="terp_tblheader_Details">Date</para>
</td>
<td>
<para style="terp_tblheader_Details">Name</para>
</td>
<td>
<para style="terp_tblheader_Details">Ref.</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Unit Price</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Qty</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Price</para>
</td>
</tr>
</blockTable>
<para style="terp_default_1">
<font color="white"> </font>
</para>
<section>
<para style="terp_default_8">[[ repeatIn(o.line_ids,'line') ]]</para>
<blockTable colWidths="60.0,200.0,50.0,70.0,70.0,85.0" style="Table_expense_line">
<tr>
<td>
<para style="terp_default_9">[[ formatLang(line.date_value,date=True) ]]</para>
</td>
<td>
<para style="terp_default_9">[[ line.name or '' ]] [[ line.description or '' ]]</para>
</td>
<td>
<para style="terp_default_9">[[ line.ref or '' ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ formatLang(line.unit_amount) ]] </para>
</td>
<td>
<para style="terp_default_Right_9">[[ formatLang(line.unit_quantity) ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ formatLang(line.total_amount, currency_obj=o.currency_id) ]] </para>
</td>
</tr>
<tr>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_italic_8">[[ line.analytic_account and line.analytic_account.complete_name or removeParentNode('tr') ]]</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_Right_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_Right_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_Right_9">
<font color="white"> </font>
</para>
</td>
</tr>
</blockTable>
</section>
<blockTable colWidths="365.0,70.0,100.0" style="Table_Final_total">
<tr>
<td>
<para style="terp_default_8">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_tblheader_Details">Total:</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">[[ formatLang(o.amount, currency_obj=o.currency_id) ]]</para>
</td>
</tr>
</blockTable>
<para style="terp_default_9">
<font color="white"> </font>
</para>
<para style="terp_default_9">[[ o.note or '' ]]</para>
<para style="terp_default_9">
<font color="white"> </font>
</para>
<para style="terp_default_9">Certified honest and conform,</para>
<para style="terp_default_9">(Date and signature)</para>
<para style="terp_default_9">
<font color="white"> </font>
</para>
<para style="terp_default_9">
<font color="white"> </font>
</para>
<para style="terp_default_9">This document must be dated and signed for reimbursement</para>
</pto>
</story>
</document>

View File

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

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_expense">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="report.external_layout">
<div class="page">
<h2>HR Expenses</h2>
<div class="row mt32 mb32">
<div class="col-xs-3">
<strong>Employee:</strong>
<p t-field="o.employee_id.name"/>
</div>
<div class="col-xs-3">
<strong>Date:</strong>
<p t-field="o.date"/>
</div>
<div class="col-xs-3">
<strong>Description:</strong>
<p t-field="o.name"/>
</div>
<div class="col-xs-3">
<strong>Validated By:</strong>
<p t-field="o.user_valid"/>
</div>
</div>
<table class="table table-condensed">
<thead>
<tr>
<th>Date</th>
<th>Name</th>
<th class="text-center">Ref.</th>
<th>Unit Price</th>
<th class="text-center">Qty</th>
<th class="text-right">Price</th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.line_ids" t-as="line">
<td><span t-field="line.date_value"/></td>
<td>
<span t-field="line.name"/>
<span t-field="line.description"/><br/>
<span t-field="line.analytic_account.complete_name"/>
</td>
<td style="text-center">
<span t-field="line.ref"/>
</td>
<td>
<span t-field="line.unit_amount"/>
</td>
<td class="text-center">
<span t-field="line.unit_quantity"/>
</td>
<td class="text-right">
<span t-field="line.total_amount"
t-field-options='{"widget": "monetary", "display_currency":"o.currency_id"}'/>
</td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-xs-4 pull-right">
<table class="table table-condensed">
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span t-field="o.amount"
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
</td>
</tr>
</table>
</div>
</div>
<p t-field="o.note"/>
<p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p>
<p>This document must be dated and signed for reimbursement.</p>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

View File

@ -19,6 +19,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'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,

View File

@ -1,31 +1,30 @@
<?xml version="1.0"?>
<openerp>
<data>
<report
id="action_contribution_register"
model="hr.contribution.register"
string="PaySlip Lines By Conribution Register"
report_type="qweb-pdf"
name="hr_payroll.report_contributionregister"
file="hr_payroll.report_contributionregister"
menu="False"
/>
<report
id="action_report_payslip"
model="hr.payslip"
string="Payslip"
report_type="qweb-pdf"
name="hr_payroll.report_payslip"
file="hr_payroll.report_payslip"
/>
<report
auto="False"
id="payslip_report"
model="hr.payslip"
name="payslip"
rml="hr_payroll/report/report_payslip.rml"
string="Employee PaySlip" />
<report
auto="False"
id="payslip_details_report"
model="hr.payslip"
name="paylip.details"
rml="hr_payroll/report/report_payslip_details.rml"
string="PaySlip Details" />
<report
auto="False"
menu="False"
id="contribution_register"
model="hr.contribution.register"
name="contribution.register.lines"
rml="hr_payroll/report/report_contribution_register.rml"
string="PaySlip Lines By Conribution Register" />
string="PaySlip Details"
report_type="qweb-pdf"
name="hr_payroll.report_payslipdetails"
file="hr_payroll.report_payslipdetails"
/>
</data>
</openerp>

View File

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

View File

@ -1,234 +0,0 @@
<?xml version="1.0"?>
<document filename="test.pdf">
<template title="Test" author="Martin Simon" allowSplitting="20">
<pageTemplate id="first">
<frame id="first" x1="28.0" y1="28.0" width="539" height="786"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="Standard_Outline">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table1">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table3">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
</blockTableStyle>
<blockTableStyle id="Table4">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
</blockTableStyle>
<blockTableStyle id="Table2">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="5,-1" stop="5,-1"/>
</blockTableStyle>
<blockTableStyle id="Table16">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="5,-1" stop="5,-1"/>
</blockTableStyle>
<blockTableStyle id="Table5">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="1,0" stop="1,0"/>
<lineStyle kind="LINEABOVE" colorName="#000000" start="2,0" stop="2,0"/>
</blockTableStyle>
<initialize>
<paraStyle name="all" alignment="justify"/>
</initialize>
<paraStyle name="P1" rightIndent="-56.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P2" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P3" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P4" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P5" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P6" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P7" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P9" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT"/>
<paraStyle name="P10" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT"/>
<paraStyle name="P11" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT"/>
<paraStyle name="P12" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P13" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P14" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P15" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P16" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P17" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P18" fontName="Helvetica" fontSize="2.0" leading="3"/>
<paraStyle name="Standard" fontName="Helvetica"/>
<paraStyle name="Heading" fontName="Helvetica" fontSize="14.0" leading="17" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="Text body" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="List" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Caption" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Index" fontName="Helvetica"/>
<paraStyle name="terp_header" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_space" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Centre" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Table Contents" fontName="Helvetica"/>
<paraStyle name="terp_tblheader_Details" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_Details_Right" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_General" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Table Heading" fontName="Helvetica" alignment="CENTER"/>
<paraStyle name="payslip_adj" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<images/>
</stylesheet>
<story>
<para style="P1">[[repeatIn(objects,'o')]]</para>
<blockTable colWidths="539.0" style="Table1">
<tr>
<td>
<para style="P6">PaySlip Lines by Contribution Register</para>
</td>
</tr>
</blockTable>
<para style="P7">
<font color="white"> </font>
</para>
<para style="P5">
<font color="white"> </font>
</para>
<para style="P16"/>
<blockTable colWidths="254.0,143.0,141.0" style="Table3">
<tr>
<td>
<para style="P17">Register Name</para>
</td>
<td>
<para style="P13">Date From</para>
</td>
<td>
<para style="P14">Date To</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="254.0,143.0,141.0" style="Table4">
<tr>
<td>
<para style="P4">[[ o.name or '']]</para>
</td>
<td>
<para style="P4">[[ data['form']['date_from'] or '']]</para>
</td>
<td>
<para style="P4">[[ data['form']['date_to'] or '' ]]</para>
</td>
</tr>
</blockTable>
<para style="terp_default_Bold_8"/>
<para style="terp_default_Bold_8">
<font color="white"> </font>
</para>
<para style="P8">
<font color="white"> </font>
</para>
<blockTable colWidths="194.0,35.0,126.0,72.0,43.0,69.0" style="Table2">
<tr>
<td>
<para style="P9">PaySlip Name</para>
</td>
<td>
<para style="P9">Code</para>
</td>
<td>
<para style="P9">Name</para>
</td>
<td>
<para style="P9">Quantity/Rate</para>
</td>
<td>
<para style="P9">Amount</para>
</td>
<td>
<para style="P10">Total </para>
</td>
</tr>
</blockTable>
<para style="P18">
<font color="white"> </font>
</para>
<section>
<para style="P2">[[repeatIn(get_payslip_lines(o),'r') ]]</para>
<blockTable colWidths="194.0,35.0,126.0,72.0,43.0,68.0" style="Table16">
<tr>
<td>
<para style="P12">[[ r.get('payslip_name', False) ]]<font face="Helvetica">[[ r.get('payslip_name', False) and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]</font></para>
</td>
<td>
<para style="P2">[[ r['code'] ]]</para>
</td>
<td>
<para style="P2">[[ r['name'] ]]</para>
</td>
<td>
<para style="P2">[[ formatLang(r['quantity']) ]]</para>
</td>
<td>
<para style="P2">[[ formatLang(r['amount']) ]]</para>
</td>
<td>
<para style="P3">[[ formatLang(r['total'], currency_obj = o.company_id and o.company_id.currency_id)]]</para>
</td>
</tr>
</blockTable>
</section>
<blockTable colWidths="397.0,31.0,111.0" style="Table5">
<tr>
<td>
<para style="P15">
<font color="white"> </font>
</para>
</td>
<td>
<para style="P15">Total:</para>
</td>
<td>
<para style="P11">[[ formatLang(sum_total(), currency_obj = o.company_id and o.company_id.currency_id)]]</para>
</td>
</tr>
</blockTable>
<para style="Standard">
<font color="white"> </font>
</para>
</story>
</document>

View File

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

View File

@ -1,340 +0,0 @@
<?xml version="1.0"?>
<document filename="test.pdf">
<template title="Test" author="Martin Simon" allowSplitting="20">
<pageTemplate id="first">
<frame id="first" x1="28.0" y1="28.0" width="539" height="786"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="Standard_Outline">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table1">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table2">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table3">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
</blockTableStyle>
<blockTableStyle id="Table4">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table5">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table6">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table8">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
</blockTableStyle>
<blockTableStyle id="Table9">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="4,-1" stop="4,-1"/>
</blockTableStyle>
<blockTableStyle id="Table13">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<initialize>
<paraStyle name="all" alignment="justify"/>
</initialize>
<paraStyle name="P1" fontName="Helvetica" fontSize="2.0" leading="3"/>
<paraStyle name="P2" rightIndent="-56.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P3" rightIndent="-56.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P4" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P5" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P6" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P7" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P10" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P11" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P12" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P13" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT"/>
<paraStyle name="P14" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT"/>
<paraStyle name="P15" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P16" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Standard" fontName="Helvetica"/>
<paraStyle name="Heading" fontName="Helvetica" fontSize="14.0" leading="17" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="Text body" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="List" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Caption" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Index" fontName="Helvetica"/>
<paraStyle name="terp_header" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_space" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Centre" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Table Contents" fontName="Helvetica"/>
<paraStyle name="terp_tblheader_Details" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_Details_Right" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_General" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Table Heading" fontName="Helvetica" alignment="CENTER"/>
<paraStyle name="payslip_adj" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<images/>
</stylesheet>
<story>
<para style="P2">[[repeatIn(objects,'o')]]</para>
<blockTable colWidths="539.0" style="Table1">
<tr>
<td>
<para style="P7">Pay Slip</para>
</td>
</tr>
</blockTable>
<para style="terp_header_Centre">
<font face="Helvetica" size="6.0">[[o.credit_note==False and removeParentNode('para')]]</font>
<font face="Helvetica-Bold" size="14.0">Credit</font>
<font face="Helvetica" size="14.0"/>
<font face="Helvetica-Bold" size="14.0">Note</font>
</para>
<para style="P8">([[o.name or removeParentNode('para')]])</para>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table2">
<tr>
<td>
<para style="P15">Name</para>
</td>
<td>
<para style="P15">[[o.employee_id.name]]</para>
</td>
<td>
<para style="P15">Designation </para>
</td>
<td>
<para style="P4">[[ o.employee_id.job_id.name or '' ]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,476.0" style="Table3">
<tr>
<td>
<para style="terp_default_Bold_8">
<font face="Helvetica">Address </font>
</para>
</td>
<td>
<para style="P4">[[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)]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table4">
<tr>
<td>
<para style="P15">Email</para>
</td>
<td>
<para style="P4">[[ o.employee_id.work_email or '' ]]</para>
</td>
<td>
<para style="terp_default_Bold_8">
<font face="Helvetica">Identification No</font>
</para>
</td>
<td>
<para style="P4">[[ o.employee_id.identification_id or '' ]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table5">
<tr>
<td>
<para style="P15">Reference</para>
</td>
<td>
<para style="P4">[[ o.number or '' ]]</para>
</td>
<td>
<para style="P15">Bank Account</para>
</td>
<td>
<para style="P4">[[ o.employee_id.otherid or '' ]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table6">
<tr>
<td>
<para style="P15">Date From</para>
</td>
<td>
<para style="P4">[[ o.date_from or '']]</para>
</td>
<td>
<para style="terp_default_Bold_8">
<font face="Helvetica" size="8.0">Date To</font>
</para>
</td>
<td>
<para style="P4">[[ o.date_to or '' ]]</para>
</td>
</tr>
</blockTable>
<para style="P6">
<font color="white"> </font>
</para>
<para style="P6"/>
<blockTable colWidths="67.0,218.0,88.0,85.0,81.0" style="Table8">
<tr>
<td>
<para style="P13">Code</para>
</td>
<td>
<para style="P13">Name</para>
</td>
<td>
<para style="P13">Quantity/Rate</para>
</td>
<td>
<para style="P13">Amount</para>
</td>
<td>
<para style="P14">Total</para>
</td>
</tr>
</blockTable>
<section>
<para style="P4">[[repeatIn(get_payslip_lines(o.line_ids),'p') ]]</para>
<blockTable colWidths="67.0,218.0,88.0,85.0,81.0" style="Table9">
<tr>
<td>
<para style="P4">[[ p.code ]]</para>
</td>
<td>
<para style="P4">[[ p.name ]]</para>
</td>
<td>
<para style="P4">[[ formatLang(p.quantity) ]]</para>
</td>
<td>
<para style="P4">[[ formatLang(p.amount) ]]</para>
</td>
<td>
<para style="P5">[[ formatLang(p.total, currency_obj = o.company_id and o.company_id.currency_id)]]</para>
</td>
</tr>
</blockTable>
</section>
<para style="P10">
<font color="white"> </font>
</para>
<para style="P16">
<font color="white"> </font>
</para>
<para style="P6">
<font color="white"> </font>
</para>
<para style="P1">
<font color="white"> </font>
</para>
<blockTable colWidths="269.0,269.0" style="Table13">
<tr>
<td>
<para style="P4">
<font color="white"> </font>
</para>
</td>
<td>
<para style="P12">
<font color="white"> </font>
</para>
<para style="P12">
<font color="white"> </font>
</para>
<para style="P12">
<font color="white"> </font>
</para>
<para style="P12">Authorized Signature </para>
</td>
</tr>
</blockTable>
<para style="P3">
<font color="white"> </font>
</para>
</story>
</document>

View File

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

View File

@ -1,426 +0,0 @@
<?xml version="1.0"?>
<document filename="test.pdf">
<template title="Test" author="Martin Simon" allowSplitting="20">
<pageTemplate id="first">
<frame id="first" x1="28.0" y1="28.0" width="539" height="786"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="Standard_Outline">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table1">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table2">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table3">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
</blockTableStyle>
<blockTableStyle id="Table4">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table5">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table6">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEAFTER" colorName="#cccccc" start="3,0" stop="3,-1"/>
<lineStyle kind="LINEABOVE" colorName="#cccccc" start="3,0" stop="3,0"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table10">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table11">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table12">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
</blockTableStyle>
<blockTableStyle id="Table8">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table7">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="5,-1" stop="5,-1"/>
</blockTableStyle>
<blockTableStyle id="Table16">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="5,-1" stop="5,-1"/>
</blockTableStyle>
<blockTableStyle id="Table13">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<initialize>
<paraStyle name="all" alignment="justify"/>
</initialize>
<paraStyle name="P1" fontName="Helvetica" fontSize="2.0" leading="3"/>
<paraStyle name="P2" fontName="Helvetica" fontSize="2.0" leading="3"/>
<paraStyle name="P3" rightIndent="-56.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P4" rightIndent="-56.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P5" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P6" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P7" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P8" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P9" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P10" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P11" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P12" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P13" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT"/>
<paraStyle name="P14" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT"/>
<paraStyle name="P15" fontName="Helvetica-Bold" fontSize="8.0" leading="10"/>
<paraStyle name="P16" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P17" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P18" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P19" fontName="Helvetica" fontSize="2.0" leading="3" alignment="LEFT"/>
<paraStyle name="P20" fontName="Helvetica" fontSize="2.0" leading="3"/>
<paraStyle name="P21" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P22" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT"/>
<paraStyle name="P23" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT"/>
<paraStyle name="P24" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P25" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="P26" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Standard" fontName="Helvetica"/>
<paraStyle name="Heading" fontName="Helvetica" fontSize="14.0" leading="17" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="Text body" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="List" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Caption" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Index" fontName="Helvetica"/>
<paraStyle name="terp_header" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_space" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Centre" fontName="Helvetica-Bold" fontSize="14.0" leading="17" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_8" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_9" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_10" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Table Contents" fontName="Helvetica"/>
<paraStyle name="terp_tblheader_Details" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_Details_Right" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_General" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Table Heading" fontName="Helvetica" alignment="CENTER"/>
<paraStyle name="payslip_adj" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<images/>
</stylesheet>
<story>
<para style="P3">[[repeatIn(objects,'o')]]</para>
<blockTable colWidths="539.0" style="Table1">
<tr>
<td>
<para style="P8">Pay Slip Details</para>
</td>
</tr>
</blockTable>
<para style="terp_header_Centre">
<font face="Helvetica" size="6.0">[[o.credit_note==False and removeParentNode('para')]]</font>
<font face="Helvetica-Bold" size="14.0">Credit</font>
<font face="Helvetica" size="14.0"/>
<font face="Helvetica-Bold" size="14.0">Note</font>
</para>
<para style="P9">([[o.name or removeParentNode('para')]])</para>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table2">
<tr>
<td>
<para style="P16">Name</para>
</td>
<td>
<para style="P16">[[o.employee_id.name]]</para>
</td>
<td>
<para style="P16">Designation </para>
</td>
<td>
<para style="P5">[[ o.employee_id.job_id.name or '' ]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,476.0" style="Table3">
<tr>
<td>
<para style="terp_default_Bold_8">
<font face="Helvetica">Address </font>
</para>
</td>
<td>
<para style="P5">[[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)]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table4">
<tr>
<td>
<para style="P16">Email</para>
</td>
<td>
<para style="P5">[[ o.employee_id.work_email or '' ]]</para>
</td>
<td>
<para style="terp_default_Bold_8">
<font face="Helvetica">Identification No</font>
</para>
</td>
<td>
<para style="P5">[[ o.employee_id.identification_id or '' ]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table5">
<tr>
<td>
<para style="P16">Reference</para>
</td>
<td>
<para style="P5">[[ o.number or '' ]]</para>
</td>
<td>
<para style="P16">Bank Account</para>
</td>
<td>
<para style="P5">[[ o.employee_id.otherid or '' ]]</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="63.0,206.0,89.0,181.0" style="Table6">
<tr>
<td>
<para style="P16">Date From</para>
</td>
<td>
<para style="P5">[[ o.date_from or '']]</para>
</td>
<td>
<para style="terp_default_Bold_8">
<font face="Helvetica" size="8.0">Date To</font>
</para>
</td>
<td>
<para style="P5">[[ o.date_to or '' ]]</para>
</td>
</tr>
</blockTable>
<para style="P7"/>
<para style="P11">
<font color="white"> </font>
</para>
<para style="P5">
<font color="white"> </font>
</para>
<blockTable colWidths="539.0" style="Table10">
<tr>
<td>
<para style="P10">Details by Salary Rule Category: </para>
</td>
</tr>
</blockTable>
<blockTable colWidths="54.0,388.0,97.0" style="Table11">
<tr>
<td>
<para style="P15">Code</para>
</td>
<td>
<para style="P15">Salary Rule Category</para>
</td>
<td>
<para style="P14">Total</para>
</td>
</tr>
</blockTable>
<para style="P1">
<font color="white"> </font>
</para>
<section>
<para style="P16">[[repeatIn(get_details_by_rule_category(o.details_by_salary_rule_category),'h') ]]</para>
<blockTable colWidths="54.0,388.0,97.0" style="Table12">
<tr>
<td>
<para style="P16">
<font face="Helvetica">[[ h['code'] ]]</font>
<font face="Helvetica">[[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]</font>
</para>
</td>
<td>
<para style="P17"><font face="Helvetica" color="white">[[ '..'*h['level'] ]]</font>[[ h['rule_category'] ]]<font face="Helvetica">[[ h['level']!=0 and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font') ]]</font></para>
</td>
<td>
<para style="P6">[[ formatLang(h['total'], currency_obj = o.company_id and o.company_id.currency_id)]] <font face="Helvetica" size="8.0">[[ h['level']==0 and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font') ]]</font></para>
</td>
</tr>
</blockTable>
</section>
<para style="P7">
<font color="white"> </font>
</para>
<blockTable colWidths="539.0" style="Table8">
<tr>
<td>
<para style="P10">Payslip Lines by Contribution Register:</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="114.0,42.0,170.0,85.0,56.0,71.0" style="Table7">
<tr>
<td>
<para style="P13">Register Name</para>
</td>
<td>
<para style="P13">Code</para>
</td>
<td>
<para style="P13">Name</para>
</td>
<td>
<para style="P13">Quantity/Rate</para>
</td>
<td>
<para style="P13">Amount</para>
</td>
<td>
<para style="P14">Total</para>
</td>
</tr>
</blockTable>
<section>
<para style="P16">[[repeatIn(get_lines_by_contribution_register(o.details_by_salary_rule_category),'r') ]]</para>
<blockTable colWidths="113.0,44.0,169.0,85.0,56.0,72.0" style="Table16">
<tr>
<td>
<para style="P16">[[ r.get('register_name', False) ]]<font face="Helvetica">[[ h.get('register_name', False) and ( setTag('para','para',{'style':'terp_default_8'})) or removeParentNode('font')]]</font></para>
</td>
<td>
<para style="P5">[[ r['code'] ]]</para>
</td>
<td>
<para style="P5">[[ r['name'] ]]</para>
</td>
<td>
<para style="P5">[[ formatLang(r['quantity']) ]]</para>
</td>
<td>
<para style="P5">[[ formatLang(r['amount']) ]]</para>
</td>
<td>
<para style="P6">[[ formatLang(r['total'], currency_obj = o.company_id and o.company_id.currency_id)]]<font face="Helvetica">[[ r.get('register_name', False) and ( setTag('para','para',{'style':'terp_default_10'})) or removeParentNode('font')]]</font></para>
</td>
</tr>
</blockTable>
</section>
<blockTable colWidths="269.0,269.0" style="Table13">
<tr>
<td>
<para style="P5">
<font color="white"> </font>
</para>
</td>
<td>
<para style="P12">
<font color="white"> </font>
</para>
<para style="P12">
<font color="white"> </font>
</para>
<para style="P12">
<font color="white"> </font>
</para>
<para style="P12">Authorized Signature </para>
</td>
</tr>
</blockTable>
<para style="P4">
<font color="white"> </font>
</para>
</story>
</document>

View File

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

View File

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

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_contributionregister">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="report.external_layout">
<div class="page">
<h2>PaySlip Lines by Contribution Register</h2>
<div class="row mt32 mb32">
<div class="col-xs-3">
<strong>Register Name:</strong>
<p t-field="o.name"/>
</div>
<div class="col-xs-3">
<strong>Date From:</strong>
<p t-esc="data['form']['date_from']"/>
</div>
<div class="col-xs-3">
<strong>Date To:</strong>
<p t-esc="data['form']['date_to']"/>
</div>
</div>
<table class="table table-condensed">
<thead>
<tr>
<th>PaySlip Name</th>
<th>Code</th>
<th>Name</th>
<th>Quantity/Rate</th>
<th>Amount</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="get_payslip_lines(o)" t-as="r">
<td><span t-esc="r.get('payslip_name')"/></td>
<td><span t-esc="r['code']"/></td>
<td><span t-esc="r['name']"/></td>
<td><span t-esc="formatLang(r['quantity'])"/></td>
<td><span t-esc="formatLang(r['amount'])"/></td>
<td><span t-esc=" formatLang(r['total'], currency_obj=o.company_id and o.company_id.currency_id)"/></td>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-xs-4 pull-right">
<table class="table table-condensed">
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span t-esc="formatLang(sum_total(), currency_obj = o.company_id and o.company_id.currency_id)"/>
</td>
</tr>
</table>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_payslip">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="report.external_layout">
<div class="page">
<h2>Pay Slip</h2>
<p t-field="o.name"/>
<table class="table table-condensed table-bordered">
<tr>
<td><strong>Name</strong></td>
<td><span t-field="o.employee_id"/></td>
<td><strong>Designation</strong></td>
<td><span t-field="o.employee_id.job_id"/></td>
</tr>
<tr>
<td><strong>Address</strong></td>
<td colspan="3">
<div t-filed="o.employee_id.address_home_id"
t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
</td>
</tr>
<tr>
<td><strong>Email</strong></td>
<td><span t-field="o.employee_id.work_email"/></td>
<td><strong>Identification No</strong></td>
<td><span t-field="o.employee_id.job_id"/></td>
</tr>
<tr>
<td><strong>Reference</strong></td>
<td><span t-field="o.number"/></td>
<td><strong>Bank Account</strong></td>
<td><span t-field="o.employee_id.otherid"/></td>
</tr>
<tr>
<td><strong>Date From</strong></td>
<td><span t-field="o.date_from"/></td>
<td><strong>Date To</strong></td>
<td><span t-field="o.date_to"/></td>
</tr>
</table>
<table class="table table-condensed">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Quantity/rate</th>
<th>Amount</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="get_payslip_lines(o.line_ids)" t-as="p">
<td><span t-field="p.code"/></td>
<td><span t-field="p.name"/></td>
<td><span t-field="p.quantity"/></td>
<td><span t-esc="formatLang(p.amount, currency_obj=o.company_id.currency_id)"/></td>
<td><span t-esc="formatLang(p.total, currency_obj=o.company_id.currency_id)"/></td>
</tr>
</tbody>
</table>
<p class="text-right"><strong>Authorized signature</strong></p>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_payslipdetails">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="report.external_layout">
<div class="page">
<h2>Pay Slip</h2>
<p t-field="o.name"/>
<table class="table table-condensed table-bordered">
<tr>
<td><strong>Name</strong></td>
<td><span t-field="o.employee_id"/></td>
<td><strong>Designation</strong></td>
<td><span t-field="o.employee_id.job_id"/></td>
</tr>
<tr>
<td><strong>Address</strong></td>
<td colspan="3">
<div t-field="o.employee_id.address_home_id"
t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
</td>
</tr>
<tr>
<td><strong>Email</strong></td>
<td><span t-field="o.employee_id.work_email"/></td>
<td><strong>Identification No</strong></td>
<td><span t-field="o.employee_id.job_id"/></td>
</tr>
<tr>
<td><strong>Reference</strong></td>
<td><span t-field="o.number"/></td>
<td><strong>Bank Account</strong></td>
<td><span t-field="o.employee_id.otherid"/></td>
</tr>
<tr>
<td><strong>Date From</strong></td>
<td><span t-field="o.date_from"/></td>
<td><strong>Date To</strong></td>
<td><span t-field="o.date_to"/></td>
</tr>
</table>
<h3>Details by Salary Rule Category</h3>
<table class="table table-condensed mb32">
<thead>
<tr>
<th>Code</th>
<th>Salary Rule Category</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="get_details_by_rule_category(o.details_by_salary_rule_category)" t-as="h">
<td>
<span t-esc="h['code']"/>
</td>
<td>
<span t-esc="'..'*h['level']"/><span t-esc="h['rule_category']"/>
</td>
<td>
<span t-esc="formatLang(h['total'], currency_obj=o.company_id.currency_id)"/>
</td>
</tr>
</tbody>
</table>
<h3>Payslip Lines by Contribution Register</h3>
<table class="table table-condensed mt32">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Quantity/rate</th>
<th>Amount</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="get_lines_by_contribution_register(o.line_ids)" t-as="p">
<td><span t-esc="p.get('code', '')"/></td>
<td><span t-esc="p.get('name', '')"/></td>
<td><span t-esc="p.get('quantity', '')"/></td>
<td><span t-esc="formatLang(p.get('amount', 0))"/></td>
<td><span t-esc="formatLang(p.get('total', 0), currency_obj=o.company_id.currency_id)"/></td>
</tr>
</tbody>
</table>
<p class="text-right"><strong>Authorized signature</strong></p>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>

View File

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

View File

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

View File

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

View File

@ -2,13 +2,12 @@
<openerp>
<data>
<report
auto="False"
id="report_analytical_profit"
menu="False"
id="action_report_analytic_profit"
model="account.analytic.line"
name="account.analytic.profit"
rml="hr_timesheet_invoice/report/account_analytic_profit.rml"
string="Timesheet Profit"/>
name="hr_timesheet_invoice.report_analyticprofit"
file="hr_timesheet_invoice.report_analyticprofit"
report_type="qweb-pdf"
string="Timesheet Profit"
/>
</data>
</openerp>

View File

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

View File

@ -1,341 +0,0 @@
<?xml version="1.0"?>
<document filename="test.pdf">
<template title="Invoice rate by user" author="OpenERP S.A. (sales@openerp.com)" allowSplitting="20">
<pageTemplate id="first">
<frame id="first" x1="35.0" y1="57.0" width="525" height="728"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="Standard_Outline">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table_Title">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table_header_Date">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_Content_Date">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="0,0" stop="0,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="0,0" stop="0,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="1,0" stop="1,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="1,0" stop="1,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBEFORE" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEAFTER" colorName="#e6e6e6" start="2,0" stop="2,-1"/>
<lineStyle kind="LINEABOVE" colorName="#e6e6e6" start="2,0" stop="2,0"/>
<lineStyle kind="LINEBELOW" colorName="#e6e6e6" start="2,-1" stop="2,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_Header_Employee">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="5,-1" stop="5,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="6,-1" stop="6,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_Final_Total">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="1,-1" stop="1,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="2,-1" stop="2,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="3,-1" stop="3,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="4,-1" stop="4,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="5,-1" stop="5,-1"/>
<lineStyle kind="LINEBELOW" colorName="#000000" start="6,-1" stop="6,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_Journal_Total_detail">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="0,-1" stop="0,-1"/>
<lineStyle kind="LINEBELOW" colorName="#cccccc" start="1,-1" stop="1,-1"/>
</blockTableStyle>
<blockTableStyle id="Table_Journal_title">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<blockTableStyle id="Table1">
<blockAlignment value="LEFT"/>
<blockValign value="TOP"/>
</blockTableStyle>
<initialize>
<paraStyle name="all" alignment="justify"/>
</initialize>
<paraStyle name="Standard" fontName="Helvetica"/>
<paraStyle name="Text body" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Heading" fontName="Helvetica" fontSize="14.0" leading="17" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="List" fontName="Helvetica" spaceBefore="0.0" spaceAfter="6.0"/>
<paraStyle name="Table Contents" fontName="Helvetica"/>
<paraStyle name="Table Heading" fontName="Helvetica" alignment="CENTER"/>
<paraStyle name="Caption" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="Index" fontName="Helvetica"/>
<paraStyle name="Preformatted Text" fontName="Courier" fontSize="10.0" leading="13" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="Footer" fontName="Helvetica"/>
<paraStyle name="Horizontal Line" fontName="Helvetica" fontSize="6.0" leading="8" spaceBefore="0.0" spaceAfter="14.0"/>
<paraStyle name="terp_header" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="LEFT" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="Heading 9" fontName="Helvetica-Bold" fontSize="75%" leading="NaN" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_8" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_tblheader_General_Centre" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_General_Right" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details_Centre" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_tblheader_Details_Right" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="6.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_Right_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_8" fontName="Helvetica" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_header_Right" fontName="Helvetica-Bold" fontSize="15.0" leading="19" alignment="LEFT" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_header_Centre" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="CENTER" spaceBefore="12.0" spaceAfter="6.0"/>
<paraStyle name="terp_default_address" fontName="Helvetica" fontSize="10.0" leading="13" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Bold_9" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Centre_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Right_9_bold" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_centre_bold_9" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_2" fontName="Helvetica" fontSize="2.0" leading="3" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Right_9" fontName="Helvetica" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<paraStyle name="terp_default_Right_9_bold_U" fontName="Helvetica-Bold" fontSize="9.0" leading="11" alignment="RIGHT" spaceBefore="0.0" spaceAfter="0.0"/>
<images/>
</stylesheet>
<story>
<blockTable colWidths="175.0,175.0,175.0" repeatRows="1" style="Table_Title">
<tr>
<td>
<para style="terp_default_8">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_header_Centre">Invoice rate by user</para>
</td>
<td>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<para style="terp_default_8">
<font color="white"> </font>
</para>
</td>
</tr>
</blockTable>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<blockTable colWidths="175.0,175.0,175.0" style="Table_header_Date">
<tr>
<td>
<para style="terp_tblheader_General_Centre">Period from startdate</para>
</td>
<td>
<para style="terp_tblheader_General_Centre">Period to enddate</para>
</td>
<td>
<para style="terp_tblheader_General_Centre">Currency</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="175.0,175.0,175.0" style="Table_Content_Date">
<tr>
<td>
<para style="terp_default_Centre_8">[[ formatLang(data['form']['date_from'],date=True) ]]</para>
</td>
<td>
<para style="terp_default_Centre_8">[[ formatLang (data['form']['date_to'] ,date=True)]]</para>
</td>
<td>
<para style="terp_default_Centre_8">[[ company.currency_id.name ]]</para>
</td>
</tr>
</blockTable>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<para style="terp_default_8">
<font color="white"> </font>
</para>
<blockTable colWidths="159.0,62.0,63.0,68.0,65.0,53.0,52.0" style="Table_Header_Employee">
<tr>
<td>
<para style="terp_tblheader_Details">User or Journal Name</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Units</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Theorical</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Income</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Cost</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Profit</para>
</td>
<td>
<para style="terp_tblheader_Details_Right">Eff.</para>
</td>
</tr>
</blockTable>
<para style="terp_default_2">
<font color="white"> </font>
</para>
<blockTable colWidths="154.0,65.0,60.0,72.0,66.0,52.0,51.0" style="Table_Final_Total">
<tr>
<td>
<para style="terp_default_Bold_9">Totals:</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['unit_amount'], line(data['form'], data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['amount'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['profit'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ 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)]] %</para>
</td>
</tr>
</blockTable>
<para style="terp_default_2"/>
<section>
<para style="terp_default_8">[[ repeatIn(user_ids(lines(data['form'])), 'e') ]]</para>
<blockTable colWidths="137.0,29.0,52.0,61.0,71.0,66.0,52.0,51.0" style="Table_Journal_Total_detail">
<tr>
<td>
<para style="terp_default_Bold_9">[[ e.name ]]</para>
</td>
<td>
<para style="terp_default_8">[[ repeatIn(journal_ids(data['form'], [e.id]), 'j') ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['unit_amount'], line(data['form'], [j.id], [e.id]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['amount_th'], line(data['form'], [j.id], [e.id]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['amount'], line(data['form'], [j.id], [e.id]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[ reduce(lambda x, y: x+y['profit'], line(data['form'], [j.id], [e.id]), 0) ]]</para>
</td>
<td>
<para style="terp_default_Right_9_bold_U">[[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)]] %</para>
</td>
</tr>
</blockTable>
<blockTable colWidths="154.0,65.0,61.0,71.0,66.0,52.0,51.0" style="Table_Journal_title">
<tr>
<td>
<para style="terp_default_9">[[ j.name ]]</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
<td>
<para style="terp_default_9">
<font color="white"> </font>
</para>
</td>
</tr>
</blockTable>
<para style="terp_default_2">
<font color="white"> </font>
</para>
<section>
<para style="terp_default_8">[[ repeatIn(line(data['form'], [j.id],[e.id]), 'l') ]] </para>
<blockTable colWidths="155.0,64.0,61.0,71.0,66.0,52.0,50.0" style="Table1">
<tr>
<td>
<para style="terp_default_8">[[ l['name'] ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ l['unit_amount'] ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ l['amount_th'] ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ l['amount'] ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ l['cost'] ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ l['profit'] ]]</para>
</td>
<td>
<para style="terp_default_Right_9">[[ l['eff'] ]] %</para>
</td>
</tr>
</blockTable>
<para style="terp_default_2">
<font color="white"> </font>
</para>
</section>
<para style="terp_default_2">
<font color="white"> </font>
</para>
</section>
<para style="terp_default_8">
<font color="white"> </font>
</para>
</story>
</document>

View File

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

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_analyticprofit">
<t t-call="report.html_container">
<t t-call="report.external_layout">
<div class="page">
<h2>Invoice rate by user</h2>
<div class="row mt32 mb32">
<div class="col-xs-3">
<strong>Period from startdate:</strong>
<p t-esc="formatLang(data['form']['date_from'],date=True)"/>
</div>
<div class="col-xs-3">
<strong>Period to enddate:</strong>
<p t-esc="formatLang(data['form']['date_to'],date=True)"/>
</div>
<div class="col-xs-3">
<strong>Currency:</strong>
<p t-esc="res_company.currency_id.name"/>
</div>
</div>
<table class="table table-condensed">
<thead>
<tr>
<th>User or Journal Name</th>
<th>Units</th>
<th>Theorical</th>
<th>Income</th>
<th>Cost</th>
<th>Profit</th>
<th>Eff.</th>
</tr>
</thead>
<tbdody>
<tr>
<td>Totals:</td>
<td><span t-esc="reduce(lambda x, y: x+y['unit_amount'], line(data['form'], data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)"/></td>
<td></td>
<td><span t-esc="reduce(lambda x, y: x+y['amount'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)"/></td>
<td><span t-esc="reduce(lambda x, y: x+y['cost'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)"/></td>
<td><span t-esc="reduce(lambda x, y: x+y['profit'], line(data['form'],data['form']['journal_ids'][0][2], data['form']['employee_ids'][0][2]), 0)"/></td>
<td><span t-esc="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)"/><span> %</span></td>
</tr>
<t t-foreach="user_ids(lines(data['form']))" t-as="e">
<t t-foreach="journal_ids(data['form'], [e.id])" t-as="j">
<tr>
<td><span t-esc="e.name"/></td>
<td><span t-esc="reduce(lambda x, y: x+y['unit_amount'], line(data['form'], [j.id], [e.id]), 0) "/></td>
<td><span t-esc="reduce(lambda x, y: x+y['amount_th'], line(data['form'], [j.id], [e.id]), 0)"/></td>
<td><span t-esc="reduce(lambda x, y: x+y['amount'], line(data['form'], [j.id], [e.id]), 0)"/></td>
<td><span t-esc="reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0) "/></td>
<td><span t-esc="reduce(lambda x, y: x+y['profit'], line(data['form'], [j.id], [e.id]), 0)"/></td>
<td>
<t t-if="reduce(lambda x, y: x+y['cost'], line(data['form'], [j.id], [e.id]), 0)">
<span t-esc="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"/>
<span> %</span>
</t>
</td>
</tr>
<tr>
<th colspan="7"><span t-esc="j.name"/></th>
</tr>
<tr t-foreach="line(data['form'], [j.id],[e.id])" t-as="l">
<td><span t-esc="l['name']"/></td>
<td><span t-esc="l['unit_amount']"/></td>
<td><span t-esc="l['amount_th']"/></td>
<td><span t-esc="l['amount']"/></td>
<td><span t-esc="l['cost']"/></td>
<td><span t-esc="l['profit']"/></td>
<td><span t-esc="l['eff']"/><span> %</span></td>
</tr>
</t>
</t>
</tbdody>
</table>
</div>
</t>
</t>
</template>
</data>
</openerp>

View File

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

View File

@ -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: '<Author> posted on <Resource>'
"""If subject is void, set the subject as 'Re: <Resource>' or
'Re: <mail.parent_id.subject>'
: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' <email>
# 2. if 'partner' is specified, but no related document: Partner Name <email>
# 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' <email>)
- elif 'partner', no notificatoin or no doc: recipient specific (Partner Name <email>)
- 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

View File

@ -36,6 +36,7 @@
<div>
<group string="Status">
<field name="auto_delete"/>
<field name="notification"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>

View File

@ -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 = """<div>
% 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)}
</div>
<%def name="display_message(message)">
<div>
Subject: ${message.subject}<br />
Body: ${message.body}
</div>
</%def>
<%def name="display_expandable()">
<div>This is an expandable.</div>
</%def>
"""
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,
}
}

View File

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

View File

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

View File

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

View File

@ -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, '<p>%s</p>' % 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, '<p>%s</p>' % 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',

View File

@ -21,6 +21,7 @@
from openerp.addons.mail.tests.common import TestMail
from openerp.tools import mute_logger
import socket
MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
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 <test_raoul@email.com>',
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 <test_raoul@email.com>',
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 <test_raoul@email.com>',
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')])

View File

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

View File

@ -11,6 +11,7 @@
<field name="composition_mode" invisible="1"/>
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="is_log" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name="mail_server_id" invisible="1"/>
<!-- Various warnings -->
@ -28,29 +29,27 @@
<field name="email_from"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="subject" placeholder="Subject..." required="True"/>
<!-- classic message composer -->
<label for="partner_ids" string="Recipients"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}"/>
<div groups="base.group_user"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}">
<span attrs="{'invisible':[('model', '=', False)]}">
Followers of
<field name="record_name" readonly="1" class="oe_inline oe_compose_recipients"/>
and
<!-- recipients -->
<label for="partner_ids" string="Recipients" attrs="{'invisible': [('is_log', '=', True)]}" groups="base.group_user"/>
<div groups="base.group_user" attrs="{'invisible': [('is_log', '=', True)]}">
<span attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}">
<strong>Email mass mailing</strong> on
<span attrs="{'invisible': [('use_active_domain', '=', True)]}">the selected records</span>
<span attrs="{'invisible': [('use_active_domain', '=', False)]}">the current search filter</span>.
</span>
<span attrs="{'invisible':[('composition_mode', '!=', 'comment')]}">Followers of the document and</span>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"/>
</div>
<!-- mass post / mass mailing -->
<field name="post"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<!-- mass post -->
<field name="notify"
attrs="{'invisible':['|', ('post', '!=', True), ('composition_mode', '!=', 'mass_mail')]}"/>
<field name="same_thread"
attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_mail'), ('post', '=', False)]}"/>
<field name="reply_to" placeholder="Email address te redirect replies..."
attrs="{'invisible':['|', '&amp;', ('same_thread', '=', True), ('post', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':['&amp;', '|', ('post', '=', False), ('same_thread', '=', False), ('composition_mode', '=', 'mass_mail')]}"/>
attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_post')]}"/>
<!-- mass mailing -->
<field name="same_thread" attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="reply_to" placeholder="Email address to redirect replies..."
attrs="{'invisible':['|', ('same_thread', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':[('same_thread', '!=', True), ('composition_mode', '=', 'mass_mail')]}"/>
</group>
<field name="body"/>
<field name="attachment_ids" widget="many2many_binary" string="Attach a file"/>

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
#
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-TODAY OpenERP SA (http://www.openerp.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -15,7 +15,7 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

View File

@ -1,29 +1,9 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'Marketing',
'version': '1.1',
'depends': ['base', 'base_setup', 'crm'],
'depends': ['base', 'base_setup'],
'author': 'OpenERP SA',
'category': 'Hidden/Dependency',
'description': """
@ -35,7 +15,6 @@ Contains the installer for marketing-related modules.
'website': 'http://www.openerp.com',
'data': [
'security/marketing_security.xml',
'security/ir.model.access.csv',
'marketing_view.xml',
'res_config_view.xml',
],
@ -44,4 +23,3 @@ Contains the installer for marketing-related modules.
'auto_install': False,
'images': ['images/config_marketing.jpeg'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -3,39 +3,12 @@
<data>
<!-- Top menu item -->
<menuitem name="Marketing"
id="base.marketing_menu"
groups="base.group_user"
sequence="85"/>
<menuitem name="Marketing" id="base.marketing_menu" sequence="85"
groups="base.group_user"/>
<record id="view_crm_lead_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_leads"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='categorization']" position="attributes">
<attribute name="string">Marketing</attribute>
<attribute name="groups"></attribute>
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<field name="type_id"/>
<field name="channel_id" widget="selection"/>
</xpath>
</field>
</record>
<!-- Reporting for Marketing -->
<menuitem name="Marketing" id="base.marketing_reporting_menu" sequence="10"
parent="base.menu_reporting" />
<record id="view_crm_opportunity_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_oppor"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='mailings']" position="before">
<group string="Marketing">
<field name="type_id" />
<field name="channel_id" widget="selection"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -1,40 +1,19 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, osv
class marketing_config_settings(osv.osv_memory):
class marketing_config_settings(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'res.config.settings'
_columns = {
'module_marketing_campaign': fields.boolean('Marketing campaigns',
'module_mass_mailing': fields.boolean(
'Mass Mailing',
help='Provide a way to perform mass mailings.\n'
'-This installs the module mass_mailing.'),
'module_marketing_campaign': fields.boolean(
'Marketing campaigns',
help='Provides leads automation through marketing campaigns. '
'Campaigns can in fact be defined on any resource, not just CRM leads.\n'
'-This installs the module marketing_campaign.'),
'module_marketing_campaign_crm_demo': fields.boolean('Demo data for marketing campaigns',
help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
'-This installs the module marketing_campaign_crm_demo.'),
'module_crm_profiling': fields.boolean('Track customer profile to focus your campaigns',
help='Allows users to perform segmentation within partners.\n'
'-This installs the module crm_profiling.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -11,24 +11,25 @@
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
or
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header>
<separator string="Campaigns"/>
<separator string="Mass Mailing"/>
<group>
<label for="id" string="Campaigns Settings"/>
<label for="id" string="Settings"/>
<div>
<div>
<div name="module_mass_mailing">
<field name="module_mass_mailing" class="oe_inline"/>
<label for="module_mass_mailing"/>
</div>
</div>
</group>
<separator string="Marketing Campaigns"/>
<group>
<label for="id" string="Settings"/>
<div>
<div name="module_marketing_campaign">
<field name="module_marketing_campaign" class="oe_inline"/>
<label for="module_marketing_campaign"/>
</div>
<div attrs="{'invisible':[('module_marketing_campaign','=',False)]}">
<field name="module_marketing_campaign_crm_demo" class="oe_inline"/>
<label for="module_marketing_campaign_crm_demo"/>
</div>
<div>
<field name="module_crm_profiling" class="oe_inline"/>
<label for="module_crm_profiling"/>
</div>
</div>
</group>
</form>

View File

@ -1 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
#
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-TODAY OpenERP SA (http://www.openerp.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -15,23 +15,8 @@
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import datetime
import time
from openerp.report import report_sxw
class expense(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context):
super(expense, self).__init__(cr, uid, name, context=context)
self.localcontext.update({'time': time, })
report_sxw.report_sxw('report.hr.expense', 'hr.expense.expense', 'addons/hr_expense/report/expense.rml',parser=expense)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
import models

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
{
'name': 'Marketing in CRM',
'version': '1.0',
'depends': ['marketing', 'crm'],
'author': 'OpenERP SA',
'category': 'Hidden/Dependency',
'description': """
Bridge module between marketing and CRM
""",
'website': 'http://www.openerp.com',
'data': [
'views/crm.xml',
'views/res_config.xml',
],
'demo': [],
'installable': True,
'auto_install': True,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import res_config

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from openerp.osv import fields, osv
class CrmMarketingConfig(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'marketing.config.settings'
_columns = {
'module_marketing_campaign_crm_demo': fields.boolean(
'Demo data for marketing campaigns',
help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
'-This installs the module marketing_campaign_crm_demo.'),
'module_crm_profiling': fields.boolean(
'Track customer profile to focus your campaigns',
help='Allows users to perform segmentation within partners.\n'
'-This installs the module crm_profiling.'),
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="view_crm_lead_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_leads"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='categorization']" position="attributes">
<attribute name="string">Marketing</attribute>
<attribute name="groups"></attribute>
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<field name="type_id"/>
<field name="channel_id" widget="selection"/>
</xpath>
</field>
</record>
<record id="view_crm_opportunity_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_oppor"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='mailings']" position="before">
<group string="Marketing">
<field name="type_id" />
<field name="channel_id" widget="selection"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_marketing_configuration" model="ir.ui.view">
<field name="name">marketing.config.settings.crm</field>
<field name="model">marketing.config.settings</field>
<field name="inherit_id" ref="marketing.view_marketing_configuration"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='module_marketing_campaign']" position="after">
<div attrs="{'invisible':[('module_marketing_campaign','=',False)]}">
<field name="module_marketing_campaign_crm_demo" class="oe_inline"/>
<label for="module_marketing_campaign_crm_demo"/>
</div>
<div>
<field name="module_crm_profiling" class="oe_inline"/>
<label for="module_crm_profiling"/>
</div>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -1,26 +1,5 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
import mass_mailing
import mail_mail
import mail_thread
import models
import wizard
import controllers

View File

@ -21,26 +21,33 @@
{
'name': 'Mass Mailing Campaigns',
'summary': 'Design, send and track emails',
'description': """
Easily send mass mailing to your leads, opportunities or customers. Track
marketing campaigns performance to improve conversion rates. Design
professional emails and reuse templates in a few clicks.
""",
'version': '1.0',
'version': '2.0',
'author': 'OpenERP',
'website': 'http://www.openerp.com',
'category': 'Marketing',
'depends': [
'mail',
'email_template',
'marketing',
'web_kanban_gauge',
'web_kanban_sparkline',
'website_mail',
],
'data': [
'mail_data.xml',
'data/mail_data.xml',
'data/mass_mailing_data.xml',
'wizard/mail_compose_message_view.xml',
'wizard/mail_mass_mailing_create_segment.xml',
'mass_mailing_view.xml',
'wizard/test_mailing.xml',
'views/mass_mailing.xml',
'views/res_config.xml',
'views/res_partner.xml',
'views/email_template.xml',
'security/ir.model.access.csv',
],
'js': [
@ -48,10 +55,11 @@ professional emails and reuse templates in a few clicks.
],
'qweb': [],
'css': [
'static/src/css/mass_mailing.css'
'static/src/css/mass_mailing.css',
'static/src/css/email_template.css'
],
'demo': [
'mass_mailing_demo.xml',
'data/mass_mailing_demo.xml',
],
'installable': True,
'auto_install': False,

View File

@ -1,11 +1,42 @@
import werkzeug
from openerp import http, SUPERUSER_ID
from openerp.http import request
class MassMailController(http.Controller):
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='none')
def track_mail_open(self, mail_id):
def track_mail_open(self, mail_id, **post):
""" Email tracking. """
mail_mail_stats = request.registry.get('mail.mail.statistics')
mail_mail_stats.set_opened(request.cr, SUPERUSER_ID, mail_mail_ids=[mail_id])
return "data:image/gif;base64,R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif'
response.set_data('R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='.decode('base64'))
return response
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', auth='none')
def mailing(self, mailing_id, email=None, res_id=None, **post):
cr, uid, context = request.cr, request.uid, request.context
MassMailing = request.registry['mail.mass_mailing']
mailing_ids = MassMailing.exists(cr, SUPERUSER_ID, [mailing_id], context=context)
if not mailing_ids:
return 'KO'
mailing = MassMailing.browse(cr, SUPERUSER_ID, mailing_ids[0], context=context)
if mailing.mailing_model == 'mail.mass_mailing.contact':
list_ids = [l.id for l in mailing.contact_list_ids]
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('list_id', 'in', list_ids), ('id', '=', res_id), ('email', 'ilike', email)], context=context)
request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
else:
email_fname = None
if 'email_from' in request.registry[mailing.mailing_model]._all_columns:
email_fname = 'email_from'
elif 'email' in request.registry[mailing.mailing_model]._all_columns:
email_fname = 'email'
if email_fname:
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('id', '=', res_id), (email_fname, 'ilike', email)], context=context)
if 'opt_out' in request.registry[mailing.mailing_model]._all_columns:
request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
return 'OK'

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<!-- After installation of the module, open the related menu -->
<record id="action_client_marketing_menu" model="ir.actions.client">
<field name="name">Open Marketing Menu</field>
<field name="tag">reload</field>
<field name="params" eval="{'menu_id': ref('base.marketing_menu')}"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_client_marketing_menu"/>
<field name="state">open</field>
</record>
<!-- Group to manage campaigns -->
<record id="group_mass_mailing_campaign" model="res.groups">
<field name="name">Manage Mass Mailing Campaigns</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<!-- Default stages of mass mailing campaigns -->
<record id="campaign_stage_1" model="mail.mass_mailing.stage">
<field name="name">Schedule</field>
<field name="sequence">10</field>
</record>
<record id="campaign_stage_2" model="mail.mass_mailing.stage">
<field name="name">Design</field>
<field name="sequence">20</field>
</record>
<record id="campaign_stage_3" model="mail.mass_mailing.stage">
<field name="name">Sent</field>
<field name="sequence">30</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,146 @@
<?xml version="1.0"?>
<openerp>
<data noupdate="1">
<record id="mass_mail_attach_1" model="ir.attachment">
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
<field name="datas_fname">SampleDoc.doc</field>
<field name="name">SampleDoc.doc</field>
</record>
<!-- Create mailing lists -->
<record id="mass_mail_list_1" model="mail.mass_mailing.list">
<field name="name">Imported Contacts</field>
</record>
<!-- Create Contacts -->
<record id="mass_mail_contact_1" model="mail.mass_mailing.contact">
<field name="name">Aristide Antario</field>
<field name="email">aa@example.com</field>
<field name="list_id" ref="mass_mailing.mass_mail_list_1"/>
</record>
<record id="mass_mail_contact_2" model="mail.mass_mailing.contact">
<field name="name">Beverly Bridge</field>
<field name="email">bb@example.com</field>
<field name="list_id" ref="mass_mailing.mass_mail_list_1"/>
</record>
<record id="mass_mail_contact_3" model="mail.mass_mailing.contact">
<field name="name">Carol Cartridge</field>
<field name="email">cc@example.com</field>
<field name="list_id" ref="mass_mailing.mass_mail_list_1"/>
<field name="opt_out" eval="True"/>
</record>
<!-- Create campaign and mailings -->
<record id="mass_mail_category_1" model="mail.mass_mailing.category">
<field name="name">Marketing</field>
</record>
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<field name="name">Newsletter</field>
<field name="stage_id" ref="mass_mailing.campaign_stage_1"/>
<field name="user_id" eval="ref('base.user_root')"/>
<field name="category_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_category_1')])]"/>
</record>
<record id="mass_mail_1" model="mail.mass_mailing">
<field name="name">First Newsletter</field>
<field name="state">done</field>
<field name="sent_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="mailing_model">res.partner</field>
<field name="mailing_domain">[('customer', '=', True)]</field>
<field name="reply_to_mode">email</field>
<field name="reply_to"><![CDATA[Info <info@yourcompany.example.com>]]></field>
<field name="body_html"><![CDATA[<div data-snippet-id="big-picture" style="padding:0px; margin:0px">
<table cellpadding="0" cellspacing="0" style="margin:10px 0px 0px;vertical-align:top;padding:0px;font-family:arial;font-size:12px;color:rgb(51,51,51)">
<tbody>
<tr>
<td style="width:600px" valign="top">
<h2 style="text-align: center; padding:0px 5px">A Punchy Headline</h2>
</td>
</tr>
<tr>
<td style="width:600px" valign="top"><img src="/website/static/src/img/big_picture.png" style="display:block;border:none;min-height:250px;margin:0 auto;" width="500"></td>
</tr>
<tr>
<td style="width:600px" valign="top">
<p style="text-align: center; overflow:hidden"></p>
<h3 style="text-align: center; padding:0px 5px">A Small Subtitle for ${object.name}</h3>
<p></p>
<p style="text-align: center; overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
</tr>
</tbody>
</table>
</div>
<div data-snippet-id="three-columns" style="padding:0px; margin:0px">
<table cellpadding="0" cellspacing="0" style="margin:10px 0px 0px;vertical-align:top;padding:0px;font-family:arial;font-size:12px;color:rgb(51,51,51)">
<tbody>
<tr>
<td style="width:300px" valign="top"><img src="/website/static/src/img/desert_thumb.jpg" style="display:block;border:none;min-height:50px" width="275"></td>
<td style="width:300px" valign="top"><img src="/website/static/src/img/deers_thumb.jpg" style="display:block;border:none;min-height:50px" width="275"></td>
</tr>
<tr>
<td style="width:300px" valign="top">
<h3 style="text-align: center; padding:0px 5px">Feature One</h3>
<p style="overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
<td style="width:300px" valign="top">
<h3 style="text-align: center; padding:0px 5px">Feature Two</h3>
<p style="overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
</tr>
</tbody>
</table>
</div>]]></field>
<field name="attachment_ids" eval="[(4, ref('mass_mail_attach_1'))]"/>
</record>
<record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field>
<field name="state">test</field>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="mailing_model">res.partner</field>
<field name="mailing_domain">[('customer', '=', True)]</field>
<field name="reply_to_mode">email</field>
<field name="reply_to"><![CDATA[Info <info@yourcompany.example.com>]]></field>
</record>
<record id="mass_mail_email_1" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111000@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_2" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111001@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111002@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_4" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@OpenERP.com</field>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_5" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
</data>
</openerp>

View File

@ -1,369 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from datetime import datetime
from dateutil import relativedelta
from openerp import tools
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MassMailingCampaign(osv.Model):
"""Model of mass mailing campaigns.
"""
_name = "mail.mass_mailing.campaign"
_description = 'Mass Mailing Campaign'
# number of embedded mailings in kanban view
_kanban_mailing_nbr = 4
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
results = dict.fromkeys(ids, False)
for campaign in self.browse(cr, uid, ids, context=context):
results[campaign.id] = {
'sent': len(campaign.statistics_ids),
# delivered: shouldn't be: all mails - (failed + bounced) ?
'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and
'opened': len([stat for stat in campaign.statistics_ids if stat.opened]),
'replied': len([stat for stat in campaign.statistics_ids if stat.replied]),
'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]),
}
return results
def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
""" Gather data about mass mailings to display them in kanban view as
nested kanban views is not possible currently. """
results = dict.fromkeys(ids, '')
for campaign in self.browse(cr, uid, ids, context=context):
mass_mailing_results = []
for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]:
mass_mailing_object = {}
for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']:
mass_mailing_object[attr] = getattr(mass_mailing, attr)
mass_mailing_results.append(mass_mailing_object)
results[campaign.id] = mass_mailing_results
return results
_columns = {
'name': fields.char(
'Campaign Name', required=True,
),
'user_id': fields.many2one(
'res.users', 'Responsible',
required=True,
),
'mass_mailing_ids': fields.one2many(
'mail.mass_mailing', 'mass_mailing_campaign_id',
'Mass Mailings',
),
'mass_mailing_kanban_ids': fields.function(
_get_mass_mailing_kanban_ids,
type='text', string='Mass Mailings (kanban data)',
help='This field has for purpose to gather data about mass mailings '
'to display them in kanban view as nested kanban views is not '
'possible currently',
),
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_campaign_id',
'Sent Emails',
),
'color': fields.integer('Color Index'),
# stat fields
'sent': fields.function(
_get_statistics,
string='Sent Emails',
type='integer', multi='_get_statistics'
),
'delivered': fields.function(
_get_statistics,
string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics,
string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics,
string='Replied',
type='integer', multi='_get_statistics'
),
'bounced': fields.function(
_get_statistics,
string='Bounced',
type='integer', multi='_get_statistics'
),
}
_defaults = {
'user_id': lambda self, cr, uid, ctx=None: uid,
}
def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
ctx = dict(context)
ctx.update({
'default_mass_mailing_campaign_id': ids[0],
})
return {
'name': _('Create a Mass Mailing for the Campaign'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.create',
'views': [(False, 'form')],
'view_id': False,
'target': 'new',
'context': ctx,
}
class MassMailing(osv.Model):
""" MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing'
_description = 'Wave of sending emails'
# number of periods for tracking mail_mail statistics
_period_number = 6
_order = 'date DESC'
def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
""" Generic method to generate data for bar chart values using SparklineBarWidget.
This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
:param obj: the target model (i.e. crm_lead)
:param domain: the domain applied to the read_group
:param list read_fields: the list of fields to read in the read_group
:param str value_field: the field used to compute the value of the bar slice
:param str groupby_field: the fields used to group
:return list section_result: a list of dicts: [
{ 'value': (int) bar_column_value,
'tootip': (str) bar_column_tooltip,
}
]
"""
date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
section_result = [{'value': 0,
'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
} for i in range(0, self._period_number)]
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
for group in group_obj:
group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
return section_result
def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
""" Get the daily statistics of the mass mailing. This is done by a grouping
on opened and replied fields. Using custom format in context, we obtain
results for the next 6 days following the mass mailing date. """
obj = self.pool['mail.mail.statistics']
res = {}
for id in ids:
res[id] = {}
date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened:day', context=context)
domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied:day', context=context)
return res
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
results = dict.fromkeys(ids, False)
for mass_mailing in self.browse(cr, uid, ids, context=context):
results[mass_mailing.id] = {
'sent': len(mass_mailing.statistics_ids),
'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and
'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]),
'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]),
'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]),
}
return results
_columns = {
'name': fields.char('Name', required=True),
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
ondelete='cascade', required=True,
),
'template_id': fields.many2one(
'email.template', 'Email Template',
ondelete='set null',
),
'domain': fields.char('Domain'),
'date': fields.datetime('Date'),
'color': fields.related(
'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index',
),
# statistics data
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_id',
'Emails Statistics',
),
'sent': fields.function(
_get_statistics,
string='Sent Emails',
type='integer', multi='_get_statistics'
),
'delivered': fields.function(
_get_statistics,
string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics,
string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics,
string='Replied',
type='integer', multi='_get_statistics'
),
'bounced': fields.function(
_get_statistics,
string='Bounce',
type='integer', multi='_get_statistics'
),
# monthly ratio
'opened_monthly': fields.function(
_get_daily_statistics,
string='Opened',
type='char', multi='_get_daily_statistics',
),
'replied_monthly': fields.function(
_get_daily_statistics,
string='Replied',
type='char', multi='_get_daily_statistics',
),
}
_defaults = {
'date': fields.datetime.now,
}
class MailMailStats(osv.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_columns = {
'mail_mail_id': fields.integer(
'Mail ID',
help='ID of the related mail_mail. This field is an integer field because'
'the related mail_mail can be deleted separately from its statistics.'
),
'message_id': fields.char(
'Message-ID',
),
'model': fields.char(
'Document model',
),
'res_id': fields.integer(
'Document ID',
),
# campaign / wave data
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass Mailing',
ondelete='set null',
),
'mass_mailing_campaign_id': fields.related(
'mass_mailing_id', 'mass_mailing_campaign_id',
type='many2one', ondelete='set null',
relation='mail.mass_mailing.campaign',
string='Mass Mailing Campaign',
store=True, readonly=True,
),
'template_id': fields.related(
'mass_mailing_id', 'template_id',
type='many2one', ondelete='set null',
relation='email.template',
string='Email Template',
store=True, readonly=True,
),
# Bounce and tracking
'opened': fields.datetime(
'Opened',
help='Date when this email has been opened for the first time.'),
'replied': fields.datetime(
'Replied',
help='Date when this email has been replied for the first time.'),
'bounced': fields.datetime(
'Bounced',
help='Date when this email has bounced.'
),
}
def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
""" Set as opened """
if not ids and mail_mail_ids:
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
elif not ids and mail_message_ids:
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
else:
ids = []
for stat in self.browse(cr, uid, ids, context=context):
if not stat.opened:
self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
return ids
def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
""" Set as replied """
if not ids and mail_mail_ids:
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
elif not ids and mail_message_ids:
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
else:
ids = []
for stat in self.browse(cr, uid, ids, context=context):
if not stat.replied:
self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
return ids
def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
""" Set as bounced """
if not ids and mail_mail_ids:
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
elif not ids and mail_message_ids:
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
else:
ids = []
for stat in self.browse(cr, uid, ids, context=context):
if not stat.bounced:
self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)
return ids

View File

@ -1,82 +0,0 @@
<?xml version="1.0"?>
<openerp>
<!-- <data noupdate="1"> -->
<data>
<record id="mass_mail_template_1" model="email.template">
<field name="name">Partner Newsletter 1</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="partner_to">${object.id}</field>
<field name="body_html"><![CDATA[<p>Hello</p>]]></field>
</record>
<record id="mass_mail_template_2" model="email.template">
<field name="name">Partner Newsletter 2</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="partner_to">${object.id}</field>
<field name="body_html"><![CDATA[<p>Hello</p>]]></field>
</record>
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<field name="name">Partners Newsletter</field>
<field name="user_id" eval="ref('base.user_root')"/>
</record>
<record id="mass_mail_1" model="mail.mass_mailing">
<field name="name">First Newsletter</field>
<field name="template_id" eval="ref('mass_mail_template_1')"/>
<field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record>
<record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field>
<field name="template_id" eval="ref('mass_mail_template_2')"/>
<field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record>
<record id="mass_mail_email_1" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111000@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_2" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111001@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111002@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_4" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@OpenERP.com</field>
</record>
<record id="mass_mail_email_5" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@OpenERP.com</field>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_2_1" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111005@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_2_2" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111006@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_2_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111007@OpenERP.com</field>
</record>
</data>
</openerp>

View File

@ -1,379 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<group expand="0" string="Group By...">
<filter string="Campaign" name="group_mass_mailing_campaign_id"
context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Template" name="group_template_id"
context="{'group_by': 'template_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id" invisible="1"/>
<field name="template_id" invisible="1"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<sheet>
<group>
<group>
<field name="name"/>
<field name="mass_mailing_campaign_id" readonly="True"/>
</group>
<group>
<field name="template_id"/>
<field name="domain"/>
<field name="date"/>
</group>
</group>
<group string="Email Statistics">
<field name="statistics_ids" nolabel="1" colspan="2"/>
<group>
<field name="sent"/>
<field name="opened"/>
<field name="bounced"/>
</group>
<group>
<field name="delivered"/>
<field name="replied"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<kanban>
<field name='color'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing oe_kanban_mass_mailing_segment">
<div class="oe_kanban_content">
<div>
<h3>
<field name="name"/>
</h3>
<p style="margin-left: 10px; margin-top: 8px;">
Sent: <field name="date"/><br />
Campaign: <field name="mass_mailing_campaign_id"/>
</p>
</div>
<div>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="sent"/></span><br />
Sent
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="delivered"/></span><br />
Delivered
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="opened"/></span><br />
Opened
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="replied"/></span><br />
Replied
</p>
</div>
<div>
<div class="oe_sparkline_container">
<h4 class="oe_sparkline_bar_title">Opened</h4><br />
<field name="opened_monthly" widget="sparkline_bar" options="{'height': '50px', 'barWidth': 10, 'barSpacing': 5}"/>
</div>
<div class="oe_sparkline_container">
<h4 class="oe_sparkline_bar_title">Replied</h4><br />
<field name="replied_monthly" widget="sparkline_bar" options="{'height': '50px', 'barWidth': 10, 'barSpacing': 5}"/>
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
</record>
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
}
</field>
</record>
<!-- MASS MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mass Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Responsibles" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0">
<header>
<button name="launch_mass_mailing_create_wizard" type="object"
class="oe_highlight" string="Create a New Mailing"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="user_id"/>
</group>
<group>
<group>
<field name="sent"/>
<field name="opened"/>
<field name="bounced"/>
</group>
<group>
<field name="delivered"/>
<field name="replied"/>
</group>
</group>
<group>
<field name="mass_mailing_ids" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban>
<field name="mass_mailing_kanban_ids"/>
<field name='sent'/>
<field name='color'/>
<templates>
<t t-name="mass_mailing.mass_mailing">
<div class="oe_mass_mailings">
<div>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action">
<h4><t t-raw="mass_mailing.name"/></h4>
</a>
</div>
<div>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.sent"/></span><br />
Sent
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.delivered"/></span><br />
Delivered
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.opened"/></span><br />
Opened
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.replied"/></span><br />
Replied
</p>
</div>
</div>
</t>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing oe_kanban_mass_mailing_campaign">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Settings</a></li>
</t>
<t t-if="widget.view.is_action_enabled('edit')">
<li><a name="%(action_mail_mass_mailing_create)d" type="action">New Wave</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class="oe_kanban_content">
<h3>
<field name="name"/>
</h3>
<div>
<field name="delivered" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
<field name="opened" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
<field name="replied" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
</div>
<t t-foreach='record.mass_mailing_kanban_ids.value' t-as='mass_mailing'>
<t t-call="mass_mailing.mass_mailing"/>
</t>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mass Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new mass mailing campaign.
</p><p>
Create a campaign to structure mass mailing and get analysis from email status.
</p>
</field>
</record>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<tree string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<form string="Mail Statistics" version="7.0">
<group>
<group>
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.mail.statistics</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Top menu item -->
<menuitem name="Marketing" id="base.marketing_menu" sequence="85" groups="base.group_user"/>
<!-- Add in marketing -->
<menuitem name="Mass Mailing" id="mass_mailing_campaign"
parent="base.marketing_menu" sequence="1"/>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"/>
<menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailings"/>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
</data>
</openerp>

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
import mass_mailing
import mass_mailing_stats
import mail_mail
import mail_thread
import res_config

View File

@ -19,7 +19,8 @@
#
##############################################################################
from urlparse import urljoin
import urllib
import urlparse
from openerp import tools
from openerp import SUPERUSER_ID
@ -32,6 +33,7 @@ class MailMail(osv.Model):
_inherit = ['mail.mail']
_columns = {
'mailing_id': fields.many2one('mail.mass_mailing', 'Mass Mailing'),
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mail_mail_id',
string='Statistics',
@ -50,9 +52,24 @@ class MailMail(osv.Model):
def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id)
track_url = urlparse.urljoin(
base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % {
'mail_id': mail.id,
'params': urllib.urlencode({'db': cr.dbname})
}
)
return '<img src="%s" alt=""/>' % track_url
def _get_unsubscribe_url(self, cr, uid, mail, email_to, msg=None, context=None):
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
url = urlparse.urljoin(
base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
'mailing_id': mail.mailing_id.id,
'params': urllib.urlencode({'db': cr.dbname, 'res_id': mail.res_id, 'email': email_to})
}
)
return '<small><a href="%s">%s</a></small>' % (url, msg or 'Click to unsubscribe')
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Override to add the tracking URL to the body. """
body = super(MailMail, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)
@ -63,3 +80,19 @@ class MailMail(osv.Model):
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
res = super(MailMail, self).send_get_email_dict(cr, uid, mail, partner, context=context)
if mail.mailing_id and res.get('body') and res.get('email_to'):
email_to = tools.email_split(res.get('email_to')[0])
unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context)
if unsubscribe_url:
res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p')
return res
def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
if mail_sent is True and mail.statistics_ids:
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'sent': fields.datetime.now()}, context=context)
elif mail_sent is False and mail.statistics_ids:
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'exception': fields.datetime.now()}, context=context)
return super(MailMail, self)._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)

View File

@ -29,7 +29,7 @@ from openerp.osv import osv
_logger = logging.getLogger(__name__)
class MailThread(osv.Model):
class MailThread(osv.AbstractModel):
""" Update MailThread to add the feature of bounced emails and replied emails
in message_process. """
_name = 'mail.thread'

View File

@ -0,0 +1,571 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from dateutil import relativedelta
import json
import random
import urllib
import urlparse
from openerp import tools
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MassMailingCategory(osv.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.category'
_description = 'Mass Mailing Category'
_order = 'name'
_columns = {
'name': fields.char('Name', required=True),
}
class MassMailingContact(osv.Model):
"""Model of a contact. This model is different from the partner model
because it holds only some basic information: name, email. The purpose is to
be able to deal with large contact list to email without bloating the partner
base."""
_name = 'mail.mass_mailing.contact'
_description = 'Mass Mailing Contact'
_order = 'email'
_rec_name = 'email'
_columns = {
'name': fields.char('Name'),
'email': fields.char('Email', required=True),
'create_date': fields.datetime('Create Date'),
'list_id': fields.many2one(
'mail.mass_mailing.list', string='Mailing List',
ondelete='cascade', required=True,
),
'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
}
def _get_latest_list(self, cr, uid, context={}):
lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
return lid and lid[0] or False
_defaults = {
'list_id': _get_latest_list
}
def name_create(self, cr, uid, name, context=None):
name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
if name and not email:
email = name
if email and not name:
name = email
rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
return self.name_get(cr, uid, [rec_id], context)[0]
class MassMailingList(osv.Model):
"""Model of a contact list. """
_name = 'mail.mass_mailing.list'
_order = 'name'
_description = 'Mailing List'
def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
result = dict.fromkeys(ids, 0)
Contacts = self.pool.get('mail.mass_mailing.contact')
for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context):
result[group['list_id'][0]] = group['list_id_count']
return result
_columns = {
'name': fields.char('Mailing List', required=True),
'contact_nbr': fields.function(
_get_contact_nbr, type='integer',
string='Number of Contacts',
),
}
class MassMailingStage(osv.Model):
"""Stage for mass mailing campaigns. """
_name = 'mail.mass_mailing.stage'
_description = 'Mass Mailing Campaign Stage'
_order = 'sequence'
_columns = {
'name': fields.char('Name', required=True, translate=True),
'sequence': fields.integer('Sequence'),
}
_defaults = {
'sequence': 0,
}
class MassMailingCampaign(osv.Model):
"""Model of mass mailing campaigns. """
_name = "mail.mass_mailing.campaign"
_description = 'Mass Mailing Campaign'
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
Statistics = self.pool['mail.mail.statistics']
results = dict.fromkeys(ids, False)
for cid in ids:
stat_ids = Statistics.search(cr, uid, [('mass_mailing_campaign_id', '=', cid)], context=context)
stats = Statistics.browse(cr, uid, stat_ids, context=context)
results[cid] = {
'total': len(stats),
'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
'sent': len([s for s in stats if not s.sent is False]),
'opened': len([s for s in stats if not s.opened is False]),
'replied': len([s for s in stats if not s.replied is False]),
'bounced': len([s for s in stats if not s.bounced is False]),
}
results[cid]['delivered'] = results[cid]['sent'] - results[cid]['bounced']
results[cid]['received_ratio'] = 100.0 * results[cid]['delivered'] / (results[cid]['total'] or 1)
results[cid]['opened_ratio'] = 100.0 * results[cid]['opened'] / (results[cid]['total'] or 1)
results[cid]['replied_ratio'] = 100.0 * results[cid]['replied'] / (results[cid]['total'] or 1)
return results
_columns = {
'name': fields.char('Name', required=True),
'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
'user_id': fields.many2one(
'res.users', 'Responsible',
required=True,
),
'category_ids': fields.many2many(
'mail.mass_mailing.category', 'mail_mass_mailing_category_rel',
'category_id', 'campaign_id', string='Categories'),
'mass_mailing_ids': fields.one2many(
'mail.mass_mailing', 'mass_mailing_campaign_id',
'Mass Mailings',
),
'unique_ab_testing': fields.boolean(
'AB Testing',
help='If checked, recipients will be mailed only once, allowing to send'
'various mailings in a single campaign to test the effectiveness'
'of the mailings.'),
'color': fields.integer('Color Index'),
# stat fields
'total': fields.function(
_get_statistics, string='Total',
type='integer', multi='_get_statistics'
),
'scheduled': fields.function(
_get_statistics, string='Scheduled',
type='integer', multi='_get_statistics'
),
'failed': fields.function(
_get_statistics, string='Failed',
type='integer', multi='_get_statistics'
),
'sent': fields.function(
_get_statistics, string='Sent Emails',
type='integer', multi='_get_statistics'
),
'delivered': fields.function(
_get_statistics, string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics, string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics, string='Replied',
type='integer', multi='_get_statistics'
),
'bounced': fields.function(
_get_statistics, string='Bounced',
type='integer', multi='_get_statistics'
),
'received_ratio': fields.function(
_get_statistics, string='Received Ratio',
type='integer', multi='_get_statistics',
),
'opened_ratio': fields.function(
_get_statistics, string='Opened Ratio',
type='integer', multi='_get_statistics',
),
'replied_ratio': fields.function(
_get_statistics, string='Replied Ratio',
type='integer', multi='_get_statistics',
),
}
def _get_default_stage_id(self, cr, uid, context=None):
stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
return stage_ids and stage_ids[0] or False
_defaults = {
'user_id': lambda self, cr, uid, ctx=None: uid,
'stage_id': lambda self, *args: self._get_default_stage_id(*args),
}
def get_recipients(self, cr, uid, ids, model=None, context=None):
"""Return the recipients of a mailing campaign. This is based on the statistics
build for each mailing. """
Statistics = self.pool['mail.mail.statistics']
res = dict.fromkeys(ids, False)
for cid in ids:
domain = [('mass_mailing_campaign_id', '=', cid)]
if model:
domain += [('model', '=', model)]
stat_ids = Statistics.search(cr, uid, domain, context=context)
res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
return res
class MassMailing(osv.Model):
""" MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing'
_description = 'Mass Mailing'
# number of periods for tracking mail_mail statistics
_period_number = 6
_order = 'sent_date DESC'
def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, context=None):
""" Generic method to generate data for bar chart values using SparklineBarWidget.
This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
:param obj: the target model (i.e. crm_lead)
:param domain: the domain applied to the read_group
:param list read_fields: the list of fields to read in the read_group
:param str value_field: the field used to compute the value of the bar slice
:param str groupby_field: the fields used to group
:return list section_result: a list of dicts: [
{ 'value': (int) bar_column_value,
'tootip': (str) bar_column_tooltip,
}
]
"""
date_begin = date_begin.date()
section_result = [{'value': 0,
'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
} for i in range(0, self._period_number)]
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
for group in group_obj:
group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
return section_result
def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
""" Get the daily statistics of the mass mailing. This is done by a grouping
on opened and replied fields. Using custom format in context, we obtain
results for the next 6 days following the mass mailing date. """
obj = self.pool['mail.mail.statistics']
res = {}
for mailing in self.browse(cr, uid, ids, context=context):
res[mailing.id] = {}
date = mailing.sent_date if mailing.sent_date else mailing.create_date
date_begin = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('mass_mailing_id', '=', mailing.id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
res[mailing.id]['opened_dayly'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened:day', date_begin, context=context))
domain = [('mass_mailing_id', '=', mailing.id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
res[mailing.id]['replied_dayly'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied:day', date_begin, context=context))
return res
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
Statistics = self.pool['mail.mail.statistics']
results = dict.fromkeys(ids, False)
for mid in ids:
stat_ids = Statistics.search(cr, uid, [('mass_mailing_id', '=', mid)], context=context)
stats = Statistics.browse(cr, uid, stat_ids, context=context)
results[mid] = {
'total': len(stats),
'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
'sent': len([s for s in stats if not s.sent is False]),
'opened': len([s for s in stats if not s.opened is False]),
'replied': len([s for s in stats if not s.replied is False]),
'bounced': len([s for s in stats if not s.bounced is False]),
}
results[mid]['delivered'] = results[mid]['sent'] - results[mid]['bounced']
results[mid]['received_ratio'] = 100.0 * results[mid]['delivered'] / (results[mid]['total'] or 1)
results[mid]['opened_ratio'] = 100.0 * results[mid]['opened'] / (results[mid]['total'] or 1)
results[mid]['replied_ratio'] = 100.0 * results[mid]['replied'] / (results[mid]['total'] or 1)
return results
def _get_mailing_model(self, cr, uid, context=None):
res = []
for model_name in self.pool:
model = self.pool[model_name]
if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'):
res.append((model._name, getattr(model, '_mail_mass_mailing')))
res.append(('mail.mass_mailing.contact', _('Mailing List')))
return res
# indirections for inheritance
_mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs)
_columns = {
'name': fields.char('Subject', required=True),
'email_from': fields.char('From', required=True),
'create_date': fields.datetime('Creation Date'),
'sent_date': fields.datetime('Sent Date'),
'body_html': fields.html('Body'),
'attachment_ids': fields.many2many(
'ir.attachment', 'mass_mailing_ir_attachments_rel',
'mass_mailing_id', 'attachment_id', 'Attachments'
),
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
ondelete='set null',
),
'state': fields.selection(
[('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
string='Status', required=True,
),
'color': fields.related(
'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index',
),
# mailing options
'reply_to_mode': fields.selection(
[('thread', 'In Document'), ('email', 'Specified Email Address')],
string='Reply-To Mode', required=True,
),
'reply_to': fields.char('Reply To', help='Preferred Reply-To Address'),
# recipients
'mailing_model': fields.selection(_mailing_model, string='Recipients Model', required=True),
'mailing_domain': fields.char('Domain'),
'contact_list_ids': fields.many2many(
'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists',
),
'contact_ab_pc': fields.integer(
'AB Testing percentage',
help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
),
# statistics data
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_id',
'Emails Statistics',
),
'total': fields.function(
_get_statistics, string='Total',
type='integer', multi='_get_statistics',
),
'scheduled': fields.function(
_get_statistics, string='Scheduled',
type='integer', multi='_get_statistics',
),
'failed': fields.function(
_get_statistics, string='Failed',
type='integer', multi='_get_statistics',
),
'sent': fields.function(
_get_statistics, string='Sent',
type='integer', multi='_get_statistics',
),
'delivered': fields.function(
_get_statistics, string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics, string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics, string='Replied',
type='integer', multi='_get_statistics',
),
'bounced': fields.function(
_get_statistics, string='Bounced',
type='integer', multi='_get_statistics',
),
'received_ratio': fields.function(
_get_statistics, string='Received Ratio',
type='integer', multi='_get_statistics',
),
'opened_ratio': fields.function(
_get_statistics, string='Opened Ratio',
type='integer', multi='_get_statistics',
),
'replied_ratio': fields.function(
_get_statistics, string='Replied Ratio',
type='integer', multi='_get_statistics',
),
# dayly ratio
'opened_dayly': fields.function(
_get_daily_statistics, string='Opened',
type='char', multi='_get_daily_statistics',
oldname='opened_monthly',
),
'replied_dayly': fields.function(
_get_daily_statistics, string='Replied',
type='char', multi='_get_daily_statistics',
oldname='replied_monthly',
)
}
def default_get(self, cr, uid, fields, context=None):
res = super(MassMailing, self).default_get(cr, uid, fields, context=context)
if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'):
if res['mailing_model'] in ['res.partner', 'mail.mass_mailing.contact']:
res['reply_to_mode'] = 'email'
else:
res['reply_to_mode'] = 'thread'
return res
_defaults = {
'state': 'draft',
'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
'reply_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
'mailing_model': 'mail.mass_mailing.contact',
'contact_ab_pc': 100,
}
#------------------------------------------------------
# Technical stuff
#------------------------------------------------------
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
mailing = self.browse(cr, uid, id, context=context)
default.update({
'state': 'draft',
'statistics_ids': [],
'name': _('%s (duplicate)') % mailing.name,
'sent_date': False,
})
return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
""" Override read_group to always display all states. """
if groupby and groupby[0] == "state":
# Default result structure
# states = self._get_state_list(cr, uid, context=context)
states = [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')]
read_group_all_states = [{
'__context': {'group_by': groupby[1:]},
'__domain': domain + [('state', '=', state_value)],
'state': state_value,
'state_count': 0,
} for state_value, state_name in states]
# Get standard results
read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
# Update standard results with default results
result = []
for state_value, state_name in states:
res = filter(lambda x: x['state'] == state_value, read_group_res)
if not res:
res = filter(lambda x: x['state'] == state_value, read_group_all_states)
res[0]['state'] = [state_value, state_name]
result.append(res[0])
return result
else:
return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
#------------------------------------------------------
# Views & Actions
#------------------------------------------------------
def on_change_model_and_list(self, cr, uid, ids, mailing_model, list_ids, context=None):
value = {}
if mailing_model == 'mail.mass_mailing.contact':
list_ids = map(lambda item: item if isinstance(item, (int, long)) else [lid for lid in item[2]], list_ids)
if list_ids:
value['mailing_domain'] = "[('list_id', 'in', %s)]" % list_ids
else:
value['mailing_domain'] = "[('list_id', '=', False)]"
else:
value['mailing_domain'] = False
return {'value': value}
def action_duplicate(self, cr, uid, ids, context=None):
copy_id = None
for mid in ids:
copy_id = self.copy(cr, uid, mid, context=context)
if copy_id:
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'res_id': copy_id,
'context': context,
}
return False
def action_test_mailing(self, cr, uid, ids, context=None):
ctx = dict(context, default_mass_mailing_id=ids[0])
return {
'name': _('Test Mailing'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.test',
'target': 'new',
'context': ctx,
}
def action_edit_html(self, cr, uid, ids, context=None):
if not len(ids) == 1:
raise ValueError('One and only one ID allowed for this action')
mail = self.browse(cr, uid, ids[0], context=context)
url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % (ids[0], mail.mailing_model)
return {
'name': _('Open with Visual Editor'),
'type': 'ir.actions.act_url',
'url': url,
'target': 'self',
}
#------------------------------------------------------
# Email Sending
#------------------------------------------------------
def get_recipients(self, cr, uid, mailing, context=None):
domain = eval(mailing.mailing_domain)
res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
# randomly choose a fragment
if mailing.contact_ab_pc < 100:
contact_nbr = self.pool[mailing.mailing_model].search(cr, uid, domain, count=True, context=context)
topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
else:
already_mailed = set([])
remaining = set(res_ids).difference(already_mailed)
if topick > len(remaining):
topick = len(remaining)
res_ids = random.sample(remaining, topick)
return res_ids
def send_mail(self, cr, uid, ids, context=None):
author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
for mailing in self.browse(cr, uid, ids, context=context):
# instantiate an email composer + send emails
res_ids = self.get_recipients(cr, uid, mailing, context=context)
comp_ctx = dict(context, active_ids=res_ids)
composer_values = {
'author_id': author_id,
'body': mailing.body_html,
'subject': mailing.name,
'model': mailing.mailing_model,
'email_from': mailing.email_from,
'record_name': False,
'composition_mode': 'mass_mail',
'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
}
if mailing.reply_to_mode == 'email':
composer_values['reply_to'] = mailing.reply_to
composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context)
return True

View File

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from datetime import datetime
from dateutil import relativedelta
import random
try:
import simplejson as json
except ImportError:
import json
import urllib
import urlparse
from openerp import tools
from openerp.exceptions import Warning
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MailMailStats(osv.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_columns = {
'mail_mail_id': fields.integer(
'Mail ID',
help='ID of the related mail_mail. This field is an integer field because'
'the related mail_mail can be deleted separately from its statistics.'
),
'message_id': fields.char('Message-ID'),
'model': fields.char('Document model'),
'res_id': fields.integer('Document ID'),
# campaign / wave data
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass Mailing',
ondelete='set null',
),
'mass_mailing_campaign_id': fields.related(
'mass_mailing_id', 'mass_mailing_campaign_id',
type='many2one', ondelete='set null',
relation='mail.mass_mailing.campaign',
string='Mass Mailing Campaign',
store=True, readonly=True,
),
# Bounce and tracking
'scheduled': fields.datetime('Scheduled', help='Date when the email has been created'),
'sent': fields.datetime('Sent', help='Date when the email has been sent'),
'exception': fields.datetime('Exception', help='Date of technical error leading to the email not being sent'),
'opened': fields.datetime('Opened', help='Date when the email has been opened the first time'),
'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'),
'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'),
}
_defaults = {
'scheduled': fields.datetime.now,
}
def _get_ids(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, domain=None, context=None):
if not ids and mail_mail_ids:
base_domain = [('mail_mail_id', 'in', mail_mail_ids)]
elif not ids and mail_message_ids:
base_domain = [('message_id', 'in', mail_message_ids)]
else:
base_domain = [('id', 'in', ids or [])]
if domain:
base_domain = ['&'] + domain + base_domain
return self.search(cr, uid, base_domain, context=context)
def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('opened', '=', False)], context)
self.write(cr, uid, stat_ids, {'opened': fields.datetime.now()}, context=context)
return stat_ids
def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('replied', '=', False)], context)
self.write(cr, uid, stat_ids, {'replied': fields.datetime.now()}, context=context)
return stat_ids
def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('bounced', '=', False)], context)
self.write(cr, uid, stat_ids, {'bounced': fields.datetime.now()}, context=context)
return stat_ids

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from openerp.osv import fields, osv
class MassMailingConfiguration(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'marketing.config.settings'
_columns = {
'group_mass_mailing_campaign': fields.boolean(
'Manage Mass Mailing using Campaign',
implied_group='mass_mailing.group_mass_mailing_campaign',
help="""Manage mass mailign using Campaigns"""),
}

View File

@ -1,4 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mass_mailing_category,mail.mass_mailing.category,model_mail_mass_mailing_category,base.group_user,1,1,1,1
access_mass_mailing_contact,mail.mass_mailing.contact,model_mail_mass_mailing_contact,base.group_user,1,1,1,1
access_mass_mailing_list,mail.mass_mailing.list,model_mail_mass_mailing_list,base.group_user,1,1,1,1
access_mass_mailing_stage,mail.mass_mailing.stage,model_mail_mass_mailing_stage,base.group_user,1,1,1,1
access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,base.group_user,1,1,1,0
access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1
access_mass_mailing,mail.mass_mailing,model_mail_mass_mailing,base.group_user,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mass_mailing_category mail.mass_mailing.category model_mail_mass_mailing_category base.group_user 1 1 1 1
3 access_mass_mailing_contact mail.mass_mailing.contact model_mail_mass_mailing_contact base.group_user 1 1 1 1
4 access_mass_mailing_list mail.mass_mailing.list model_mail_mass_mailing_list base.group_user 1 1 1 1
5 access_mass_mailing_stage mail.mass_mailing.stage model_mail_mass_mailing_stage base.group_user 1 1 1 1
6 access_mass_mailing_campaign mail.mass_mailing.campaign model_mail_mass_mailing_campaign base.group_user 1 1 1 0
7 access_mass_mailing_campaign_system mail.mass_mailing.campaign.system model_mail_mass_mailing_campaign base.group_system 1 1 1 1
8 access_mass_mailing mail.mass_mailing model_mail_mass_mailing base.group_user 1 1 1 0

View File

@ -0,0 +1,17 @@
.openerp .oe_kanban_email_template {
width: 360px;
min-height: 270px !important;
}
.kanban_html_preview {
width: 600px;
height: 600px;
-webkit-transform: scale(.50);
-ms-transform: scale(.50);
transform: scale(.50);
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
margin: 0 0px -300px 0;
overflow: hidden !important;
}

View File

@ -1,61 +1,13 @@
.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_campaign {
/* Customize to manage content */
width: 552px;
min-height: 278px !important;
/* End of customize */
.openerp .oe_kanban_view .oe_kanban_mass_mailing_campaign {
width: 280px;
min-height: 141px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_segment {
/* Customize to manage content */
width: 282px;
min-height: 246px !important;
/* End of customize */
.openerp .oe_kanban_view .oe_kanban_mass_mailing_campaign .oe_kanban_header_right {
float: right;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_stats {
width: 122px; /* Manage space in between stats */
display: inline-block;
margin: 2px 5px 0px 5px;
text-align: center;
border: 1px solid rgba(0, 0, 0, 0.16);
-webkit-border-radius: 2px;
border-radius: 2px;
background-color: #FFFFFF;
.openerp .oe_kanban_view .oe_kanban_mass_mailing {
width: 280px;
min-height: 141px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_result {
font-weight: bold;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_gauge {
width: 120px;
height: 120px;
display: inline-block;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_kanban_content div.oe_sparkline_container {
height: 60px;
width: 120px;
display: inline-block;
margin: 8px 5px 0px 5px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_sparkline_bar_title {
text-align: center;
display: inline;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_sparkline_bar {
width: 100px;
height: 60px;
display: inline-block;
}
/*
* Campaign related CSS
*/
/*
* Segment related CSS
*/

View File

@ -1,13 +1,13 @@
openerp.mass_mailing = function(openerp) {
openerp.mass_mailing = function (instance) {
var _t = instance.web._t;
openerp.web_kanban.KanbanRecord.include({
on_card_clicked: function (event) {
if (this.view.dataset.model === 'mail.mass_mailing.campaign') {
this.$('.oe_mass_mailings a').first().click();
this.$('.oe_mailings').click();
} else {
this._super.apply(this, arguments);
}
},
});
};

View File

@ -0,0 +1,97 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Email Templates -->
<record model="ir.ui.view" id="email_template_form_minimal">
<field name="name">email.template.form.minimal</field>
<field name="model">email.template</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<form string="Templates" version="7.0">
<sheet>
<group>
<group>
<field name="name" required="True"/>
<field name="model_id" required="1" options="{'no_open': True, 'no_create': True}"
on_change="onchange_model_id(model_id)"
domain="[('model', 'in', ['res.partner', 'mail.mass_mailing.contact'])]"/>
<field name="model" invisible="True"/>
<field name="use_default_to" invisible="1"/>
</group>
<group>
<div class="oe_right oe_button_box" name="buttons">
<button name="%(email_template.wizard_email_template_preview)d" string="Preview"
type="action" target="new"
context="{'template_id':active_id}"/>
<br />
<!-- <field name="website_link" widget='html' radonly='1'
style='margin: 0px; padding: 0px;'/> -->
</div>
</group>
</group>
<notebook>
<page string="Body">
<field name="body_html" nolabel="1"/>
<field name="attachment_ids" widget="many2many_binary"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_email_template_kanban">
<field name="name">email.template.kanban</field>
<field name="model">email.template</field>
<field name="arch" type="xml">
<kanban>
<field name="body_html"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click oe_kanban_email_template">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Edit</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
</ul>
</div>
<div class="oe_kanban_content">
<h3>
<field name="name"/>
</h3>
<div class="kanban_html_preview">
<t t-raw="record.body_html.raw_value"/>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="action_email_template_marketing">
<field name="name">Templates</field>
<field name="res_model">email.template</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'form_view_ref': 'mass_mailing.email_template_form_minimal',
'default_use_default_to': True,
}</field>
</record>
<!-- Add Templates in Marketing / Mass mailing menu -->
<menuitem name="Mail Templates" id="menu_email_template"
parent="mass_mailing_campaign" sequence="3"
action="action_email_template_marketing"/>
</data>
</openerp>

View File

@ -0,0 +1,626 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Marketing / Mass Mailing -->
<menuitem name="Mass Mailing" id="mass_mailing_campaign"
parent="base.marketing_menu" sequence="1"/>
<!-- Marketing / Mailing Lists -->
<menuitem name="Mailing Lists" id="mass_mailing_list"
parent="base.marketing_menu" sequence="2"/>
<!-- Marketing / Configuration -->
<menuitem name="Configuration" id="marketing_configuration"
parent="base.marketing_menu" sequence="99"/>
<!-- MASS MAILING CONTACT -->
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
<field name="name">mail.mass_mailing.contact.search</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="arch" type="xml">
<search string="Mailing Lists Subscribers">
<field name="name"/>
<field name="email"/>
<field name="list_id"/>
<separator/>
<filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"/>
<group expand="0" string="Group By...">
<filter string="Creation Date" name="group_create_date"
context="{'group_by': 'create_date'}"/>
<filter string="Mailing Lists" name="group_list_id"
context="{'group_by': 'list_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_tree">
<field name="name">mail.mass_mailing.contact.tree</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Lists Subscribers" editable="top">
<field name="email"/>
<field name="name"/>
<field name="list_id"/>
<field name="opt_out"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts">
<field name="name">Mailing List Subscribers</field>
<field name="res_model">mail.mass_mailing.contact</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="context">{'search_default_not_opt_out': 1}</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts_from_list">
<field name="name">Recipients</field>
<field name="res_model">mail.mass_mailing.contact</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="context">{'search_default_list_id': active_id, 'search_default_not_opt_out': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a recipient.
</p>
</field>
</record>
<menuitem name="Contacts" id="menu_email_mass_mailing_contacts"
parent="mass_mailing_list" sequence="50"
action="action_view_mass_mailing_contacts"/>
<!-- MASS MAILING LIST -->
<record model="ir.ui.view" id="view_mail_mass_mailing_list_search">
<field name="name">mail.mass_mailing.list.search</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<search string="Mailing Lists">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_tree">
<field name="name">mail.mass_mailing.list.tree</field>
<field name="model">mail.mass_mailing.list</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<field name="name"/>
<field name="contact_nbr"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_form">
<field name="name">mail.mass_mailing.list.form</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List" version="7.0">
<sheet>
<div class="oe_right oe_button_box" name="buttons">
<button name="%(mass_mailing.action_view_mass_mailing_contacts_from_list)d"
type="action" icon="fa-user" class="oe_stat_button pull-right">
<field name="contact_nbr" string="Recipients" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
</div>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_lists">
<field name="name">Contact Lists</field>
<field name="res_model">mail.mass_mailing.list</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing list.
</p><p>
Mailing lists allows you to to manage customers and
contacts easily and to send to mailings in a single click.
</p></field>
</record>
<menuitem name="Mailing Lists" id="menu_email_mass_mailing_lists"
parent="mass_mailing_list" sequence="40"
action="action_view_mass_mailing_lists"/>
<!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<group expand="0" string="Group By...">
<filter string="State" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Campaign" name="group_mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"
context="{'group_by': 'mass_mailing_campaign_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<header>
<button name="action_test_mailing" type="object"
class="oe_highlight" string="Test Mailing" states="draft"/>
<button name="send_mail" type="object" states="draft,test"
class="oe_highlight" string="Send to All"/>
<button name="action_test_mailing" type="object" states="test,done"
string="Send Test Sample"/>
<field name="state" widget="statusbar"/>
</header>
<div class="oe_form_box_info oe_text_center" attrs="{'invisible': [('scheduled', '=', 0)]}">
<p><strong>
<field name="scheduled" class="oe_inline"/>
emails are in queue and will be sent soon.
</strong></p>
</div>
<sheet>
<div class="oe_button_box pull-right" attrs="{'invisible': [('state', 'in', ('draft','test'))]}">
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="received_ratio" string="Received" widget="percentpie"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="opened_ratio" string="Opened" widget="percentpie"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="replied_ratio" string="Replied" widget="percentpie"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="opened_dayly" string="Opened Daily" widget="barchart"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="replied_dayly" string="Replied Daily" widget="barchart"/>
</button>
</div>
<div class="oe_button_box" attrs="{'invisible': [('total', '=', 0)]}" style="margin-bottom: 32px">
<button name="%(action_view_mass_mailing_contacts)d" type="action"
icon="fa-envelope-o" class="oe_stat_button">
<field name="total" string="Emails" widget="statinfo"/>
</button>
</div>
<group>
<field name="email_from"/>
<field name="name"/>
<label for="mailing_model" string="Recipients"/>
<div>
<field name="mailing_model" widget="radio" style="margin-bottom: 8px"
on_change="on_change_model_and_list(mailing_model, contact_list_ids)"/>
<field name="mailing_domain" widget="char_domain"
placeholder="Select recipients"
options="{'model_field': 'mailing_model'}"/>
<div attrs="{'invisible': [('mailing_model', '&lt;&gt;', 'mail.mass_mailing.contact')]}">
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
<field name="contact_list_ids" widget="many2many_tags"
placeholder="Select mailing lists..." class="oe_inline"
on_change="on_change_model_and_list(mailing_model, contact_list_ids)"/>
</div>
</div>
</group>
<notebook>
<page string="Mail Body">
<button name="action_edit_html" type="object" string="Design Email" class="oe_highlight" states="draft"/>
<button name="action_edit_html" type="object" string="Change Email Design" states="test"/>
<div attrs="{'invisible' : ['|', '|', ('state', '=', 'done'), ('body_html','!=',False), ('mailing_domain', '=', False)]}" class="oe_view_nocontent oe_clear">
<p class="oe_view_nocontent_create oe_edit_only">
Click to design your email.
</p>
</div>
<field name="body_html" readonly="1"/>
<field name="attachment_ids" widget="many2many_binary" string="Attach a file"/>
</page>
<page string="Options">
<group>
<group string="Mailing">
<label for="reply_to"/>
<div>
<p class="alert alert-danger"
attrs="{'invisible': ['|', ('reply_to_mode', '!=', 'thread'), ('mailing_model', 'not in', ['mail.mass_mailing.contact', 'res.partner'])]}">
This option is not available for the recipients you selected.
Please use a specific reply-to email address.
</p>
<field name="reply_to_mode" widget="radio"/>
<field name="reply_to" style="margin-left: 16px;"
attrs="{'required': [('reply_to_mode', '=', 'email')]}"/>
</div>
<field name="create_date" readonly="1"/>
<field name="sent_date" readonly="1"/>
</group>
<group string="Campaign">
<field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
<label for="contact_ab_pc" groups="mass_mailing.group_mass_mailing_campaign"/>
<div>
<field name="contact_ab_pc" class="oe_inline"/> %
</div>
</group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<kanban default_group_by='state'>
<field name='color'/>
<field name='total'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<h3><field name="name"/></h3>
<h4 style="display: inline;"><field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h4>
<t t-if="record.mass_mailing_campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="sent_date"/>
</div>
<div>
<div style="display: inline-block">
<field name="delivered" widget="gauge" style="width:120px; height: 90px;"
options="{'max_field': 'total'}"/>
</div>
<div style="display: inline-block; vertical-align: top;">
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> %
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing.
</p><p>
Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
</p></field>
</record>
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
}
</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing.
</p><p>
Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
</p></field>
</record>
<menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailings"/>
<!-- MASS MAILING CAMPAIGN STAGE !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_search">
<field name="name">mail.mass_mailing.stage.search</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_tree">
<field name="name">mail.mass_mailing.stage.tree</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings" editable="top">
<field name="sequence" widget="handle"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="action_view_mass_mailing_stages" model="ir.actions.act_window">
<field name="name">Mass Mailing Stages</field>
<field name="res_model">mail.mass_mailing.stage</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Campaign Stages" id="menu_view_mass_mailing_stages"
parent="marketing_configuration" sequence="1"
groups="mass_mailing.group_mass_mailing_campaign"
action="action_view_mass_mailing_stages"/>
<!-- MASS MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mass Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="category_ids"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Stage" name="group_stage_id"
context="{'group_by': 'stage_id'}"/>
<filter string="Responsible" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
<field name="stage_id"/>
<field name="category_ids"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0">
<header>
<field name="stage_id" widget="statusbar" clickable="True"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="user_id"/>
<field name="category_ids" widget="many2many_tags"/>
</group>
<group>
<field name="total" invisible="1"/>
<div class="oe_right oe_button_box" name="buttons"
attrs="{'invisible': [('total', '=', 0)]}">
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="received_ratio" widget="percentpie" string="Received"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="opened_ratio" widget="percentpie" string="Opened"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="replied_ratio" widget="percentpie" string="Replied"/>
</button>
</div>
</group>
</group>
<strong>Related Mailing(s)</strong>
<field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)">
<tree>
<field name="name"/>
<field name="sent_date"/>
<field name="state"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban default_group_by='stage_id'>
<field name='total'/>
<field name='color'/>
<field name='user_id'/>
<field name='mass_mailing_ids'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Settings</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar oe_kanban_header_right"/>
<h3 style="margin-bottom: 8px;"><field name="name"/></h3>
<field name="category_ids"/>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings">
<h4 style="margin-top: 8px;"><t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings</h4>
</a>
</div>
<div class="oe_clear"></div>
<div>
<div style="display: inline-block">
<field name="delivered" widget="gauge" style="width:120px; height: 90px;"
options="{'max_field': 'total'}"/>
</div>
<div style="display: inline-block; vertical-align: top;">
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> %
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mass Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new mass mailing campaign.
</p><p>
Create a campaign to structure mass mailing and get analysis from email status.
</p>
</field>
</record>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"
groups="mass_mailing.group_mass_mailing_campaign"/>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<tree string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="sent"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<form string="Mail Statistics" version="7.0">
<group>
<group>
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="exception"/>
<field name="sent"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.mail.statistics</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
</data>
</openerp>

Some files were not shown because too many files have changed in this diff Show More