[MERGE] chm - improved subtype - fixes

bzr revid: fp@openerp.com-20121002204023-8v3jf2n84xe2jol9
This commit is contained in:
Fabien Pinckaers 2012-10-02 22:40:23 +02:00
commit c93d70e919
84 changed files with 2066 additions and 883 deletions

View File

@ -1308,17 +1308,19 @@ class account_invoice(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], body=_("%s <b>created</b>.") % (_(self._get_document_type(obj.type))), context=context)
self.message_post(cr, uid, [obj.id], body=_("%s <b>created</b>.") % (self._get_document_type(obj.type)),
subtype="account.mt_invoice_new", context=context)
def confirm_paid_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], body=_("%s <b>paid</b>.") % (_(self._get_document_type(obj.type))), context=context)
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], body=_("%s <b>paid</b>.") % (self._get_document_type(obj.type)),
subtype="account.mt_invoice_paid", context=context)
def invoice_cancel_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], body=_("%s <b>cancelled</b>.") % (_(self._get_document_type(obj.type))), context=context)
self.message_post(cr, uid, [obj.id], body=_("%s <b>cancelled</b>.") % (self._get_document_type(obj.type)),
context=context)
account_invoice()
class account_invoice_line(osv.osv):

View File

@ -278,7 +278,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -438,7 +437,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -518,7 +518,6 @@
<!--
Account Statement Sequences
-->
<record id="sequence_reconcile" model="ir.sequence.type">
<field name="name">Account Reconcile</field>
<field name="code">account.reconcile</field>
@ -554,8 +553,6 @@
<field eval="1" name="number_next"/>
<field eval="1" name="number_increment"/>
</record>
<!--
Invoice requests (deprecated)
-->
@ -564,7 +561,14 @@
<field name="object">account.invoice</field>
</record>
<!-- mail: subtypes -->
<record id="mt_invoice_new" model="mail.message.subtype">
<field name="name">created</field>
<field name="res_model">account.invoice</field>
</record>
<record id="mt_invoice_paid" model="mail.message.subtype">
<field name="name">paid</field>
<field name="res_model">account.invoice</field>
</record>
</data>
</openerp>

View File

@ -1304,17 +1304,17 @@ class account_voucher(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s <b>created</b>." % self._document_type[obj.type or False]
self.message_post(cr, uid, [obj.id], body=message, context=context)
self.message_post(cr, uid, [obj.id], body=message, subtype="account_voucher.mt_voucher", context=context)
def post_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s '%s' is <b>posted</b>." % (self._document_type[obj.type or False], obj.move_id.name)
self.message_post(cr, uid, [obj.id], body=message, context=context)
self.message_post(cr, uid, [obj.id], body=message, subtype="account_voucher.mt_voucher", context=context)
def reconcile_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
message = "%s <b>reconciled</b>." % self._document_type[obj.type or False]
self.message_post(cr, uid, [obj.id], body=message, context=context)
self.message_post(cr, uid, [obj.id], body=message, subtype="account_voucher.mt_voucher", context=context)
account_voucher()

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data noupdate="1">
<!-- notify all employees of module installation -->
<record model="mail.message" id="module_install_notification">
<field name="model">mail.group</field>
@ -13,5 +14,12 @@
<p>You can track customer payments easily and automate follow-ups. You get an overview of the discussion with your customers on each invoice for easier traceability. For advanced accounting features, you should install the "Accounting and Finance" module.</p>
]]></field>
</record>
<!-- mail: subtypes -->
<record id="mt_voucher" model="mail.message.subtype">
<field name="name">Status Change</field>
<field name="res_model">account.voucher</field>
</record>
</data>
</openerp>

View File

@ -108,7 +108,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -243,7 +243,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -519,7 +518,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -146,7 +146,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -302,7 +301,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -38,7 +38,8 @@ that have no counterpart in the general financial accounts.
'security/analytic_security.xml',
'security/ir.model.access.csv',
'analytic_sequence.xml',
'analytic_view.xml'
'analytic_view.xml',
'analytic_data.xml'
],
'demo': [],
'installable': True,

View File

@ -284,7 +284,8 @@ class account_analytic_account(osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], body=_("Contract for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name), context=context)
self.message_post(cr, uid, [obj.id], body=_("Contract for <em>%s</em> has been <b>created</b>.") % (obj.partner_id.name),
subtype="analytic.mt_account_status", context=context)
account_analytic_account()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<!-- mail: subtypes -->
<record id="mt_account_status" model="mail.message.subtype">
<field name="name">Status Change</field>
<field name="res_model">account.analytic.account</field>
</record>
</data>
</openerp>

View File

@ -53,7 +53,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -223,7 +223,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -359,6 +359,9 @@ class base_stage(object):
"""
return ''
def find_subtype(self, cr, uid, ids, name, context=None):
return "mail.mt_comment"
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Send a notification when the stage changes. This method has
to be overriden, because each document will have its particular
@ -370,19 +373,22 @@ class base_stage(object):
def case_open_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been <b>opened</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
self.message_post(cr, uid, [id], body=msg, context=context)
xml_id = self.find_subtype(cr, uid, ids, context=context)
self.message_post(cr, uid, [id], body=msg, subtype=xml_id, context=context)
return True
def case_close_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been <b>closed</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
self.message_post(cr, uid, [id], body=msg, context=context)
xml_id = self.find_subtype(cr, uid, ids, context=context)
self.message_post(cr, uid, [id], body=msg, subtype=xml_id, context=context)
return True
def case_cancel_send_note(self, cr, uid, ids, context=None):
for id in ids:
msg = _('%s has been <b>canceled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
self.message_post(cr, uid, [id], body=msg, context=context)
msg = _('%s has been <b>cancelled</b>.') % (self.case_get_note_msg_prefix(cr, uid, id, context=context))
xml_id = self.find_subtype(cr, uid, ids, context=context)
self.message_post(cr, uid, [id], body=msg, subtype=xml_id, context=context)
return True
def case_pending_send_note(self, cr, uid, ids, context=None):

View File

@ -223,7 +223,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -526,7 +525,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -149,7 +149,6 @@
<field name="description" placeholder="Description..."/>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -181,7 +181,6 @@
</group>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -95,7 +95,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -167,8 +167,9 @@ class test_message_compose(test_mail.TestMailMockups):
# Test: partner_ids: p_a_id (default) + 3 newly created partners
message_pigs_pids = [partner.id for partner in message_pigs.partner_ids]
message_bird_pids = [partner.id for partner in message_bird.partner_ids]
partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) + [p_a_id]
partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])])
self.assertEqual(len(message_pigs_pids), len(partner_ids), 'mail.message on pigs incorrect number of partner_ids')
self.assertEqual(len(message_bird_pids), len(partner_ids), 'mail.message on bird partner_ids incorrect')
self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of partner_ids')
self.assertEqual(len(message_bird_pids), len(partner_ids), 'mail.message on bird partner_ids incorrect')
self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird partner_ids incorrect')

View File

@ -45,6 +45,7 @@ Key Features
'security/ir.model.access.csv',
'wizard/event_confirm_view.xml',
'event_view.xml',
'event_data.xml',
'report/report_event_registration_view.xml',
'board_association_view.xml',
'res_partner_view.xml',

View File

@ -360,8 +360,10 @@ class event_registration(osv.osv):
return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
def confirm_registration(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_('State set to open'), context=context)
return self.write(cr, uid, ids, {'state': 'open'}, context=context)
for reg in self.browse(cr, uid, ids, context=context or {}):
self.pool.get('event.event').message_post(cr, uid, [reg.event_id.id], body=_('New registration confirmed: %s.') % (reg.name or '', ),subtype="event.mt_event_registration", context=context)
self.message_post(cr, uid, ids, body=_('Registration confirmed.'), context=context)
return self.write(cr, uid, ids, {'state': 'open'},context=context)
def create(self, cr, uid, vals, context=None):
obj_id = super(event_registration, self).create(cr, uid, vals, context)
@ -392,7 +394,7 @@ class event_registration(osv.osv):
self.write(cr, uid, ids, values)
self.message_post(cr, uid, ids, body=_('State set to Done'), context=context)
else:
raise osv.except_osv(_('Error!'),_("You must wait for the starting day of the event to do this action.") )
raise osv.except_osv(_('Error!'), _("You must wait for the starting day of the event to do this action."))
return True
def button_reg_cancel(self, cr, uid, ids, context=None, *args):

View File

@ -12,14 +12,19 @@
<field name="state">open</field>
</record>
<!-- notify all employees of module installation -->
<record model="mail.message" id="module_install_notification">
<field name="model">mail.group</field>
<field name="res_id" ref="mail.group_all_employees"/>
<field name="type">notification</field>
<field name="subject">Events Organisation application installed!</field>
<field name="body">From the top Events menu, you can organize events, manage registrations, automate communication around your event and sell events through your quotations.</field>
<!-- mail: event subtype -->
<record id="event.mt_event_registration" model="mail.message.subtype">
<field name="name">New Registrations</field>
<field name="res_model">event.event</field>
<field name="default" eval="False"/>
</record>
<!-- notify all employees of module installation -->
<function model="mail.group" name="message_post">
<!-- ids, subject, body, parent_id=False, type='notification', content_subtype='html' -->
<value eval="[ref('mail.group_all_employees')]"/>
<value>From the top menu Events, you can organize events, manage registrations, automate communication around your event and sell events through your quotations.</value>
<value>Module Events has been installed</value>
</function>
</data>
</openerp>

View File

@ -196,7 +196,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -479,7 +478,6 @@
</group>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -139,7 +139,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -350,46 +350,43 @@ class hr_holidays(osv.osv):
# -----------------------------
def needaction_domain_get(self, cr, uid, ids, context=None):
# to be tested, otherwise convert into employee_id in ...
emp_obj = self.pool.get('hr.employee')
empids = emp_obj.search(cr, uid, [('parent_id.user_id','=',uid)], context=context)
dom = [
'&', ('state','=','confirm'),('employee_id', 'in', empids)
]
empids = emp_obj.search(cr, uid, [('parent_id.user_id', '=', uid)], context=context)
dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
# if this user is a hr.manager, he should do second validations
if self.pool.get('res.users').has_group(cr, uid, 'base.group_hr_manager'):
dom = ['|'] + dom + [ ('state','=','validate1') ]
dom = ['|'] + dom + [('state', '=', 'validate1')]
return dom
def create_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, ids,
_("The request has been <b>created</b> and is waiting confirmation."), context=context)
self.message_post(cr, uid, ids,
_("Request <b>created</b>, waiting confirmation."), context=context)
return True
def holidays_confirm_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids):
self.message_post(cr, uid, [obj.id],
_("The request has been <b>submitted</b> and is waiting for validation by the manager."), context=context)
_("Request <b>submitted</b>, waiting for validation by the manager."), context=context)
def holidays_first_validate_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id],
_("The request has been <b>approved</b>. A second validation is necessary and is now pending."), context=context)
_("Request <b>approved</b>, waiting second validation."), context=context)
def holidays_validate_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids):
if obj.double_validation:
self.message_post(cr, uid, [obj.id],
_("The request has been <b>double validated</b>. The validation process is now over."), context=context)
self.message_post(cr, uid, [obj.id],
_("Request <b>validated</b>."), context=context)
else:
self.message_post(cr, uid, [obj.id],
_("The request has been <b>approved</b>. The validation process is now over."), context=context)
_("The request has been <b>approved</b>."), context=context)
def holidays_refuse_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids):
self.message_post(cr, uid, [obj.id],
_("The request has been <b>refused</b>. The validation process is now over."), context=context)
_("Request <b>refused</b>"), context=context)
class resource_calendar_leaves(osv.osv):

View File

@ -121,7 +121,6 @@
</group>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -159,7 +158,6 @@
<field name="notes" nolabel="1" colspan="4" placeholder="Add a reason..."/>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -461,7 +461,7 @@ class hr_applicant(base_stage, osv.Model):
""" Override of the (void) default notification method. """
if not stage_id: return True
stage_name = self.pool.get('hr.recruitment.stage').name_get(cr, uid, [stage_id], context=context)[0][1]
return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
return self.message_post(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % (stage_name), context=context)
def case_get_note_msg_prefix(self, cr, uid, id, context=None):
return 'Applicant'
@ -474,6 +474,8 @@ class hr_applicant(base_stage, osv.Model):
if context is None:
context = {}
for applicant in self.browse(cr, uid, ids, context=context):
if applicant.job_id:
self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=_('New employee joined the company %s.')%(applicant.name,), subtype="hr_recruitment.mt_hired", context=context)
if applicant.emp_id:
message = _("Applicant has been <b>hired</b> and created as an employee.")
self.message_post(cr, uid, [applicant.id], body=message, context=context)
@ -492,6 +494,9 @@ class hr_applicant(base_stage, osv.Model):
def create_send_note(self, cr, uid, ids, context=None):
message = _("Applicant has been <b>created</b>.")
for applicant in self.browse(cr, uid, ids, context=context):
if applicant.job_id:
self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=message, subtype="hr_recruitment.mt_applicant_new", context=context)
return self.message_post(cr, uid, ids, body=message, context=context)
class hr_job(osv.osv):

View File

@ -460,6 +460,15 @@ You can automatically receive job application though an email gateway, see the H
<field name="alias_model_id" ref="model_hr_applicant"/>
<field name="alias_user_id" ref="base.user_root"/>
</record>
<!-- mail: subtypes -->
<record id="mt_hired" model="mail.message.subtype">
<field name="name">Employee Hired</field>
<field name="res_model">hr.job</field>
</record>
<record id="mt_applicant_new" model="mail.message.subtype">
<field name="name">New Applicant</field>
<field name="res_model">hr.job</field>
</record>
</data>
</openerp>

View File

@ -183,7 +183,6 @@
<field name="description" placeholder="Feedback of interviews..."/>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -85,7 +85,7 @@ class account_analytic_account(osv.osv):
return res
def on_change_partner_id(self, cr, uid, ids, partner_id, name, context=None):
res = super(account_analytic_account,self).on_change_partner_id(cr, uid, ids,partner_id, name, context=context)
res = super(account_analytic_account, self).on_change_partner_id(cr, uid, ids, partner_id, name, context=context)
part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
if pricelist:
@ -93,25 +93,25 @@ class account_analytic_account(osv.osv):
return res
def set_close(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'close'}, context=context)
self.write(cr, uid, ids, {'state': 'close'}, context=context)
message = _("Contract has been <b>closed</b>.")
self.message_post(cr, uid, ids, body=message, context=context)
self.message_post(cr, uid, ids, body=message, subtype="mt_account_closed", context=context)
return True
def set_cancel(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
message = _("Contract has been <b>cancelled</b>.")
self.message_post(cr, uid, ids, body=message, context=context)
self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
message = _("Contract has been <b>canceled</b>.")
self.message_post(cr, uid, ids, body=message, subtype="mt_account_canceled", context=context)
return True
def set_open(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'open'}, context=context)
self.write(cr, uid, ids, {'state': 'open'}, context=context)
message = _("Contract has been <b>opened</b>.")
self.message_post(cr, uid, ids, body=message, context=context)
return True
def set_pending(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'pending'}, context=context)
self.write(cr, uid, ids, {'state': 'pending'}, context=context)
message = _("Contract has been set as <b>pending</b>.")
self.message_post(cr, uid, ids, body=message, context=context)
return True

View File

@ -71,19 +71,17 @@ class idea_idea(osv.osv):
self.message_post(cr, uid, ids, body=_('Idea canceled.'), context=context)
return True
def idea_open(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, { 'state': 'open'})
def idea_open(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state': 'open'}, context=context)
self.message_post(cr, uid, ids, body=_('Idea accepted.'), context=context)
return True
def idea_close(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_('Idea done.'), context=context)
self.write(cr, uid, ids, { 'state': 'close' })
def idea_close(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state': 'close'}, context=context)
self.message_post(cr, uid, ids, body=_('Idea closed.'), context=context)
return True
def idea_draft(self, cr, uid, ids, context=None):
def idea_draft(self, cr, uid, ids, context={}):
self.write(cr, uid, ids, {'state': 'draft'}, context=context)
self.message_post(cr, uid, ids, body=_('Idea reset to draft.'), context=context)
self.write(cr, uid, ids, { 'state': 'draft' })
return True
idea_idea()

View File

@ -15,6 +15,7 @@
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4,ref('base.group_tool_user'))]"/>
</record>
</data>
</openerp>

View File

@ -79,7 +79,6 @@
<field name="description"/>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -19,6 +19,7 @@
#
##############################################################################
import mail_message_subtype
import mail_alias
import mail_followers
import mail_message

View File

@ -49,6 +49,7 @@ Main Features
'data': [
'wizard/invite_view.xml',
'wizard/mail_compose_message_view.xml',
'mail_message_subtype.xml',
'res_config_view.xml',
'mail_message_view.xml',
'mail_mail_view.xml',

View File

@ -12,5 +12,9 @@
<field eval="'process_email_queue'" name="function"/>
<field eval="'()'" name="args"/>
</record>
<record id="mt_comment" model="mail.message.subtype">
<field name="name">comment</field>
</record>
</data>
</openerp>

View File

@ -11,3 +11,4 @@ Mail Module documentation topics
mail_needaction_howto
mail_partner
mail_state
mail_subtype

View File

@ -0,0 +1,70 @@
.. _mail_message_subtype:
OpenChatter Pi (3.1415): Message Subtype
========================================
To overcome the problems of crowdy walls in system notification, We have added features of **Message Subtype** in mail.
mail.message.subtype
++++++++++++++++++++
``mail.message.subtype`` has following fields:
- ``Name``: fields.char(' Message Subtype ', size = 128,required = True,help = 'Subtype Of Message'),
- ``model_ids``: fields.many2many('ir.model','mail_message_subtyp_message_rel','message_subtype_id', 'model_id', 'Model',help = "link some subtypes to several models, for projet/task"),
- ``default``: fields.boolean('Default', help = "When subscribing to the document, users will receive by default messages related to this subtype unless they uncheck this subtype"),
mail.followers
++++++++++++++
In ``mail.followers`` we have added additional many2many field subtype ids :
- ``subtype_ids``: fields.many2many('mail.message.subtype','mail_message_subtyp_rel','subscription_id', 'subtype_id', 'Subtype',help = "linking some subscription to several subtype for projet/task")
mail.message
++++++++++++
In mail_message we have added additional field subtype_id which Indicates the Type of Message
- ``subtype_id``: fields.many2one('mail.message.subtype', 'Subtype')
mail.thread
+++++++++++
- In **message_post** method add the *subtype_id* field as parameter and set as default subtype 'Other'.
def message_post(self, cr, uid, thread_id, body='', subject=False, msg_type='notification', parent_id=False, attachments=None, subtype='other', context=None, ``**kwargs``):
- In **message_subscribe** method add the *subtype_ids* field as parameter.In this method if subtype_ids is None, it fatch the default true subtypes in mail.message.subtypes otherwise pass selected subtypes.
For update subtypes call **message_subscribe_udpate_subtypes** method
def message_subscribe(self, cr, uid, ids, partner_ids,subtype_ids = None, context=None):
- Add **message_subscribe_udpate_subtypes** method to update the subtype_ids in followers.
def message_subscribe_udpate_subtypes(self, cr, uid, ids, user_id, subtype_ids,context=None):
followers_obj = self.pool.get('mail.followers')
followers_ids = followers_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids)])
return followers_obj.write(cr, uid, followers_ids, {'subtype_ids': [(6, 0 , subtype_ids)]}, context = context)
For Each Addons:
++++++++++++++++
- Add data of subtypes for each addons module.
- Add subtype field as parameter in **message_post** Method for each addons module.
How It Works:
+++++++++++++
- In addons module when we Follow a Perticular document It display under the followers button.
- In sybtypes there are 3 default subtypes for each addons
1) Email
2) Comment
3) Other
- In document display a default subtypes(which are true) related a perticular model_ids wise.
Example:-
If I have open crm.lead, It display only subtypes of crm.lead
- When we select subtype it update subtype_ids(which are checked) in mail.follower where match res_model & res_id of the current documents.
- when message created update subtype_id of that message in mail.message.
- In Feeds display only those notifications of documents which subtypes are selected

View File

@ -46,6 +46,8 @@ class mail_followers(osv.Model):
help='Id of the followed resource'),
'partner_id': fields.many2one('res.partner', string='Related Partner',
ondelete='cascade', required=True, select=1),
'subtype_ids': fields.many2many('mail.message.subtype', string='Subtype',
help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall."),
}
@ -81,18 +83,27 @@ class mail_notification(osv.Model):
return super(mail_notification, self).create(cr, uid, vals, context=context)
return False
def set_message_read(self, cr, uid, msg_id, context=None):
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
return self.write(cr, uid, notif_ids, {'read': True}, context=context)
def set_message_read(self, cr, uid, msg_ids, read=None, context=None):
if msg_ids == None:
return False
if type(msg_ids) is not list:
msg_ids=[msg_ids]
def get_partners_to_notify(self, cr, uid, partner_ids, message, context=None):
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', 'in', msg_ids)], context=context)
return self.write(cr, uid, notif_ids, {'read': read}, context=context)
def get_partners_to_notify(self, cr, uid, message, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
"""
notify_pids = []
for partner in self.pool.get('res.partner').browse(cr, SUPERUSER_ID, partner_ids, context=context):
for notification in message.notification_ids:
if notification.read:
continue
partner = notification.partner_id
# Do not send an email to the writer
if partner.user_ids and partner.user_ids[0].id == uid:
continue
@ -111,15 +122,15 @@ class mail_notification(osv.Model):
notify_pids.append(partner.id)
return notify_pids
def notify(self, cr, uid, partner_ids, msg_id, context=None):
def _notify(self, cr, uid, msg_id, context=None):
""" Send by email the notification depending on the user preferences """
context = context or {}
# mail_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_noemail') or not partner_ids:
if context.get('mail_noemail'):
return True
msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
notify_partner_ids = self.get_partners_to_notify(cr, uid, partner_ids, msg, context=context)
notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, context=context)
if not notify_partner_ids:
return True

View File

@ -16,6 +16,28 @@
</field>
</record>
<record model="ir.ui.view" id="view_mail_subscription_form">
<field name="name">mail.followers.form</field>
<field name="model">mail.followers</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Followers Form" version="7.0">
<sheet>
<group>
<group>
<field name="res_model"/>
<field name="partner_id"/>
</group>
<group>
<field name="res_id"/>
<field name="subtype_ids" widget="many2many_tags"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- NOTIFICATION !-->
<record model="ir.ui.view" id="view_notification_tree">
<field name="name">mail.notification.tree</field>

View File

@ -30,6 +30,7 @@ class mail_group(osv.Model):
group. The group mechanics are based on the followers. """
_description = 'Discussion group'
_name = 'mail.group'
_mail_autothread = False
_inherit = ['mail.thread']
_inherits = {'mail.alias': 'alias_id', 'ir.ui.menu': 'menu_id'}
@ -157,7 +158,7 @@ class mail_group(osv.Model):
def write(self, cr, uid, ids, vals, context=None):
result = super(mail_group, self).write(cr, uid, ids, vals, context=context)
if vals.get('group_ids'):
self._subscribe_users(cr, uid, ids, vals.get('group_ids'), context=context)
self._subscribe_users(cr, uid, ids, context=context)
return result
def action_follow(self, cr, uid, ids, context=None):

View File

@ -15,9 +15,10 @@
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<kanban>
<field name="message_is_follower"/>
<field name="message_follower_ids"/>
<field name="message_is_follower"/>
<field name="message_summary"/>
<field name="description"/>
<templates>
<t t-name="kanban-description">
<div class="oe_group_description" t-if="record.description.raw_value">
@ -81,7 +82,6 @@
</group>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"
options='{"thread_level": 1}'/>
<field name="message_follower_ids" widget="mail_followers"/>

View File

@ -131,6 +131,7 @@ class mail_message(osv.Model):
'unread': fields.function(_get_unread, fnct_search=_search_unread,
type='boolean', string='Unread',
help='Functional field to search for unread messages linked to uid'),
'subtype_id': fields.many2one('mail.message.subtype', 'Subtype'),
'vote_user_ids': fields.many2many('res.users', 'mail_vote', 'message_id', 'user_id', string='Votes',
help='Users that voted for this message'),
}
@ -167,7 +168,7 @@ class mail_message(osv.Model):
self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(4, user_id)]}, context=context)
else:
self.write(cr, SUPERUSER_ID, message.get('id'), {'vote_user_ids': [(3, user_id)]}, context=context)
return True
return not(has_voted) or False
#------------------------------------------------------
# Message loading for web interface
@ -179,6 +180,7 @@ class mail_message(osv.Model):
fields allow to have the foreign record name without having
to check external access rights).
"""
child_nbr = len(msg.child_ids)
has_voted = False
vote_ids = self.pool.get('res.users').name_get(cr, SUPERUSER_ID, [user.id for user in msg.vote_user_ids], context=context)
for vote in vote_ids:
@ -191,7 +193,7 @@ class mail_message(osv.Model):
attachment_ids = []
try:
author_id = self.pool.get('res.partner').name_get(cr, uid, [msg.author_id.id], context=context)[0]
is_author = uid in msg.author_id.user_ids
is_author = uid == msg.author_id.user_ids[0].id
except (orm.except_orm, osv.except_osv):
author_id = False
is_author = False
@ -199,6 +201,7 @@ class mail_message(osv.Model):
partner_ids = self.pool.get('res.partner').name_get(cr, uid, [x.id for x in msg.partner_ids], context=context)
except (orm.except_orm, osv.except_osv):
partner_ids = []
return {
'id': msg.id,
'type': msg.type,
@ -212,12 +215,30 @@ class mail_message(osv.Model):
'author_id': author_id,
'is_author': is_author,
'partner_ids': partner_ids,
'child_ids': [],
'parent_id': msg.parent_id and msg.parent_id.id or False,
'vote_user_ids': vote_ids,
'has_voted': has_voted
'has_voted': has_voted,
'unread': msg.unread and msg.unread['unread'] or False
}
def message_read_tree_flatten(self, cr, uid, messages, current_level, level, context=None):
def message_read_tree_get_expandable(self, cr, uid, parent_message, last_message, domain=[], current_level=0, level=0, context=None):
""" . """
base_domain = [('id', '<', last_message['id'])]
if parent_message and current_level < level:
base_domain += [('parent_id', '=', parent_message['id'])]
elif parent_message:
base_domain += [('id', 'child_of', parent_message['id']), ('id', '!=', parent_message['id'])]
if domain:
base_domain += domain
extension = { 'type': 'expandable',
'domain': base_domain,
'thread_level': current_level,
'context': context,
'id': -1,
}
return extension
def message_read_tree_flatten(self, cr, uid, parent_message, messages, domain=[], level=0, current_level=0, context=None, limit=None, add_expandable=True):
""" Given a tree with several roots of following structure :
[ {'id': 1, 'child_ids': [
{'id': 11, 'child_ids': [...] },],
@ -236,69 +257,143 @@ class mail_message(osv.Model):
child_ids = msg_dict.pop('child_ids', [])
msg_dict['child_ids'] = []
return [msg_dict] + child_ids
# return sorted([msg_dict] + child_ids, key=itemgetter('id'), reverse=True)
context = context or {}
limit = limit or self._message_read_limit
# Depth-first flattening
for message in messages:
if message.get('type') == 'expandable':
continue
message['child_ids'] = self.message_read_tree_flatten(cr, uid, message['child_ids'], current_level + 1, level, context=context)
message['child_ids'] = self.message_read_tree_flatten(cr, uid, message, message['child_ids'], domain, level, current_level + 1, context=context, limit=limit)
for child in message['child_ids']:
if child.get('type') == 'expandable':
continue
message['child_nbr'] += child['child_nbr']
# Flatten if above maximum depth
if current_level < level:
return_list = messages
else:
return_list = []
for message in messages:
for flat_message in _flatten(message):
return_list.append(flat_message)
return sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True))
return_list = [flat_message for message in messages for flat_message in _flatten(message)]
def message_read(self, cr, uid, ids=False, domain=[], thread_level=0, limit=None, context=None):
""" If IDs are provided, fetch these records. Otherwise use the domain
to fetch the matching records.
After having fetched the records provided by IDs, it will fetch the
parents to have well-formed threads.
# Add expandable
return_list = sorted(return_list, key=itemgetter(context.get('sort_key', 'id')), reverse=context.get('sort_reverse', True))
if return_list and current_level == 0 and add_expandable:
expandable = self.message_read_tree_get_expandable(cr, uid, parent_message, return_list and return_list[-1] or parent_message, domain, current_level, level, context=context)
return_list.append(expandable)
elif return_list and current_level <= level and add_expandable:
expandable = self.message_read_tree_get_expandable(cr, uid, parent_message, return_list and return_list[-1] or parent_message, domain, current_level, level, context=context)
return_list.append(expandable)
return return_list
def message_read(self, cr, uid, ids=False, domain=[], level=0, context=None, parent_id=False, limit=None):
""" Read messages from mail.message, and get back a structured tree
of messages to be displayed as discussion threads. If IDs is set,
fetch these records. Otherwise use the domain to fetch messages.
After having fetch messages, their parents will be added to obtain
well formed threads.
:param domain: optional domain for searching ids
:param level: level of threads to display, 0 being flat
:param limit: number of messages to fetch
:param parent_id: if parent_id reached, stop searching for
further parents
:return list: list of trees of messages
"""
message_loaded = context and context.get('message_loaded') or [0]
# don't read the message display by .js, in context message_loaded list
if context and context.get('message_loaded'):
domain += [ ['id','not in',message_loaded] ];
limit = limit or self._message_read_limit
context = context or {}
if not ids:
ids = self.search(cr, SUPERUSER_ID, domain, context=context, limit=limit)
messages = self.browse(cr, uid, ids, context=context)
tree = []
result = []
tree = {} # key: ID, value: record
for msg in messages:
if len(result) < (limit - 1):
record = self._message_dict_get(cr, uid, msg, context=context)
if thread_level and msg.parent_id:
while msg.parent_id:
if msg.parent_id.id in tree:
record_parent = tree[msg.parent_id.id]
else:
record_parent = self._message_dict_get(cr, uid, msg.parent_id, context=context)
if msg.parent_id.parent_id:
tree[msg.parent_id.id] = record_parent
if record['id'] not in [x['id'] for x in record_parent['child_ids']]:
record_parent['child_ids'].append(record)
record = record_parent
msg = msg.parent_id
if msg.id not in tree:
result.append(record)
tree[msg.id] = record
else:
result.append({
'type': 'expandable',
'domain': [('id', '<=', msg.id)] + domain,
'context': context,
'thread_level': thread_level, # should be improve accodting to level of records
'id': -1,
})
break
record = None
# select ids
if ids:
for msg in self.browse(cr, uid, ids, context=context):
result.append(self._message_dict_get(cr, uid, msg, context=context))
return result
# key: ID, value: record
ids = self.search(cr, SUPERUSER_ID, domain, context=context, limit=limit)
for msg in self.browse(cr, uid, ids, context=context):
# if not in record and not in message_loded list
if msg.id not in tree and msg.id not in message_loaded :
record = self._message_dict_get(cr, uid, msg, context=context)
tree.append(msg.id)
result.append(record)
while msg.parent_id and msg.parent_id.id != parent_id:
parent_id = msg.parent_id.id
if msg.parent_id.id not in tree:
msg = msg.parent_id
tree.append(msg.id)
# if not in record and not in message_loded list
if msg.id not in message_loaded :
record = self._message_dict_get(cr, uid, msg, context=context)
result.append(record)
result = sorted(result, key=lambda k: k['id'])
tree_not = []
# expandable for not show message
for id_msg in tree:
# get all childs
not_loaded_ids = self.search(cr, SUPERUSER_ID, [['parent_id','=',id_msg],['id','not in',message_loaded]], None, limit=1000)
# group childs not read
id_min=None
id_max=None
nb=0
for not_loaded_id in not_loaded_ids:
if not_loaded_id not in tree:
nb+=1
if id_min==None or id_min>not_loaded_id:
id_min=not_loaded_id
if id_max==None or id_max<not_loaded_id:
id_max=not_loaded_id
tree_not.append(not_loaded_id)
else:
if nb>0:
result.append({
'domain': [['id','>=',id_min],['id','<=',id_max],['parent_id','=',id_msg]],
'nb_messages': nb,
'type': 'expandable',
'parent_id': id_msg,
'id': id_min
})
nb=0
if nb>0:
result.append({
'domain': [['id','>=',id_min],['parent_id','=',id_msg]],
'nb_messages': nb,
'type': 'expandable',
'parent_id': id_msg,
'id': id_min
})
# expandable for limit max
ids = self.search(cr, SUPERUSER_ID, domain+[['id','not in',message_loaded+tree+tree_not]], context=context, limit=1)
if len(ids) > 0:
result.append(
{
'domain': domain,
'nb_messages': 0,
'type': 'expandable',
'parent_id': parent_id,
'id': -1
});
result = sorted(result, key=lambda k: k['id'])
# Flatten the result
if thread_level > 0:
result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
return result
#------------------------------------------------------
@ -409,7 +504,7 @@ class mail_message(osv.Model):
if not values.get('message_id') and values.get('res_id') and values.get('model'):
values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s' % values)
newid = super(mail_message, self).create(cr, uid, values, context)
self.notify(cr, uid, newid, context=context)
self._notify(cr, 1, newid, context=context)
return newid
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
@ -431,26 +526,39 @@ class mail_message(osv.Model):
self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
return super(mail_message, self).unlink(cr, uid, ids, context=context)
def notify(self, cr, uid, newid, context=None):
def _notify(self, cr, uid, newid, context=None):
""" Add the related record followers to the destination partner_ids.
Call mail_notification.notify to manage the email sending
"""
message = self.browse(cr, uid, newid, context=context)
partners_to_notify = set([])
# add all partner_ids of the message
# message has no subtype_id: pure log message -> no partners, no one notified
if not message.subtype_id:
message.write({'partner_ids': [5]})
return True
# all partner_ids of the mail.message have to be notified
if message.partner_ids:
partners_to_notify |= set(partner.id for partner in message.partner_ids)
# add all followers and set add them in partner_ids
# all followers of the mail.message document have to be added as partners and notified
if message.model and message.res_id:
record = self.pool.get(message.model).browse(cr, SUPERUSER_ID, message.res_id, context=context)
extra_notified = set(partner.id for partner in record.message_follower_ids)
fol_obj = self.pool.get("mail.followers")
fol_ids = fol_obj.search(cr, uid, [('res_model', '=', message.model), ('res_id', '=', message.res_id), ('subtype_ids', 'in', message.subtype_id.id)], context=context)
fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context)
extra_notified = set(fol.partner_id.id for fol in fol_objs)
missing_notified = extra_notified - partners_to_notify
missing_notified = missing_notified
if missing_notified:
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context)
partners_to_notify |= extra_notified
# # remove uid from partners
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, uid)]}, context=context)
self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
# add myself if I wrote on my wall,
# unless remove myself author
if ((message.model=="res.partner" and message.res_id==message.author_id.id)):
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, message.author_id.id)]}, context=context)
else:
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, message.author_id.id)]}, context=context)
self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
def copy(self, cr, uid, id, default=None, context=None):
"""Overridden to avoid duplicating fields that are unique to each email"""

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-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 osv import osv
from osv import fields
class mail_message_subtype(osv.osv):
""" Class holding subtype definition for messages. Subtypes allow to tune
the follower subscription, allowing only some subtypes to be pushed
on the Wall. """
_name = 'mail.message.subtype'
_description = 'mail_message_subtype'
_columns = {
'name': fields.char('Message Type', required=True, translate=True,
help='Message subtype, gives a more precise type on the message, '\
'especially for system notifications. For example, it can be '\
'a notification related to a new record (New), or to a stage '\
'change in a process (Stage change). Message subtypes allow to '\
'precisely tune the notifications the user want to receive on its wall.'),
'res_model': fields.char('Model', help="link subtype to model"),
'default': fields.boolean('Default',
help="When subscribing to the document, this subtype will be checked by default."),
}
_defaults = {
'default': True,
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_message_subtype_tree">
<field name="name">mail.message.subtype.tree</field>
<field name="model">mail.message.subtype</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Subtype">
<field name="name"/>
<field name="res_model"/>
<field name="default"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_message_subtype_form">
<field name="name">mail.message.subtype.form</field>
<field name="model">mail.message.subtype</field>
<field name="arch" type="xml">
<form string="Email message" version="7.0">
<sheet>
<group>
<field name="name"/>
<field name="res_model"/>
<field name="default"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_message_subtype">
<field name="name">Subtypes</field>
<field name="res_model">mail.message.subtype</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Subtypes" id="menu_message_subtype" parent="base.menu_email" action="action_view_message_subtype"/>
</data>
</openerp>

View File

@ -31,6 +31,7 @@
<field name="author_id"/>
<field name="date"/>
<field name="type"/>
<field name="subtype_id"/>
</group>
<group>
<field name="model"/>
@ -55,16 +56,18 @@
<field name="subject" string="Content" filter_domain="['|', ('subject', 'ilike', self), ('body', 'ilike', self)]" />
<field name="type"/>
<field name="author_id"/>
<filter icon="terp-personal+" string="Comments"
<filter string="Unread"
name="unread_message" help="Show unread message"
domain="[('unread', '=', True)]"/>
<filter string="Comments"
name="comments" help="Comments"
domain="[('type', '=', 'comment')]"/>
<filter icon="terp-personal+" string="Notifications"
<filter string="Notifications"
name="notifications" help="Notifications"
domain="[('type', '=', 'notification')]"/>
<filter icon="terp-personal+" string="Emails"
<filter string="Emails"
name="emails" help="Emails"
domain="[('type', '=', 'email')]"/>
<field name="author_id"/>
</search>
</field>
</record>
@ -75,20 +78,28 @@
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_message_search"/>
<field name="context">{'search_default_unread_message':True}</field>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Messages" id="menu_mail_message" parent="base.menu_email" action="action_view_mail_message"/>
<record id="action_mail_all_feeds" model="ir.actions.client">
<field name="name">News Feed</field>
<record id="action_mail_inbox_feeds" model="ir.actions.client">
<field name="name">Inbox</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid])],
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid]),('unread', '=', True)],
'context': {'default_model': 'res.users', 'default_res_id': uid} }&quot;"/>
</record>
<record id="action_mail_my_feeds" model="ir.actions.client">
<field name="name">My Posts</field>
<record id="action_mail_archives_feeds" model="ir.actions.client">
<field name="name">Archives</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('notification_ids.partner_id.user_ids', 'in', [uid]),('unread', '=', False)],
'context': {'default_model': 'res.users', 'default_res_id': uid} }&quot;"/>
</record>
<record id="action_mail_sent_feeds" model="ir.actions.client">
<field name="name">Sent</field>
<field name="tag">mail.wall</field>
<field name="params" eval="&quot;{'domain': [('author_id.user_ids', 'in', [uid])],
'context': {'default_model': 'res.users', 'default_res_id': uid} }&quot;"/>

View File

@ -96,6 +96,7 @@ class many2many_reference(fields.many2many):
else:
return super(many2many_reference, self).set(cr, model, id, name, values, user, context)
class mail_thread(osv.AbstractModel):
''' mail_thread model is meant to be inherited by any model that needs to
act as a discussion topic on which messages can be attached. Public
@ -116,10 +117,13 @@ class mail_thread(osv.AbstractModel):
'''
_name = 'mail.thread'
_description = 'Email Thread'
_mail_autothread = True
def _get_message_data(self, cr, uid, ids, name, args, context=None):
""" Computes:
- message_unread: has uid unread message for the document
- message_summary: html snippet summarizing the Chatter for kanban views """
res = dict((id, dict(message_unread=False, message_summary='')) for id in ids)
partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
# search for unread messages, by reading directly mail.notification, as SUPERUSER
notif_obj = self.pool.get('mail.notification')
@ -132,10 +136,41 @@ class mail_thread(osv.AbstractModel):
for notif in notif_obj.browse(cr, SUPERUSER_ID, notif_ids, context=context):
res[notif.message_id.res_id]['message_unread'] = True
for thread in self.read(cr, uid, ids, ['message_follower_ids', 'message_comment_ids', 'message_ids'], context=context):
cls = res[thread['id']]['message_unread'] and ' class="oe_kanban_mail_new"' or ''
res[thread['id']]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread['message_comment_ids']), len(thread['message_follower_ids']))
res[thread['id']]['message_is_follower'] = partner_id in thread['message_follower_ids']
for thread in self.browse(cr, uid, ids, context=context):
cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or ''
res[thread.id]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread.message_comment_ids), len(thread.message_follower_ids))
return res
def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
""" Computes:
- message_is_follower: is uid in the document followers
- message_subtype_data: data about document subtypes: which are
available, which are followed if any """
res = dict((id, dict(message_subtype_data='', message_is_follower=False)) for id in ids)
user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
# find current model subtypes, add them to a dictionary
subtype_obj = self.pool.get('mail.message.subtype')
subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False, id=subtype.id)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
for id in ids:
res[id]['message_subtype_data'] = subtype_dict.copy()
# find the document followers, update the data
fol_obj = self.pool.get('mail.followers')
fol_ids = fol_obj.search(cr, uid, [
('partner_id', '=', user_pid),
('res_id', 'in', ids),
('res_model', '=', self._name),
], context=context)
for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
thread_subtype_dict = res[fol.res_id]['message_subtype_data']
res[fol.res_id]['message_is_follower'] = True
for subtype in fol.subtype_ids:
thread_subtype_dict[subtype.name]['followed'] = True
res[fol.res_id]['message_subtype_data'] = thread_subtype_dict
return res
def _search_unread(self, cr, uid, obj=None, name=None, domain=None, context=None):
@ -152,8 +187,13 @@ class mail_thread(osv.AbstractModel):
return [('id', 'in', res.keys())]
_columns = {
'message_is_follower': fields.function(_get_message_data,
type='boolean', string='Is a Follower', multi='_get_message_data'),
'message_is_follower': fields.function(_get_subscription_data,
type='boolean', string='Is a Follower', multi='_get_subscription_data,'),
'message_subtype_data': fields.function(_get_subscription_data,
type='text', string='Subscription data', multi="_get_subscription_data",
help="Holds data about the subtypes. The content of this field "\
"is a structure holding the current model subtypes, and the "\
"current document followed subtypes."),
'message_follower_ids': many2many_reference('res.partner',
'mail_followers', 'res_id', 'partner_id',
reference_column='res_model', string='Followers'),
@ -571,12 +611,12 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context)
def message_post(self, cr, uid, thread_id, body='', subject=False,
type='notification', parent_id=False, attachments=None, context=None, **kwargs):
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
""" Post a new message in an existing thread, returning the new
mail.message ID. Extra keyword arguments will be used as default
column values for the new mail.message record.
Auto link messages for same id and object
:param int thread_id: thread ID to post into, or list with one ID
:param str body: body of the message, usually raw HTML that will
be sanitized
@ -587,9 +627,10 @@ class mail_thread(osv.AbstractModel):
``(name,content)``, where content is NOT base64 encoded
:return: ID of newly created mail.message
"""
context = context or {}
attachments = attachments or []
assert (not thread_id) or isinstance(thread_id, (int,long)) or \
assert (not thread_id) or isinstance(thread_id, (int, long)) or \
(isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id"
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id and thread_id[0]
@ -608,39 +649,86 @@ class mail_thread(osv.AbstractModel):
}
attachment_ids.append((0, 0, data_attach))
# get subtype
if not subtype:
subtype = 'mail.mt_comment'
s = subtype.split('.')
if len(s)==1:
s = ('mail', s[0])
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s[0], s[1])
subtype_id = ref and ref[1] or False
model = context.get('thread_model', self._name) if thread_id else False
messages = self.pool.get('mail.message')
#auto link messages for same id and object
if self._mail_autothread and thread_id:
message_ids = messages.search(cr, uid, ['&',('res_id', '=', thread_id),('model','=',model)], context=context)
if len(message_ids):
parent_id = min(message_ids)
values = kwargs
values.update({
'model': context.get('thread_model', self._name) if thread_id else False,
'model': model,
'res_id': thread_id or False,
'body': body,
'subject': subject,
'subject': subject or False,
'type': type,
'parent_id': parent_id,
'attachment_ids': attachment_ids,
'subtype_id': subtype_id,
})
for x in ('from', 'to', 'cc'): values.pop(x, None) # Avoid warnings
return self.pool.get('mail.message').create(cr, uid, values, context=context)
# Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc'):
values.pop(x, None)
return messages.create(cr, uid, values, context=context)
#------------------------------------------------------
# Followers API
#------------------------------------------------------
def message_subscribe_users(self, cr, uid, ids, user_ids=None, context=None):
def message_post_api(self, cr, uid, thread_id, body='', subject=False, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
added_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=type,
subtype=subtype, parent_id=parent_id, attachments=attachments, context=context)
added_message = self.pool.get('mail.message').message_read(cr, uid, [added_message_id])
return added_message
def get_message_subtypes(self, cr, uid, ids, context=None):
""" message_subtype_data: data about document subtypes: which are
available, which are followed if any """
return self._get_subscription_data(cr, uid, ids, None, None, context=context)
def message_subscribe_users(self, cr, uid, ids, user_ids=None, subtype_ids=None, context=None):
""" Wrapper on message_subscribe, using users. If user_ids is not
provided, subscribe uid instead. """
if not user_ids: user_ids = [uid]
partner_ids = [user['partner_id'][0] for user in self.pool.get('res.users').read(cr, uid, user_ids, ['partner_id'], context=context)]
return self.message_subscribe(cr, uid, ids, partner_ids, context=context)
if not user_ids:
user_ids = [uid]
partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
return self.message_subscribe(cr, uid, ids, partner_ids, subtype_ids=subtype_ids, context=context)
def message_subscribe(self, cr, uid, ids, partner_ids, context=None):
def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None):
""" Add partners to the records followers. """
return self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
# if subtypes are not specified (and not set to a void list), fetch default ones
if subtype_ids is None:
subtype_obj = self.pool.get('mail.message.subtype')
subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
# update the subscriptions
fol_obj = self.pool.get('mail.followers')
fol_ids = fol_obj.search(cr, uid, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context)
fol_obj.write(cr, uid, fol_ids, {'subtype_ids': [(6, 0, subtype_ids)]}, context=context)
return True
def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None):
""" Wrapper on message_subscribe, using users. If user_ids is not
provided, unsubscribe uid instead. """
if not user_ids: user_ids = [uid]
partner_ids = [user['partner_id'][0] for user in self.pool.get('res.users').read(cr, uid, user_ids, ['partner_id'], context=context)]
if not user_ids:
user_ids = [uid]
partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, uid, user_ids, context=context)]
return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context)
def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
@ -674,3 +762,5 @@ class mail_thread(osv.AbstractModel):
partner_id = %s
''', (ids, self._name, partner_id))
return True
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -12,16 +12,22 @@
<menuitem id="mail_feeds" name="Feeds" parent="mail.mail_feeds_main" groups="base.group_user" sequence="10"/>
<menuitem id="mail_my_stuff" name="Organizer" parent="mail.mail_feeds_main"/>
<record id="mail_wallfeeds" model="ir.ui.menu">
<field name="name">My Feeds</field>
<record id="mail_inboxfeeds" model="ir.ui.menu">
<field name="name">Inbox</field>
<field name="sequence" eval="10"/>
<field name="action" ref="action_mail_all_feeds"/>
<field name="action" ref="action_mail_inbox_feeds"/>
<field name="parent_id" ref="mail_feeds"/>
</record>
<record id="mail_myfeeds" model="ir.ui.menu">
<field name="name">My Posts</field>
<record id="mail_archivesfeeds" model="ir.ui.menu">
<field name="name">Archives</field>
<field name="sequence" eval="11"/>
<field name="action" ref="action_mail_my_feeds"/>
<field name="action" ref="action_mail_archives_feeds"/>
<field name="parent_id" ref="mail_feeds"/>
</record>
<record id="mail_sentfeeds" model="ir.ui.menu">
<field name="name">Sent</field>
<field name="sequence" eval="12"/>
<field name="action" ref="action_mail_sent_feeds"/>
<field name="parent_id" ref="mail_feeds"/>
</record>

View File

@ -25,6 +25,7 @@ class res_partner_mail(osv.Model):
""" Update partner to add a field about notification preferences """
_name = "res.partner"
_inherit = ['res.partner', 'mail.thread']
_mail_autothread = False
_columns = {
'notification_email_send': fields.selection([
@ -41,5 +42,4 @@ class res_partner_mail(osv.Model):
'notification_email_send': lambda *args: 'comment'
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -9,7 +9,6 @@
<field name="arch" type="xml">
<xpath expr="//sheet" position="after">
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"
options='{"thread_level": 1}'/>
<field name="message_follower_ids" widget="mail_followers"/>

View File

@ -11,4 +11,7 @@ access_mail_group_all,mail.group.all,model_mail_group,,1,0,0,0
access_mail_group_user,mail.group.user,model_mail_group,base.group_user,1,1,1,1
access_mail_alias_all,mail.alias.all,model_mail_alias,,1,0,0,0
access_mail_alias_user,mail.alias,model_mail_alias,base.group_user,1,1,1,0
access_mail_alias_system,mail.alias,model_mail_alias,base.group_system,1,1,1,1
access_mail_message_subtype,mail.message.subtype,model_mail_message_subtype,,1,1,1,1
access_mail_mail_user,mail.mail,model_mail_mail,base.group_user,1,1,1,0
access_mail_vote_all,mail.vote.all,model_mail_vote,,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 access_mail_group_user mail.group.user model_mail_group base.group_user 1 1 1 1
12 access_mail_alias_all mail.alias.all model_mail_alias 1 0 0 0
13 access_mail_alias_user mail.alias model_mail_alias base.group_user 1 1 1 0
14 access_mail_alias_system mail.alias model_mail_alias base.group_system 1 1 1 1
15 access_mail_message_subtype mail.message.subtype model_mail_message_subtype 1 1 1 1
16 access_mail_mail_user mail.mail model_mail_mail base.group_user 1 1 1 0
17 access_mail_vote_all mail.vote.all model_mail_vote 1 1 1 1

View File

@ -14,7 +14,6 @@
margin: 0;
}
/* ------------------------------------------------------------ */
/* Wall
/* ------------------------------------------------------------ */
@ -37,6 +36,21 @@
list-style-type: none;
}
/* ------------------------------------------------------------ */
/* Followers
/* ------------------------------------------------------------ */
.openerp div.oe_mail_recthread_aside h4 {
display: inline-block;
}
.openerp div.oe_mail_recthread_aside button {
position: relative;
}
.openerp div.oe_mail_recthread_aside label,
.openerp div.oe_mail_recthread_aside input {
cursor:pointer;
}
/* Specific display of threads in the wall */
/* ------------------------------------------------------------ */
@ -50,22 +64,12 @@
border-top: 0;
}
.openerp div.oe_mail_thread_subthread img {
.openerp div.oe_thread_placeholder img {
width: 28px;
height: 28px;
}
.openerp div.oe_mail_msg_content {
position: relative;
width: 486px;
}
.openerp div.oe_mail_msg_content > li {
float: left;
margin-right: 3px;
}
.openerp div.oe_mail_thread_subthread div.oe_mail_msg_content {
.openerp div.oe_thread_placeholder div.oe_mail_msg_content {
width: 440px;
}
@ -78,7 +82,7 @@
overflow: auto;
}
.openerp div.oe_mail_recthread_main {
.openerp .oe_mail_record_wall > .oe_mail_wall_threads {
float: left;
width: 560px;
}
@ -96,7 +100,7 @@
width: 120px;
}
.openerp button.oe_mail_button_mouseout {
.openerp .oe_mail_recthread_aside .oe_follower.oe_follow {
color: white;
background-color: #8a89ba;
background-image: -webkit-gradient(linear, left top, left bottom, from(#8a89ba), to(#807fb4));
@ -106,9 +110,7 @@
background-image: -o-linear-gradient(top, #8a89ba, #807fb4);
background-image: linear-gradient(to bottom, #8a89ba, #807fb4);
}
.openerp button.oe_mail_button_mouseover {
display: none;
.openerp .oe_mail_recthread_aside .oe_follower.oe_following {
color: white;
background-color: #dc5f59;
background-image: -webkit-gradient(linear, left top, left bottom, from(#dc5f59), to(#b33630));
@ -119,17 +121,38 @@
background-image: linear-gradient(to bottom, #dc5f59, #b33630);
}
.openerp .oe_mail_recthread_aside .oe_follower span {
display:none;
}
.openerp .oe_mail_recthread_aside .oe_following span.oe_following,
.openerp .oe_mail_recthread_aside .oe_notfollow span.oe_follow {
display:block;
}
.openerp div.oe_mail_recthread_followers {
margin-bottom: 8px;
}
/* ------------------------------------------------------------ */
/* Followers
/* subtypes
/* ------------------------------------------------------------ */
.openerp div.oe_mail_recthread_aside h4 {
display: inline-block;
.openerp .oe_mouse_subtypes {
display:inline-block;
position: relative;
z-index: 5;
}
.openerp .oe_mouse_subtypes .oe_recthread_subtypes {
position: absolute;
z-index: 2;
}
.openerp .oe_mouse_subtypes.oe_mouseout .oe_recthread_subtypes {
display: none;
}
.openerp .oe_mouse_subtypes.oe_mouseover .oe_recthread_subtypes {
display: block;
}
/* ------------------------------------------------------------ */
@ -140,6 +163,8 @@
display: none;
white-space: normal;
padding: 8px;
z-index:5;
background: #fff;
}
.openerp div.oe_mail_thread_action:after {
@ -168,15 +193,20 @@
-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
}
.openerp .oe_mail_vote_count,
.openerp .oe_mail_msg_vote{
vertical-align: bottom;
}
.openerp div.oe_mail_thread_display {
white-space: normal;
}
.openerp div.oe_mail_thread_subthread {
.openerp div.oe_thread_placeholder {
margin-left: 66px;
}
.openerp div.oe_mail_thread_subthread li.oe_mail_thread_msg:last-child {
.openerp div.oe_thread_placeholder li.oe_mail_thread_msg:last-child {
margin-bottom: 8px;
}
@ -191,6 +221,15 @@
clear: both;
}
.openerp li.oe_mail_thread_msg.oe_mail_read,
.openerp li.oe_mail_thread_msg.oe_mail_read div {
background-color: #F0F0F0;
}
.openerp li.oe_mail_thread_msg.oe_mail_read li.oe_mail_thread_msg.oe_mail_unread,
.openerp li.oe_mail_thread_msg.oe_mail_read li.oe_mail_thread_msg.oe_mail_unread div {
background-color: #F6F6F6;
}
.openerp li.oe_mail_thread_msg > div:after {
content: "";
display: block;
@ -202,18 +241,15 @@
margin: 0 0 4px 0;
}
.openerp .oe_mail_msg_notification,
.openerp .oe_mail_msg_comment,
.openerp .oe_mail_msg_notification,
.openerp .oe_mail_msg_expandable,
.openerp .oe_mail_msg_comment,
.openerp .oe_mail_msg_email {
padding: 8px;
background: white;
border-top: 1px solid #ebebeb;
}
.openerp div.oe_mail_thread_subthread .oe_mail_msg_comment {
background: #eee;
}
.openerp .oe_mail_msg_notification:after,
.openerp .oe_mail_msg_comment:after,
.openerp .oe_mail_msg_email:after {
@ -222,8 +258,15 @@
clear: both;
}
.openerp .oe_mail_msg_content {
.openerp div.oe_mail_msg_content {
float: right;
position: relative;
width: 486px;
}
.openerp div.oe_mail_msg_content > li {
float: left;
margin-right: 3px;
}
.openerp .oe_mail_msg_content:after {
@ -266,7 +309,6 @@
display: none;
}
/* ------------------------------------------------------------ */
/* mail.compose.message form view & OpenERP hacks
/* ------------------------------------------------------------ */
@ -371,15 +413,38 @@
}
/* Dropdown menu */
.openerp .oe_mail_msg_content .oe_dropdown_toggle {
/*.openerp .oe_mail_msg_content .oe_dropdown_toggle {
position: absolute;
top: 0px;
right: 3px;
}*/
.openerp .oe_mail .oe_semantic_html_override {
position: relative;
}
.openerp .oe_mail ul.oe_header {
position: absolute;
right: 3px;
top: -6px;
display: none;
z-index: 10;
height: 18px;
}
.openerp .oe_mail ul.oe_header a {
text-decoration: none;
}
.openerp .oe_mail .oe_semantic_html_override:hover > ul.oe_header {
display: block;
}
.openerp .oe_mail ul.oe_header>li {
display: inline-block;
}
.openerp .oe_mail_msg_content .oe_dropdown_arrow:after {
border-top: 4px solid transparent;
}
.openerp .oe_mail_msg_content:hover .oe_dropdown_arrow:after {
border-top: 4px solid #404040;
}

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,9 @@ openerp_mail_followers = function(session, mail) {
this.options.context = this.node.attrs.context;
this.options.comment = this.node.attrs.help || false;
this.ds_model = new session.web.DataSetSearch(this, this.view.model);
this.sub_model = new session.web.DataSetSearch(this,'mail.message.subtype');
this.ds_follow = new session.web.DataSetSearch(this, this.field.relation);
this.follower_model = new session.web.DataSetSearch(this,'mail.followers');
},
start: function() {
@ -51,11 +53,33 @@ openerp_mail_followers = function(session, mail) {
bind_events: function() {
var self = this;
this.$('button.oe_mail_button_unfollow').on('click', function () { self.do_unfollow(); })
.mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
.mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
this.$el.on('click', 'button.oe_mail_button_follow', function () { self.do_follow(); });
this.$el.on('click', 'a.oe_mail_invite', function(event) {
this.$('div.oe_mouse_subtypes')
.on('mouseover', function () {
$(this).removeClass('oe_mouseout').addClass('oe_mouseover');
self.display_subtypes();
})
.on('mouseleave', function () {
$(this).removeClass('oe_mouseover').addClass('oe_mouseout');
self.display_subtypes();
});
this.$('button.oe_follower')
.on('click', function () {
if($(this).hasClass('oe_notfollow'))
self.do_follow();
else
self.do_unfollow();
})
.on('mouseover', function () {
$(this).removeClass('oe_mouseout').addClass('oe_mouseover');
})
.on('mouseleave', function () {
$(this).removeClass('oe_mouseover').addClass('oe_mouseout');
});
this.$el.on('click', 'ul.oe_subtypes input', function () { self.do_update_subscription(); })
this.$el.on('click', 'button.oe_invite', function(event) {
action = {
type: 'ir.actions.act_window',
res_model: 'mail.wizard.invite',
@ -74,28 +98,35 @@ openerp_mail_followers = function(session, mail) {
read_value: function() {
var self = this;
return this.ds_model.read_ids([this.view.datarecord.id], ['message_is_follower', 'message_follower_ids']).then(function (results) {
self.set_value(results[0].message_follower_ids, results[0].message_is_follower);
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).pipe(function (results) {
self.set_value(results[0].message_follower_ids);
});
},
set_value: function(value_, message_is_follower) {
set_value: function(value_) {
this.reinit();
if (! this.view.datarecord.id ||
session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) {
this.$('div.oe_mail_recthread_aside').hide();
return;
return this.fetch_followers(value_ || this.get_value());
},
set_is_follower: function(value_) {
for(var i in value_){
if(value_[i]['user_ids'][0]==this.session.uid)
this.message_is_follower=true;
this.display_buttons();
return true;
}
return this.fetch_followers(value_ || this.get_value(), message_is_follower);
this.message_is_follower=false;
this.display_buttons();
return false;
},
fetch_followers: function (value_, message_is_follower) {
this.value = value_;
this.message_is_follower = message_is_follower || (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value());
return this.ds_follow.call('read', [value_, ['name', 'user_ids']]).pipe(this.proxy('display_followers'), this.proxy('display_generic'));
fetch_followers: function (value_) {
this.value = value_ || {};
this.message_is_follower = (this.getParent().fields.message_is_follower && this.getParent().fields.message_is_follower.get_value());
if(value_)
return this.ds_follow.call('read', [this.value, ['name', 'user_ids']]).pipe(this.proxy('display_followers'), this.proxy('display_generic'));
},
/* Display generic info about follower, for people not having access to res_partner */
display_generic: function (error, event) {
event.preventDefault();
@ -117,32 +148,88 @@ openerp_mail_followers = function(session, mail) {
display_followers: function (records) {
var self = this;
var node_user_list = this.$('ul.oe_mail_followers_display').empty();
this.$('div.oe_mail_recthread_followers h4').html(this.options.title + ' (' + records.length + ')');
_(records).each(function (record) {
this.$('div.oe_mail_recthread_followers h4').html(this.options.title + (records.length>=5 ? ' (' + records.length + ')' : '') );
console.log(records);
for(var i=0; i<records.length&&i<5; i++) {
var record=records[i];
record.avatar_url = mail.ChatterUtils.get_image(self.session, 'res.partner', 'image_small', record.id);
$(session.web.qweb.render('mail.followers.partner', {'record': record})).appendTo(node_user_list);
});
this.display_buttons();
}
self.set_is_follower(records);
},
display_buttons: function () {
this.$('button.oe_mail_button_follow').hide();
this.$('button.oe_mail_button_unfollow').hide();
this.$('span.oe_mail_invite_wrapper').hide();
if (! this.view.is_action_enabled('edit')) return;
this.$('span.oe_mail_invite_wrapper').show();
if (this.message_is_follower) { this.$('button.oe_mail_button_unfollow').show(); }
else if (this.message_is_follower == false) { this.$('button.oe_mail_button_follow').show(); }
if (this.message_is_follower) {
this.$('button.oe_follower').removeClass('oe_notfollow').addClass('oe_following');
}
else {
this.$('button.oe_follower').removeClass('oe_following').addClass('oe_notfollow');
}
if (this.view.is_action_enabled('edit'))
this.$('span.oe_mail_invite_wrapper').hide();
else
this.$('span.oe_mail_invite_wrapper').show();
},
set_subtypes:function(data){
var self = this;
var records = data[this.view.datarecord.id].message_subtype_data;
_(records).each(function (record, record_name) {
record.name = record_name;
record.followed = record.followed || undefined;
$(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('ul.oe_subtypes') );
});
},
/** Display subtypes: {'name': default, followed} */
display_subtypes: function (visible) {
var self = this;
var recthread_subtypes = self.$('.oe_recthread_subtypes');
subtype_list_ul = self.$('ul.oe_subtypes');
if(subtype_list_ul.is(":empty")) {
var context = new session.web.CompoundContext(this.build_context(), {});
this.ds_model.call('get_message_subtypes',[[self.view.datarecord.id], context]).pipe(this.proxy('set_subtypes'));
}
},
do_follow: function () {
var self =this;
_(this.$('.oe_msg_subtype_check')).each(function(record){
$(record).attr('checked','checked');
});
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value'));
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, undefined, context]).pipe(function(value_){
self.read_value(value_);
if(!self.$('.oe_recthread_subtypes').is(":visible"))
self.display_subtypes(true);
});
},
do_unfollow: function () {
_(this.$('.oe_msg_subtype_check')).each(function(record){
$(record).attr('checked',false);
});
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value'));
},
do_update_subscription: function () {
var context = new session.web.CompoundContext(this.build_context(), {});
var self = this;
var checklist = new Array();
_(this.$('.oe_msg_subtype_check')).each(function(record){
if($(record).is(':checked')) {
checklist.push(parseInt($(record).data('id')))}
});
if(!checklist.length)
return this.do_unfollow();
else
return this.ds_model.call('message_subscribe_users',[[self.view.datarecord.id], undefined, checklist, context]).pipe(this.proxy('read_value'));
},
});
};
};

View File

@ -47,11 +47,11 @@
Template used to display the communication history in documents
form view.
-->
<div t-name="mail.record_thread">
<div t-name="mail.record_thread" class="oe_mail_record_wall">
<!-- <h4>History and Comments</h4> -->
<div class="oe_mail_recthread_main">
<ul class="oe_mail_wall_threads">
<!-- contains the document thread -->
</div>
</ul>
</div>
<!--
@ -62,12 +62,12 @@
for main thread composition form in document form view.
-->
<t t-name="mail.compose_message">
<div>
<div class="oe_mail_compose_textarea">
<img class="oe_mail_icon oe_mail_frame oe_left" alt="User img"/>
<div class="oe_mail_msg_content">
<!-- contains the composition form -->
<!-- default content: old basic textarea -->
<textarea class="oe_mail_compose_textarea" placeholder="Add your comment here..." onfocus="this.value = '';"/>
<textarea placeholder="Add your comment here..."/>
</div>
<div class="oe_clear"/>
</div>
@ -93,72 +93,90 @@
container, holding the composition form. Then come the various
messages. Then comes the 'more' button.
-->
<ul t-name="mail.thread" class="oe_mail oe_mail_thread oe_semantic_html_override">
<div t-name="mail.thread" class="oe_mail oe_mail_thread oe_semantic_html_override">
<div class="oe_mail_thread_action">
<!-- contains the composition box (form + image) -->
<t t-call="mail.compose_message"/>
</div>
<div class="oe_mail_thread_display">
<ul class="oe_mail_thread_display">
<!-- contains the threads -->
</div>
<div class="oe_mail_thread_more">
<button class="oe_mail_button_more" type="button">Load more messages</button>
</div>
</ul>
</ul>
</div>
<!-- default layout -->
<li t-name="mail.thread.message" class="oe_mail oe_mail_thread_msg">
<div t-attf-class="oe_mail_msg_#{record.type} oe_semantic_html_override">
<img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="record.avatar"/>
<li t-name="mail.thread.message" t-attf-class="oe_mail oe_mail_thread_msg #{widget.unread?'oe_mail_unread':'oe_mail_read'}" t-attf-data-msg_id="{widget.id}">
<div t-attf-class="oe_mail_msg_#{widget.type} oe_semantic_html_override">
<!-- message actions (read/unread, reply, delete...) -->
<ul class="oe_header">
<li class="placeholder-mail-vote"><t t-call="mail.thread.message.vote"/></li>
<li t-if="!widget.options.thread.display_on_flat and widget.unread" title="Read"><a class="oe_read oe_e">W</a></li>
<li t-if="!widget.options.thread.display_on_flat and !widget.unread" title="Set back to unread"><a class="oe_unread oe_e">h</a></li>
<li t-if="!widget.options.message.show_reply_by_email" title="Reply"><a class="oe_reply oe_e">)</a></li>
<li t-if="widget.options.message.show_reply_by_email"><a class="oe_reply_by_email oe_e" title="Reply by mail">)</a></li>
<t t-if="(widget.options.message.show_reply || widget.options.message.show_reply_by_email || (widget.is_author and widget.options.message.show_dd_delete) || widget.type == 'email')">
<li>
<span class="oe_dropdown_toggle">
<a class="oe_e" title="More options">í</a>
<ul class="oe_dropdown_menu">
<li t-if="widget.is_author and widget.options.message.show_dd_delete"><a class="oe_mail_msg_delete">Delete</a></li>
<!-- Uncomment when adding subtype hiding
<li t-if="display['show_hide']">
<a href="#" class="oe_mail_msg_hide_type" t-attf-data-subtype='{widget.subtype}'>Hide '<t t-esc="widget.subtype"/>' for this document</a>
</li> -->
<li t-if="widget.options.message.show_reply" title="Reply"><a class="oe_reply oe_full_reply">Full reply</a></li>
<li t-if="widget.options.message.show_reply_by_email"><a class="oe_reply_by_email oe_full_reply" title="Reply by mail">Full reply</a></li>
<li t-if="widget.type == 'email'"><a class="oe_mail_msg_details" t-attf-href="#model=mail.message&amp;id=#{widget.id}" >Details</a></li>
</ul>
</span>
</li>
</t>
</ul>
<a t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]">
<img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="widget.avatar"/>
</a>
<div class="oe_mail_msg_content">
<!-- dropdown menu with message options and actions -->
<span class="oe_dropdown_toggle oe_dropdown_arrow">
<ul class="oe_dropdown_menu">
<li t-if="record.is_author and options.show_dd_delete"><a class="oe_mail_msg_delete" t-attf-data-id='{record.id}'>Delete</a></li>
<li t-if="options.show_dd_hide"><a class="oe_mail_msg_hide" t-attf-data-id='{record.id}'>Remove notification</a></li>
<!-- Uncomment when adding subtype hiding
<li t-if="display['show_hide']">
<a href="#" class="oe_mail_msg_hide_type" t-attf-data-subtype='{record.subtype}'>Hide '<t t-esc="record.subtype"/>' for this document</a>
</li> -->
<li t-if="options.show_dd_reply_by_email"><a class="oe_mail_msg_reply_by_email" t-attf-data-msg_id="{record.id}">Quote and reply</a></li>
<li t-if="record.type == 'email'"><a class="oe_mail_msg_details" t-attf-href="#model=mail.message&amp;id=#{record.id}" >Details</a></li>
</ul>
</span>
<!-- message itself -->
<div class="oe_mail_msg">
<h1 t-if="record.subject" class="oe_mail_msg_title">
<t t-raw="record.subject"/>
<h1 t-if="widget.subject" class="oe_mail_msg_title">
<t t-raw="widget.subject"/>
</h1>
<div class="oe_mail_msg_body">
<t t-if="options.show_record_name and record.record_name and (!record.subject) and (options.thread_level > 0)">
<a t-attf-href="#model=#{record.model}&amp;id=#{record.res_id}"><t t-raw="record.record_name"/></a>
</t>
<t t-raw="record.body"/>
</div>
<div class="oe_clear"/>
<ul class="oe_mail_msg_footer">
<li t-if="options.show_record_name and record.record_name and record.subject and options.thread_level > 0">
<a t-attf-href="#model=#{record.model}&amp;id=#{record.res_id}"><t t-raw="record.record_name"/></a>
</li>
<li t-if="record.author_id"><a t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"><t t-raw="record.author_id[1]"/></a></li>
<li><span t-att-title="record.date"><t t-raw="record.timerelative"/></span></li>
<t t-call="mail.thread.message.vote"/>
<li t-if="options.show_reply"><a class="oe_mail_msg_reply">Reply</a></li>
<li t-if="options.show_reply_by_email"><a class="oe_mail_msg_reply_by_email" t-attf-data-msg_id="{record.id}">Reply</a></li>
<li t-if="record.attachment_ids.length > 0">
<li t-if="widget.author_id"><a t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[1]"/></a></li>
<li><span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span></li>
<li t-if="widget.attachment_ids.length > 0">
<a class="oe_mail_msg_view_attachments">
<t t-if="record.attachment_ids.length == 1">1 Attachment</t>
<t t-if="record.attachment_ids.length > 1"><t t-raw="record.attachment_ids.length"/> Attachments</t>
<t t-if="widget.attachment_ids.length == 1">1 Attachment</t>
<t t-if="widget.attachment_ids.length > 1"><t t-raw="widget.attachment_ids.length"/> Attachments</t>
</a>
</li>
</ul>
<t t-if="record.attachment_ids.length > 0">
<div class="oe_clear"/>
<div class="oe_mail_msg_body">
<t t-if="widget.options.message.show_record_name and widget.record_name and (!widget.subject) and widget.options.thread._parents.length&lt;=widget.options.thread.thread_level and widget.model!='res.partner'">
<a class="oe_mail_action_model" t-attf-href="#model=#{widget.model}&amp;id=#{widget.res_id}"><t t-raw="widget.record_name"/></a>
</t>
<t t-raw="widget.body"/>
</div>
<t t-if="widget.attachment_ids.length > 0">
<div class="oe_clear"></div>
<t t-call="mail.thread.message.attachments"/>
</t>
</div>
</div>
</div>
<div class="oe_thread_placeholder"></div>
</li>
<!-- expandable message layout -->
<li t-name="mail.thread.expandable" t-attf-class="oe_mail oe_mail_thread_msg oe_mail_unread" t-attf-data-thread_id="{widget.id}">
<div t-attf-class="oe_mail_msg_#{widget.type} oe_semantic_html_override">
<div class="oe_mail_msg_content oe_mail_msg_more_message">
<a class="oe_mail_fetch_more">Load more messages <span t-if="widget.nb_messages>0">(<t t-raw="widget.nb_messages"/> messages not display)</span>...</a>
</div>
</div>
</li>
<!--
@ -181,14 +199,25 @@
<!-- mail.thread.message.vote
Template used to display Like/Unlike in a mail.message
-->
<li t-name="mail.thread.message.vote">
<t t-if='record.vote_user_ids.length > 0'>
<span class="oe_left oe_mail_vote_count"><t t-esc="record.vote_user_ids.length"/> votes</span>
</t>
<button t-attf-class="oe_mail_msg_vote oe_tag" t-attf-data-msg_id="{record.id}">
<t t-if="! record.has_voted"><span>+1</span></t>
<t t-if="record.has_voted"><span>-1</span></t>
<span t-name="mail.thread.message.vote">
<span class="oe_left oe_mail_vote_count">
<t t-if='widget.has_voted'>
You
</t>
<t t-if='(widget.vote_user_ids.length-(widget.has_voted?1:0)) > 0'>
<t t-if='widget.has_voted'> and </t>
<t t-esc="widget.vote_user_ids.length"/> people
</t>
<t t-if='widget.vote_user_ids.length > 0'>
agree
</t>
</span>
<button t-attf-class="oe_mail_msg_vote oe_tag">
<span>
<t t-if="!widget.has_voted">Agree</t>
<t t-if="widget.has_voted">Unagree</t>
</span>
</button>
</li>
</span>
</template>

View File

@ -3,12 +3,20 @@
<!--
followers main template
Template used to display the followers and the actions in a record.
Template used to display the followers, the actions and the subtypes in a record.
-->
<div t-name="mail.followers" class="oe_mail_recthread_aside oe_semantic_html_override">
<div class="oe_mail_recthread_actions">
<button type="button" class="oe_mail_button_follow">Follow</button>
<button type="button" class="oe_mail_button_unfollow oe_mail_button_mouseout">Following</button>
<div class="oe_mouse_subtypes">
<button type="button" class="oe_follower oe_notfollow">
<span class="oe_follow">Follow</span>
<span class="oe_unfollow">Unfollow</span>
<span class="oe_following">Following</span>
</button>
<div class="oe_recthread_subtypes">
<ul class="oe_subtypes"></ul>
</div>
</div>
</div>
<div class="oe_grey">
<t t-if="widget.options.comment">
@ -16,8 +24,9 @@
</t>
</div>
<div class="oe_mail_recthread_followers">
<button type="button" class="oe_invite"><span>Invite</span></button>
<t t-if="widget.options.title">
<h4><t t-raw="widget.options.title"/></h4><span class="oe_mail_invite_wrapper"> · <a class="oe_mail_invite" >Invite partners</a></span>
<h4><t t-raw="widget.options.title"/></h4>
</t>
<ul class="oe_mail_followers_display"></ul>
</div>
@ -30,6 +39,19 @@
<li t-name="mail.followers.partner">
<img class="oe_mail_thumbnail oe_mail_frame" t-attf-src="{record.avatar_url}"/>
<a t-attf-href="#model=res.partner&amp;id=#{record.id}"><t t-raw="record.name"/></a>
</li>\
<!--
followers.subtype template
Template used to display message subtypes of a follower subscription
-->
<li t-name="mail.followers.subtype">
<table width="50%">
<tr>
<td><label t-att-for="'input_mail_followers_subtype_'+record.id"><t t-raw="record.name"/></label></td>
<td width="10%"><input type="checkbox" t-att-checked="record.followed" t-att-id="'input_mail_followers_subtype_'+record.id" t-att-data-id="record.id" t-att-name="record.name" class="oe_msg_subtype_check"/></td>
</tr>
</table>
</li>
</template>

View File

@ -131,9 +131,12 @@ class test_mail(TestMailMockups):
self.mail_message = self.registry('mail.message')
self.mail_notification = self.registry('mail.notification')
self.mail_followers = self.registry('mail.followers')
self.mail_message_subtype = self.registry('mail.message.subtype')
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
self.user_demo = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, 'base', 'user_demo')[1]
# Mock send_get_mail_body to test its functionality without other addons override
self._send_get_mail_body = self.registry('mail.mail').send_get_mail_body
self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body
@ -252,30 +255,64 @@ class test_mail(TestMailMockups):
follower_ids = set([follower.partner_id.id for follower in self.mail_followers.browse(cr, uid, fol_obj_ids)])
self.assertEqual(follower_ids, set([partner_bert_id, user_admin.partner_id.id]), 'Bert and Admin should be the followers of dummy mail.group data')
def test_11_message_followers(self):
""" Tests designed for the subscriber API. """
def test_11_message_followers_and_subtypes(self):
""" Tests designed for the subscriber API as well as message subtypes """
cr, uid = self.cr, self.uid
user_admin = self.res_users.browse(cr, uid, uid)
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
# Create user Raoul
# Data: user Raoul
user_raoul_id = self.res_users.create(cr, uid, {'name': 'Raoul Grosbedon', 'login': 'raoul'})
user_raoul = self.res_users.browse(cr, uid, user_raoul_id)
# Data: message subtypes
self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.group'})
self.mail_message_subtype.create(cr, uid, {'name': 'mt_other_def', 'default': True, 'res_model': 'crm.lead'})
self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_def', 'default': True, 'res_model': False})
mt_mg_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.group'})
mt_all_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_nodef', 'default': False, 'res_model': False})
default_group_subtypes = self.mail_message_subtype.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', 'mail.group'), ('res_model', '=', False)])
# Subscribe Raoul three times (niak niak) through message_subscribe_users
# ----------------------------------------
# CASE1: test subscriptions with subtypes
# ----------------------------------------
# Do: Subscribe Raoul three times (niak niak) through message_subscribe_users
group_pigs.message_subscribe_users([user_raoul_id, user_raoul_id])
group_pigs.message_subscribe_users([user_raoul_id])
group_pigs.refresh()
# Test: 2 followers (Admin and Raoul)
follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
self.assertEqual(len(follower_ids), 2, 'There should be 2 Pigs fans')
self.assertEqual(set(follower_ids), set([user_raoul.partner_id.id, user_admin.partner_id.id]), 'Admin and Raoul should be the only 2 Pigs fans')
# Test: Raoul follows default subtypes
fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id), ('partner_id', '=', user_raoul.partner_id.id)])
fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0]
fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids])
self.assertEqual(set(fol_subtype_ids), set(default_group_subtypes), 'subscription subtypes are incorrect')
# Unsubscribe Raoul twice through message_unsubscribe_users
# Do: Unsubscribe Raoul twice through message_unsubscribe_users
group_pigs.message_unsubscribe_users([user_raoul_id, user_raoul_id])
group_pigs.refresh()
# Test: 1 follower (Admin)
follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
self.assertEqual(follower_ids, [user_admin.partner_id.id], 'Admin must be the only Pigs fan')
# Do: subscribe Admin with subtype_ids
group_pigs.message_subscribe_users([uid], [mt_mg_nodef, mt_all_nodef])
fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id), ('partner_id', '=', user_admin.partner_id.id)])
fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0]
fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids])
self.assertEqual(set(fol_subtype_ids), set([mt_mg_nodef, mt_all_nodef]), 'subscription subtypes are incorrect')
# ----------------------------------------
# CASE2: test mail_thread fields
# ----------------------------------------
group_pigs.refresh()
self.assertEqual(set(group_pigs.message_subtype_data.keys()), set(['comment', 'mt_mg_def', 'mt_all_def', 'mt_mg_nodef', 'mt_all_nodef']), 'mail.group available subtypes incorrect')
self.assertFalse(group_pigs.message_subtype_data['comment']['followed'], 'Admin should not follow comments in pigs')
self.assertTrue(group_pigs.message_subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs')
self.assertTrue(group_pigs.message_subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs')
def test_20_message_post(self):
""" Tests designed for message_post. """
cr, uid = self.cr, self.uid
@ -283,8 +320,6 @@ class test_mail(TestMailMockups):
user_admin = self.res_users.browse(cr, uid, uid)
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
# 0 - Admin
p_a_id = user_admin.partner_id.id
# 1 - Bert Tartopoils, with email, should receive emails for comments and emails
p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
# 2 - Carine Poilvache, with email, should never receive emails
@ -306,9 +341,12 @@ class test_mail(TestMailMockups):
_mail_bodyalt2 = 'Pigs rules\nAdmin'
_attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
# ----------------------------------------
# CASE1: post comment, body and subject specified
# ----------------------------------------
self._init_mock_build_email()
msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment')
msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment', subtype='mt_comment')
message = self.mail_message.browse(cr, uid, msg_id)
sent_emails = self._build_email_kwargs_list
# Test: notifications have been deleted
@ -325,7 +363,7 @@ class test_mail(TestMailMockups):
self.assertIn(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils\n', 'sent_email body_alternative is incorrect')
# Test: mail_message: partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids])
test_pids = set([p_a_id, p_b_id, p_c_id])
test_pids = set([p_b_id, p_c_id])
self.assertEqual(test_pids, message_pids, 'mail.message partners incorrect')
# Test: notification linked to this message = group followers = partner_ids
notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
@ -335,10 +373,13 @@ class test_mail(TestMailMockups):
for sent_email in sent_emails:
self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
# ----------------------------------------
# CASE2: post an email with attachments, parent_id, partner_ids
# ----------------------------------------
# TESTS: automatic subject, signature in body_html, attachments propagation
self._init_mock_build_email()
msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email',
msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment',
partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments)
message = self.mail_message.browse(cr, uid, msg_id2)
sent_emails = self._build_email_kwargs_list
@ -356,7 +397,7 @@ class test_mail(TestMailMockups):
self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect')
# Test: mail_message: partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids])
test_pids = set([p_a_id, p_b_id, p_c_id, p_d_id])
test_pids = set([p_b_id, p_c_id, p_d_id])
self.assertEqual(message_pids, test_pids, 'mail.message partners incorrect')
# Test: notifications linked to this message = group followers = partner_ids
notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
@ -394,8 +435,6 @@ class test_mail(TestMailMockups):
_attachments_test = [('first.txt', 'My first attachment'), ('second.txt', 'My second attachment')]
# Create partners
# 0 - Admin
p_a_id = user_admin.partner_id.id
# 1 - Bert Tartopoils, with email, should receive emails for comments and emails
p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
# 2 - Carine Poilvache, with email, should never receive emails
@ -429,9 +468,10 @@ class test_mail(TestMailMockups):
self.assertEqual(message.body, _msg_body, 'mail.message incorrect body')
# Test: mail.message: partner_ids = entries in mail.notification: group_pigs fans (a, b) + mail.compose.message partner_ids (c, d)
msg_pids = [partner.id for partner in message.partner_ids]
test_pids = [p_a_id, p_b_id, p_c_id, p_d_id]
test_pids = [p_b_id, p_c_id, p_d_id]
notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
self.assertEqual(len(notif_ids), 4, 'mail.message: too much notifications created')
self.assertEqual(len(notif_ids), 3, 'mail.message: too much notifications created')
self.assertEqual(set(msg_pids), set(test_pids), 'mail.message partner_ids incorrect')
# ----------------------------------------
@ -449,22 +489,15 @@ class test_mail(TestMailMockups):
self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
self.assertEqual(compose.parent_id.id, message.id, 'mail.compose.message incorrect parent_id')
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
# 2. Post the comment, get created message
parent_id = message.id
mail_compose.send_mail(cr, uid, [compose_id])
group_pigs.refresh()
message = group_pigs.message_ids[0]
# Test: mail.message: subject as Re:.., body in html, parent_id
self.assertEqual(message.subject, _msg_reply, 'mail.message incorrect subject')
self.assertIn('Administrator wrote:<blockquote><pre>Pigs rules</pre></blockquote></div>', message.body, 'mail.message body is incorrect')
self.assertEqual(message.parent_id and message.parent_id.id, parent_id, 'mail.message parent_id incorrect')
self.assertEqual(compose.subject, _msg_reply, 'mail.message incorrect subject')
self.assertIn('Administrator wrote:<blockquote><pre>Pigs rules</pre></blockquote>', compose.body, 'mail.message body is incorrect')
self.assertEqual(compose.parent_id and compose.parent_id.id, message.id, 'mail.message parent_id incorrect')
# Test: mail.message: attachments
for attach in message.attachment_ids:
for attach in compose.attachment_ids:
self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
self.assertIn((attach.name, attach.datas.decode('base64')), _attachments_test,
'mail.message attachment name / data incorrect')
self.assertIn((attach.datas_fname, attach.datas.decode('base64')), _attachments_test, 'mail.message attachment name / data incorrect')
# ----------------------------------------
# CASE3: mass_mail on Pigs and Bird
@ -472,8 +505,8 @@ class test_mail(TestMailMockups):
# 1. mass_mail on pigs and bird
compose_id = mail_compose.create(cr, uid,
{'subject': _subject, 'body': '${object.description}'},
{'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': -1,
{'subject': _subject, 'body': '${object.description}', 'content_type': 'html'},
{'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': False,
'active_ids': [self.group_pigs_id, group_bird_id]})
compose = mail_compose.browse(cr, uid, compose_id)
# Test: content_subtype is html
@ -496,107 +529,114 @@ class test_mail(TestMailMockups):
self.assertEqual(message2.subject, _subject, 'mail.message subject incorrect')
self.assertEqual(message2.body, group_bird.description, 'mail.message body incorrect')
def test_30_message_read(self):
""" Tests designed for message_read. """
# TDE NOTE: this test is not finished, as the message_read method is not fully specified.
# It will be updated as soon as we have fixed specs !
cr, uid = self.cr, self.uid
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
# FP Note: to be reviewed to be more generic, not depending on the algorythm of
# message_read
#def test_30_message_read(self):
# """ Tests designed for message_read. """
# # TDE NOTE: this test is not finished, as the message_read method is not fully specified.
# # It will be updated as soon as we have fixed specs !
# cr, uid = self.cr, self.uid
# group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
def _compare_structures(struct1, struct2, n=0):
# print '%scompare structure' % ('\t' * n)
self.assertEqual(len(struct1), len(struct2), 'message_read structure number of childs incorrect')
for x in range(len(struct1)):
# print '%s' % ('\t' * n), struct1[x]['id'], struct2[x]['id'], struct1[x].get('subject') or ''
self.assertEqual(struct1[x]['id'], struct2[x]['id'], 'message_read failure %s' % struct1[x].get('subject'))
_compare_structures(struct1[x]['child_ids'], struct2[x]['child_ids'], n + 1)
# print '%send compare' % ('\t' * n)
# def _compare_structures(struct1, struct2, n=0):
# # print '%scompare structure' % ('\t' * n)
# # self.assertEqual(len(struct1), len(struct2), 'message_read structure number of childs incorrect')
# ----------------------------------------
# CASE1: Flattening test
# ----------------------------------------
# for x in range(len(struct1)):
# if struct1[x].get('type') == 'expandable':
# continue
# # print '%s' % ('\t' * n), struct1[x]['id'], struct1[x]['child_nbr'], struct2[x]['id'], struct2[x].get('child_nbr', 'XX'), struct1[x].get('subject') or ''
# self.assertEqual(struct1[x]['id'], struct2[x]['id'], 'message_read failure %s' % struct1[x].get('subject'))
# _compare_structures(struct1[x]['child_ids'], struct2[x]['child_ids'], n + 1)
# # print '%send compare' % ('\t' * n)
# Create dummy message structure
import copy
tree = [{'id': 2, 'child_ids': [
{'id': 6, 'child_ids': [
{'id': 8, 'child_ids': []},
]},
]},
{'id': 1, 'child_ids':[
{'id': 7, 'child_ids': [
{'id': 9, 'child_ids': []},
]},
{'id': 4, 'child_ids': [
{'id': 10, 'child_ids': []},
{'id': 5, 'child_ids': []},
]},
{'id': 3, 'child_ids': []},
]},
]
# Test: completely flat
new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 0)
self.assertEqual(len(new_tree), 10, 'message_read_tree_flatten wrong in flat')
# Test: 1 thread level
tree_test = [{'id': 2, 'child_ids': [
{'id': 8, 'child_ids': []}, {'id': 6, 'child_ids': []},
]},
{'id': 1, 'child_ids': [
{'id': 10, 'child_ids': []}, {'id': 9, 'child_ids': []},
{'id': 7, 'child_ids': []}, {'id': 5, 'child_ids': []},
{'id': 4, 'child_ids': []}, {'id': 3, 'child_ids': []},
]},
]
new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 1)
_compare_structures(new_tree, tree_test)
# Test: 2 thread levels
new_tree = self.mail_message.message_read_tree_flatten(cr, uid, copy.deepcopy(tree), 0, 2)
_compare_structures(new_tree, tree)
# # ----------------------------------------
# # CASE1: Flattening test
# # ----------------------------------------
# ----------------------------------------
# CASE2: message_read test
# ----------------------------------------
# # Create dummy message structure
# import copy
# tree = [{'id': 2, 'child_nbr': 1, 'child_ids': [
# {'id': 6, 'child_nbr': 1, 'child_ids': [
# {'id': 8, 'child_nbr': 0, 'child_ids': []},
# ]},
# ]},
# {'id': 1, 'child_nbr': 3, 'child_ids':[
# {'id': 7, 'child_nbr': 1, 'child_ids': [
# {'id': 9, 'child_nbr': 0, 'child_ids': []},
# ]},
# {'id': 4, 'child_nbr': 2, 'child_ids': [
# {'id': 10, 'child_nbr': 0, 'child_ids': []},
# {'id': 5, 'child_nbr': 0, 'child_ids': []},
# ]},
# {'id': 3, 'child_nbr': 0, 'child_ids': []},
# ]},
# ]
# # Test: completely flat
# new_tree = self.mail_message.message_read_tree_flatten(cr, uid, None, copy.deepcopy(tree), [('type', 'in', 'borderlands')], 0, limit=15, add_expandable=False)
# _compare_structures(new_tree, new_tree)
# self.assertEqual(len(new_tree), 10, 'message_read_tree_flatten wrong in flat')
# # Test: 1 thread level
# tree_test = [{'id': 2, 'child_ids': [
# {'id': 8, 'child_ids': []}, {'id': 6, 'child_ids': []},
# ]},
# {'id': 1, 'child_ids': [
# {'id': 10, 'child_ids': []}, {'id': 9, 'child_ids': []},
# {'id': 7, 'child_ids': []}, {'id': 5, 'child_ids': []},
# {'id': 4, 'child_ids': []}, {'id': 3, 'child_ids': []},
# ]},
# ]
# new_tree = self.mail_message.message_read_tree_flatten(cr, uid, None, copy.deepcopy(tree), [('type', 'in', 'borderlands')], 1, limit=15, add_expandable=False)
# _compare_structures(new_tree, tree_test)
# # Test: 2 thread levels
# new_tree = self.mail_message.message_read_tree_flatten(cr, uid, None, copy.deepcopy(tree), [('type', 'in', 'borderlands')], 2, limit=15, add_expandable=False)
# _compare_structures(new_tree, tree)
# 1. Add a few messages to pigs group
msgid1 = group_pigs.message_post(body='1', subject='1', parent_id=False)
msgid2 = group_pigs.message_post(body='2', subject='1-1', parent_id=msgid1)
msgid3 = group_pigs.message_post(body='3', subject='1-2', parent_id=msgid1)
msgid4 = group_pigs.message_post(body='4', subject='2', parent_id=False)
msgid5 = group_pigs.message_post(body='5', subject='1-1-1', parent_id=msgid2)
msgid6 = group_pigs.message_post(body='6', subject='2-1', parent_id=msgid4)
# # ----------------------------------------
# # CASE2: message_read test
# # ----------------------------------------
# Test: read all messages flat
tree_test = [{'id': msgid6, 'child_ids': []}, {'id': msgid5, 'child_ids': []},
{'id': msgid4, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
{'id': msgid2, 'child_ids': []}, {'id': msgid1, 'child_ids': []}]
tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=0, limit=10)
_compare_structures(tree, tree_test)
# Test: read with 1 level of thread
tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
{'id': msgid1, 'child_ids': [
{'id': msgid5, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
{'id': msgid2, 'child_ids': []},
]},
]
tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=1, limit=10)
_compare_structures(tree, tree_test)
# Test: read with 2 levels of thread
tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
{'id': msgid1, 'child_ids': [
{'id': msgid3, 'child_ids': []},
{'id': msgid2, 'child_ids': [{'id': msgid5, 'child_ids': []}, ]},
]},
]
tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], thread_level=2, limit=10)
_compare_structures(tree, tree_test)
# # 1. Add a few messages to pigs group
# msgid1 = group_pigs.message_post(body='1', subject='1', parent_id=False)
# msgid2 = group_pigs.message_post(body='2', subject='1-1', parent_id=msgid1)
# msgid3 = group_pigs.message_post(body='3', subject='1-2', parent_id=msgid1)
# msgid4 = group_pigs.message_post(body='4', subject='2', parent_id=False)
# msgid5 = group_pigs.message_post(body='5', subject='1-1-1', parent_id=msgid2)
# msgid6 = group_pigs.message_post(body='6', subject='2-1', parent_id=msgid4)
# 2. Test expandables
# TDE FIXME: add those tests when expandables are specified and implemented
# # Test: read all messages flat
# tree_test = [{'id': msgid6, 'child_ids': []}, {'id': msgid5, 'child_ids': []},
# {'id': msgid4, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
# {'id': msgid2, 'child_ids': []}, {'id': msgid1, 'child_ids': []}]
# tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], level=0, limit=15)
# _compare_structures(tree, tree_test)
# # Test: read with 1 level of thread
# tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
# {'id': msgid1, 'child_ids': [
# {'id': msgid5, 'child_ids': []}, {'id': msgid3, 'child_ids': []},
# {'id': msgid2, 'child_ids': []},
# ]},
# ]
# tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], level=1, limit=15)
# _compare_structures(tree, tree_test)
# # Test: read with 2 levels of thread
# tree_test = [{'id': msgid4, 'child_ids': [{'id': msgid6, 'child_ids': []}, ]},
# {'id': msgid1, 'child_ids': [
# {'id': msgid3, 'child_ids': []},
# {'id': msgid2, 'child_ids': [{'id': msgid5, 'child_ids': []}, ]},
# ]},
# ]
# tree = self.mail_message.message_read(cr, uid, ids=False, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], level=2, limit=15)
# _compare_structures(tree, tree_test)
# # 2. Test expandables
# # TDE FIXME: add those tests when expandables are specified and implemented
def test_40_needaction(self):
""" Tests for mail.message needaction. """
cr, uid = self.cr, self.uid
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
group_pigs_demo = self.mail_group.browse(cr, self.user_demo, self.group_pigs_id)
user_admin = self.res_users.browse(cr, uid, uid)
# Demo values: check unread notification = needaction on mail.message
@ -607,9 +647,12 @@ class test_mail(TestMailMockups):
na_count = self.mail_message._needaction_count(cr, uid, domain=[])
self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
# Post 4 message on group_pigs
for dummy in range(4):
group_pigs.message_post(body='My Body')
na_count1 = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
# Post 2 message on group_pigs as admin, 3 messages as demo user
for dummy in range(2):
group_pigs.message_post(body='My Body', subtype='mt_comment')
for dummy in range(3):
group_pigs_demo.message_post(body='My Demo Body', subtype='mt_comment')
# Check there are 4 new needaction on mail.message
notif_ids = self.mail_notification.search(cr, uid, [
@ -621,7 +664,19 @@ class test_mail(TestMailMockups):
# Check there are 4 needaction on mail.message with particular domain
na_count = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
self.assertEqual(na_count, 4, 'posted message count does not match needaction count')
notif_ids = self.mail_notification.search(cr, uid, [
('partner_id', '=', user_admin.partner_id.id),
('read', '=', False),
('message_id.model','=','mail.group'),
('message_id.res_id','=',self.group_pigs_id)
])
self.assertEqual(len(notif_ids), na_count, 'posted message count does not match needaction count')
na_count3 = self.mail_message._needaction_count(cr, self.user_demo, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
self.assertEqual(na_count3-na_count1, 0, 'demo has 0 message: not a follower and do not follow his own messages')
na_count2 = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
self.assertEqual(na_count2-na_count1, 3, 'admin has 3 messages: 0 from itself as they are marked as read, 3 from demo')
def test_50_thread_parent_resolution(self):
"""Verify parent/child relationships are correctly established when processing incoming mails"""
@ -636,15 +691,18 @@ class test_mail(TestMailMockups):
# 1. In-Reply-To header
reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1',
extra='In-Reply-To: %s' % msg1.message_id)
self.mail_thread.message_process(cr, uid, None, reply_msg)
self.mail_group.message_process(cr, uid, None, reply_msg)
# 2. References header
reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: Re: 1',
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
self.mail_thread.message_process(cr, uid, None, reply_msg2)
self.mail_group.message_process(cr, uid, None, reply_msg2)
# 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, not to mail
reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com',
extra='', subject='Re: [%s] 1' % self.group_pigs_id)
self.mail_thread.message_process(cr, uid, 'mail.group', reply_msg3)
self.mail_group.message_process(cr, uid, 'mail.group', reply_msg3)
group_pigs.refresh()
msg1.refresh()
self.assertEqual(5, len(group_pigs.message_ids), 'group should contain 5 messages')
@ -653,8 +711,8 @@ class test_mail(TestMailMockups):
def test_60_vote(self):
""" Test designed for the vote/unvote feature. """
cr, uid = self.cr, self.uid
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
user_admin = self.res_users.browse(cr, uid, uid)
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
msg1 = group_pigs.message_post(body='My Body', subject='1')
msg1 = self.mail_message.browse(cr, uid, msg1)

View File

@ -102,53 +102,53 @@ class test_mail_access_rights(test_mail.TestMailMockups):
""" Test mail_message search override about access rights. """
self.assertTrue(1 == 1, 'Test not implemented, do not replace by return True')
def test_10_mail_flow_access_rights(self):
""" Test a Chatter-looks alike flow. """
cr, uid = self.cr, self.uid
partner_bert_id, partner_raoul_id = self.partner_bert_id, self.partner_raoul_id
user_bert_id, user_raoul_id = self.user_bert_id, self.user_raoul_id
# def test_10_mail_flow_access_rights(self):
# """ Test a Chatter-looks alike flow. """
# cr, uid = self.cr, self.uid
# partner_bert_id, partner_raoul_id = self.partner_bert_id, self.partner_raoul_id
# user_bert_id, user_raoul_id = self.user_bert_id, self.user_raoul_id
# Prepare groups: Pigs (employee), Jobs (public)
self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message')
self.group_jobs_id = self.mail_group.create(cr, uid, {'name': 'Jobs', 'public': 'public'})
# # Prepare groups: Pigs (employee), Jobs (public)
# self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message')
# self.group_jobs_id = self.mail_group.create(cr, uid, {'name': 'Jobs', 'public': 'public'})
# ----------------------------------------
# CASE1: Bert, without groups
# ----------------------------------------
# Do: Bert creates a group, should crash because perm_create only for employees
self.assertRaises(except_orm,
self.mail_group.create,
cr, user_bert_id, {'name': 'Bert\'s Group'})
# Do: Bert reads Jobs basic fields, ok because public = read access on the group
self.mail_group.read(cr, user_bert_id, self.group_jobs_id, ['name', 'description'])
# Do: Bert browse Pigs, ok (no direct browse of partners)
self.mail_group.browse(cr, user_bert_id, self.group_jobs_id)
# Do: Bert reads Jobs messages, ok because read access on the group => read access on its messages
jobs_message_ids = self.mail_group.read(cr, user_bert_id, self.group_jobs_id, ['message_ids'])['message_ids']
self.mail_message.read(cr, user_bert_id, jobs_message_ids)
# Do: Bert reads Jobs followers, ko because partner are accessible to employees or partner manager
jobs_followers_ids = self.mail_group.read(cr, user_bert_id, self.group_jobs_id, ['message_follower_ids'])['message_follower_ids']
self.assertRaises(except_orm,
self.res_partner.read,
cr, user_bert_id, jobs_followers_ids)
# Do: Bert comments Jobs, ko because no write access on the group and not in the followers
self.assertRaises(except_orm,
self.mail_group.message_post,
cr, user_bert_id, self.group_jobs_id, body='I love Pigs')
# Do: add Bert to jobs followers
self.mail_group.message_subscribe(cr, uid, [self.group_jobs_id], [partner_bert_id])
# Do: Bert comments Jobs, ok because he is now in the followers
self.mail_group.message_post(cr, user_bert_id, self.group_jobs_id, body='I love Pigs')
# # ----------------------------------------
# # CASE1: Bert, without groups
# # ----------------------------------------
# # Do: Bert creates a group, should crash because perm_create only for employees
# self.assertRaises(except_orm,
# self.mail_group.create,
# cr, user_bert_id, {'name': 'Bert\'s Group'})
# # Do: Bert reads Jobs basic fields, ok because public = read access on the group
# self.mail_group.read(cr, user_bert_id, self.group_jobs_id, ['name', 'description'])
# # Do: Bert browse Pigs, ok (no direct browse of partners)
# self.mail_group.browse(cr, user_bert_id, self.group_jobs_id)
# # Do: Bert reads Jobs messages, ok because read access on the group => read access on its messages
# jobs_message_ids = self.mail_group.read(cr, user_bert_id, self.group_jobs_id, ['message_ids'])['message_ids']
# self.mail_message.read(cr, user_bert_id, jobs_message_ids)
# # Do: Bert reads Jobs followers, ko because partner are accessible to employees or partner manager
# jobs_followers_ids = self.mail_group.read(cr, user_bert_id, self.group_jobs_id, ['message_follower_ids'])['message_follower_ids']
# self.assertRaises(except_orm,
# self.res_partner.read,
# cr, user_bert_id, jobs_followers_ids)
# # Do: Bert comments Jobs, ko because no write access on the group and not in the followers
# self.assertRaises(except_orm,
# self.mail_group.message_post,
# cr, user_bert_id, self.group_jobs_id, body='I love Pigs')
# # Do: add Bert to jobs followers
# self.mail_group.message_subscribe(cr, uid, [self.group_jobs_id], [partner_bert_id])
# # Do: Bert comments Jobs, ok because he is now in the followers
# self.mail_group.message_post(cr, user_bert_id, self.group_jobs_id, body='I love Pigs')
# Do: Bert reads Pigs, should crash because mail.group security=groups only for employee group
self.assertRaises(except_orm,
self.mail_group.read,
cr, user_bert_id, self.group_pigs_id)
# # Do: Bert reads Pigs, should crash because mail.group security=groups only for employee group
# self.assertRaises(except_orm,
# self.mail_group.read,
# cr, user_bert_id, self.group_pigs_id)
# ----------------------------------------
# CASE1: Raoul, employee
# ----------------------------------------
# Do: Bert read Pigs, ok because public
self.mail_group.read(cr, user_raoul_id, self.group_pigs_id)
# Do: Bert read Jobs, ok because group_public_id = employee
self.mail_group.read(cr, user_raoul_id, self.group_jobs_id)
# # ----------------------------------------
# # CASE1: Raoul, employee
# # ----------------------------------------
# # Do: Bert read Pigs, ok because public
# self.mail_group.read(cr, user_raoul_id, self.group_pigs_id)
# # Do: Bert read Jobs, ok because group_public_id = employee
# self.mail_group.read(cr, user_raoul_id, self.group_jobs_id)

View File

@ -102,9 +102,9 @@ class mail_compose_message(osv.TransientModel):
'partner_ids': fields.many2many('res.partner',
'mail_compose_message_res_partner_rel',
'wizard_id', 'partner_id', 'Additional contacts'),
'attachment_ids': fields.many2many('ir.attachment',
'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments'),
'attachment_ids': fields.one2many('ir.attachment', 'res_id',
domain=lambda self: [('res_model', '=', self._name)],
string='Attachments'),
'filter_id': fields.many2one('ir.filters', 'Filters'),
'body_text': fields.text('Plain-text Contents'),
'content_subtype': fields.char('Message content subtype', size=32, readonly=1,
@ -219,19 +219,29 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
print '**', context
active_ids = context.get('active_ids')
for wizard in self.browse(cr, uid, ids, context=context):
mass_mail_mode = wizard.composition_mode == 'mass_mail'
active_model_pool = self.pool.get(wizard.model if wizard.model else 'mail.thread')
if wizard.content_subtype == 'html':
if not wizard.body:
return False
body = wizard.body
else: # wizard.content_subtype == 'plain':
if not wizard.body_text:
return False
body = '<pre>%s</pre>' % tools.ustr(wizard.body_text or '')
# wizard works in batch mode: [res_id] or active_ids
res_ids = active_ids if mass_mail_mode and wizard.model and active_ids else [wizard.res_id]
for res_id in res_ids:
# default values, according to the wizard options
post_values = {
'subject': wizard.subject if wizard.content_subtype == 'html' else False,
'body': wizard.body if wizard.content_subtype == 'html' else '<pre>%s</pre>' % tools.ustr(wizard.body_text),
'body': body,
'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [(4, partner.id) for partner in wizard.partner_ids],
'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],
@ -245,11 +255,13 @@ class mail_compose_message(osv.TransientModel):
post_values['attachments'] += new_attachments
post_values.update(email_dict)
# post the message
active_model_pool.message_post(cr, uid, [res_id], type='comment', context=context, **post_values)
# post process: update attachments, because id is not necessarily known when adding attachments in Chatter
self.pool.get('ir.attachment').write(cr, uid, [attach.id for attach in wizard.attachment_ids], {'res_id': wizard.id}, context=context)
id=active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype='mt_comment', context=context, **post_values)
return {'type': 'ir.actions.act_window_close'}
# post process: update attachments, because id is not necessarily known when adding attachments in Chatter
# self.pool.get('ir.attachment').write(cr, uid, [attach.id for attach in wizard.attachment_ids], {
# 'res_id': wizard.id, 'res_model': wizard.model or False}, context=context)
return {'type': 'ir.actions.act_window_close', 'res_model':'mail.compose.message', 'id': id}
def render_message(self, cr, uid, wizard, res_id, context=None):
""" Generate an email from the template for given (wizard.model, res_id)

View File

@ -75,7 +75,7 @@
<div colspan="2" class="oe_mail_compose_message_attachments"/>
<!-- buttons, with as few Chatter logic as possible -->
<div>
<button name="send_mail" string="Post" type="object"
<button name="send_mail" string="Post message" type="object"
class="oe_mail_compose_message_button_send"/>
</div>
<div class='oe_mail_compose_message_icons'>

View File

@ -26,6 +26,6 @@ From the Manufacturing Settings, you can choose to compute production schedules
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
</data>
</openerp>

View File

@ -409,9 +409,8 @@
</page>
</notebook>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
</form>
</field>
@ -798,7 +797,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -58,6 +58,7 @@ So, that we can compare the theoretic delay and real delay.
'depends': ['mrp'],
'data': [
'security/ir.model.access.csv',
'mrp_operation_data.xml',
'mrp_operations_workflow.xml',
'mrp_operations_view.xml',
'mrp_operations_report.xml',
@ -65,7 +66,7 @@ So, that we can compare the theoretic delay and real delay.
'process/mrp_operation_process.xml',
'mrp_operations_workflow_instance.xml'
],
'demo': ['mrp_operation_data.xml',
'demo': [
'mrp_operations_demo.yml'
],
'test': [

View File

@ -107,7 +107,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -43,6 +43,7 @@ The following topics should be covered by this module:
'data': [
'security/ir.model.access.csv',
'security/mrp_repair_security.xml',
'mrp_repair_data.xml',
'mrp_repair_sequence.xml',
'wizard/mrp_repair_cancel_view.xml',
'wizard/mrp_repair_make_invoice_view.xml',

View File

@ -0,0 +1,5 @@
<?xml version="1.0"?>
<openerp>
<data>
</data>
</openerp>

View File

@ -189,7 +189,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -108,22 +108,21 @@
<!-- New note Form View -->
<record model="ir.ui.view" id="view_note_note_form">
<field name="name">note.note.form</field>
<field name="model">note.note</field>
<field name="arch" type="xml">
<form string="Note" version="7.0">
<header>
<field name="tag_ids" widget="many2many_tags" class="oe_inline" placeholder="Tags"/>
<field name="stage_id" domain="[('user_id','=',uid)]" widget="statusbar" clickable="1"/>
</header>
<field name="memo" widget="html"/><!-- editor_width="100%%" editor_height="60%%" -->
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
</form>
</field>
<field name="name">note.note.form</field>
<field name="model">note.note</field>
<field name="arch" type="xml">
<form string="Note" version="7.0">
<header>
<field name="tag_ids" widget="many2many_tags" class="oe_inline" placeholder="Tags"/>
<field name="stage_id" domain="[('user_id','=',uid)]" widget="statusbar" clickable="1"/>
</header>
<field name="memo" widget="html"/><!-- editor_width="100%%" editor_height="60%%" -->
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field class="oe_chatter" name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<!-- Search note -->

View File

@ -498,22 +498,22 @@ class procurement_order(osv.osv):
return obj_id
def create_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Procurement has been <b>created</b>."), context=context)
self.message_post(cr, uid, ids, body=_("Procurement <b>created</b>."), context=context)
def confirm_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Procurement has been <b>confirmed</b>."), context=context)
self.message_post(cr, uid, ids, body=_("Procurement <b>confirmed</b>."), context=context)
def running_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Procurement has been set to <b>running</b>."), context=context)
self.message_post(cr, uid, ids, body=_("Procurement set to <b>running</b>."), context=context)
def ready_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Procurement has been set to <b>ready</b>."), context=context)
self.message_post(cr, uid, ids, body=_("Procurement set to <b>ready</b>."), context=context)
def cancel_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Procurement has been <b>cancelled</b>."), context=context)
self.message_post(cr, uid, ids, body=_("Procurement <b>cancelled</b>."), context=context)
def done_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Procurement has been <b>done</b>."), context=context)
self.message_post(cr, uid, ids, body=_("Procurement <b>done</b>."), context=context)
procurement_order()

View File

@ -103,7 +103,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -203,7 +203,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -519,20 +519,16 @@ def Project():
return self.message_post(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
def set_open_send_note(self, cr, uid, ids, context=None):
message = _("Project has been <b>opened</b>.")
return self.message_post(cr, uid, ids, body=message, context=context)
return self.message_post(cr, uid, ids, body=_("Project has been <b>opened</b>."), context=context)
def set_pending_send_note(self, cr, uid, ids, context=None):
message = _("Project is now <b>pending</b>.")
return self.message_post(cr, uid, ids, body=message, context=context)
return self.message_post(cr, uid, ids, body=_("Project is now <b>pending</b>."), context=context)
def set_cancel_send_note(self, cr, uid, ids, context=None):
message = _("Project has been <b>cancelled</b>.")
return self.message_post(cr, uid, ids, body=message, context=context)
return self.message_post(cr, uid, ids, body=_("Project has been <b>canceled</b>."), context=context)
def set_close_send_note(self, cr, uid, ids, context=None):
message = _("Project has been <b>closed</b>.")
return self.message_post(cr, uid, ids, body=message, context=context)
return self.message_post(cr, uid, ids, body=_("Project has been <b>closed</b>."), context=context)
def write(self, cr, uid, ids, vals, context=None):
# if alias_model has been changed, update alias_model_id accordingly
@ -1100,10 +1096,10 @@ class task(base_stage, osv.osv):
def create(self, cr, uid, vals, context=None):
task_id = super(task, self).create(cr, uid, vals, context=context)
project_id = self.browse(cr, uid, task_id, context=context).project_id
if project_id:
followers = [follower.id for follower in project_id.message_follower_ids]
self.message_subscribe(cr, uid, [task_id], followers, context=context)
task_record = self.browse(cr, uid, task_id, context=context)
if task_record.project_id:
project_follower_ids = [follower.id for follower in task_record.project_id.message_follower_ids]
self.message_subscribe(cr, uid, [task_id], project_follower_ids, context=context)
self._store_history(cr, uid, [task_id], context=context)
self.create_send_note(cr, uid, [task_id], context=context)
return task_id
@ -1235,14 +1231,14 @@ class task(base_stage, osv.osv):
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
return self.message_post(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % (stage_name),
context=context)
def create_send_note(self, cr, uid, ids, context=None):
return self.message_post(cr, uid, ids, body=_("Task has been <b>created</b>."), context=context)
def case_draft_send_note(self, cr, uid, ids, context=None):
msg = _('Task has been set as <b>draft</b>.')
return self.message_post(cr, uid, ids, body=msg, context=context)
return self.message_post(cr, uid, ids, body=_('Task has been set as <b>draft</b>.'), context=context)
def do_delegation_send_note(self, cr, uid, ids, context=None):
for task in self.browse(cr, uid, ids, context=context):

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<data>
<!-- This will set the unit of measure used in projects and tasks.-->
<record id="base.main_company" model="res.company">
<field name="project_time_mode_id" ref="product.product_uom_hour"></field>
@ -84,6 +84,42 @@
<field name="fold" eval="True"/>
</record>
<!-- mail: subtypes -->
<record id="mt_project_new" model="mail.message.subtype">
<field name="name">New Task</field>
<field name="res_model">project.project</field>
<field name="default" eval="False"/>
</record>
<record id="mt_project_closed" model="mail.message.subtype">
<field name="name">Task Closed</field>
<field name="res_model">project.project</field>
</record>
<record id="mt_project_canceled" model="mail.message.subtype">
<field name="name">Task Canceled</field>
<field name="res_model">project.task</field>
</record>
<record id="mt_project_stage" model="mail.message.subtype">
<field name="name">Task Stage Changed</field>
<field name="res_model">project.task</field>
</record>
<record id="mt_task_new" model="mail.message.subtype">
<field name="name">New Task</field>
<field name="res_model">project.task</field>
</record>
<record id="mt_task_closed" model="mail.message.subtype">
<field name="name">Task Closed</field>
<field name="res_model">project.task</field>
</record>
<record id="mt_task_canceled" model="mail.message.subtype">
<field name="name">Task canceled</field>
<field name="res_model">project.task</field>
</record>
<record id="mt_task_change" model="mail.message.subtype">
<field name="name">Task Stage Changed</field>
<field name="res_model">project.task</field>
</record>
<!-- notify all employees of module installation -->
<record model="mail.message" id="module_install_notification">
<field name="model">mail.group</field>

View File

@ -150,7 +150,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers" help="Follow this project to automatically follow all related tasks and issues."/>
</div>
@ -468,7 +467,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -511,7 +511,7 @@ class project_issue(base_stage, osv.osv):
def stage_set_send_note(self, cr, uid, ids, stage_id, context=None):
""" Override of the (void) default notification method. """
stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1]
return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), context=context)
return self.message_post(cr, uid, ids, body= _("Stage changed to <b>%s</b>.") % (stage_name), subtype="mt_issue_new", context=context)
def case_get_note_msg_prefix(self, cr, uid, id, context=None):
""" Override of default prefix for notifications. """
@ -523,7 +523,7 @@ class project_issue(base_stage, osv.osv):
def create_send_note(self, cr, uid, ids, context=None):
message = _("Project issue <b>created</b>.")
return self.message_post(cr, uid, ids, body=message, context=context)
return self.message_post(cr, uid, ids, body=message, subtype="mt_issue_new", context=context)
def case_escalate_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):

View File

@ -42,5 +42,21 @@ You can record issues, assign them to a responsible person, and keep track of th
Access all issues from the top Project menu, and access the issues of a specific project via the projects gallery view.</field>
</record>
<!-- Mail subtypes -->
<record id="mail.mt_issue_new" model="mail.message.subtype">
<field name="name">created</field>
<field name="res_model">project.issue</field>
<field name="default" eval="False"/>
</record>
<record id="mail.mt_issue_new" model="mail.message.subtype">
<field name="name">stage change</field>
<field name="res_model">project.issue</field>
<field name="default" eval="False"/>
</record>
<record id="mail.mt_issue_closed" model="mail.message.subtype">
<field name="name">closed</field>
<field name="res_model">project.issue</field>
</record>
</data>
</openerp>

View File

@ -161,7 +161,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -285,7 +285,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -92,13 +92,13 @@ class purchase_requisition(osv.osv):
def in_progress_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Draft Requisition has been <b>sent to suppliers</b>."), context=context)
def reset_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Purchase Requisition has been set to <b>draft</b>."), context=context)
def done_to_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Purchase Requisition has been <b>done</b>."), context=context)
def cancel_send_note(self, cr, uid, ids, context=None):
self.message_post(cr, uid, ids, body=_("Purchase Requisition has been <b>cancelled</b>."), context=context)

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<function
<function
eval="('default',False,'warehouse_id', [('purchase.requisition', False)], ref('stock.warehouse0'), True, False, False, False, True)"
id="purchase_default_set"
model="ir.values"

View File

@ -102,9 +102,8 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
</form>
</field>

View File

@ -310,7 +310,6 @@
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>

View File

@ -1450,7 +1450,7 @@ class stock_picking(osv.osv):
'internal': _("Products have been <b>moved</b>."),
}
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], body=type_dict.get(obj.type, _('Products have been moved.')), context=context)
self.message_post(cr, uid, [obj.id], body=_("Products have been <b>%s</b>.") % (type_dict.get(obj.type, 'move done')), context=context)
def ship_cancel_send_note(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context):

View File

@ -914,7 +914,6 @@
</xpath>
<xpath expr="/form/sheet" position="after">
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>
@ -1041,7 +1040,6 @@
</xpath>
<xpath expr="/form/sheet" position="after">
<div class="oe_chatter">
<field name="message_is_follower" invisible="1"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/>
</div>