[Merge] Merge with main addons.

bzr revid: mdi@tinyerp.com-20121030044702-as91g4l1lr89d8x1
This commit is contained in:
Divyesh Makwana (Open ERP) 2012-10-30 10:17:02 +05:30
commit d3466a9624
75 changed files with 2339 additions and 1837 deletions

View File

@ -39,7 +39,6 @@ class account_analytic_line(osv.osv):
} }
_defaults = { _defaults = {
'date': fields.date.context_today,
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c), 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
} }
_order = 'date desc' _order = 'date desc'

View File

@ -865,8 +865,11 @@ class account_invoice(osv.osv):
self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj) self.check_tax_lines(cr, uid, inv, compute_taxes, ait_obj)
# I disabled the check_total feature # I disabled the check_total feature
#if inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0): group_check_total_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'group_supplier_inv_check_total')[1]
# raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe real total does not match the computed total.')) group_check_total = self.pool.get('res.groups').browse(cr, uid, group_check_total_id, context=context)
if group_check_total and uid in [x.id for x in group_check_total.users]:
if (inv.type in ('in_invoice', 'in_refund') and abs(inv.check_total - inv.amount_total) >= (inv.currency_id.rounding/2.0)):
raise osv.except_osv(_('Bad total !'), _('Please verify the price of the invoice !\nThe encoded total does not match the computed total.'))
if inv.payment_term: if inv.payment_term:
total_fixed = total_percent = 0 total_fixed = total_percent = 0

View File

@ -186,6 +186,7 @@
<field name="journal_id" groups="account.group_account_user" <field name="journal_id" groups="account.group_account_user"
on_change="onchange_journal_id(journal_id, context)" widget="selection"/> on_change="onchange_journal_id(journal_id, context)" widget="selection"/>
<field name="currency_id" groups="base.group_multi_currency"/> <field name="currency_id" groups="base.group_multi_currency"/>
<field name="check_total" groups="account.group_supplier_inv_check_total"/>
</group> </group>
</group> </group>
<notebook> <notebook>
@ -279,8 +280,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -436,8 +437,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -10,7 +10,7 @@
<group col="4"> <group col="4">
<field name="date1"/> <field name="date1"/>
<field name="date2"/> <field name="date2"/>
<field name="analytic_account_journal_id" widget="many2many_tags" class="oe_inline" required="1"/> <field name="analytic_account_journal_id" widget="many2many_tags" class="oe_inline" required="1" colspan="4"/>
</group> </group>
<footer> <footer>
<button name="check_report" string="Print" type="object" class="oe_highlight"/> <button name="check_report" string="Print" type="object" class="oe_highlight"/>

View File

@ -120,6 +120,8 @@ class account_config_settings(osv.osv_memory):
'group_analytic_accounting': fields.boolean('Analytic accounting', 'group_analytic_accounting': fields.boolean('Analytic accounting',
implied_group='analytic.group_analytic_accounting', implied_group='analytic.group_analytic_accounting',
help="Allows you to use the analytic accounting."), help="Allows you to use the analytic accounting."),
'group_check_supplier_invoice_total': fields.boolean('Check the total of supplier invoices',
implied_group="account.group_supplier_inv_check_total"),
} }
def _default_company(self, cr, uid, context=None): def _default_company(self, cr, uid, context=None):

View File

@ -220,6 +220,10 @@
<field name="module_account_check_writing" class="oe_inline"/> <field name="module_account_check_writing" class="oe_inline"/>
<label for="module_account_check_writing"/> <label for="module_account_check_writing"/>
</div> </div>
<div>
<field name="group_check_supplier_invoice_total" class="oe_inline"/>
<label for="group_check_supplier_invoice_total"/>
</div>
</div> </div>
</group> </group>
<separator string="Bank &amp; Cash"/> <separator string="Bank &amp; Cash"/>

View File

@ -26,6 +26,11 @@
<field name="category_id" ref="base.module_category_hidden"/> <field name="category_id" ref="base.module_category_hidden"/>
</record> </record>
<record id="group_supplier_inv_check_total" model="res.groups">
<field name="name">Check Total on supplier invoices</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="account_move_comp_rule" model="ir.rule"> <record id="account_move_comp_rule" model="ir.rule">
<field name="name">Account Entry</field> <field name="name">Account Entry</field>
<field name="model_id" ref="model_account_move"/> <field name="model_id" ref="model_account_move"/>

View File

@ -988,11 +988,11 @@ class account_voucher(osv.osv):
if amount_residual > 0: if amount_residual > 0:
account_id = line.voucher_id.company_id.expense_currency_exchange_account_id account_id = line.voucher_id.company_id.expense_currency_exchange_account_id
if not account_id: if not account_id:
raise osv.except_osv(_('Warning!'),_("First you have to configure the 'Expense Currency Rate' on the company, then create accounting entry for currency rate difference.")) raise osv.except_osv(_('Insufficient Configuration!'),_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
else: else:
account_id = line.voucher_id.company_id.income_currency_exchange_account_id account_id = line.voucher_id.company_id.income_currency_exchange_account_id
if not account_id: if not account_id:
raise osv.except_osv(_('Warning!'),_("First you have to configure the 'Income Currency Rate' on the company, then create accounting entry for currency rate difference.")) raise osv.except_osv(_('Insufficient Configuration!'),_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
# Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise # Even if the amount_currency is never filled, we need to pass the foreign currency because otherwise
# the receivable/payable account may have a secondary currency, which render this field mandatory # the receivable/payable account may have a secondary currency, which render this field mandatory
account_currency_id = company_currency <> current_currency and current_currency or False account_currency_id = company_currency <> current_currency and current_currency or False

View File

@ -109,8 +109,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -240,8 +240,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -512,8 +512,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -147,8 +147,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -303,8 +303,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -96,9 +96,29 @@ class account_analytic_account(osv.osv):
res[row['id']][field] = row[field] res[row['id']][field] = row[field]
return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context) return self._compute_level_tree(cr, uid, ids, child_ids, res, fields, context)
def _complete_name_calc(self, cr, uid, ids, prop, unknow_none, unknow_dict): def name_get(self, cr, uid, ids, context=None):
res = self.name_get(cr, uid, ids) res = []
return dict(res) for id in ids:
elmt = self.browse(cr, uid, id, context=context)
res.append((id, self._get_one_full_name(elmt)))
return res
def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
if context == None:
context = {}
res = {}
for elmt in self.browse(cr, uid, ids, context=context):
res[elmt.id] = self._get_one_full_name(elmt)
return res
def _get_one_full_name(self, elmt, level=6):
if level<=0:
return '...'
if elmt.parent_id:
parent_path = self._get_one_full_name(elmt.parent_id, level-1) + "/"
else:
parent_path = ''
return parent_path + elmt.name
def _child_compute(self, cr, uid, ids, name, arg, context=None): def _child_compute(self, cr, uid, ids, name, arg, context=None):
result = {} result = {}
@ -139,7 +159,7 @@ class account_analytic_account(osv.osv):
_columns = { _columns = {
'name': fields.char('Account/Contract Name', size=128, required=True), 'name': fields.char('Account/Contract Name', size=128, required=True),
'complete_name': fields.function(_complete_name_calc, type='char', string='Full Account Name'), 'complete_name': fields.function(_get_full_name, type='char', string='Full Account Name'),
'code': fields.char('Reference', size=24, select=True), 'code': fields.char('Reference', size=24, select=True),
'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True, 'type': fields.selection([('view','Analytic View'), ('normal','Analytic Account'),('contract','Contract or Project'),('template','Template of Contract')], 'Type of Account', required=True,
help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\ help="If you select the View Type, it means you won\'t allow to create journal entries using that account.\n"\
@ -305,8 +325,15 @@ class account_analytic_line(osv.osv):
'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), 'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
} }
def _get_default_date(self, cr, uid, context=None):
return fields.date.context_today(self, cr, uid, context=context)
def __get_default_date(self, cr, uid, context=None):
return self._get_default_date(cr, uid, context=context)
_defaults = { _defaults = {
'date': lambda *a: time.strftime('%Y-%m-%d'), 'date': __get_default_date,
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c), 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
'amount': 0.00 'amount': 0.00
} }

View File

@ -56,8 +56,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -22,6 +22,7 @@
import logging import logging
import os import os
import tempfile import tempfile
import getpass
import urllib import urllib
import werkzeug.urls import werkzeug.urls
@ -43,7 +44,16 @@ from .. import utils
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
oidutil.log = _logger.debug oidutil.log = _logger.debug
_storedir = os.path.join(tempfile.gettempdir(), 'openerp-auth_openid-store') def get_system_user():
"""Return system user info string, such as USERNAME-EUID"""
info = getpass.getuser()
euid = getattr(os, 'geteuid', None) # Non available on some platforms
if euid is not None:
info = '%s-%d' % (info, euid())
return info
_storedir = os.path.join(tempfile.gettempdir(),
'openerp-auth_openid-%s-store' % get_system_user())
class GoogleAppsAwareConsumer(consumer.GenericConsumer): class GoogleAppsAwareConsumer(consumer.GenericConsumer):
def complete(self, message, endpoint, return_to): def complete(self, message, endpoint, return_to):

View File

@ -1151,7 +1151,7 @@ rule or repeating pattern of time to exclude from the recurring rule."),
context = {} context = {}
result = [] result = []
for data in super(calendar_event, self).read(cr, uid, select, context=context): for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'exdate', 'exrule', 'date'], context=context):
if not data['rrule']: if not data['rrule']:
result.append(data['id']) result.append(data['id'])
continue continue

View File

@ -219,8 +219,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -223,8 +223,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
@ -525,8 +525,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -150,8 +150,8 @@
<field name="description" placeholder="Description..."/> <field name="description" placeholder="Description..."/>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -132,8 +132,8 @@
</page> </page>
</notebook> </notebook>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers" help="Followers of this salesteam follow automatically all opportunities related to this salesteam."/> <field name="message_follower_ids" widget="mail_followers" help="Followers of this salesteam follow automatically all opportunities related to this salesteam."/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -179,8 +179,8 @@
</group> </group>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -165,11 +165,8 @@ class test_message_compose(test_mail.TestMailMockups):
self.assertEqual(message_pigs.body, _body_html1, 'mail.message body on Pigs incorrect') self.assertEqual(message_pigs.body, _body_html1, 'mail.message body on Pigs incorrect')
self.assertEqual(message_bird.body, _body_html2, 'mail.message body on Bird incorrect') self.assertEqual(message_bird.body, _body_html2, 'mail.message body on Bird incorrect')
# Test: partner_ids: p_a_id (default) + 3 newly created partners # Test: partner_ids: p_a_id (default) + 3 newly created partners
message_pigs_pids = [partner.id for partner in message_pigs.partner_ids] message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids]
message_bird_pids = [partner.id for partner in message_bird.partner_ids] message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids]
partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])]) partner_ids = 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(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids')
self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of partner_ids') self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect')
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

@ -8,7 +8,7 @@
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/> <field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data>
<xpath expr="//form/notebook" position="after"> <xpath expr="//form/group" position="after">
<group attrs="{'invisible':[('use_template','=',False)]}"> <group attrs="{'invisible':[('use_template','=',False)]}">
<field name="use_template" invisible="1" <field name="use_template" invisible="1"
on_change="onchange_use_template(use_template, template_id, composition_mode, model, res_id, context)"/> on_change="onchange_use_template(use_template, template_id, composition_mode, model, res_id, context)"/>

View File

@ -204,8 +204,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
</div> </div>
</form> </form>
</field> </field>
@ -486,8 +486,8 @@
</group> </group>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -359,8 +359,8 @@
</div> </div>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -42,7 +42,7 @@ class hr_config_settings(osv.osv_memory):
help ="""This installs the module hr_contract."""), help ="""This installs the module hr_contract."""),
'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation', 'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation',
help ="""This installs the module hr_evaluation."""), help ="""This installs the module hr_evaluation."""),
'module_account_analytic_analysis': fields.boolean('Allow invoicing based on timesheets (will install the sale application)', 'module_account_analytic_analysis': fields.boolean('Allow invoicing based on timesheets (the sale application will be installed)',
help ="""This installs the module account_analytic_analysis, which will install sales management too."""), help ="""This installs the module account_analytic_analysis, which will install sales management too."""),
'module_hr_payroll': fields.boolean('Manage payroll', 'module_hr_payroll': fields.boolean('Manage payroll',
help ="""This installs the module hr_payroll."""), help ="""This installs the module hr_payroll."""),

View File

@ -210,8 +210,8 @@
</group> </group>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -139,8 +139,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

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

View File

@ -183,8 +183,8 @@
<field name="description" placeholder="Feedback of interviews..."/> <field name="description" placeholder="Feedback of interviews..."/>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -79,7 +79,7 @@ class hr_timesheet_sheet(osv.osv):
if not new_user_id: if not new_user_id:
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must assign it to a user.')) raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must assign it to a user.'))
if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id): if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id):
raise osv.except_osv(_('Error!'), _('You cannot have 2 timesheets that overlaps!\nYou should use the menu \'My Timesheet\' to avoid this problem.')) raise osv.except_osv(_('Error!'), _('You cannot have 2 timesheets that overlap!\nYou should use the menu \'My Timesheet\' to avoid this problem.'))
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).product_id: if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).product_id:
raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product.')) raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product.'))
if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).journal_id: if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).journal_id:
@ -187,7 +187,7 @@ class hr_timesheet_sheet(osv.osv):
_constraints = [ _constraints = [
(_sheet_date, 'You cannot have 2 timesheets that overlaps !\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']), (_sheet_date, 'You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']),
] ]
def action_set_to_draft(self, cr, uid, ids, *args): def action_set_to_draft(self, cr, uid, ids, *args):
@ -223,16 +223,26 @@ class hr_timesheet_sheet(osv.osv):
hr_timesheet_sheet() hr_timesheet_sheet()
class account_analytic_line(osv.osv):
class hr_timesheet_line(osv.osv): _inherit = "account.analytic.line"
_inherit = "hr.analytic.timesheet"
def _get_default_date(self, cr, uid, context=None): def _get_default_date(self, cr, uid, context=None):
if context is None: if context is None:
context = {} context = {}
if 'date' in context: #get the default date (should be: today)
return context['date'] res = super(account_analytic_line, self)._get_default_date(cr, uid, context=context)
return time.strftime('%Y-%m-%d') #if we got the dates from and to from the timesheet and if the default date is in between, we use the default
#but if the default isn't included in those dates, we use the date start of the timesheet as default
if context.get('timesheet_date_from') and context.get('timesheet_date_to'):
if context['timesheet_date_from'] <= res <= context['timesheet_date_to']:
return res
return context.get('timesheet_date_from')
#if we don't get the dates from the timesheet, we return the default value from super()
return res
class hr_timesheet_line(osv.osv):
_inherit = "hr.analytic.timesheet"
def _sheet(self, cursor, user, ids, name, args, context=None): def _sheet(self, cursor, user, ids, name, args, context=None):
sheet_obj = self.pool.get('hr_timesheet_sheet.sheet') sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
@ -278,9 +288,6 @@ class hr_timesheet_line(osv.osv):
}, },
), ),
} }
_defaults = {
'date': _get_default_date,
}
def _check_sheet_state(self, cr, uid, ids, context=None): def _check_sheet_state(self, cr, uid, ids, context=None):
if context is None: if context is None:

View File

@ -103,7 +103,7 @@
</widget> </widget>
</page> </page>
<page string="Details"> <page string="Details">
<field context="{'user_id':user_id}" name="timesheet_ids" nolabel="1"> <field context="{'user_id':user_id, 'timesheet_date_from': date_from, 'timesheet_date_to': date_to}" name="timesheet_ids" nolabel="1">
<tree editable="top" string="Timesheet Activities"> <tree editable="top" string="Timesheet Activities">
<field name="date"/> <field name="date"/>
<field domain="[('type','in',['normal', 'contract']), ('state', '&lt;&gt;', 'close'),('use_timesheets','=',1)]" name="account_id" on_change="on_change_account_id(account_id, user_id)" context="{'default_use_timesheets': 1}"/> <field domain="[('type','in',['normal', 'contract']), ('state', '&lt;&gt;', 'close'),('use_timesheets','=',1)]" name="account_id" on_change="on_change_account_id(account_id, user_id)" context="{'default_use_timesheets': 1}"/>
@ -303,18 +303,21 @@
</record> </record>
<act_window <act_window
context="{'search_default_sheet_id': [active_id]}" context="{'search_default_sheet_id': [active_id]}"
id="act_hr_timesheet_sheet_sheet_by_account" id="act_hr_timesheet_sheet_sheet_by_account"
name="Timesheet by Account" name="Timesheet by Account"
res_model="hr_timesheet_sheet.sheet.account" groups="base.group_hr_attendance"
src_model="hr_timesheet_sheet.sheet"/> res_model="hr_timesheet_sheet.sheet.account"
src_model="hr_timesheet_sheet.sheet"/>
<act_window <act_window
context="{'search_default_sheet_id': [active_id]}" context="{'search_default_sheet_id': [active_id]}"
id="act_hr_timesheet_sheet_sheet_by_day" id="act_hr_timesheet_sheet_sheet_by_day"
name="Timesheet by Day" name="Timesheet by Day"
res_model="hr_timesheet_sheet.sheet.day" groups="base.group_hr_attendance"
src_model="hr_timesheet_sheet.sheet"/> res_model="hr_timesheet_sheet.sheet.day"
src_model="hr_timesheet_sheet.sheet"/>
<record id="hr_timesheet_sheet_tree_simplified" model="ir.ui.view"> <record id="hr_timesheet_sheet_tree_simplified" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.tree</field> <field name="name">hr.timesheet.sheet.tree</field>
<field name="model">hr_timesheet_sheet.sheet</field> <field name="model">hr_timesheet_sheet.sheet</field>

View File

@ -79,8 +79,8 @@
<field name="description"/> <field name="description"/>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -84,7 +84,6 @@ Main Features
'css': [ 'css': [
'static/src/css/mail.css', 'static/src/css/mail.css',
'static/src/css/mail_group.css', 'static/src/css/mail_group.css',
'static/src/css/mail_compose_message.css',
], ],
'js': [ 'js': [
'static/lib/jquery.expander/jquery.expander.js', 'static/lib/jquery.expander/jquery.expander.js',

View File

@ -8,6 +8,7 @@
<field name="body">Your monthly meal vouchers arrived. You can get them at Christine's office. <field name="body">Your monthly meal vouchers arrived. You can get them at Christine's office.
This month you also get 250 EUR of eco-vouchers if you have been in the company for more than a year.</field> This month you also get 250 EUR of eco-vouchers if you have been in the company for more than a year.</field>
<field name="type">comment</field> <field name="type">comment</field>
<field name="subtype_id" ref="mt_comment"/>
</record> </record>
<record id="message_blogpost0_comment0" model="mail.message"> <record id="message_blogpost0_comment0" model="mail.message">
@ -16,6 +17,7 @@ This month you also get 250 EUR of eco-vouchers if you have been in the company
<field name="body"><![CDATA[Great.]]></field> <field name="body"><![CDATA[Great.]]></field>
<field name="parent_id" ref="message_blogpost0"/> <field name="parent_id" ref="message_blogpost0"/>
<field name="type">comment</field> <field name="type">comment</field>
<field name="subtype_id" ref="mt_comment"/>
</record> </record>
<record id="message_blogpost0_comment1" model="mail.message"> <record id="message_blogpost0_comment1" model="mail.message">
@ -24,22 +26,25 @@ This month you also get 250 EUR of eco-vouchers if you have been in the company
<field name="body">Thanks, but where is Christine's office, if I may ask? (I'm new here)</field> <field name="body">Thanks, but where is Christine's office, if I may ask? (I'm new here)</field>
<field name="parent_id" ref="message_blogpost0"/> <field name="parent_id" ref="message_blogpost0"/>
<field name="type">comment</field> <field name="type">comment</field>
<field name="subtype_id" ref="mt_comment"/>
</record> </record>
<record id="message_blogpost0_comment2" model="mail.message"> <record id="message_blogpost0_comment2" model="mail.message">
<field name="model">mail.group</field>
<field name="res_id" ref="group_all_employees"/>
<field name="body">Building B3, second floor on the right :-)</field>
<field name="parent_id" ref="message_blogpost0"/>
<field name="type">comment</field>
<field name="subtype_id" ref="mt_comment"/>
</record>
<record id="message_blogpost0_comment3" model="mail.message">
<field name="model">mail.group</field> <field name="model">mail.group</field>
<field name="res_id" ref="group_all_employees"/> <field name="res_id" ref="group_all_employees"/>
<field name="body">Great news, I need to buy a new fridge, I think I can pay it with the eco-vouchers!</field> <field name="body">Great news, I need to buy a new fridge, I think I can pay it with the eco-vouchers!</field>
<field name="parent_id" ref="message_blogpost0"/> <field name="parent_id" ref="message_blogpost0"/>
<field name="type">comment</field> <field name="type">comment</field>
</record> <field name="subtype_id" ref="mt_comment"/>
<record id="message_blogpost0_comment1_2" model="mail.message">
<field name="model">mail.group</field>
<field name="res_id" ref="group_all_employees"/>
<field name="body">Building B3, second floor on the right :-)</field>
<field name="parent_id" ref="message_blogpost0_comment1"/>
<field name="type">comment</field>
</record> </record>
</data> </data>

View File

@ -84,9 +84,16 @@ class mail_notification(osv.Model):
return False return False
def set_message_read(self, cr, uid, msg_ids, read=None, context=None): def set_message_read(self, cr, uid, msg_ids, read=None, context=None):
""" TDE note: add a comment, verify method calls, because js seems obfuscated. """ """ Set a message and its child messages as (un)read for uid.
:param bool read: read / unread
"""
# TDE note: use child_of or front-end send correct values ?
user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
notif_ids = self.search(cr, uid, [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)], context=context) notif_ids = self.search(cr, uid, [
('partner_id', '=', user_pid),
('message_id', 'in', msg_ids)
], context=context)
# all message have notifications: already set them as (un)read # all message have notifications: already set them as (un)read
if len(notif_ids) == len(msg_ids): if len(notif_ids) == len(msg_ids):
@ -130,7 +137,8 @@ class mail_notification(osv.Model):
def _notify(self, cr, uid, msg_id, context=None): def _notify(self, cr, uid, msg_id, context=None):
""" Send by email the notification depending on the user preferences """ """ Send by email the notification depending on the user preferences """
context = context or {} if context is None:
context = {}
# mail_noemail (do not send email) or no partner_ids: do not send, return # mail_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_noemail'): if context.get('mail_noemail'):
return True return True
@ -140,9 +148,15 @@ class mail_notification(osv.Model):
if not notify_partner_ids: if not notify_partner_ids:
return True return True
# add the context in the email
# TDE FIXME: commented, to be improved in a future branch
# quote_context = self.pool.get('mail.message').message_quote_context(cr, uid, msg_id, context=context)
mail_mail = self.pool.get('mail.mail') mail_mail = self.pool.get('mail.mail')
# add signature # add signature
body_html = msg.body body_html = msg.body
# if quote_context:
# body_html = tools.append_content_to_html(body_html, quote_context, plaintext=False)
signature = msg.author_id and msg.author_id.user_ids[0].signature or '' signature = msg.author_id and msg.author_id.user_ids[0].signature or ''
if signature: if signature:
body_html = tools.append_content_to_html(body_html, signature) body_html = tools.append_content_to_html(body_html, signature)

View File

@ -82,9 +82,8 @@
</group> </group>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"
options='{"thread_level": 1}'/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}'/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -24,11 +24,17 @@ import tools
from email.header import decode_header from email.header import decode_header
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from osv import osv, orm, fields from openerp.osv import osv, orm, fields
from tools.translate import _ from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
try:
from mako.template import Template as MakoTemplate
except ImportError:
_logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
""" Some tools for parsing / creating email fields """ """ Some tools for parsing / creating email fields """
def decode(text): def decode(text):
"""Returns unicode() string conversion of the the given encoded smtp header text""" """Returns unicode() string conversion of the the given encoded smtp header text"""
@ -46,7 +52,7 @@ class mail_message(osv.Model):
_order = 'id desc' _order = 'id desc'
_message_read_limit = 10 _message_read_limit = 10
_message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids'] 'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids']
_message_record_name_length = 18 _message_record_name_length = 18
_message_read_more_limit = 1024 _message_read_more_limit = 1024
@ -57,17 +63,14 @@ class mail_message(osv.Model):
return name[:self._message_record_name_length] + '...' return name[:self._message_record_name_length] + '...'
def _get_record_name(self, cr, uid, ids, name, arg, context=None): def _get_record_name(self, cr, uid, ids, name, arg, context=None):
""" Return the related document name, using name_get. It is included in """ Return the related document name, using name_get. It is done using
a try/except statement, because if uid cannot read the related SUPERUSER_ID, to be sure to have the record name correctly stored. """
document, he should see a void string instead of crashing. """ # TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False) result = dict.fromkeys(ids, False)
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context): for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
if not message['model'] or not message['res_id']: if not message.get('model') or not message.get('res_id'):
continue continue
try: result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, uid, [message['res_id']], context=context)[0][1])
except (orm.except_orm, osv.except_osv):
pass
return result return result
def _get_to_read(self, cr, uid, ids, name, arg, context=None): def _get_to_read(self, cr, uid, ids, name, arg, context=None):
@ -78,10 +81,10 @@ class mail_message(osv.Model):
notif_ids = notif_obj.search(cr, uid, [ notif_ids = notif_obj.search(cr, uid, [
('partner_id', 'in', [partner_id]), ('partner_id', 'in', [partner_id]),
('message_id', 'in', ids), ('message_id', 'in', ids),
('read', '=', False) ('read', '=', False),
], context=context) ], context=context)
for notif in notif_obj.browse(cr, uid, notif_ids, context=context): for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
res[notif.message_id.id] = not notif.read res[notif.message_id.id] = True
return res return res
def _search_to_read(self, cr, uid, obj, name, domain, context=None): def _search_to_read(self, cr, uid, obj, name, domain, context=None):
@ -115,16 +118,21 @@ class mail_message(osv.Model):
], 'Type', ], 'Type',
help="Message type: email for email message, notification for system "\ help="Message type: email for email message, notification for system "\
"message, comment for other messages such as user replies"), "message, comment for other messages such as user replies"),
'author_id': fields.many2one('res.partner', 'Author', required=True), 'email_from': fields.char('From',
'partner_ids': fields.many2many('res.partner', 'mail_notification', 'message_id', 'partner_id', 'Recipients'), help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
'author_id': fields.many2one('res.partner', 'Author',
help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
'partner_ids': fields.many2many('res.partner', string='Recipients'),
'notified_partner_ids': fields.many2many('res.partner', 'mail_notification',
'message_id', 'partner_id', 'Recipients'),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
'message_id', 'attachment_id', 'Attachments'), 'message_id', 'attachment_id', 'Attachments'),
'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."), 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Initial thread message."),
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'), 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
'model': fields.char('Related Document Model', size=128, select=1), 'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1), 'res_id': fields.integer('Related Document ID', select=1),
'record_name': fields.function(_get_record_name, type='string', 'record_name': fields.function(_get_record_name, type='char',
string='Message Record Name', store=True, string='Message Record Name',
help="Name get of the related document."), help="Name get of the related document."),
'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'), 'notification_ids': fields.one2many('mail.notification', 'message_id', 'Notifications'),
'subject': fields.char('Subject'), 'subject': fields.char('Subject'),
@ -198,19 +206,33 @@ class mail_message(osv.Model):
:param dict message: read result of a mail.message :param dict message: read result of a mail.message
""" """
# TDE note: this method should be optimized, to lessen the number of queries, will be done ASAP
is_author = False
if message['author_id']:
is_author = message['author_id'][0] == self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
author_id = message['author_id']
elif message['email_from']:
author_id = (0, message['email_from'])
has_voted = False has_voted = False
if uid in message['vote_user_ids']: if uid in message.get('vote_user_ids'):
has_voted = True has_voted = True
is_favorite = False is_favorite = False
if uid in message['favorite_user_ids']: if uid in message.get('favorite_user_ids'):
is_favorite = True is_favorite = True
is_private = True
if message.get('model') and message.get('res_id'):
is_private = False
try: try:
attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)] attachment_ids = [{'id': attach[0], 'name': attach[1]} for attach in self.pool.get('ir.attachment').name_get(cr, uid, message['attachment_ids'], context=context)]
except (orm.except_orm, osv.except_osv): except (orm.except_orm, osv.except_osv):
attachment_ids = [] attachment_ids = []
# TDE note: should we send partner_ids ?
# TDE note: shouldn't we separated followers and other partners ? costly to compute maybe ,
try: try:
partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context) partner_ids = self.pool.get('res.partner').name_get(cr, uid, message['partner_ids'], context=context)
except (orm.except_orm, osv.except_osv): except (orm.except_orm, osv.except_osv):
@ -226,95 +248,106 @@ class mail_message(osv.Model):
'record_name': message['record_name'], 'record_name': message['record_name'],
'subject': message['subject'], 'subject': message['subject'],
'date': message['date'], 'date': message['date'],
'author_id': message['author_id'], 'author_id': author_id,
'is_author': message['author_id'] and message['author_id'][0] == uid, 'is_author': is_author,
# TDE note: is this useful ? to check
'partner_ids': partner_ids, 'partner_ids': partner_ids,
'parent_id': message['parent_id'] and message['parent_id'][0] or False, 'parent_id': False,
# TDE note: see with CHM about votes, how they are displayed (only number, or name_get ?)
# vote: should only use number of votes
'vote_nb': len(message['vote_user_ids']), 'vote_nb': len(message['vote_user_ids']),
'has_voted': has_voted, 'has_voted': has_voted,
'is_private': message['model'] and message['res_id'], 'is_private': is_private,
'is_favorite': is_favorite, 'is_favorite': is_favorite,
'to_read': message['to_read'], 'to_read': message['to_read'],
} }
def _message_read_expandable(self, cr, uid, message_list, read_messages, def _message_read_add_expandables(self, cr, uid, message_list, read_messages,
message_loaded_ids=[], domain=[], context=None, parent_id=False, limit=None): thread_level=0, message_loaded_ids=[], domain=[], parent_id=False, context=None, limit=None):
""" Create the expandable message for all parent message read """ Create expandables for message_read, to load new messages.
this function is used by message_read 1. get the expandable for new threads
if display is flat (thread_level == 0):
fetch message_ids < min(already displayed ids), because we
want a flat display, ordered by id
else:
fetch message_ids that are not childs of already displayed
messages
2. get the expandables for new messages inside threads if display
is not flat
for each thread header, search for its childs
for each hole in the child list based on message displayed,
create an expandable
:param list message_list: list of messages given by message_read to :param list message_list:list of message structure for the Chatter
which we have to add expandables widget to which expandables are added
:param dict read_messages: dict [id]: read result of the messages to :param dict read_messages: dict [id]: read result of the messages to
easily have access to their values, given their ID easily have access to their values, given their ID
:return bool: True
""" """
# sort for group items / TDE: move to message_read def _get_expandable(domain, message_nb, parent_id, id, model):
# result = sorted(result, key=lambda k: k['id']) return {
tree_not = [] 'domain': domain,
# expandable for not show message 'nb_messages': message_nb,
'type': 'expandable',
'parent_id': parent_id,
'id': id,
# TDE note: why do we need model sometimes, and sometimes not ???
'model': model,
}
# all_not_loaded_ids = []
id_list = sorted(read_messages.keys()) id_list = sorted(read_messages.keys())
if not id_list:
return message_list
# 1. get the expandable for new threads
if thread_level == 0:
exp_domain = domain + [('id', '<', min(message_loaded_ids + id_list))]
else:
exp_domain = domain + ['!', ('id', 'child_of', message_loaded_ids + id_list)]
ids = self.search(cr, uid, exp_domain, context=context, limit=1)
if ids:
message_list.append(_get_expandable(exp_domain, -1, parent_id, -1, None))
# 2. get the expandables for new messages inside threads if display is not flat
if thread_level == 0:
return True
for message_id in id_list: for message_id in id_list:
message = read_messages[message_id] message = read_messages[message_id]
# message is not a thread header (has a parent_id)
# TDE note: parent_id is false is there is a parent we can not see -> ok
if message.get('parent_id'):
continue
# TDE note: check search is correctly implemented in mail.message # TDE note: check search is correctly implemented in mail.message
not_loaded_ids = self.search(cr, uid, [ not_loaded_ids = self.search(cr, uid, [
('parent_id', '=', message['id']), ('id', 'child_of', message['id']),
('id', 'not in', message_loaded_ids), ('id', 'not in', message_loaded_ids),
], context=context, limit=self._message_read_more_limit) ], context=context, limit=self._message_read_more_limit)
# group childs not read if not not_loaded_ids:
id_min = None continue
id_max = None
nb = 0
# all_not_loaded_ids += not_loaded_ids
# group childs not read
id_min, id_max, nb = max(not_loaded_ids), 0, 0
for not_loaded_id in not_loaded_ids: for not_loaded_id in not_loaded_ids:
if not read_messages.get(not_loaded_id): if not read_messages.get(not_loaded_id):
nb += 1 nb += 1
if id_min == None or id_min > not_loaded_id: if id_min > not_loaded_id:
id_min = not_loaded_id id_min = not_loaded_id
if id_max == None or id_max < not_loaded_id: if id_max < not_loaded_id:
id_max = not_loaded_id id_max = not_loaded_id
tree_not.append(not_loaded_id) elif nb > 0:
exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model')))
id_min, id_max, nb = max(not_loaded_ids), 0, 0
else: else:
if nb > 0: id_min, id_max, nb = max(not_loaded_ids), 0, 0
message_list.append({
'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', message_id)],
'nb_messages': nb,
'type': 'expandable',
'parent_id': message_id,
'id': id_min,
'model': message['model']
})
id_min = None
id_max = None
nb = 0
if nb > 0: if nb > 0:
message_list.append({ exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)]
'domain': [('id', '>=', id_min), ('id', '<=', id_max), ('parent_id', '=', message_id)], message_list.append(_get_expandable(exp_domain, nb, message_id, id_min, message.get('model')))
'nb_messages': nb,
'type': 'expandable',
'parent_id': message_id,
'id': id_min,
'model': message['model'],
})
for msg_id in read_messages.keys() + tree_not: # message_loaded_ids = list(set(message_loaded_ids + read_messages.keys() + all_not_loaded_ids))
message_loaded_ids.append(msg_id)
# expandable for limit max return True
ids = self.search(cr, uid, domain + [('id', 'not in', message_loaded_ids)], context=context, limit=1)
if len(ids) > 0:
message_list.append({
'domain': domain,
'nb_messages': 0,
'type': 'expandable',
'parent_id': parent_id,
'id': -1,
'max_limit': True,
})
return message_list
def _get_parent(self, cr, uid, message, context=None): def _get_parent(self, cr, uid, message, context=None):
""" Tools method that tries to get the parent of a mail.message. If """ Tools method that tries to get the parent of a mail.message. If
@ -331,57 +364,70 @@ class mail_message(osv.Model):
except (orm.except_orm, osv.except_osv): except (orm.except_orm, osv.except_osv):
return False return False
def message_read(self, cr, uid, ids=False, domain=[], message_loaded_ids=[], context=None, parent_id=False, limit=None): def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None, thread_level=0, context=None, parent_id=False, limit=None):
""" Read messages from mail.message, and get back a structured tree """ Read messages from mail.message, and get back a list of structured
of messages to be displayed as discussion threads. If IDs is set, messages to be displayed as discussion threads. If IDs is set,
fetch these records. Otherwise use the domain to fetch messages. fetch these records. Otherwise use the domain to fetch messages.
After having fetch messages, their parents & child will be added to obtain After having fetch messages, their ancestors will be added to obtain
well formed threads. well formed threads, if uid has access to them.
TDE note: update this comment after final method implementation After reading the messages, expandable messages are added in the
message list (see ``_message_read_add_expandables``). It consists
in messages holding the 'read more' data: number of messages to
read, domain to apply.
:param domain: optional domain for searching ids :param list ids: optional IDs to fetch
:param limit: number of messages to fetch :param list domain: optional domain for searching ids if ids not set
:param parent_id: if parent_id reached, stop searching for :param list message_unload_ids: optional ids we do not want to fetch,
further parents because i.e. they are already displayed somewhere
:return list: list of trees of messages :param int parent_id: context of parent_id
- if parent_id reached when adding ancestors, stop going further
in the ancestor search
- if set in flat mode, ancestor_id is set to parent_id
:param int limit: number of messages to fetch, before adding the
ancestors and expandables
:return list: list of message structure for the Chatter widget
""" """
if message_loaded_ids: # print 'message_read', ids, domain, message_unload_ids, thread_level, context, parent_id, limit
domain += [('id', 'not in', message_loaded_ids)] assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level
domain = domain if domain is not None else []
message_unload_ids = message_unload_ids if message_unload_ids is not None else []
if message_unload_ids:
domain += [('id', 'not in', message_unload_ids)]
limit = limit or self._message_read_limit limit = limit or self._message_read_limit
read_messages = {} read_messages = {}
message_list = [] message_list = []
# specific IDs given: fetch those ids and return directly the message list # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to
if ids: if ids is None:
for message in self.read(cr, uid, ids, self._message_read_fields, context=context): ids = self.search(cr, uid, domain, context=context, limit=limit)
message_list.append(self._message_get_dict(cr, uid, message, context=context))
message_list = sorted(message_list, key=lambda k: k['id'])
return message_list
# TDE FIXME: check access rights on search are implemented for mail.message
# fetch messages according to the domain, add their parents if uid has access to
ids = self.search(cr, uid, domain, context=context, limit=limit)
for message in self.read(cr, uid, ids, self._message_read_fields, context=context): for message in self.read(cr, uid, ids, self._message_read_fields, context=context):
# if not in tree and not in message_loded list message_id = message['id']
if not read_messages.get(message.get('id')) and message.get('id') not in message_loaded_ids:
read_messages[message.get('id')] = message # if not in tree and not in message_loaded list
if not message_id in read_messages and not message_id in message_unload_ids:
read_messages[message_id] = message
message_list.append(self._message_get_dict(cr, uid, message, context=context)) message_list.append(self._message_get_dict(cr, uid, message, context=context))
# get all parented message if the user have the access # get the older ancestor the user can read, update its ancestor field
if not thread_level:
message_list[-1]['parent_id'] = parent_id
continue
parent = self._get_parent(cr, uid, message, context=context) parent = self._get_parent(cr, uid, message, context=context)
while parent and parent.get('id') != parent_id: while parent and parent.get('id') != parent_id:
if not read_messages.get(parent.get('id')) and parent.get('id') not in message_loaded_ids: message_list[-1]['parent_id'] = parent.get('id')
read_messages[parent.get('id')] = parent message = parent
message_list.append(self._message_get_dict(cr, uid, parent, context=context)) parent = self._get_parent(cr, uid, message, context=context)
parent = self._get_parent(cr, uid, parent, context=context) # if in thread: add its ancestor to the list of messages
if not message['id'] in read_messages and not message['id'] in message_unload_ids:
read_messages[message['id']] = message
message_list.append(self._message_get_dict(cr, uid, message, context=context))
# get the child expandable messages for the tree # get the child expandable messages for the tree
message_list = sorted(message_list, key=lambda k: k['id']) message_list = sorted(message_list, key=lambda k: k['id'])
message_list = self._message_read_expandable(cr, uid, message_list, read_messages, self._message_read_add_expandables(cr, uid, message_list, read_messages, thread_level=thread_level,
message_loaded_ids=message_loaded_ids, domain=domain, context=context, parent_id=parent_id, limit=limit) message_loaded_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context, limit=limit)
# message_list = sorted(message_list, key=lambda k: k['id'])
return message_list return message_list
# TDE Note: do we need this ? # TDE Note: do we need this ?
@ -394,7 +440,7 @@ class mail_message(osv.Model):
# return attachment_list # return attachment_list
#------------------------------------------------------ #------------------------------------------------------
# Email api # mail_message internals
#------------------------------------------------------ #------------------------------------------------------
def init(self, cr): def init(self, cr):
@ -402,23 +448,75 @@ class mail_message(osv.Model):
if not cr.fetchone(): if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""") cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
context=None, count=False, access_rights_uid=None):
""" Override that adds specific access rights of mail.message, to remove
ids uid could not see according to our custom rules. Please refer
to check_access_rule for more details about those rules.
After having received ids of a classic search, keep only:
- if author_id == pid, uid is the author, OR
- a notification (id, pid) exists, uid has been notified, OR
- uid have read access to the related document is model, res_id
- otherwise: remove the id
"""
# Rules do not apply to administrator
# print '_search', uid, args
if uid == SUPERUSER_ID:
return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=count, access_rights_uid=access_rights_uid)
# Perform a super with count as False, to have the ids, not a counter
ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=False, access_rights_uid=access_rights_uid)
if not ids and count:
return 0
elif not ids:
return ids
pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'])['partner_id'][0]
author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
model_ids = {}
messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
for message in messages:
if message.get('author_id') and message.get('author_id')[0] == pid:
author_ids.add(message.get('id'))
elif pid in message.get('notified_partner_ids'):
partner_ids.add(message.get('id'))
elif message.get('model') and message.get('res_id'):
model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
model_access_obj = self.pool.get('ir.model.access')
for doc_model, doc_dict in model_ids.iteritems():
if not model_access_obj.check(cr, uid, doc_model, 'read', False):
continue
doc_ids = doc_dict.keys()
allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
allowed_ids |= set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
final_ids = author_ids | partner_ids | allowed_ids
if count:
return len(final_ids)
else:
return list(final_ids)
def check_access_rule(self, cr, uid, ids, operation, context=None): def check_access_rule(self, cr, uid, ids, operation, context=None):
""" Access rules of mail.message: """ Access rules of mail.message:
- read: if - read: if
- notification exist (I receive pushed message) OR - author_id == pid, uid is the author, OR
- author_id = pid (I am the author) OR - mail_notification (id, pid) exists, uid has been notified, OR
- I can read the related document if res_model, res_id - uid have read access to the related document if model, res_id
- Otherwise: raise - otherwise: raise
- create: if - create: if
- I am in the document message_follower_ids OR - no model, no res_id, I create a private message
- I can write on the related document if res_model, res_id OR - pid in message_follower_ids if model, res_id OR
- I create a private message (no model, no res_id) - uid have write access on the related document if model, res_id, OR
- Otherwise: raise - otherwise: raise
- write: if - write: if
- I can write on the related document if res_model, res_id - uid has write access on the related document if model, res_id
- Otherwise: raise - Otherwise: raise
- unlink: if - unlink: if
- I can write on the related document if res_model, res_id - uid has write access on the related document if model, res_id
- Otherwise: raise - Otherwise: raise
""" """
if uid == SUPERUSER_ID: if uid == SUPERUSER_ID:
@ -428,15 +526,25 @@ class mail_message(osv.Model):
partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0] partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
# Read mail_message.ids to have their values # Read mail_message.ids to have their values
model_record_ids = {}
message_values = dict.fromkeys(ids) message_values = dict.fromkeys(ids)
model_record_ids = {}
cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,)) cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
for id, rmod, rid, author_id in cr.fetchall(): for id, rmod, rid, author_id in cr.fetchall():
message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id} message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
if rmod: if rmod:
model_record_ids.setdefault(rmod, set()).add(rid) model_record_ids.setdefault(rmod, dict()).setdefault(rid, set()).add(id)
# Read: Check for received notifications -> could become an ir.rule, but not till we do not have a many2one variable field # Author condition, for read and create (private message) -> could become an ir.rule, but not till we do not have a many2one variable field
if operation == 'read':
author_ids = [mid for mid, message in message_values.iteritems()
if message.get('author_id') and message.get('author_id') == partner_id]
elif operation == 'create':
author_ids = [mid for mid, message in message_values.iteritems()
if not message.get('model') and not message.get('res_id')]
else:
author_ids = []
# Notification condition, for read (check for received notifications and create (in message_follower_ids)) -> could become an ir.rule, but not till we do not have a many2one variable field
if operation == 'read': if operation == 'read':
not_obj = self.pool.get('mail.notification') not_obj = self.pool.get('mail.notification')
not_ids = not_obj.search(cr, SUPERUSER_ID, [ not_ids = not_obj.search(cr, SUPERUSER_ID, [
@ -444,38 +552,24 @@ class mail_message(osv.Model):
('message_id', 'in', ids), ('message_id', 'in', ids),
], context=context) ], context=context)
notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)] notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
else:
notified_ids = []
# Read: Check messages you are author -> could become an ir.rule, but not till we do not have a many2one variable field
if operation == 'read':
author_ids = [mid for mid, message in message_values.iteritems()
if message.get('author_id') and message.get('author_id') == partner_id]
# Create: Check messages you create that are private messages -> ir.rule ?
elif operation == 'create': elif operation == 'create':
author_ids = [mid for mid, message in message_values.iteritems() notified_ids = []
if not message.get('model') and not message.get('res_id')] for doc_model, doc_dict in model_record_ids.items():
else:
author_ids = []
# Create: Check message_follower_ids
if operation == 'create':
doc_follower_ids = []
for model, mids in model_record_ids.items():
fol_obj = self.pool.get('mail.followers') fol_obj = self.pool.get('mail.followers')
fol_ids = fol_obj.search(cr, SUPERUSER_ID, [ fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
('res_model', '=', model), ('res_model', '=', doc_model),
('res_id', 'in', list(mids)), ('res_id', 'in', list(doc_dict.keys())),
('partner_id', '=', partner_id), ('partner_id', '=', partner_id),
], context=context) ], context=context)
fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)] fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
doc_follower_ids += [mid for mid, message in message_values.iteritems() notified_ids += [mid for mid, message in message_values.iteritems()
if message.get('res_model') == model and message.get('res_id') in fol_mids] if message.get('res_model') == doc_model and message.get('res_id') in fol_mids]
else: else:
doc_follower_ids = [] notified_ids = []
# Calculate remaining ids, and related model/res_ids # Calculate remaining ids, and related model/res_ids
model_record_ids = {} model_record_ids = {}
other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids)) other_ids = set(ids).difference(set(author_ids), set(notified_ids))
for id in other_ids: for id in other_ids:
if message_values[id]['res_model']: if message_values[id]['res_model']:
model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id']) model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
@ -495,7 +589,7 @@ class mail_message(osv.Model):
if message.get('res_model') == model and message.get('res_id') in mids] if message.get('res_model') == model and message.get('res_id') in mids]
# Calculate remaining ids: if not void, raise an error # Calculate remaining ids: if not void, raise an error
other_ids = set(ids).difference(set(notified_ids), set(author_ids), set(doc_follower_ids), set(document_related_ids)) other_ids = other_ids - set(document_related_ids)
if not other_ids: if not other_ids:
return return
raise orm.except_orm(_('Access Denied'), raise orm.except_orm(_('Access Denied'),
@ -529,50 +623,123 @@ class mail_message(osv.Model):
self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context) self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
return super(mail_message, self).unlink(cr, uid, ids, context=context) return super(mail_message, self).unlink(cr, uid, ids, context=context)
def _notify_followers(self, cr, uid, newid, message, context=None): def copy(self, cr, uid, id, default=None, context=None):
""" Add the related record followers to the destination partner_ids. """ Overridden to avoid duplicating fields that are unique to each email """
if default is None:
default = {}
default.update(message_id=False, headers=False)
return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
#------------------------------------------------------
# Messaging API
#------------------------------------------------------
# TDE note: this code is not used currently, will be improved in a future merge, when quoted context
# will be added to email send for notifications. Currently only WIP.
MAIL_TEMPLATE = """<div>
% if message:
${display_message(message)}
% endif
% for ctx_msg in context_messages:
${display_message(ctx_msg)}
% endfor
% if add_expandable:
${display_expandable()}
% endif
${display_message(header_message)}
</div>
<%def name="display_message(message)">
<div>
Subject: ${message.subject}<br />
Body: ${message.body}
</div>
</%def>
<%def name="display_expandable()">
<div>This is an expandable.</div>
</%def>
"""
def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
""" """
partners_to_notify = set([]) 1. message.parent_id = False: new thread, no quote_context
# message has no subtype_id: pure log message -> no partners, no one notified 2. get the lasts messages in the thread before message
if not message.subtype_id: 3. get the message header
message.write({'partner_ids': [5]}) 4. add an expandable between them
return True
# all partner_ids of the mail.message have to be notified :param dict quote_context: options for quoting
if message.partner_ids: :return string: html quote
partners_to_notify |= set(partner.id for partner in message.partner_ids) """
# all followers of the mail.message document have to be added as partners and notified add_expandable = False
if message.model and message.res_id:
fol_obj = self.pool.get("mail.followers") message = self.browse(cr, uid, id, context=context)
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) if not message.parent_id:
fol_objs = fol_obj.browse(cr, uid, fol_ids, context=context) return ''
extra_notified = set(fol.partner_id.id for fol in fol_objs) context_ids = self.search(cr, uid, [
missing_notified = extra_notified - partners_to_notify ('parent_id', '=', message.parent_id.id),
if missing_notified: ('id', '<', message.id),
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, p_id) for p_id in missing_notified]}, context=context) ], limit=limit, context=context)
if len(context_ids) >= limit:
add_expandable = True
context_ids = context_ids[0:-1]
context_ids.append(message.parent_id.id)
context_messages = self.browse(cr, uid, context_ids, context=context)
header_message = context_messages.pop()
try:
if not add_original:
message = False
result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
context_messages=context_messages,
header_message=header_message,
add_expandable=add_expandable,
# context kw would clash with mako internals
ctx=context,
format_exceptions=True)
result = result.strip()
return result
except Exception:
_logger.exception("failed to render mako template for quoting message")
return ''
return result
def _notify(self, cr, uid, newid, context=None): def _notify(self, cr, uid, newid, context=None):
""" Add the related record followers to the destination partner_ids if is not a private message. """ Add the related record followers to the destination partner_ids if is not a private message.
Call mail_notification.notify to manage the email sending Call mail_notification.notify to manage the email sending
""" """
message = self.browse(cr, uid, newid, context=context) message = self.read(cr, uid, newid, ['model', 'res_id', 'author_id', 'subtype_id', 'partner_ids'], context=context)
if message.model and message.res_id:
self._notify_followers(cr, uid, newid, message, context=context)
# add myself if I wrote on my wall, otherwise remove myself author partners_to_notify = set([])
if ((message.model == "res.partner" and message.res_id == message.author_id.id)): # message has no subtype_id: pure log message -> no partners, no one notified
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(4, message.author_id.id)]}, context=context) if not message.get('subtype_id'):
else: return True
self.write(cr, SUPERUSER_ID, [newid], {'partner_ids': [(3, message.author_id.id)]}, context=context) # all partner_ids of the mail.message have to be notified
if message.get('partner_ids'):
partners_to_notify |= set(message.get('partner_ids'))
# all followers of the mail.message document have to be added as partners and notified
if message.get('model') and message.get('res_id'):
fol_obj = self.pool.get("mail.followers")
fol_ids = fol_obj.search(cr, uid, [
('res_model', '=', message.get('model')),
('res_id', '=', message.get('res_id')),
('subtype_ids', 'in', message.get('subtype_id')[0])
], context=context)
fol_objs = fol_obj.read(cr, uid, fol_ids, ['partner_id'], context=context)
partners_to_notify |= set(fol['partner_id'][0] for fol in fol_objs)
# when writing to a wall
if message.get('author_id') and message.get('model') == "res.partner" and message.get('res_id') == message.get('author_id')[0]:
partners_to_notify |= set([message.get('author_id')[0]])
elif message.get('author_id'):
partners_to_notify = partners_to_notify - set([message.get('author_id')[0]])
if partners_to_notify:
self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p_id) for p_id in partners_to_notify]}, context=context)
self.pool.get('mail.notification')._notify(cr, uid, newid, 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"""
if default is None:
default = {}
default.update(message_id=False, headers=False)
return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
#------------------------------------------------------ #------------------------------------------------------
# Tools # Tools
#------------------------------------------------------ #------------------------------------------------------

View File

@ -29,6 +29,7 @@
<group> <group>
<field name="subject"/> <field name="subject"/>
<field name="author_id"/> <field name="author_id"/>
<field name="email_from"/>
<field name="date"/> <field name="date"/>
<field name="type"/> <field name="type"/>
<field name="subtype_id"/> <field name="subtype_id"/>
@ -38,6 +39,7 @@
<field name="res_id"/> <field name="res_id"/>
<field name="parent_id"/> <field name="parent_id"/>
<field name="partner_ids" widget="many2many_tags"/> <field name="partner_ids" widget="many2many_tags"/>
<field name="notified_partner_ids" widget="many2many_tags"/>
</group> </group>
</group> </group>
<field name="body"/> <field name="body"/>
@ -63,6 +65,9 @@
<filter string="Comments" <filter string="Comments"
name="comments" help="Comments" name="comments" help="Comments"
domain="[('type', '=', 'comment')]"/> domain="[('type', '=', 'comment')]"/>
<filter string="Has attachments"
name="attachments"
domain="[('attachment_ids', '!=', False)]"/>
<filter string="Notifications" <filter string="Notifications"
name="notifications" help="Notifications" name="notifications" help="Notifications"
domain="[('type', '=', 'notification')]"/> domain="[('type', '=', 'notification')]"/>

View File

@ -554,7 +554,11 @@ class mail_thread(osv.AbstractModel):
('file2', 'bytes')} ('file2', 'bytes')}
} }
""" """
msg_dict = {} msg_dict = {
'type': 'email',
'subtype': 'mail.mt_comment',
'author_id': False,
}
if not isinstance(message, Message): if not isinstance(message, Message):
if isinstance(message, unicode): if isinstance(message, unicode):
# Warning: message_from_string doesn't always work correctly on unicode, # Warning: message_from_string doesn't always work correctly on unicode,
@ -572,7 +576,7 @@ class mail_thread(osv.AbstractModel):
if 'Subject' in message: if 'Subject' in message:
msg_dict['subject'] = decode(message.get('Subject')) msg_dict['subject'] = decode(message.get('Subject'))
# Envelope fields not stored in mail.message but made available for message_new() # Envelope fields not stored in mail.message but made available for message_new()
msg_dict['from'] = decode(message.get('from')) msg_dict['from'] = decode(message.get('from'))
msg_dict['to'] = decode(message.get('to')) msg_dict['to'] = decode(message.get('to'))
msg_dict['cc'] = decode(message.get('cc')) msg_dict['cc'] = decode(message.get('cc'))
@ -581,6 +585,8 @@ class mail_thread(osv.AbstractModel):
author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context) author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
if author_ids: if author_ids:
msg_dict['author_id'] = author_ids[0] msg_dict['author_id'] = author_ids[0]
else:
msg_dict['email_from'] = message.get('from')
partner_ids = self._message_find_partners(cr, uid, message, ['From', 'To', 'Cc'], context=context) partner_ids = self._message_find_partners(cr, uid, message, ['From', 'To', 'Cc'], context=context)
msg_dict['partner_ids'] = partner_ids msg_dict['partner_ids'] = partner_ids
@ -670,6 +676,18 @@ class mail_thread(osv.AbstractModel):
if self._mail_flat_thread and not parent_id and thread_id: if self._mail_flat_thread and not parent_id and thread_id:
message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model)], context=context, order="id ASC", limit=1) message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model)], context=context, order="id ASC", limit=1)
parent_id = message_ids and message_ids[0] or False parent_id = message_ids and message_ids[0] or False
# we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
elif parent_id:
message_ids = mail_message.search(cr, SUPERUSER_ID, [('id', '=', parent_id), ('parent_id', '!=', False)], context=context)
# avoid loops when finding ancestors
processed_list = []
if message_ids:
_counter, _counter_max = 0, 200
message = mail_message.browse(cr, SUPERUSER_ID, message_ids[0], context=context)
while (message.parent_id and message.parent_id.id not in processed_list):
processed_list.append(message.parent_id.id)
message = message.parent_id
parent_id = message.id
values = kwargs values = kwargs
values.update({ values.update({
@ -689,27 +707,28 @@ class mail_thread(osv.AbstractModel):
return mail_message.create(cr, uid, values, context=context) return mail_message.create(cr, uid, values, context=context)
def message_post_api(self, cr, uid, thread_id, body='', subject=False, type='notification', def message_post_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False, attachment_ids=None, context=None):
subtype=None, parent_id=False, attachments=None, context=None, **kwargs): """ Wrapper on message_post, used only in Chatter (JS). The purpose is
# TDE FIXME: body is plaintext: convert it into html to handle attachments.
# when writing on res.partner, without specific thread_id -> redirect to the user's partner # TDE FIXME: body is plaintext: convert it into html
if self._name == 'res.partner' and not thread_id: """
thread_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0] new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type='comment',
subtype='mail.mt_comment', parent_id=parent_id, context=context)
new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=type, # HACK FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
subtype=subtype, parent_id=parent_id, context=context) if attachment_ids:
# Chatter: attachments linked to the document (not done JS-side), load the message
if attachments:
ir_attachment = self.pool.get('ir.attachment') ir_attachment = self.pool.get('ir.attachment')
mail_message = self.pool.get('mail.message') mail_message = self.pool.get('mail.message')
attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [('res_model', '=', 'mail.message'), ('res_id', '=', 0), ('create_uid', '=', uid), ('id', 'in', attachments)], context=context) filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
if attachment_ids: ('res_model', '=', 'mail.compose.message'),
('res_id', '=', 0),
('create_uid', '=', uid),
('id', 'in', attachment_ids)], context=context)
if filtered_attachment_ids:
ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': self._name, 'res_id': thread_id}, context=context) ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': self._name, 'res_id': thread_id}, context=context)
mail_message.write(cr, SUPERUSER_ID, [new_message_id], {'attachment_ids': [(6, 0, [pid for pid in attachment_ids])]}, context=context) mail_message.write(cr, SUPERUSER_ID, [new_message_id], {'attachment_ids': [(6, 0, [pid for pid in attachment_ids])]}, context=context)
new_message = self.pool.get('mail.message').message_read(cr, uid, [new_message_id], context=context) return new_message_id
return new_message
#------------------------------------------------------ #------------------------------------------------------
# Followers API # Followers API

View File

@ -39,7 +39,7 @@
<!-- MENU --> <!-- MENU -->
<!-- Top menu item --> <!-- Top menu item -->
<menuitem name="Mails" <menuitem name="Emails"
id="mail.mail_feeds_main" id="mail.mail_feeds_main"
groups="base.group_user" groups="base.group_user"
sequence="10"/> sequence="10"/>

View File

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

View File

@ -111,8 +111,7 @@ class res_users(osv.Model):
alias_pool.unlink(cr, uid, alias_ids, context=context) alias_pool.unlink(cr, uid, alias_ids, context=context)
return res return res
def message_post_api(self, cr, uid, thread_id, body='', subject=False, type='notification', def message_post_api(self, cr, uid, thread_id, body='', subject=False, parent_id=False, attachment_ids=None, context=None):
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner. """ Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the This is done because when giving the context of Chatter on the
various mailboxes, we do not have access to the current partner_id. various mailboxes, we do not have access to the current partner_id.
@ -124,7 +123,7 @@ class res_users(osv.Model):
thread_id = thread_id[0] thread_id = thread_id[0]
partner_id = self.pool.get('res.users').read(cr, uid, thread_id, ['partner_id'], context=context)['partner_id'][0] partner_id = self.pool.get('res.users').read(cr, uid, thread_id, ['partner_id'], context=context)['partner_id'][0]
return self.pool.get('res.partner').message_post_api(cr, uid, partner_id, body=body, subject=subject, return self.pool.get('res.partner').message_post_api(cr, uid, partner_id, body=body, subject=subject,
type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs) parent_id=parent_id, attachment_ids=attachment_ids, context=context)
def message_post(self, cr, uid, thread_id, context=None, **kwargs): def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner. """ Redirect the posting of message on res.users to the related partner.

View File

@ -2,6 +2,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_message_all,mail.message.all,model_mail_message,,1,0,1,0 access_mail_message_all,mail.message.all,model_mail_message,,1,0,1,0
access_mail_message_group_user,mail.message.group.user,model_mail_message,base.group_user,1,1,1,1 access_mail_message_group_user,mail.message.group.user,model_mail_message,base.group_user,1,1,1,1
access_mail_mail_all,mail.mail.all,model_mail_mail,,0,0,1,0 access_mail_mail_all,mail.mail.all,model_mail_mail,,0,0,1,0
access_mail_mail_user,mail.mail,model_mail_mail,base.group_user,1,1,1,0
access_mail_mail_system,mail.mail.system,model_mail_mail,base.group_system,1,1,1,1 access_mail_mail_system,mail.mail.system,model_mail_mail,base.group_system,1,1,1,1
access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0 access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0
access_mail_followers_system,mail.followers.system,model_mail_followers,base.group_system,1,1,1,1 access_mail_followers_system,mail.followers.system,model_mail_followers,base.group_system,1,1,1,1
@ -12,6 +13,6 @@ 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_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_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_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_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0
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 access_mail_vote_all,mail.vote.all,model_mail_vote,,1,1,1,1
access_mail_favorite_all,mail.favorite.all,model_mail_favorite,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mail_message_all mail.message.all model_mail_message 1 0 1 0
3 access_mail_message_group_user mail.message.group.user model_mail_message base.group_user 1 1 1 1
4 access_mail_mail_all mail.mail.all model_mail_mail 0 0 1 0
5 access_mail_mail_user mail.mail model_mail_mail base.group_user 1 1 1 0
6 access_mail_mail_system mail.mail.system model_mail_mail base.group_system 1 1 1 1
7 access_mail_followers_all mail.followers.all model_mail_followers 1 0 0 0
8 access_mail_followers_system mail.followers.system model_mail_followers base.group_system 1 1 1 1
13 access_mail_alias_all mail.alias.all model_mail_alias 1 0 0 0
14 access_mail_alias_user mail.alias model_mail_alias base.group_user 1 1 1 0
15 access_mail_alias_system mail.alias model_mail_alias base.group_system 1 1 1 1
16 access_mail_message_subtype access_mail_message_subtype_all mail.message.subtype mail.message.subtype.all model_mail_message_subtype 1 1 0 1 0 1 0
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
18 access_mail_favorite_all mail.favorite.all model_mail_favorite 1 1 1 1

View File

@ -1,439 +1,382 @@
/* ------------------------------------------------------------ */
/* Reset because of ugly display of end of August
/* ------------------------------------------------------------ */
.openerp .oe_mail_wall ul, .openerp .oe_mail_wall li { /* ------------ MAIL WIDGET --------------- */
list-style-type: none; .openerp .oe_mail, .openerp .oe_mail *{
padding: 0; box-sizing: border-box;
margin: 0;
} }
.openerp .oe_mail {
.openerp .oe_chatter ul, .openerp .oe_chatter li { display: block;
list-style-type: none;
padding: 0;
margin: 0;
}
/* ------------------------------------------------------------ */
/* Wall
/* ------------------------------------------------------------ */
.openerp div.oe_mail_wall {
overflow: auto;
padding: 0;
background: white;
}
.openerp div.oe_mail_wall div.oe_mail_wall_aside {
margin-left: 565px;
margin: 8px;
}
.openerp div.oe_mail_wall ul.oe_mail_wall_threads {
float: left;
width: 560px;
margin: 8px;
list-style-type: none;
}
/* ------------------------------------------------------------ */
/* Followers
/* ------------------------------------------------------------ */
.openerp div.oe_mail_recthread_aside h4 {
display: inline-block;
}
.openerp div.oe_mail_recthread_aside button {
position: relative; position: relative;
margin: 0px;
} }
.openerp div.oe_mail_recthread_aside label, .openerp .oe_mail .oe_thread{
.openerp div.oe_mail_recthread_aside input { margin-left: 32px;
cursor:pointer; }
.openerp .oe_mail > .oe_thread{
margin-left: 0px;
} }
/* Specific display of threads in the wall */ /* ---------------- MESSAGES ------------------ */
/* ------------------------------------------------------------ */
.openerp ul.oe_mail_wall_threads .oe_msg_content textarea.oe_mail_compose_textarea { .openerp .oe_mail .oe_msg{
width: 434px;
height: 30px;
padding: 4px;
}
.openerp li.oe_mail_wall_thread:first .oe_msg_notification {
border-top: 0;
}
.openerp div.oe_thread_placeholder img {
width: 28px;
height: 28px;
}
.openerp div.oe_thread_placeholder div.oe_msg_content {
width: 440px;
}
/* ------------------------------------------------------------ */
/* RecordThread
/* ------------------------------------------------------------ */
.openerp .oe_form div.oe_chatter {
overflow: auto;
}
.openerp .oe_mail_record_wall {
margin: auto;
width: 560px;
}
.openerp .oe_mail_record_wall > .oe_mail_wall_threads {
float: left;
}
.openerp div.oe_mail_recthread_aside {
float: right;
width: 250px;
}
.openerp div.oe_mail_recthread_actions {
margin-bottom: 8px;
}
.openerp div.oe_mail_recthread_actions button {
width: 120px;
}
.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));
background-image: -webkit-linear-gradient(top, #8a89ba, #807fb4);
background-image: -moz-linear-gradient(top, #8a89ba, #807fb4);
background-image: -ms-linear-gradient(top, #8a89ba, #807fb4);
background-image: -o-linear-gradient(top, #8a89ba, #807fb4);
background-image: linear-gradient(to bottom, #8a89ba, #807fb4);
}
.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));
background-image: -webkit-linear-gradient(top, #dc5f59, #b33630);
background-image: -moz-linear-gradient(top, #dc5f59, #b33630);
background-image: -ms-linear-gradient(top, #dc5f59, #b33630);
background-image: -o-linear-gradient(top, #dc5f59, #b33630);
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;
}
/* ------------------------------------------------------------ */
/* subtypes
/* ------------------------------------------------------------ */
.openerp .oe_mail_subtypes {
display:inline-block;
position: relative; position: relative;
z-index: 5; background: #F4F5FA;
border-radius: 2px;
margin-bottom: 2px;
min-height: 42px;
border: solid 1px rgba(0,0,0,0.03);
} }
.openerp .oe_mail_subtypes .oe_recthread_subtypes { .openerp .oe_mail .oe_msg .oe_msg_left{
background: #fff;
padding: 2px;
border: 1px solid #aaaaaa;
border-top: 0px;
position: absolute; position: absolute;
z-index: 2; left:0; top: 0; bottom: 0; width: 40px;
overflow: hidden;
} }
.openerp .oe_mail_subtypes.oe_mouseout .oe_recthread_subtypes { .openerp .oe_mail .oe_msg .oe_msg_icon{
display: none; width: 32px;
margin: 4px;
border-radius: 2px;
} }
.openerp .oe_mail_subtypes.oe_mouseover .oe_recthread_subtypes { .openerp .oe_mail .oe_msg .oe_msg_center{
display: block;
}
/* ------------------------------------------------------------ */
/* Thread
/* ------------------------------------------------------------ */
.openerp div.oe_mail_thread_action {
white-space: normal;
padding: 8px;
z-index:5;
background: #fff;
}
.openerp div.oe_mail_thread_action:after {
content: "";
display: block;
clear: both;
}
/* default textarea (oe_mail_compose_textarea), and body textarea for compose form view */
.openerp .oe_msg_content textarea.oe_mail_compose_textarea:focus,
.openerp .oe_msg_content div.oe_mail_compose_message_body textarea:focus {
outline: 0;
border-color: rgba(82, 168, 236, 0.8);
-moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
-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_msg_vote{
vertical-align: bottom;
}
.openerp button.oe_mail_starbox{
background: #ff0000;
}
.openerp button.oe_mail_starbox.oe_stared{
background: #00FF00;
}
.openerp div.oe_mail_thread_display {
white-space: normal;
}
.openerp div.oe_thread_placeholder {
margin-left: 66px;
}
.openerp li.oe_mail_thread_msg {
width: 560px;
}
.openerp div.oe_thread_placeholder li.oe_mail_thread_msg:last-child {
margin-bottom: 8px;
}
.openerp div.oe_mail_thread_more {
display: none;
border-bottom: 1px solid #D2D9E7;
}
.openerp li.oe_mail_thread_msg:after {
content: "";
display: block;
clear: both;
}
.openerp li.oe_mail_thread_msg.oe_mail_read,
.openerp li.oe_mail_thread_msg.oe_mail_read div {
border-left: #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.oe_mail_unread>div>ul>li.oe_unread,
.openerp li.oe_mail_thread_msg.oe_mail_read>div>ul>li.oe_read {
display: none;
}
.openerp li.oe_mail_thread_msg > div:after {
content: "";
display: block;
clear: both;
}
.openerp div.oe_mail_msg {
padding: 0;
margin: 0 0 4px 0;
}
.openerp .oe_msg_notification,
.openerp .oe_msg_expandable,
.openerp .oe_msg_comment,
.openerp .oe_msg_email {
padding: 8px;
background: white;
position: relative; position: relative;
}
.openerp .oe_msg_notification:after,
.openerp .oe_msg_comment:after,
.openerp .oe_msg_email:after {
content: "";
display: block; display: block;
clear: both; margin-left: 40px;
}
.openerp .oe_mail .oe_msg .oe_msg_footer{
padding-left: 4px;
overflow: hidden;
opacity:0.8;
-webkit-transition: opacity 0.2s linear;
}
.openerp .oe_mail .oe_msg .oe_msg_content{
display: block;
overflow: hidden;
padding: 4px;
padding-bottom:1px;
}
.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_title{
font-size: 13px;
margin-bottom: 0px;
margin-top: 2px;
} }
.openerp div.oe_msg_content { /* a) Indented Messages */
float: left;
.openerp .oe_mail .oe_msg_indented{
background: #FFF;
border: none;
margin-bottom:0px;
min-height:38px;
}
.openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_icon{
width:32px;
margin:2px;
border-radius:2px;
}
.openerp .oe_mail .oe_msg .oe_subtle{
color: #B7B7D5;
}
.openerp .oe_mail .oe_msg_indented .oe_msg_center{
margin-left:34px;
}
.openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_content{
padding-top:2px;
}
/* b) Votes (likes) */
.openerp .oe_mail .oe_mail_vote_count{
display: inline;
position: relative; position: relative;
width: 486px; background: #7C7BAD;
} color: white;
text-shadow: none;
.openerp div.oe_msg_content > li { border-radius: 3px;
float: left; margin: 0px;
padding-left: 3px;
padding-right: 18px;
margin-right: 3px; margin-right: 3px;
} }
.openerp .oe_mail .oe_mail_vote_count .oe_e{
.openerp .oe_msg_content:after { position: absolute;
content: ""; bottom: 1px;
display: block; right: 2px;
clear: both; font-size: 26px;
} }
.openerp .oe_chatter a { /* c) Message action icons */
cursor: pointer;
}
.openerp img.oe_mail_icon { .openerp .oe_mail .oe_msg.oe_msg_unread .oe_unread{
width: 50px; display:none;
height: 50px;
} }
.openerp .oe_mail .oe_msg.oe_msg_read .oe_read{
.openerp img.oe_mail_thumbnail { display:none;
width: 28px;
height: 28px;
margin: 4px;
} }
.openerp .oe_mail .oe_msg .oe_msg_icons{
.openerp img.oe_mail_frame { float: right;
margin-top: 4px;
margin-right: 8px;
margin-left: 8px;
height: 24px;
-webkit-user-select: none;
}
.openerp .oe_mail .oe_msg .oe_msg_icons span{
float:right;
width:24px;
height:24px;
line-height:24px;
text-align: center; text-align: center;
overflow: hidden; }
-moz-border-radius: 3px; .openerp .oe_mail .oe_msg .oe_msg_icons a {
-webkit-border-radius: 3px; text-decoration: none;
-o-border-radius: 3px; color: #FFF;
-ms-border-radius: 3px; text-shadow: 0px 1px #AAA,0px -1px #AAA, -1px 0px #AAA, 1px 0px #AAA, 0px 3px 3px rgba(0,0,0,0.1);
border-radius: 3px; -webkit-transition: all 0.2s linear;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); }
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); .openerp .oe_mail .oe_msg:hover .oe_msg_icons a{
-o-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); opacity: 1;
-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); -webkit-transition: all 0.1s linear;
clip: rect(5px, 40px, 45px, 0px); }
.openerp .oe_mail .oe_msg .oe_msg_icons .oe_star:hover a{
color: #FFF6C0;
text-shadow: 0px 1px #FFA162,0px -1px #FFA162, -1px 0px #FFA162, 1px 0px #FFA162, 0px 3px 3px rgba(0,0,0,0.1);
}
.openerp .oe_mail .oe_msg .oe_msg_icons .oe_star.oe_starred a{
color: #FFE41F;
text-shadow: 0px 1px #DF6200,0px -1px #DF6200, -1px 0px #DF6200, 1px 0px #DF6200, 0px 3px 3px rgba(0,0,0,0.1);
}
.openerp .oe_mail .oe_msg .oe_msg_icons .oe_reply:hover a{
color: #1fc0ff;
text-shadow: 0px 1px #184fc5,0px -1px #184fc5, -1px 0px #184fc5, 1px 0px #184fc5, 0px 3px 3px rgba(0,0,0,0.1);
}
.openerp .oe_mail .oe_msg .oe_msg_icons .oe_read:hover a{
color: #bbbaff;
text-shadow: 0px 1px #7c7bad,0px -1px #7c7bad, -1px 0px #7c7bad, 1px 0px #7c7bad, 0px 3px 3px rgba(0,0,0,0.1);
}
.openerp .oe_mail .oe_msg .oe_msg_icons .oe_unread:hover a{
color: #c2ff00;
text-shadow: 0px 1px #009441,0px -1px #009441, -1px 0px #009441, 1px 0px #009441, 0px 3px 3px rgba(0,0,0,0.1);
}
.openerp .oe_mail .oe_msg .oe_msg_content textarea{
width: 100%;
height: 32px;
margin: 0px;
padding: 0px;
resize: vertical;
padding: 4px;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact, .openerp .oe_mail .oe_msg.oe_msg_expandable{
padding:4px;
min-height:0px;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact textarea{
height: 24px;
width: 100%;
} }
.openerp .oe_mail_invisible { /* ---------------- MESSAGE QUICK COMPOSER --------------- */
.openerp .oe_mail .oe_msg_composer .oe_msg_footer{
padding-right:4px;
padding-top: 2px;
padding-bottom:6px;
}
.openerp .oe_mail .oe_msg_attachments.oe_hidden,
.openerp .oe_mail .oe_msg_images.oe_hidden{
margin:0px;
border: none;
display: none; display: none;
} }
.openerp .oe_mail .oe_msg_attachments{
/* ------------------------------------------------------------ */ margin-bottom: 4px;
/* Messages layout margin-right: 0px;
/* ------------------------------------------------------------ */ font-size: 12px;
border-radius: 2px;
.openerp .oe_mail_msg .oe_msg_title { border: solid 1px rgba(124,123,173,0.14);
margin: 0;
font-size: 1.3em;
font-weight: bold;
} }
.openerp .oe_mail_msg .oe_msg_title a:link, .openerp .oe_mail .oe_msg_attachments .oe_attachment{
.openerp .oe_mail_msg .oe_msg_title a:visited { padding: 2px;
color: #4C4C4C; padding-left: 4px;
padding-right: 4px;
}
.openerp .oe_mail .oe_msg_attachments .oe_attachment .oe_e{
font-size: 23px;
margin-top: -5px;
}
.openerp .oe_mail .oe_msg_attachments .oe_attachment .oe_e:hover{
text-decoration: none;
}
.openerp .oe_mail .oe_msg_attachments .oe_attachment:nth-child(odd){
background:white;
}
.openerp .oe_mail .oe_msg_attachments .oe_attachment:nth-child(even){
background: #F4F5FA;
}
.openerp .oe_mail .oe_msg_images {
display: block;
}
.openerp .oe_mail .oe_msg_footer button{
display: inline;
height: 24px;
font-size: 12px;
line-height: 12px;
vertical-align: middle;
}
.openerp .oe_mail .oe_msg_footer button.oe_attach{
width: 24px;
overflow: hidden;
}
.openerp .oe_mail .oe_msg_footer button.oe_attach .oe_e{
position: relative;
top: -1px;
left: -9px;
}
.openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{
display:inline;
}
.openerp .oe_mail .oe_msg_footer button.oe_full{
width:24px;
overflow:hidden;
float: right;
}
.openerp .oe_mail .oe_msg_footer button.oe_full .oe_e{
position: relative;
top: -1px;
left: -9px;
}
.openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{
background: transparent;
color: #7C7BAD;
box-shadow: none;
border: none;
text-shadow: none;
}
.openerp .oe_mail .oe_attach_label{
color: #7C7BAD;
margin-left: -3px;
}
.openerp .oe_mail .oe_msg_footer .oe_attachment_file .oe_form_binary_file{
display: inline-block;
margin-left: -47px;
height: 28px;
width: 52px;
margin-top: -6px;
}
.openerp .oe_mail .oe_mail_list_recipients{
font-size: 12px;
margin-top: 4px;
margin-bottom: 4px;
}
/* ---------------- HIDDEN MESSAGES ------------------ */
.openerp .oe_mail .oe_msg_content.oe_msg_more_message{
text-align: right;
}
.openerp .oe_mail .oe_msg_content.oe_msg_more_message .oe_separator{
height: 0;
border-bottom: dashed 1px #e6e6e6;
margin-left: -4px;
margin-right: 8px;
margin-top: 6px;
margin-bottom: -9px;
}
.openerp .oe_mail .oe_msg_more_message .oe_msg_fetch_more {
background: white;
margin-right: 280px;
padding-left: 8px;
padding-right: 8px;
text-decoration: none;
color: #b4b4b4;
}
.openerp .oe_mail .oe_msg_more_message .oe_msg_fetch_more:hover{
text-decoration: none; text-decoration: none;
} }
.openerp .oe_mail_msg .oe_msg_body { /* ---------------- FOLLOWERS ------------------ */
margin-bottom: .5em;
text-align: justify; .openerp .oe_followers{
position: relative;
display: inline-block;
padding-top: 5px;
width: 160px;
float: right;
} }
.openerp .oe_mail_msg .oe_msg_body pre { /* a) THE FOLLOW BUTTON */
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
margin: 0px; .openerp .oe_followers button.oe_follower{
white-space: pre-wrap; display: block;
text-align: center;
width:100%;
}
.openerp .oe_followers button.oe_follower.oe_following{
background-color: #3465A4;
background-image: -webkit-linear-gradient(top, #729FCF, #3465A4);
color: white;
}
.openerp .oe_followers button.oe_follower .oe_follow,
.openerp .oe_followers button.oe_follower .oe_unfollow,
.openerp .oe_followers button.oe_follower .oe_following{
display: none;
}
/* a.1) when following, show 'following' */
.openerp .oe_followers button.oe_follower.oe_following .oe_following{
display: inline;
}
/* a.2) when following and hovering, show 'unfollow' */
.openerp .oe_followers button.oe_follower.oe_following:hover .oe_following{
display: none;
}
.openerp .oe_followers button.oe_follower.oe_following:hover .oe_unfollow{
display: inline;
}
/* a.3) when not following show 'follow' */
.openerp .oe_followers button.oe_follower.oe_notfollow .oe_follow{
display: inline; display: inline;
} }
/* Read more/less link */ .openerp .oe_followers .oe_subtype_list{
.openerp .oe_mail_msg span.oe_mail_reduce { margin-top: 4px;
position: absolute;
right: 0;
} }
/* Dropdown menu */ /* b) THE FOLLOWERS */
.openerp .oe_followers .oe_follower_title{
.openerp .oe_mail ul.oe_mail_thread_display ul.oe_mail_thread_display { display: inline;
position: relative;
border-left: 1px #DDD dashed;
} }
.openerp .oe_followers .oe_follower_title_box{
.openerp .oe_mail ul.oe_header { margin-top: 12px;
position: absolute; margin-bottom: 4px;
right: 3px;
top: -6px;
z-index: 10;
} }
.openerp .oe_followers .oe_invite{
.openerp .oe_mail ul.oe_header a {
text-decoration: none;
}
.openerp .oe_mail ul.oe_header>li {
display: inline-block;
height: 20px;
text-align: right;
}
/* Message footer */
.openerp .oe_mail_msg .oe_msg_footer {
color: #888;
}
.openerp .oe_mail_msg .oe_msg_footer li {
float: left;
margin-right: 3px;
}
.openerp .oe_mail_msg .oe_msg_footer li:after {
content: " · ";
}
.openerp .oe_mail_msg .oe_msg_footer li:last-child:after {
content: "";
}
/* Attachments list */
.openerp .oe_msg_content ul.oe_msg_attachments {
width: 100%;
margin: .5em 0 0 0;
padding: .5em 0;
list-style-position: inside;
}
.openerp .oe_msg_content ul.oe_msg_attachments.oe_hidden {
display: none;
}
.openerp .oe_msg_content ul.oe_msg_attachments li {
float: none;
height: 20px;
line-height: 20px;
margin: 0;
padding: 0;
list-style-type: square;
}
.openerp .oe_msg_content ul.oe_msg_attachments .oe_upload_in_process {
float: right; float: right;
width: 200px;
height: 16px;
} }
.openerp .oe_msg_content ul.oe_msg_attachments .oe_upload_in_process div { .openerp .oe_followers .oe_partner {
float: left; height: 32px;
width: 38px; overflow: hidden;
height: 16px;
margin-right: 2px;
background: #66FF66;
} }
.openerp .oe_msg_content ul.oe_msg_attachments .oe_upload_in_process span { .openerp .oe_followers .oe_partner img{
color: #aaaaaa; width: 32px;
position: absolute; margin-right:4px;
border-radius: 2px;
} }
/* ------------------------------------------------------------ */ /* ---------------- MESSAGES BODY ------------------ */
/* Topbar button .openerp .oe_mail .oe_msg_content .oe_blockquote,
/* ------------------------------------------------------------ */ .openerp .oe_mail .oe_msg_content blockquote {
padding: 4px;
border-radius: 2px;
border: solid 1px rgba(124,123,173,0.14);
}
.openerp .oe_topbar .oe_topbar_compose_full_email { /* ----------- FORM INTEGRATION ------------ */
float: right;
margin: 3px 25px 0 0; .openerp .oe_record_thread{
} display: block;
margin-right: 180px;
}
/* ----------- INBOX INTEGRATION ----------- */
.openerp .oe_mail_wall .oe_mail{
margin: 16px;
width: 720px;
}

View File

@ -1,166 +0,0 @@
/* ------------------------------ */
/* Compose Message */
/* ------------------------------ */
.openerp .oe_msg_content .oe_mail_compose_message_footer {
height: 24px;
}
.openerp .oe_msg_content .oe_mail_compose_message_footer button.oe_mail_compose_message_button_send {
float: left;
}
.openerp .oe_mail .oe_mail_compose_textarea
{
display: none;
}
.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_header,
.openerp .oe_mail .oe_mail_compose_textarea .oe_mail_post_footer,
{
position: relative;
}
.openerp .oe_mail .oe_mail_compose_textarea a.oe_cancel {
position: absolute;
right: -8px;
top: -8px;
}
.openerp .oe_mail .oe_mail_compose_textarea a.oe_cancel:first-of-type {
display:none;
}
.openerp .oe_mail .oe_mail_compose_textarea button.oe_full {
float: right;
position: relative;
right: -10px;
}
/* ------------------------------------------------------------ */
/* mail.compose.message : list_recipients
/* ------------------------------------------------------------ */
.openerp .oe_mail .oe_mail_list_recipients {
display: inline;
}
.openerp .oe_mail .oe_mail_list_recipients .oe_all_follower {
color: blue;
}
.openerp .oe_mail .oe_mail_list_recipients .oe_partner_follower a {
color: red;
}
.openerp .oe_mail .oe_mail_list_recipients .oe_hidden,
.openerp .oe_mail .oe_mail_list_recipients .oe_more_hidden {
display: none;
}
/* ------------------------------------------------------------ */
/* mail.compose.message : attachment
/* ------------------------------------------------------------ */
.openerp .oe_mail .oe_attachment_file {
display: inline-block;
}
.openerp .oe_mail .oe_attachment_file .oe_add {
float: left;
width: 24px;
height: 24px;
position: relative;
z-index: 10;
left: +2px;
top: +7px;
overflow: hidden;
}
/* attachment button: override of openerp values */
.openerp .oe_mail .oe_attachment_file .oe_add button,
.openerp .oe_mail .oe_attachment_file .oe_add input.oe_insert_file {
position: absolute;
bottom: +0px;
left: +0px;
height: 24px;
width: 24px;
margin: 0px;
padding: 0px;
}
.openerp .oe_mail .oe_attachment_file .oe_add input.oe_insert_file {
z-index:2;
width: 300px;
left: -100px;
background: transparent;
border: 0;
color: transparent;
}
.openerp .oe_mail .oe_attachment_file .oe_add button span {
position: relative;
bottom: +4px;
font-size: 30px;
}
.openerp .oe_mail .oe_msg_attachments input {
visibility: hidden;
}
.openerp .oe_mail .oe_mail_compose_attachment_list {
clear: both;
}
/* ------------------------------------------------------------ */
/* mail.compose.message
/* ------------------------------------------------------------ */
/* default textarea (oe_mail_compose_textarea), and body textarea for compose form view */
.openerp .oe_mail.oe_semantic_html_override .oe_mail_compose_textarea textarea.field_text,
.openerp .oe_mail div.oe_mail_compose_message_body textarea.field_text {
width: 100%;
min-height: 120px;
height: auto;
padding: 4px;
font-size: 12px;
border: 1px solid #cccccc;
}
/* not top textarea */
.openerp .oe_mail.oe_semantic_html_override .oe_semantic_html_override .oe_mail_compose_textarea textarea.field_text {
height: 60px;
}
/* form_view: delete white background */
.openerp .oe_msg_content div.oe_formview {
background-color: transparent;
}
.openerp .oe_msg_content div.oe_form_nosheet {
margin: 0px;
}
.openerp .oe_msg_content table.oe_form_group {
margin: 0px;
}
.openerp .oe_msg_content table.oe_form_field,
.openerp .oe_msg_content div.oe_form_field {
padding: 0px;
}
.openerp .oe_msg_content td.oe_form_group_cell {
vertical-align: bottom;
}
/* subject: change width */
.openerp .oe_msg_content .oe_form .oe_form_field input[type='text'] {
width: 472px;
}
/* body_html: cleditor */
.openerp .oe_msg_content div.cleditorMain {
border: 1px solid #cccccc;
}
/* destination_partner_ids */
.openerp .oe_msg_content div.text-core {
height: 22px !important;
width: 472px;
}

File diff suppressed because it is too large Load Diff

View File

@ -24,10 +24,9 @@ openerp_mail_followers = function(session, mail) {
init: function() { init: function() {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.options.image = this.node.attrs.image || 'image_small'; this.image = this.node.attrs.image || 'image_small';
this.options.title = this.node.attrs.title || 'Followers'; this.comment = this.node.attrs.help || false;
this.options.comment = this.node.attrs.help || false; this.displayed_nb = this.node.attrs.displayed_nb || 10;
this.options.displayed_nb = this.node.attrs.displayed_nb || 10;
this.ds_model = new session.web.DataSetSearch(this, this.view.model); this.ds_model = new session.web.DataSetSearch(this, this.view.model);
this.ds_follow = new session.web.DataSetSearch(this, this.field.relation); this.ds_follow = new session.web.DataSetSearch(this, this.field.relation);
this.ds_users = new session.web.DataSetSearch(this, 'res.users'); this.ds_users = new session.web.DataSetSearch(this, 'res.users');
@ -61,7 +60,7 @@ openerp_mail_followers = function(session, mail) {
self.do_unfollow(); self.do_unfollow();
}); });
// event: click on a subtype, that (un)subscribe for this subtype // event: click on a subtype, that (un)subscribe for this subtype
this.$el.on('click', 'ul.oe_subtypes input', self.do_update_subscription); this.$el.on('click', '.oe_subtype_list input', self.do_update_subscription);
// event: click on 'invite' button, that opens the invite wizard // event: click on 'invite' button, that opens the invite wizard
this.$('.oe_invite').on('click', function (event) { this.$('.oe_invite').on('click', function (event) {
action = { action = {
@ -115,20 +114,24 @@ openerp_mail_followers = function(session, mail) {
self.message_is_follower = (_.indexOf(self.get('value'), pid) != -1); self.message_is_follower = (_.indexOf(self.get('value'), pid) != -1);
}).pipe(self.proxy('display_generic')); }).pipe(self.proxy('display_generic'));
}, },
_format_followers: function(count){
// TDE note: why redefining _t ?
function _t(str) { return str; }
var str = '';
if(count <= 0){
str = _t('No followers');
}else if(count === 1){
str = _t('One follower');
}else{
str = ''+count+' '+_t('followers');
}
return str;
},
/* Display generic info about follower, for people not having access to res_partner */ /* Display generic info about follower, for people not having access to res_partner */
display_generic: function () { display_generic: function () {
var self = this; var self = this;
var node_user_list = this.$('ul.oe_mail_followers_display').empty(); var node_user_list = this.$('.oe_follower_list').empty();
// format content: Followers (You and 0 other) // Followers (3) this.$('.oe_follower_title').html(this._format_followers(this.get('value').length));
var content = this.options.title;
if (this.message_is_follower) {
content += ' (You and ' + (this.get('value').length-1) + ' other)';
}
else {
content += ' (' + this.get('value').length + ')'
}
this.$('div.oe_mail_recthread_followers h4').html(content);
}, },
/** Display the followers */ /** Display the followers */
@ -137,16 +140,17 @@ openerp_mail_followers = function(session, mail) {
records = records || []; records = records || [];
this.message_is_follower = this.set_is_follower(records); this.message_is_follower = this.set_is_follower(records);
// clean and display title // clean and display title
var node_user_list = this.$('ul.oe_mail_followers_display').empty(); var node_user_list = this.$('.oe_follower_list').empty();
this.$('div.oe_mail_recthread_followers h4').html(this.options.title + ' (' + records.length + ')'); this.$('.oe_follower_title').html(this._format_followers(records.length));
// truncate number of displayed followers // truncate number of displayed followers
truncated = records.splice(0, this.options.displayed_nb); truncated = records.splice(0, this.displayed_nb);
_(truncated).each(function (record) { _(truncated).each(function (record) {
record.avatar_url = mail.ChatterUtils.get_image(self.session, 'res.partner', 'image_small', record.id); 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); $(session.web.qweb.render('mail.followers.partner', {'record': record})).appendTo(node_user_list);
}); });
// FVA note: be sure it is correctly translated
if (truncated.length < records.length) { if (truncated.length < records.length) {
$('<li>And ' + (records.length - truncated.length) + ' more.</li>').appendTo(node_user_list); $('<div class="oe_partner">And ' + (records.length - truncated.length) + ' more.</div>').appendTo(node_user_list);
} }
}, },
@ -172,7 +176,7 @@ openerp_mail_followers = function(session, mail) {
/** Fetch subtypes, only if current user is follower */ /** Fetch subtypes, only if current user is follower */
fetch_subtypes: function () { fetch_subtypes: function () {
var subtype_list_ul = this.$('.oe_subtypes').empty(); var subtype_list_ul = this.$('.oe_subtype_list').empty();
if (! this.message_is_follower) return; if (! this.message_is_follower) return;
var context = new session.web.CompoundContext(this.build_context(), {}); var context = new session.web.CompoundContext(this.build_context(), {});
this.ds_model.call('message_get_subscription_data', [[this.view.datarecord.id], context]).pipe(this.proxy('display_subtypes')); this.ds_model.call('message_get_subscription_data', [[this.view.datarecord.id], context]).pipe(this.proxy('display_subtypes'));
@ -181,13 +185,12 @@ openerp_mail_followers = function(session, mail) {
/** Display subtypes: {'name': default, followed} */ /** Display subtypes: {'name': default, followed} */
display_subtypes:function (data) { display_subtypes:function (data) {
var self = this; var self = this;
var subtype_list_ul = this.$('.oe_subtypes'); var subtype_list_ul = this.$('.oe_subtype_list');
var records = data[this.view.datarecord.id].message_subtype_data; var records = data[this.view.datarecord.id || this.view.dataset.ids[0]].message_subtype_data;
_(records).each(function (record, record_name) { _(records).each(function (record, record_name) {
record.name = record_name; record.name = record_name;
record.followed = record.followed || undefined; record.followed = record.followed || undefined;
$(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('ul.oe_subtypes') ); $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
}); });
}, },
@ -210,7 +213,7 @@ openerp_mail_followers = function(session, mail) {
var self = this; var self = this;
var checklist = new Array(); var checklist = new Array();
_(this.$('.oe_mail_recthread_actions input[type="checkbox"]')).each(function (record) { _(this.$('.oe_actions input[type="checkbox"]')).each(function (record) {
if ($(record).is(':checked')) { if ($(record).is(':checked')) {
checklist.push(parseInt($(record).data('id'))); checklist.push(parseInt($(record).data('id')));
} }

View File

@ -1,33 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<template> <template>
<!-- this template contains the mail widget and is used to namespace the css -->
<t t-name="mail.Widget">
<div class="oe_mail">
</div>
</t>
<!-- <!--
mail.compose_message template mail.compose_message template
This template holds the composition form to write a note or send This template holds the composition form to write a note or send
an e-mail. It contains by default a textarea, that will be replaced an e-mail. It contains by default a textarea, that will be replaced
by another composition form in the main wall composition form, or by another composition form in the main wall composition form, or
for main thread composition form in document form view. for main thread composition form in document form view.
mail.compose_message.compact template
This template holds the composition form to write a message, this box is converted into
mail.compose_message when focus on textarea
--> -->
<t t-name="mail.compose_message"> <t t-name="mail.compose_message">
<div class="oe_mail_compose_textarea"> <div t-if="widget.show_composer" t-attf-class="oe_msg oe_msg_composer #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<img class="oe_mail_icon oe_mail_frame oe_left" alt="User img"/> <div class="oe_msg_left">
<div class="oe_msg_content"> <img class="oe_msg_icon" alt="User img" t-attf-src="#{widget.avatar}"/>
<!-- contains the composition form --> </div>
<!-- default content: old basic textarea --> <div class="oe_msg_center">
<div class="oe_mail_post_header"> <div class="oe_msg_content">
<t t-call="mail.thread.list_recipients"/> <t t-call="mail.thread.list_recipients"/>
<a class="oe_cancel oe_e">X</a> <textarea class="field_text"></textarea>
</div> </div>
<textarea class="field_text" placeholder="Add your comment here..."/> <div class="oe_msg_footer">
<div class="oe_mail_post_footer"> <div class="oe_msg_attachment_list"></div>
<div class="oe_mail_compose_attachment_list"/> <button class="oe_post">Post</button>
<button class="oe_full">Full mail message</button>
<button class="oe_post">Post message</button>
<t t-call="mail.compose_message.add_attachment"/> <t t-call="mail.compose_message.add_attachment"/>
<!--<a class="oe_cancel oe_e">X</a>-->
<button class="oe_full"><span class='oe_e'>&amp;ograve</span></button>
</div> </div>
</div> </div>
<div class="oe_clear"/>
</div> </div>
<div t-if="widget.show_compact_message and !widget.show_composer" t-attf-class="oe_msg oe_msg_composer_compact #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<textarea class="field_text oe_compact" placeholder="Write a reply..."/>
</div>
<span t-if="!(widget.show_compact_message and !widget.show_composer) and !widget.show_composer" class="oe_placeholder_compose"></span>
</t> </t>
<!-- <!--
@ -35,19 +48,20 @@
Small template to be inserted in the composition for add attachments Small template to be inserted in the composition for add attachments
--> -->
<t t-name="mail.compose_message.add_attachment"> <t t-name="mail.compose_message.add_attachment">
<div class="oe_attachment_file"> <span class="oe_attachment_file">
<div class="oe_add"> <span class="oe_add">
<!-- uploader of file --> <!-- uploader of file -->
<button><span class="oe_e">p</span></button> <button class="oe_attach"><span class="oe_e">'</span></button>
<span class='oe_attach_label'>File</span>
<t t-call="HiddenInputFile"> <t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/> <t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action">/web/binary/upload_attachment</t> <t t-set="fileupload_action">/web/binary/upload_attachment</t>
<input type="hidden" name="model" value="mail.message"/> <input type="hidden" name="model" value="mail.compose.message"/>
<input type="hidden" name="id" value="0"/> <input type="hidden" name="id" value="0"/>
<input type="hidden" name="session_id" t-att-value="widget.session.session_id"/> <input type="hidden" name="session_id" t-att-value="widget.session.session_id"/>
</t> </t>
</div> </span>
</div> </span>
</t> </t>
<!-- <!--
@ -55,25 +69,33 @@
Template used to display attachments in a mail.message Template used to display attachments in a mail.message
--> -->
<t t-name="mail.thread.message.attachments"> <t t-name="mail.thread.message.attachments">
<ul t-attf-class="oe_msg_attachments #{widget.datasets.attachment_ids[0] and widget.options.thread.show_attachment_link?'':'oe_hidden'}"> <span class="oe_msg_attachments">
<t t-foreach="widget.datasets.attachment_ids" t-as="attachment"> <t t-foreach="widget.attachment_ids" t-as="attachment" t-if="!attachment.is_image">
<li> <div class="oe_attachment">
<span t-if="(attachment.upload or attachment.percent_loaded&lt;100)" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}" t-attf-name="{attachment.name || attachment.filename}"> <span t-if="(attachment.upload and attachment.percent_loaded&lt;100)" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}" t-attf-name="{attachment.name || attachment.filename}">
<div class="oe_upload_in_process"> <div class="oe_upload_in_process">
<span>...Upload in progress...</span> <span>...Upload in progress...</span>
</div> </div>
<t t-raw="attachment.name || attachment.filename"/> <t t-raw="attachment.name || attachment.filename"/>
</span> </span>
<a t-if="(!attachment.upload or attachment.percent_loaded&gt;=100)" t-att-href="attachment.url" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}"> <t t-if="(!attachment.upload or attachment.percent_loaded&gt;=100)">
<t t-raw="attachment.name || attachment.filename"/> <a t-att-href="attachment.url" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}">
</a> <t t-raw="attachment.name || attachment.filename"/>
<t t-if="widget.options.thread.show_attachment_delete and (!attachment.upload or attachment.percent_loaded&gt;=100)"> </a>
<a class="oe_right oe_mail_attachment_delete" title="Delete this attachment" t-attf-data-id="{attachment.id}">x</a>
</t> </t>
<t t-if="(widget.show_delete_attachment and (!attachment.upload or attachment.percent_loaded&gt;=100))">
</li> <a class="oe_right oe_mail_attachment_delete oe_e" title="Delete this attachment" t-attf-data-id="{attachment.id}">[</a>
</t>
</div>
</t> </t>
</ul> </span>
<span class="oe_msg_images">
<t t-foreach="widget.attachment_ids" t-as="attachment" t-if="attachment.is_image">
<a t-att-href="attachment.url" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}">
<img t-if="attachment.is_image" t-attf-title="{(attachment.name || attachment.filename) + (attachment.date?' \n('+attachment.date+')':'' )}" t-att-src="attachment.url"/>
</a>
</t>
</span>
</t> </t>
<t t-name="mail.thread.message.private"> <t t-name="mail.thread.message.private">
@ -89,14 +111,14 @@
--> -->
<t t-name="mail.thread.list_recipients"> <t t-name="mail.thread.list_recipients">
<div class="oe_mail_list_recipients"> <div class="oe_mail_list_recipients">
Post to: To:
<span t-if="!widget.datasets.is_private" class="oe_all_follower">All Followers</span> <span t-if="!widget.is_private" class="oe_all_follower">Everyone</span>
<t t-if="!widget.datasets.is_private and widget.datasets.partner_ids.length"> and </t> <t t-if="!widget.is_private and widget.partner_ids.length"> and </t>
<t t-set="inc" t-value="0"/> <t t-set="inc" t-value="0"/>
<t t-if="widget.datasets.partner_ids.length" t-foreach="widget.datasets.partner_ids" t-as="partner"><span t-attf-class="oe_partner_follower #{inc>=3?'oe_hidden':''}"><t t-if="inc" t-raw="', '"/><a t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-raw="partner[1]"/></a></span><t t-set="inc" t-value="inc+1"/> <t t-if="widget.partner_ids.length" t-foreach="widget.partner_ids" t-as="partner"><span t-attf-class="oe_partner_follower #{inc>=3?'oe_hidden':''}"><t t-if="inc" t-raw="', '"/><a t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-raw="partner[1]"/></a></span><t t-set="inc" t-value="inc+1"/>
</t> </t>
<t t-if="widget.datasets.partner_ids.length>=3"> <t t-if="widget.partner_ids.length>=3">
<span class="oe_more">, <a><t t-raw="widget.datasets.partner_ids.length-3"/> others...</a></span> <span class="oe_more">, <a><t t-raw="widget.partner_ids.length-3"/> others...</a></span>
<a class="oe_more_hidden">&lt;&lt;&lt;</a> <a class="oe_more_hidden">&lt;&lt;&lt;</a>
</t> </t>
</div> </div>
@ -131,122 +153,95 @@
<button type="button" class="oe_write_full oe_highlight"> <button type="button" class="oe_write_full oe_highlight">
Compose a new message Compose a new message
</button> </button>
<button type="button" class="oe_write_onwall" help="Your followers can read this message"> <span class='oe_alternative'>
Write to your followers or
</button> <a href='#' class='oe_write_onwall oe_bold' help='Your followers can read this message'>Write to your followers</a>
</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- placeholder for the wall threads --> <div class="oe_mail-placeholder"></div>
<div class="oe_mail_wall_threads"/>
<div class="oe_mail_wall_aside">
<!-- contains currently nothing -->
</div>
</div> </div>
<!-- <!--
display message on the wall when there are no message display message on the wall when there are no message
--> -->
<li t-name="mail.wall_no_message" class="oe_wall_no_message"> <t t-name="mail.wall_no_message">
You have no messages <div class="oe_wall_no_message">You have no messages</div>
</li> </t>
<!-- <!--
record_thread main template record_thread main template
Template used to display the communication history in documents Template used to display the communication history in documents
form view. form view.
--> -->
<div t-name="mail.record_thread" class="oe_mail_record_wall"> <div t-name="mail.record_thread" class="oe_record_thread">
<!-- <h4>History and Comments</h4> --> <div class="oe_mail-placeholder">
<ul class="oe_mail_wall_threads"> </div>
<!-- contains the document thread -->
</ul>
</div> </div>
<!-- <t t-name="mail.thread">
thread template <div t-attf-class="oe_thread #{widget.root?'oe_root_thread':''}"/>
This template holds a thread of comments. It begins with an actions </t>
container, holding the composition form. Then come the various
messages. Then comes the 'more' button.
-->
<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) -->
</div>
<ul class="oe_mail_thread_display">
<!-- contains the threads -->
</ul>
</div>
<!-- default layout --> <!-- default layout -->
<li t-name="mail.thread.message" t-attf-class="oe_mail oe_mail_thread_msg #{widget.datasets.to_read ?'oe_mail_unread':'oe_mail_read'}"> <t t-name="mail.thread.message">
<div t-attf-class="oe_msg_#{widget.datasets.type} oe_semantic_html_override"> <div t-attf-class="oe_msg #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} oe_msg_#{widget.type} oe_msg_#{widget.to_read?'unread':'read'}">
<!-- message actions (read/unread, reply, delete...) -->
<ul class="oe_header">
<li class="placeholder-mail-vote"><t t-call="mail.thread.message.vote"/></li>
<li class="placeholder-mail-star"><t t-call="mail.thread.message.star"/></li>
<li t-if="widget.datasets.show_read_unread" title="Read" class="oe_read"><a class="oe_read oe_e">W</a></li>
<li t-if="widget.datasets.show_read_unread" title="Set back to unread" class="oe_unread"><a class="oe_unread oe_e">h</a></li>
<li title="Quick reply" t-if="widget.datasets.show_reply"><a class="oe_reply oe_e">)</a></li>
<t t-if="(widget.datasets.is_author and widget.options.message.show_dd_delete) or widget.datasets.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.datasets.is_author and widget.options.message.show_dd_delete"><a class="oe_msg_delete">Delete</a></li>
<li t-if="widget.datasets.type == 'email'"><a class="oe_msg_details" t-attf-href="#model=mail.message&amp;id=#{widget.datasets.id}" >Details</a></li>
</ul>
</span>
</li>
</t>
</ul>
<a t-attf-href="#model=res.partner&amp;id=#{widget.datasets.author_id[0]}" t-att-title="widget.datasets.author_id[1]"> <div class='oe_msg_left'>
<img class="oe_mail_icon oe_mail_frame oe_left" t-att-src="widget.datasets.avatar"/> <a t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]">
</a> <img class="oe_msg_icon" t-att-src="widget.avatar"/>
</a>
</div>
<div class="oe_msg_content"> <div class="oe_msg_center">
<div class='oe_msg_icons'>
<span class='oe_read' t-if="widget.show_read_unread_button"><a title="Read" class="oe_e">X</a></span>
<span class='oe_unread' t-if="widget.show_read_unread_button"><a title="Set back to unread" class="oe_e">v</a></span>
<span class='oe_reply' t-if="widget.show_reply_button"><a title="Reply" class="oe_e">(</a></span>
<span t-attf-class="oe_star #{widget.is_favorite?'oe_starred':''}"><a title="Add To Favorites" class="oe_e">7</a></span>
</div>
<!-- message itself --> <!-- message itself -->
<div class="oe_mail_msg"> <div class="oe_msg_content">
<h1 t-if="widget.datasets.subject" class="oe_msg_title"> <h1 t-if="widget.subject and !widget.thread_level" class="oe_msg_title">
<t t-raw="widget.datasets.subject"/> <t t-raw="widget.subject"/>
</h1> </h1>
<ul class="oe_msg_footer">
<li t-if="widget.datasets.author_id"><a t-attf-href="#model=res.partner&amp;id=#{widget.datasets.author_id[0]}"><t t-raw="widget.datasets.author_id[1]"/></a></li>
<li><span t-att-title="widget.datasets.date"><t t-raw="widget.datasets.timerelative"/></span></li>
<li t-if="widget.datasets.attachment_ids.length > 0">
<a class="oe_msg_view_attachments">
<t t-if="widget.datasets.attachment_ids.length == 1">1 Attachment</t>
<t t-if="widget.datasets.attachment_ids.length > 1"><t t-raw="widget.datasets.attachment_ids.length"/> Attachments</t>
</a>
</li>
</ul>
<div class="oe_clear"/>
<div class="oe_msg_body"> <div class="oe_msg_body">
<t t-if="widget.options.message.show_record_name and widget.datasets.record_name and (!widget.datasets.subject) and !widget.options.thread.thread_level and !widget.options.thread.display_on_thread[0] and widget.datasets.model!='res.partner'"> <t t-if="widget.options.show_record_name and widget.record_name and (!widget.subject) and !widget.options.thread_level and !widget.options.display_on_thread[0] and widget.model!='res.partner'">
<a class="oe_mail_action_model" t-attf-href="#model=#{widget.datasets.model}&amp;id=#{widget.res_id}"><t t-raw="widget.datasets.record_name"/></a> <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 t-raw="widget.datasets.body"/> <t t-raw="widget.body"/>
</div> </div>
<t t-if="widget.datasets.attachment_ids.length > 0"> <t t-if="widget.attachment_ids.length > 0">
<div class="oe_clear"></div>
<t t-call="mail.thread.message.attachments"/> <t t-call="mail.thread.message.attachments"/>
</t> </t>
</div> </div>
<div class="oe_msg_footer">
<a t-if="widget.author_id" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[1]"/></a>
<span class='oe_subtle'></span>
<span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span>
<a t-if="widget.attachment_ids.length > 0" class="oe_mail_msg_view_attachments">
<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>
<span class='oe_subtle'></span>
<t t-call="mail.thread.message.vote"/>
</div>
</div> </div>
</div> </div>
<div class="oe_thread_placeholder"></div> </t>
</li>
<!-- expandable message layout --> <!-- expandable message layout -->
<li t-name="mail.thread.expandable" class="oe_mail oe_mail_thread_msg oe_mail_unread"> <t t-name="mail.thread.expandable">
<div t-attf-class="oe_msg_#{widget.datasets.type} oe_semantic_html_override"> <div t-attf-class="oe_msg oe_msg_#{widget.type} #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} oe_msg_unread">
<div class="oe_msg_content oe_msg_more_message"> <div class="oe_msg_content oe_msg_more_message">
<a class="oe_mail_fetch_more">Load more messages <span t-if="widget.datasets.nb_messages>0">(<t t-raw="widget.datasets.nb_messages"/> messages not display)</span>...</a> <div class='oe_separator'></div>
<a t-if="widget.nb_messages === 1" class="oe_msg_fetch_more">show one more message</a>
<a t-if="widget.nb_messages !== 1" class="oe_msg_fetch_more">show <t t-raw="widget.nb_messages" /> more messages</a>
</div> </div>
</div> </div>
</li> </t>
<!-- <!--
mail.compose_message.button_top_bar mail.compose_message.button_top_bar
@ -254,7 +249,7 @@
--> -->
<t t-name="mail.compose_message.button_top_bar"> <t t-name="mail.compose_message.button_top_bar">
<div class="oe_topbar_compose_full_email"> <div class="oe_topbar_compose_full_email">
<button class="oe_button oe_highlight">Write an email</button> <!-- <button class="oe_button oe_highlight">Write an email</button> -->
</div> </div>
</t> </t>
@ -262,33 +257,18 @@
Template used to display Like/Unlike in a mail.message Template used to display Like/Unlike in a mail.message
--> -->
<span t-name="mail.thread.message.vote"> <span t-name="mail.thread.message.vote">
<span class="oe_left oe_mail_vote_count">
<t t-if='widget.datasets.has_voted'> <span class="oe_mail_vote_count" t-if='widget.vote_nb > 0'>
You <t t-raw='widget.vote_nb' />
</t> <span class='oe_e'>8</span>
<t t-if='(widget.datasets.vote_user_ids.length-(widget.datasets.has_voted?1:0)) > 0'>
<t t-if='widget.datasets.has_voted'> and </t>
<t t-esc="widget.datasets.vote_user_ids.length"/> people
</t>
<t t-if='widget.datasets.vote_user_ids.length > 0'>
agree
</t>
</span> </span>
<button t-attf-class="oe_msg_vote oe_tag"> <a href='#' t-attf-class="oe_msg_vote">
<span> <t t-if="!widget.has_voted">like</t>
<t t-if="!widget.has_voted">Agree</t> <t t-if="widget.has_voted">unlike</t>
<t t-if="widget.has_voted">Undo</t> </a>
</span>
</button>
</span> </span>
<!-- mail.thread.message.star <!-- mail.thread.message.star
Template used to display stared/unstared message in a mail.message Template used to display stared/unstared message in a mail.message
--> -->
<span t-name="mail.thread.message.star">
<span class="oe_left">
<button t-attf-class="oe_mail_starbox oe_tag #{widget.datasets.is_favorite?'oe_stared':''}">*</button>
</span>
</span>
</template> </template>

View File

@ -5,51 +5,45 @@
followers main template followers main template
Template used to display the followers, the actions and the subtypes 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 t-name="mail.followers" class="oe_followers">
<div class="oe_mail_recthread_actions"> <div class="oe_actions">
<div class="oe_mail_subtypes"> <button type="button" class="oe_follower oe_notfollow">
<button type="button" class="oe_follower oe_notfollow"> <span class="oe_follow">Follow</span>
<span class="oe_follow">Follow</span> <span class="oe_unfollow">Unfollow</span>
<span class="oe_unfollow">Unfollow</span> <span class="oe_following">Following</span>
<span class="oe_following">Following</span> </button>
</button> <div class="oe_subtype_list"></div>
</div>
<div class="oe_recthread_subtypes">
<ul class="oe_subtypes"></ul>
</div>
</div> </div>
<t t-if="widget.options.comment"> <t t-if="widget.comment">
<h5 class="oe_grey"><t t-raw="widget.options.comment"/></h5> <h5 class="oe_comment"><t t-raw="widget.comment"/></h5>
</t> </t>
<div class="oe_mail_recthread_followers"> <div class='oe_follower_title_box'>
<button type="button" class="oe_invite"><span>Invite</span></button> <h4 class='oe_follower_title'>Followers</h4>
<t t-if="widget.options.title"> <a href='#' class="oe_invite">Invite others</a>
<h4><t t-raw="widget.options.title"/></h4>
</t>
<ul class="oe_mail_followers_display"></ul>
</div> </div>
<div class="oe_follower_list"></div>
</div> </div>
<!-- <!--
followers.partner template followers.partner template
Template used to display a partner following the record Template used to display a partner following the record
--> -->
<li t-name="mail.followers.partner"> <div t-name="mail.followers.partner" class='oe_partner'>
<img class="oe_mail_thumbnail oe_mail_frame" t-attf-src="{record.avatar_url}"/> <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> <a t-attf-href="#model=res.partner&amp;id=#{record.id}"><t t-raw="record.name"/></a>
</li>\ </div>
<!-- <!--
followers.subtype template followers.subtype template
Template used to display message subtypes of a follower subscription Template used to display message subtypes of a follower subscription
--> -->
<li t-name="mail.followers.subtype"> <t t-name="mail.followers.subtype">
<table width="50%"> <table class='oe_subtype'>
<tr> <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> <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>
<td><label t-att-for="'input_mail_followers_subtype_'+record.id"><t t-raw="record.name"/></label></td>
</tr> </tr>
</table> </table>
</li> </t>
</template> </template>

View File

@ -94,6 +94,8 @@ class TestMailMockups(common.TransactionCase):
self._build_email_kwargs_list = [] self._build_email_kwargs_list = []
def _mock_build_email(self, *args, **kwargs): def _mock_build_email(self, *args, **kwargs):
""" Mock build_email to be able to test its values. Store them into
some internal variable for latter processing. """
self._build_email_args_list.append(args) self._build_email_args_list.append(args)
self._build_email_kwargs_list.append(kwargs) self._build_email_kwargs_list.append(kwargs)
return self._build_email(*args, **kwargs) return self._build_email(*args, **kwargs)
@ -123,6 +125,7 @@ class test_mail(TestMailMockups):
def setUp(self): def setUp(self):
super(test_mail, self).setUp() super(test_mail, self).setUp()
cr, uid = self.cr, self.uid
self.ir_model = self.registry('ir.model') self.ir_model = self.registry('ir.model')
self.mail_alias = self.registry('mail.alias') self.mail_alias = self.registry('mail.alias')
self.mail_thread = self.registry('mail.thread') self.mail_thread = self.registry('mail.thread')
@ -135,22 +138,27 @@ class test_mail(TestMailMockups):
self.res_users = self.registry('res.users') self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner') self.res_partner = self.registry('res.partner')
# Find Employee group
group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
group_employee_id = group_employee_ref and group_employee_ref[1] or False
# Test users # Test users
self.user_demo_id = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, 'base', 'user_demo')[1] self.user_raoul_id = self.res_users.create(cr, uid,
self.user_admin = self.res_users.browse(self.cr, self.uid, self.uid) {'name': 'Raoul Grosbedon', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'groups_id': [(6, 0, [group_employee_id])]})
self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
self.user_admin = self.res_users.browse(cr, uid, uid)
# Mock send_get_mail_body to test its functionality without other addons override # 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._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 self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body
# groups@.. will cause the creation of new mail groups # groups@.. will cause the creation of new mail groups
self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model', '=', 'mail.group')])[0] self.mail_group_model_id = self.ir_model.search(cr, uid, [('model', '=', 'mail.group')])[0]
self.mail_alias.create(self.cr, self.uid, {'alias_name': 'groups', self.mail_alias.create(cr, uid, {'alias_name': 'groups',
'alias_model_id': self.mail_group_model_id}) 'alias_model_id': self.mail_group_model_id})
# create a 'pigs' group that will be used through the various tests # create a 'pigs' group that will be used through the various tests
self.group_pigs_id = self.mail_group.create(self.cr, self.uid, self.group_pigs_id = self.mail_group.create(cr, uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'}) {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
self.group_pigs = self.mail_group.browse(self.cr, self.uid, self.group_pigs_id) self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
def tearDown(self): def tearDown(self):
# Remove mocks # Remove mocks
@ -159,7 +167,7 @@ class test_mail(TestMailMockups):
def test_00_message_process(self): def test_00_message_process(self):
""" Testing incoming emails processing. """ """ Testing incoming emails processing. """
cr, uid = self.cr, self.uid cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
# Incoming mail creates a new mail_group "frogs" # Incoming mail creates a new mail_group "frogs"
self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', 'frogs')]), []) self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', 'frogs')]), [])
mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='') mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='')
@ -197,6 +205,26 @@ class test_mail(TestMailMockups):
self.assertEqual(new_mail.body, '\n<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>\n', self.assertEqual(new_mail.body, '\n<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>\n',
'plaintext mail incorrectly parsed') 'plaintext mail incorrectly parsed')
# Do: post a new message, with a known partner
test_msg_id = '<deadcafe.1337-2@smtp.agrolait.com>'
TEMPLATE_MOD = MAIL_TEMPLATE_PLAINTEXT.replace('Sylvie Lelitre <sylvie.lelitre@agrolait.com>', user_raoul.email)
mail_new = TEMPLATE_MOD.format(to='Friendly Frogs <group+frogs@example.com>', subject='extra news', extra='', msg_id=test_msg_id)
self.mail_thread.message_process(cr, uid, None, mail_new)
new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
# Test: author_id set, not email_from
self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found')
self.assertFalse(new_mail.email_from, 'message process should not set the email_from when an author is found')
# Do: post a new message, with a unknown partner
test_msg_id = '<deadcafe.1337-3@smtp.agrolait.com>'
TEMPLATE_MOD = MAIL_TEMPLATE_PLAINTEXT.replace('Sylvie Lelitre <sylvie.lelitre@agrolait.com>', '_abcd_')
mail_new = TEMPLATE_MOD.format(to='Friendly Frogs <group+frogs@example.com>', subject='super news', extra='', msg_id=test_msg_id)
self.mail_thread.message_process(cr, uid, None, mail_new)
new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
# Test: author_id set, not email_from
self.assertFalse(new_mail.author_id, 'message process shnould not have found a partner for _abcd_ email address')
self.assertIn('_abcd_', new_mail.email_from, 'message process should set en email_from when not finding a partner_id')
def test_10_followers_function_field(self): def test_10_followers_function_field(self):
""" Tests designed for the many2many function field 'follower_ids'. """ Tests designed for the many2many function field 'follower_ids'.
We will test to perform writes using the many2many commands 0, 3, 4, We will test to perform writes using the many2many commands 0, 3, 4,
@ -260,7 +288,7 @@ class test_mail(TestMailMockups):
""" Tests designed for the subscriber API as well as message subtypes """ """ Tests designed for the subscriber API as well as message subtypes """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
# Data: user Raoul # Data: user Raoul
user_raoul_id = self.res_users.create(cr, uid, {'name': 'Raoul Grosbedon', 'login': 'raoul'}) user_raoul_id = self.user_raoul_id
user_raoul = self.res_users.browse(cr, uid, user_raoul_id) user_raoul = self.res_users.browse(cr, uid, user_raoul_id)
# Data: message subtypes # 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_mg_def', 'default': True, 'res_model': 'mail.group'})
@ -311,7 +339,25 @@ class test_mail(TestMailMockups):
self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs') self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs')
self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs') self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs')
def test_20_message_post(self): def test_20_message_quote_context(self):
""" Tests designed for message_post. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'})
msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id})
msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id})
result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3)
self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.')
self.assertIn('Second answer', result, 'Answer should be in quote.')
self.assertIn('Third answer', result, 'Answer should be in quote.')
self.assertIn('expandable', result, 'Expandable should be present.')
self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.')
self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.')
def test_21_message_post(self):
""" Tests designed for message_post. """ """ Tests designed for message_post. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'}) self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
@ -357,11 +403,11 @@ class test_mail(TestMailMockups):
# the html2plaintext uses etree or beautiful soup, so the result may be slighly different # the html2plaintext uses etree or beautiful soup, so the result may be slighly different
# depending if you have installed beautiful soup. # depending if you have installed beautiful soup.
self.assertIn(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils\n', 'sent_email body_alternative is incorrect') self.assertIn(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils\n', 'sent_email body_alternative is incorrect')
# Test: mail_message: partner_ids = group followers # Test: mail_message: notified_partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids]) message_pids = set([partner.id for partner in message.notified_partner_ids])
test_pids = set([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') self.assertEqual(test_pids, message_pids, 'mail.message partners incorrect')
# Test: notification linked to this message = group followers = partner_ids # Test: notification linked to this message = group followers = notified_partner_ids
notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)]) notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)]) notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect') self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
@ -390,11 +436,11 @@ class test_mail(TestMailMockups):
self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect') self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect') self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect')
self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect') self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect')
# Test: mail_message: partner_ids = group followers # Test: mail_message: notified_partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids]) message_pids = set([partner.id for partner in message.notified_partner_ids])
test_pids = set([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') self.assertEqual(message_pids, test_pids, 'mail.message partners incorrect')
# Test: notifications linked to this message = group followers = partner_ids # Test: notifications linked to this message = group followers = notified_partner_ids
notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)]) notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)]) notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect') self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
@ -408,6 +454,11 @@ class test_mail(TestMailMockups):
self.assertIn((attach.name, attach.datas.decode('base64')), _attachments, self.assertIn((attach.name, attach.datas.decode('base64')), _attachments,
'mail.message attachment name / data incorrect') 'mail.message attachment name / data incorrect')
# 3. Reply to the last message, check that its parent will be the first message
msg_id3 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Test', parent_id=msg_id2)
message = self.mail_message.browse(cr, uid, msg_id3)
self.assertEqual(message.parent_id.id, msg_id, 'message_post did not flatten the thread structure')
def test_25_message_compose_wizard(self): def test_25_message_compose_wizard(self):
""" Tests designed for the mail.compose.message wizard. """ """ Tests designed for the mail.compose.message wizard. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
@ -458,12 +509,12 @@ class test_mail(TestMailMockups):
# Test: mail.message: subject, body inside pre # Test: mail.message: subject, body inside pre
self.assertEqual(message.subject, False, 'mail.message incorrect subject') self.assertEqual(message.subject, False, 'mail.message incorrect subject')
self.assertEqual(message.body, _msg_body, 'mail.message incorrect body') 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) # Test: mail.message: notified_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] msg_pids = [partner.id for partner in message.notified_partner_ids]
test_pids = [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)]) notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
self.assertEqual(len(notif_ids), 3, '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') self.assertEqual(set(msg_pids), set(test_pids), 'mail.message notified_partner_ids incorrect')
# ---------------------------------------- # ----------------------------------------
# CASE2: reply to last comment with attachments # CASE2: reply to last comment with attachments
@ -482,7 +533,7 @@ class test_mail(TestMailMockups):
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype') self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
# Test: mail.message: subject as Re:.., body in html, parent_id # Test: mail.message: subject as Re:.., body in html, parent_id
self.assertEqual(compose.subject, _msg_reply, 'mail.message incorrect subject') 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.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') self.assertEqual(compose.parent_id and compose.parent_id.id, message.id, 'mail.message parent_id incorrect')
# Test: mail.message: attachments # Test: mail.message: attachments
for attach in compose.attachment_ids: for attach in compose.attachment_ids:
@ -522,15 +573,158 @@ class test_mail(TestMailMockups):
def test_30_message_read(self): def test_30_message_read(self):
""" Tests for message_read and expandables. """ """ Tests for message_read and expandables. """
self.assertTrue(1 == 1, 'Test not implemented, do not replace by return True') cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
pigs_domain = [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)]
# Data: create a discussion in Pigs (2 messages, one with 2 and one with 3 answers)
msg_id0 = self.group_pigs.message_post(body='0', subtype='mt_comment')
msg_id1 = self.group_pigs.message_post(body='1', subtype='mt_comment')
msg_id2 = self.group_pigs.message_post(body='2', subtype='mt_comment')
msg_id3 = self.group_pigs.message_post(body='1-1', subtype='mt_comment', parent_id=msg_id1)
msg_id4 = self.group_pigs.message_post(body='2-1', subtype='mt_comment', parent_id=msg_id2)
msg_id5 = self.group_pigs.message_post(body='1-2', subtype='mt_comment', parent_id=msg_id1)
msg_id6 = self.group_pigs.message_post(body='2-2', subtype='mt_comment', parent_id=msg_id2)
msg_id7 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=msg_id3)
msg_id8 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=msg_id4)
msg_id9 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=msg_id3)
msg_id10 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=msg_id4)
msg_ids = [msg_id0, msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8, msg_id9, msg_id10]
# Test: read some specific ids
read_msg_list = self.mail_message.message_read(cr, uid, ids=msg_ids[2:4], domain=[('body', 'like', 'dummy')])
read_msg_ids = [msg.get('id') for msg in read_msg_list]
self.assertEqual(msg_ids[2:4], read_msg_ids, 'message_read with direct ids should read only the requested ids')
# Test: read messages of Pigs through a domain, being thread or not threaded
read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=200)
read_msg_ids = [msg.get('id') for msg in read_msg_list]
self.assertEqual(msg_ids, read_msg_ids, 'message_read flat with domain on Pigs should equal all messages of Pigs')
read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=200, thread_level=1)
read_msg_ids = [msg.get('id') for msg in read_msg_list]
self.assertEqual(msg_ids, read_msg_ids, 'message_read threaded with domain on Pigs should equal all messages of Pigs')
# ----------------------------------------
# CASE1: message_read with domain, threaded
# We simulate an entire flow, using the expandables to test them
# ----------------------------------------
# Do: read last message, threaded
read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=1, thread_level=1)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(read_msg_list), 4, 'message_read on last Pigs message should return 2 messages and 2 expandables')
self.assertEqual(set([msg_id2, msg_id10]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent')
self.assertEqual(read_msg_list[1].get('parent_id'), read_msg_list[0].get('id'), 'message_read should set the ancestor to the thread header')
# Data: get expandables
new_threads_exp, new_msg_exp = None, None
for msg in read_msg_list:
if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('id') == -1:
new_threads_exp = msg
elif msg.get('type') == 'expandable':
new_msg_exp = msg
# Do: fetch new messages in first thread, domain from expandable
self.assertIsNotNone(new_msg_exp, 'message_read on last Pigs message should have returned a new messages expandable')
domain = new_msg_exp.get('domain', [])
# Test: expandable, conditions in domain
self.assertIn(('id', 'child_of', msg_id2), domain, 'new messages expandable domain should contain a child_of condition')
self.assertIn(('id', '>=', msg_id4), domain, 'new messages expandable domain should contain an id greater than condition')
self.assertIn(('id', '<=', msg_id8), domain, 'new messages expandable domain should contain an id less than condition')
self.assertEqual(new_msg_exp.get('parent_id'), msg_id2, 'new messages expandable should have ancestor_id set to the thread header')
# Do: message_read with domain, thread_level=0, parent_id=msg_id2 (should be imposed by JS)
read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=200, thread_level=0, parent_id=msg_id2)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: other message in thread have been fetch
self.assertEqual(set([msg_id4, msg_id6, msg_id8]), set(read_msg_ids), 'message_read in Pigs thread should return all the previous messages')
# Do: fetch a new thread, domain from expandable
self.assertIsNotNone(new_threads_exp, 'message_read on last Pigs message should have returned a new threads expandable')
domain = new_threads_exp.get('domain', [])
# Test: expandable, conditions in domain
for condition in pigs_domain:
self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter')
self.assertFalse(new_threads_exp.get('parent_id'), 'new threads expandable should not have an ancestor_id')
# Do: message_read with domain, thread_level=1 (should be imposed by JS)
read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=1, thread_level=1)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(read_msg_list), 4, 'message_read on Pigs should return 2 messages and 2 expandables')
self.assertEqual(set([msg_id1, msg_id9]), set(read_msg_ids), 'message_read on a Pigs message should also get its parent')
self.assertEqual(read_msg_list[1].get('parent_id'), read_msg_list[0].get('id'), 'message_read should set the ancestor to the thread header')
# Data: get expandables
new_threads_exp, new_msg_exp = None, None
for msg in read_msg_list:
if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('id') == -1:
new_threads_exp = msg
elif msg.get('type') == 'expandable':
new_msg_exp = msg
# Do: fetch new messages in second thread, domain from expandable
self.assertIsNotNone(new_msg_exp, 'message_read on Pigs message should have returned a new messages expandable')
domain = new_msg_exp.get('domain', [])
# Test: expandable, conditions in domain
self.assertIn(('id', 'child_of', msg_id1), domain, 'new messages expandable domain should contain a child_of condition')
self.assertIn(('id', '>=', msg_id3), domain, 'new messages expandable domain should contain an id greater than condition')
self.assertIn(('id', '<=', msg_id7), domain, 'new messages expandable domain should contain an id less than condition')
self.assertEqual(new_msg_exp.get('parent_id'), msg_id1, 'new messages expandable should have ancestor_id set to the thread header')
# Do: message_read with domain, thread_level=0, parent_id=msg_id1 (should be imposed by JS)
read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=200, thread_level=0, parent_id=msg_id1)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: other message in thread have been fetch
self.assertEqual(set([msg_id3, msg_id5, msg_id7]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent')
# Test: fetch a new thread, domain from expandable
self.assertIsNotNone(new_threads_exp, 'message_read should have returned a new threads expandable')
domain = new_threads_exp.get('domain', [])
# Test: expandable, conditions in domain
for condition in pigs_domain:
self.assertIn(condition, domain, 'general expandable domain should contain the message_read domain parameter')
# Do: message_read with domain, thread_level=1 (should be imposed by JS)
read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=1, thread_level=1)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(read_msg_list), 1, 'message_read on Pigs should return 1 message because everything else has been fetched')
self.assertEqual([msg_id0], read_msg_ids, 'message_read after 2 More should return only 1 last message')
# ----------------------------------------
# CASE2: message_read with domain, flat
# ----------------------------------------
# Do: read 2 lasts message, flat
read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=2, thread_level=0)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is not set, 1 expandable
self.assertEqual(len(read_msg_list), 3, 'message_read on last Pigs message should return 2 messages and 1 expandable')
self.assertEqual(set([msg_id9, msg_id10]), set(read_msg_ids), 'message_read flat on Pigs last messages should only return those messages')
self.assertFalse(read_msg_list[0].get('parent_id'), 'message_read flat should set the ancestor as False')
self.assertFalse(read_msg_list[1].get('parent_id'), 'message_read flat should set the ancestor as False')
# Data: get expandables
new_threads_exp, new_msg_exp = None, None
for msg in read_msg_list:
if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('id') == -1:
new_threads_exp = msg
# Do: fetch new messages, domain from expandable
self.assertIsNotNone(new_threads_exp, 'message_read flat on the 2 last Pigs messages should have returns a new threads expandable')
domain = new_threads_exp.get('domain', [])
# Test: expandable, conditions in domain
for condition in pigs_domain:
self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter')
# Do: message_read with domain, thread_level=0 (should be imposed by JS)
read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=20, thread_level=0)
read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
# Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
self.assertEqual(len(read_msg_list), 9, 'message_read on Pigs should return 9 messages and 0 expandable')
self.assertEqual([msg_id0, msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8], read_msg_ids,
'message_read, More on flat, should return all remaning messages')
def test_40_needaction(self): def test_40_needaction(self):
""" Tests for mail.message needaction. """ """ Tests for mail.message needaction. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
user_demo = self.res_users.browse(cr, uid, self.user_demo_id) user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
group_pigs_demo = self.mail_group.browse(cr, self.user_demo_id, self.group_pigs_id) group_pigs_demo = self.mail_group.browse(cr, self.user_raoul_id, self.group_pigs_id)
na_admin_base = self.mail_message._needaction_count(cr, uid, domain=[]) na_admin_base = self.mail_message._needaction_count(cr, uid, domain=[])
na_demo_base = self.mail_message._needaction_count(cr, user_demo.id, domain=[]) na_demo_base = self.mail_message._needaction_count(cr, user_raoul.id, domain=[])
# Test: number of unread notification = needaction on mail.message # Test: number of unread notification = needaction on mail.message
notif_ids = self.mail_notification.search(cr, uid, [ notif_ids = self.mail_notification.search(cr, uid, [
@ -558,12 +752,12 @@ class test_mail(TestMailMockups):
self.assertEqual(na_admin_group, 3, 'Admin should have 3 needaction related to Pigs') self.assertEqual(na_admin_group, 3, 'Admin should have 3 needaction related to Pigs')
# Test: demo has 0 new notifications (not a follower, not receiving its own messages), and 0 new needaction # Test: demo has 0 new notifications (not a follower, not receiving its own messages), and 0 new needaction
notif_ids = self.mail_notification.search(cr, uid, [ notif_ids = self.mail_notification.search(cr, uid, [
('partner_id', '=', user_demo.partner_id.id), ('partner_id', '=', user_raoul.partner_id.id),
('read', '=', False) ('read', '=', False)
]) ])
self.assertEqual(len(notif_ids), na_demo_base + 0, 'Demo should have 0 new unread notifications') self.assertEqual(len(notif_ids), na_demo_base + 0, 'Demo should have 0 new unread notifications')
na_demo = self.mail_message._needaction_count(cr, user_demo.id, domain=[]) na_demo = self.mail_message._needaction_count(cr, user_raoul.id, domain=[])
na_demo_group = self.mail_message._needaction_count(cr, user_demo.id, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)]) na_demo_group = self.mail_message._needaction_count(cr, user_raoul.id, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
self.assertEqual(na_demo, na_demo_base + 0, 'Demo should have 0 new needaction') self.assertEqual(na_demo, na_demo_base + 0, 'Demo should have 0 new needaction')
self.assertEqual(na_demo_group, 0, 'Demo should have 0 needaction related to Pigs') self.assertEqual(na_demo_group, 0, 'Demo should have 0 needaction related to Pigs')
@ -581,19 +775,11 @@ class test_mail(TestMailMockups):
reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1', 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) extra='In-Reply-To: %s' % msg1.message_id)
self.mail_group.message_process(cr, uid, None, reply_msg) self.mail_group.message_process(cr, uid, None, reply_msg)
# TDE note: temp various asserts because of the random bug about msg1.child_ids
msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], limit=1)
new_msg = self.mail_message.browse(cr, uid, msg_ids[0])
self.assertEqual(new_msg.parent_id, msg1, 'Newly processed mail_message (%d) should have msg1 as parent' % (new_msg.id))
# 2. References header # 2. References header
reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: Re: 1', 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) extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
self.mail_group.message_process(cr, uid, None, reply_msg2) self.mail_group.message_process(cr, uid, None, reply_msg2)
# TDE note: temp various asserts because of the random bug about msg1.child_ids
msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], limit=1)
new_msg = self.mail_message.browse(cr, uid, msg_ids[0])
self.assertEqual(new_msg.parent_id, msg1, 'Newly processed mail_message should have msg1 as parent')
# 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, not to mail # 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', reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com',
@ -613,34 +799,46 @@ class test_mail(TestMailMockups):
def test_60_message_vote(self): def test_60_message_vote(self):
""" Test designed for the vote/unvote feature. """ """ Test designed for the vote/unvote feature. """
cr, uid = self.cr, self.uid cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
user_admin = self.res_users.browse(cr, uid, uid) # Data: post a message on Pigs
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id) msg_id = group_pigs.message_post(body='My Body', subject='1')
msg1 = group_pigs.message_post(body='My Body', subject='1') msg = self.mail_message.browse(cr, uid, msg_id)
msg1 = self.mail_message.browse(cr, uid, msg1)
# Create user Bert Tartopoils # Do: Admin vote for msg
user_bert_id = self.res_users.create(cr, uid, {'name': 'Bert', 'login': 'bert'}) self.mail_message.vote_toggle(cr, uid, [msg.id])
user_bert = self.res_users.browse(cr, uid, user_bert_id) msg.refresh()
# Test: msg has Admin as voter
# Test: msg1 and msg2 have void vote_user_ids self.assertEqual(set(msg.vote_user_ids), set([user_admin]), 'mail_message vote: after voting, Admin should be in the voter')
self.assertFalse(msg1.vote_user_ids, 'newly created message msg1 has not void vote_user_ids') # Do: Bert vote for msg
# Do: Admin vote for msg1 self.mail_message.vote_toggle(cr, user_raoul.id, [msg.id])
self.mail_message.vote_toggle(cr, uid, [msg1.id]) msg.refresh()
msg1.refresh() # Test: msg has Admin and Bert as voters
# Test: msg1 has Admin as voter self.assertEqual(set(msg.vote_user_ids), set([user_admin, user_raoul]), 'mail_message vote: after voting, Admin and Bert should be in the voters')
self.assertEqual(set(msg1.vote_user_ids), set([user_admin]), 'after voting, Admin is not the voter') # Do: Admin unvote for msg
# Do: Bert vote for msg1 self.mail_message.vote_toggle(cr, uid, [msg.id])
self.mail_message.vote_toggle(cr, user_bert_id, [msg1.id]) msg.refresh()
msg1.refresh() # Test: msg has Bert as voter
# Test: msg1 has Admin and Bert as voters self.assertEqual(set(msg.vote_user_ids), set([user_raoul]), 'mail_message vote: after unvoting, Bert should be in the voter')
self.assertEqual(set(msg1.vote_user_ids), set([user_admin, user_bert]), 'after voting, Admin and Bert are not the voters')
# Do: Admin unvote for msg1
self.mail_message.vote_toggle(cr, uid, [msg1.id])
msg1.refresh()
# Test: msg1 has Bert as voter
self.assertEqual(set(msg1.vote_user_ids), set([user_bert]), 'after unvoting for Admin, Bert is not the voter')
def test_70_message_favorite(self): def test_70_message_favorite(self):
""" Tests for favorites. """ """ Tests for favorites. """
self.assertTrue(1 == 1, 'Test not implemented, do not replace by return True') cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
# Data: post a message on Pigs
msg_id = group_pigs.message_post(body='My Body', subject='1')
msg = self.mail_message.browse(cr, uid, msg_id)
# Do: Admin stars msg
self.mail_message.favorite_toggle(cr, uid, [msg.id])
msg.refresh()
# Test: msg starred by Admin
self.assertEqual(set(msg.favorite_user_ids), set([user_admin]), 'mail_message favorite: after starring, Admin should be in favorite_user_ids')
# Do: Bert stars msg
self.mail_message.favorite_toggle(cr, user_raoul.id, [msg.id])
msg.refresh()
# Test: msg starred by Admin and Raoul
self.assertEqual(set(msg.favorite_user_ids), set([user_admin, user_raoul]), 'mail_message favorite: after starring, Admin and Raoul should be in favorite_user_ids')
# Do: Admin unvote for msg
self.mail_message.favorite_toggle(cr, uid, [msg.id])
msg.refresh()
# Test: msg starred by Raoul
self.assertEqual(set(msg.favorite_user_ids), set([user_raoul]), 'mail_message favorite: after unstarring, Raoul should be in favorite_user_ids')

View File

@ -51,7 +51,40 @@ class test_mail_access_rights(test_mail.TestMailMockups):
self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id) self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
self.partner_raoul_id = self.user_raoul.partner_id.id self.partner_raoul_id = self.user_raoul.partner_id.id
def test_00_mail_message_read_access_rights(self): def test_00_mail_message_search_access_rights(self):
""" Test mail_message search override about access rights. """
cr, uid, group_pigs_id = self.cr, self.uid, self.group_pigs_id
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
# Data: comment subtype for mail.message creation
ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'mail', 'mt_comment')
subtype_id = ref and ref[1] or False
# Data: Birds group, private
group_birds_id = self.mail_group.create(self.cr, self.uid, {'name': 'Birds', 'public': 'private'})
# Data: raoul is member of Pigs
self.mail_group.message_subscribe(cr, uid, [group_pigs_id], [partner_raoul_id])
# Data: various author_ids, partner_ids, documents
msg_id1 = self.mail_message.create(cr, uid, {'subject': '_Test', 'body': 'A', 'subtype_id': subtype_id})
msg_id2 = self.mail_message.create(cr, uid, {'subject': '_Test', 'body': 'A+B', 'partner_ids': [(6, 0, [partner_bert_id])], 'subtype_id': subtype_id})
msg_id3 = self.mail_message.create(cr, uid, {'subject': '_Test', 'body': 'A Pigs', 'model': 'mail.group', 'res_id': group_pigs_id, 'subtype_id': subtype_id})
msg_id4 = self.mail_message.create(cr, uid, {'subject': '_Test', 'body': 'A+B Pigs', 'model': 'mail.group', 'res_id': group_pigs_id, 'partner_ids': [(6, 0, [partner_bert_id])], 'subtype_id': subtype_id})
msg_id5 = self.mail_message.create(cr, uid, {'subject': '_Test', 'body': 'A+R Pigs', 'model': 'mail.group', 'res_id': group_pigs_id, 'partner_ids': [(6, 0, [partner_raoul_id])], 'subtype_id': subtype_id})
msg_id6 = self.mail_message.create(cr, uid, {'subject': '_Test', 'body': 'A Birds', 'model': 'mail.group', 'res_id': group_birds_id, 'subtype_id': subtype_id})
msg_id7 = self.mail_message.create(cr, user_bert_id, {'subject': '_Test', 'body': 'B', 'subtype_id': subtype_id})
msg_id8 = self.mail_message.create(cr, user_bert_id, {'subject': '_Test', 'body': 'B+R', 'partner_ids': [(6, 0, [partner_raoul_id])], 'subtype_id': subtype_id})
# Test: Bert: 2 messages that have Bert in partner_ids + 2 messages as author
msg_ids = self.mail_message.search(cr, user_bert_id, [('subject', 'like', '_Test')])
self.assertEqual(set([msg_id2, msg_id4, msg_id7, msg_id8]), set(msg_ids), 'mail_message search failed')
# Test: Raoul: 3 messages on Pigs Raoul can read (employee can read group with default values), 0 on Birds (private group)
msg_ids = self.mail_message.search(cr, user_raoul_id, [('subject', 'like', '_Test'), ('body', 'like', 'A')])
self.assertEqual(set([msg_id3, msg_id4, msg_id5]), set(msg_ids), 'mail_message search failed')
# Test: Admin: all messages
msg_ids = self.mail_message.search(cr, uid, [('subject', 'like', '_Test')])
self.assertEqual(set([msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8]), set(msg_ids), 'mail_message search failed')
def test_05_mail_message_read_access_rights(self):
""" Test basic mail_message read access rights. """ """ Test basic mail_message read access rights. """
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid
partner_bert_id, partner_raoul_id = self.partner_bert_id, self.partner_raoul_id partner_bert_id, partner_raoul_id = self.partner_bert_id, self.partner_raoul_id
@ -98,10 +131,6 @@ class test_mail_access_rights(test_mail.TestMailMockups):
self.assertRaises(except_orm, self.mail_message.read, self.assertRaises(except_orm, self.mail_message.read,
cr, user_bert_id, message_id) cr, user_bert_id, message_id)
def test_05_mail_message_search_access_rights(self):
""" 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): def test_10_mail_flow_access_rights(self):
""" Test a Chatter-looks alike flow. """ """ Test a Chatter-looks alike flow. """
cr, uid = self.cr, self.uid cr, uid = self.cr, self.uid

View File

@ -156,12 +156,6 @@ class mail_compose_message(osv.TransientModel):
reply_subject = tools.ustr(message_data.subject or '') reply_subject = tools.ustr(message_data.subject or '')
if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject: if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject:
reply_subject = "%s %s" % (re_prefix, reply_subject) reply_subject = "%s %s" % (re_prefix, reply_subject)
# create the reply in the body
reply_body = _('<div>On %(date)s, %(sender_name)s wrote:<blockquote>%(body)s</blockquote></div>') % {
'date': message_data.date if message_data.date else '',
'sender_name': message_data.author_id.name,
'body': message_data.body,
}
# get partner_ids from original message # get partner_ids from original message
partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else [] partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else []
@ -170,7 +164,6 @@ class mail_compose_message(osv.TransientModel):
'model': message_data.model, 'model': message_data.model,
'res_id': message_data.res_id, 'res_id': message_data.res_id,
'parent_id': message_data.id, 'parent_id': message_data.id,
'body': reply_body,
'subject': reply_subject, 'subject': reply_subject,
'partner_ids': partner_ids, 'partner_ids': partner_ids,
'content_subtype': 'html', 'content_subtype': 'html',

View File

@ -14,22 +14,16 @@
<field name="parent_id" invisible="1"/> <field name="parent_id" invisible="1"/>
<field name="content_subtype" invisible="1"/> <field name="content_subtype" invisible="1"/>
<!-- visible wizard --> <!-- visible wizard -->
<field name="subject" placeholder="Subject..."
attrs="{'invisible':[('content_subtype', '=', 'plain')]}"/>/>
<field name="partner_ids" widget="many2many_tags" placeholder="Add contacts to notify..." <field name="partner_ids" widget="many2many_tags" placeholder="Add contacts to notify..."
context="{'force_email':True}" context="{'force_email':True}"
on_change="onchange_partner_ids(partner_ids)"/> on_change="onchange_partner_ids(partner_ids)" required="1"/>
<field name="subject" placeholder="Subject..."
attrs="{'invisible':[('content_subtype', '=', 'plain')]}"/>
</group> </group>
<notebook> <field name="body"/>
<page string="Body"> <field name="attachment_ids" widget="one2many_binary" blockui="0"/>
<field name="body" nolabel="1"/>
</page>
<page string="Attachments">
<field name="attachment_ids" colspan="4" nolabel="1"/>
</page>
</notebook>
<footer> <footer>
<button string="Send" name="send_mail" type="object" class="oe_highlight" /> <button string="Send" name="send_mail" type="object" class="oe_highlight"/>
or or
<button string="Cancel" class="oe_link" special="cancel" /> <button string="Cancel" class="oe_link" special="cancel" />
</footer> </footer>

View File

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

View File

@ -107,8 +107,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -189,8 +189,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -42,7 +42,7 @@ Notes can be found in the 'Home' menu.
'mail', 'mail',
], ],
'data': [ 'data': [
'security/res.groups.csv', 'security/note_security.xml',
'security/ir.rule.xml', 'security/ir.rule.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'note_data.xml', 'note_data.xml',

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="group_note_fancy" model="res.groups">
<field name="name">Notes / Fancy mode</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_note_fancy'))]"/>
</record>
</data>
</openerp>

View File

@ -1,2 +0,0 @@
id,name,implied_ids/id,category_id/id
group_note_fancy,Notes / Fancy mode,,base.module_category_hidden
1 id name implied_ids/id category_id/id
2 group_note_fancy Notes / Fancy mode base.module_category_hidden

View File

@ -6,6 +6,7 @@ openerp.pad = function(instance) {
content: "", content: "",
render_value: function() { render_value: function() {
var self = this; var self = this;
var _super = _.bind(this._super, this);
if (this.get("value") === false || this.get("value") === "") { if (this.get("value") === false || this.get("value") === "") {
self.view.dataset.call('pad_generate_url',{context:{ self.view.dataset.call('pad_generate_url',{context:{
model: self.view.model, model: self.view.model,
@ -13,7 +14,7 @@ openerp.pad = function(instance) {
object_id: self.view.datarecord.id object_id: self.view.datarecord.id
}}).then(function(data) { }}).then(function(data) {
if(data&&data.url){ if(data&&data.url){
_super.apply(self,[data.url]); _super(data.url);
self.renderElement(); self.renderElement();
} }
}); });

View File

@ -103,8 +103,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -166,8 +166,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -10,7 +10,7 @@
<menuitem id="menu_project_management" name="Project" parent="base.menu_main_pm" sequence="1"/> <menuitem id="menu_project_management" name="Project" parent="base.menu_main_pm" sequence="1"/>
<menuitem id="base.menu_definitions" name="Configuration" parent="base.menu_main_pm" sequence="60"/> <menuitem id="base.menu_definitions" name="Configuration" parent="base.menu_main_pm" sequence="60"/>
<record id="act_project_project_2_project_task_all" model="ir.actions.act_window"> <record id="act_project_project_2_project_task_all" model="ir.actions.act_window">
<field name="res_model">project.task</field> <field name="res_model">project.task</field>
<field name="view_type">form</field> <field name="view_type">form</field>
@ -141,9 +141,8 @@
<field name="date" string="End Date"/> <field name="date" string="End Date"/>
<field name="priority" groups="base.group_no_one"/> <field name="priority" groups="base.group_no_one"/>
<field name="active" attrs="{'invisible':[('state','in',['open', 'pending', 'template'])]}"/> <field name="active" attrs="{'invisible':[('state','in',['open', 'pending', 'template'])]}"/>
<field name="currency_id" groups="base.group_multi_currency" required="1"/> <field name="currency_id" groups="base.group_multi_currency" required="1"/>
<field name="parent_id" domain="[('id','!=',analytic_account_id)]" context="{'current_model': 'project.project'}"/> <field name="parent_id" string="Parent" help="Append this project to another one using analytic accounts hierarchy" domain="[('id','!=',analytic_account_id)]" context="{'current_model': 'project.project'}" />
</group> </group>
</group> </group>
</page> </page>
@ -153,14 +152,14 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<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."/> <field name="message_follower_ids" widget="mail_followers" help="Follow this project to automatically follow all related tasks and issues."/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
</record> </record>
<record id="view_project_project_filter" model="ir.ui.view"> <record id="view_project_project_filter" model="ir.ui.view">
<field name="name">project.project.select</field> <field name="name">project.project.select</field>
<field name="model">project.project</field> <field name="model">project.project</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
@ -230,7 +229,7 @@
<div class="oe_dropdown_toggle oe_dropdown_kanban"> <div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">í</span> <span class="oe_e">í</span>
<ul class="oe_dropdown_menu"> <ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')"><li><a type="edit">Edit...</a></li></t> <t t-if="widget.view.is_action_enabled('edit')"><li><a type="edit">Project Settings</a></li></t>
<t t-if="widget.view.is_action_enabled('delete')"><li><a type="delete">Delete</a></li></t> <t t-if="widget.view.is_action_enabled('delete')"><li><a type="delete">Delete</a></li></t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li> <li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul> </ul>
@ -468,14 +467,14 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>
</record> </record>
<!-- Project Task Kanban View --> <!-- Project Task Kanban View -->
<record model="ir.ui.view" id="view_task_kanban"> <record model="ir.ui.view" id="view_task_kanban">
<field name="name">project.task.kanban</field> <field name="name">project.task.kanban</field>
<field name="model">project.task</field> <field name="model">project.task</field>

View File

@ -159,8 +159,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -14,7 +14,7 @@
<field name="partner_id"/> <field name="partner_id"/>
</field> </field>
<xpath expr="//div[contains(@class, 'oe_kanban_project_list')]" position="inside"> <xpath expr="//div[contains(@class, 'oe_kanban_project_list')]" position="inside">
<a t-if="record.use_timesheets.raw_value" <a t-if="record.use_timesheets.raw_value" style="margin-right: 10px"
name="open_timesheets" type="object"><field name="hours_quantity"/> Hours</a> name="open_timesheets" type="object"><field name="hours_quantity"/> Hours</a>
</xpath> </xpath>
</field> </field>

View File

@ -287,8 +287,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

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

View File

@ -69,7 +69,7 @@ class sale_configuration(osv.osv_memory):
But the possibility to change these values is still available. But the possibility to change these values is still available.
This installs the module analytic_user_function."""), This installs the module analytic_user_function."""),
'module_project': fields.boolean("Project"), 'module_project': fields.boolean("Project"),
'module_sale_stock': fields.boolean("Sale and Warehouse Management", 'module_sale_stock': fields.boolean("Trigger delivery orders automatically from sale orders",
help="""Allows you to Make Quotation, Sale Order using different Order policy and Manage Related Stock. help="""Allows you to Make Quotation, Sale Order using different Order policy and Manage Related Stock.
This installs the module sale_stock."""), This installs the module sale_stock."""),
} }

View File

@ -309,8 +309,8 @@
</notebook> </notebook>
</sheet> </sheet>
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" widget="mail_thread"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div> </div>
</form> </form>
</field> </field>

View File

@ -37,7 +37,7 @@ modules.
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
'images': ['images/crm_statistics_dashboard.jpeg', 'images/opportunity_to_quote.jpeg'], 'images': ['images/crm_statistics_dashboard.jpeg', 'images/opportunity_to_quote.jpeg'],
'depends': ['sale_stock', 'crm'], 'depends': ['sale', 'crm'],
'data': [ 'data': [
'wizard/crm_make_sale_view.xml', 'wizard/crm_make_sale_view.xml',
'sale_crm_view.xml', 'sale_crm_view.xml',

View File

@ -31,7 +31,7 @@ Price and Cost Price.
""", """,
'author':'OpenERP SA', 'author':'OpenERP SA',
'images':['images/sale_margin.jpeg'], 'images':['images/sale_margin.jpeg'],
'depends':['sale_stock'], 'depends':['sale'],
'demo':['sale_margin_demo.xml'], 'demo':['sale_margin_demo.xml'],
'test': ['test/sale_margin.yml'], 'test': ['test/sale_margin.yml'],
'data':['security/ir.model.access.csv','sale_margin_view.xml'], 'data':['security/ir.model.access.csv','sale_margin_view.xml'],

View File

@ -14,7 +14,6 @@
product_uom: product.product_uom_unit product_uom: product.product_uom_unit
product_uom_qty: 100.0 product_uom_qty: 100.0
state: draft state: draft
delay: 7.0
product_id: product.product_product_24 product_id: product.product_product_24
product_uos_qty: 100.0 product_uos_qty: 100.0
th_weight: 0.0 th_weight: 0.0
@ -23,7 +22,6 @@
partner_id: base.res_partner_4 partner_id: base.res_partner_4
partner_invoice_id: base.res_partner_address_7 partner_invoice_id: base.res_partner_address_7
partner_shipping_id: base.res_partner_address_7 partner_shipping_id: base.res_partner_address_7
picking_policy: direct
pricelist_id: product.list0 pricelist_id: product.list0
shop_id: sale.sale_shop_1 shop_id: sale.sale_shop_1
- -

View File

@ -618,7 +618,7 @@ class stock_picking(osv.osv):
def create(self, cr, user, vals, context=None): def create(self, cr, user, vals, context=None):
if ('name' not in vals) or (vals.get('name')=='/'): if ('name' not in vals) or (vals.get('name')=='/'):
seq_obj_name = 'stock.picking.' + vals['type'] seq_obj_name = self._name
vals['name'] = self.pool.get('ir.sequence').get(cr, user, seq_obj_name) vals['name'] = self.pool.get('ir.sequence').get(cr, user, seq_obj_name)
new_id = super(stock_picking, self).create(cr, user, vals, context) new_id = super(stock_picking, self).create(cr, user, vals, context)
if new_id: if new_id:

View File

@ -912,8 +912,8 @@
</xpath> </xpath>
<xpath expr="/form/sheet" position="after"> <xpath expr="/form/sheet" position="after">
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
</div> </div>
</xpath> </xpath>
</data> </data>
@ -1038,8 +1038,8 @@
</xpath> </xpath>
<xpath expr="/form/sheet" position="after"> <xpath expr="/form/sheet" position="after">
<div class="oe_chatter"> <div class="oe_chatter">
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
<field name="message_follower_ids" widget="mail_followers"/> <field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
</div> </div>
</xpath> </xpath>
</data> </data>

View File

@ -82,7 +82,7 @@ instance.web_shortcuts.Shortcuts = instance.web.Widget.extend({
// TODO: Use do_action({menu_id: id, type: 'ir.actions.menu'}) // TODO: Use do_action({menu_id: id, type: 'ir.actions.menu'})
self.rpc('/web/menu/action', {'menu_id': id}).then(function(ir_menu_data) { self.rpc('/web/menu/action', {'menu_id': id}).then(function(ir_menu_data) {
if (ir_menu_data.action.length){ if (ir_menu_data.action.length){
instance.webclient.user_menu.on_action({action_id: ir_menu_data.action[0][2].id}); instance.webclient.on_menu_action({action_id: ir_menu_data.action[0][2].id});
} }
}); });
this.$el.find('.oe_systray_shortcuts').trigger('mouseout'); this.$el.find('.oe_systray_shortcuts').trigger('mouseout');