[MERGE] Forward porting of the 7.0 addons branch inside trunk until rev 8561.

bzr revid: tde@openerp.com-20130110125129-9g99h3heldcnspfh
This commit is contained in:
Thibault Delavallée 2013-01-10 13:51:29 +01:00
commit fb3dd0ebc2
29 changed files with 558 additions and 302 deletions

View File

@ -1463,6 +1463,7 @@ class account_move(osv.osv):
line_id = self.pool.get('account.move.line').create(cr, uid, { line_id = self.pool.get('account.move.line').create(cr, uid, {
'name': _(mode.capitalize()+' Centralisation'), 'name': _(mode.capitalize()+' Centralisation'),
'centralisation': mode, 'centralisation': mode,
'partner_id': False,
'account_id': account_id, 'account_id': account_id,
'move_id': move.id, 'move_id': move.id,
'journal_id': move.journal_id.id, 'journal_id': move.journal_id.id,
@ -1501,6 +1502,7 @@ class account_move(osv.osv):
line_id = self.pool.get('account.move.line').create(cr, uid, { line_id = self.pool.get('account.move.line').create(cr, uid, {
'name': _('Currency Adjustment'), 'name': _('Currency Adjustment'),
'centralisation': 'currency', 'centralisation': 'currency',
'partner_id': False,
'account_id': account_id, 'account_id': account_id,
'move_id': move.id, 'move_id': move.id,
'journal_id': move.journal_id.id, 'journal_id': move.journal_id.id,

View File

@ -1484,11 +1484,10 @@ class account_invoice_line(osv.osv):
new_price = res_final['value']['price_unit'] * currency.rate new_price = res_final['value']['price_unit'] * currency.rate
res_final['value']['price_unit'] = new_price res_final['value']['price_unit'] = new_price
if result['uos_id'] != res.uom_id.id: if result['uos_id'] and result['uos_id'] != res.uom_id.id:
selected_uom = self.pool.get('product.uom_id').browse(cr, uid, result['uos_id'], context=context) selected_uom = self.pool.get('product.uom').browse(cr, uid, result['uos_id'], context=context)
if res.uom_id.category_id.id == selected_uom.category_id.id: new_price = self.pool.get('product.uom')._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uos_id'])
new_price = res_final['value']['price_unit'] * uom_id.factor_inv res_final['value']['price_unit'] = new_price
res_final['value']['price_unit'] = new_price
return res_final return res_final
def uos_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None): def uos_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, currency_id=False, context=None, company_id=None):

View File

@ -97,7 +97,7 @@
<form string="Account Period" version="7.0"> <form string="Account Period" version="7.0">
<header> <header>
<button string="Close Period" name="%(account.action_account_period_close)d" type="action" class="oe_highlight" states="draft"/> <button string="Close Period" name="%(account.action_account_period_close)d" type="action" class="oe_highlight" states="draft"/>
<button name="action_draft" states="done" string="Set to Draft" type="object" groups="account.group_account_manager"/> <button name="action_draft" states="done" string="Re-Open Period" type="object" groups="account.group_account_manager"/>
<field name="state" widget="statusbar" nolabel="1"/> <field name="state" widget="statusbar" nolabel="1"/>
</header> </header>
<sheet> <sheet>

View File

@ -5,8 +5,8 @@
<field name="name">account.open.closed.fiscalyear.form</field> <field name="name">account.open.closed.fiscalyear.form</field>
<field name="model">account.open.closed.fiscalyear</field> <field name="model">account.open.closed.fiscalyear</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Cancel Fiscal Year Opening Entries" version="7.0"> <form string="Cancel Fiscal Year Closing Entries" version="7.0">
<separator string="Cancel Fiscal Year Opening Entries"/> <separator string="Cancel Fiscal Year Closing Entries"/>
<p class="oe_grey"> <p class="oe_grey">
This wizard will remove the end of year journal entries of selected fiscal year. Note that you can run this wizard many times for the same fiscal year. This wizard will remove the end of year journal entries of selected fiscal year. Note that you can run this wizard many times for the same fiscal year.
</p> </p>
@ -14,7 +14,7 @@ This wizard will remove the end of year journal entries of selected fiscal year.
<field name="fyear_id" domain="[('state','=','draft'), ('end_journal_period_id', '!=', False)]"/> <field name="fyear_id" domain="[('state','=','draft'), ('end_journal_period_id', '!=', False)]"/>
</group> </group>
<footer> <footer>
<button string="Cancel Opening Entries" name="remove_entries" type="object" class="oe_highlight"/> <button string="Cancel Closing Entries" name="remove_entries" type="object" class="oe_highlight"/>
or or
<button string="Discard" class="oe_link" special="cancel"/> <button string="Discard" class="oe_link" special="cancel"/>
</footer> </footer>
@ -23,7 +23,7 @@ This wizard will remove the end of year journal entries of selected fiscal year.
</record> </record>
<record id="action_account_open_closed_fiscalyear" model="ir.actions.act_window"> <record id="action_account_open_closed_fiscalyear" model="ir.actions.act_window">
<field name="name">Cancel Opening Entries</field> <field name="name">Cancel Closing Entries</field>
<field name="res_model">account.open.closed.fiscalyear</field> <field name="res_model">account.open.closed.fiscalyear</field>
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>

View File

@ -52,7 +52,7 @@ class res_partner(osv.Model):
(not partner.signup_expiration or dt <= partner.signup_expiration) (not partner.signup_expiration or dt <= partner.signup_expiration)
return res return res
def _get_signup_url_for_action(self, cr, uid, ids, action='login', view_type=None, menu_id=None, res_id=None, context=None): def _get_signup_url_for_action(self, cr, uid, ids, action='login', view_type=None, menu_id=None, res_id=None, model=None, context=None):
""" generate a signup url for the given partner ids and action, possibly overriding """ generate a signup url for the given partner ids and action, possibly overriding
the url state components (menu_id, id, view_type) """ the url state components (menu_id, id, view_type) """
res = dict.fromkeys(ids, False) res = dict.fromkeys(ids, False)
@ -61,6 +61,7 @@ class res_partner(osv.Model):
# when required, make sure the partner has a valid signup token # when required, make sure the partner has a valid signup token
if context and context.get('signup_valid') and not partner.user_ids: if context and context.get('signup_valid') and not partner.user_ids:
self.signup_prepare(cr, uid, [partner.id], context=context) self.signup_prepare(cr, uid, [partner.id], context=context)
partner.refresh()
# the parameters to encode for the query and fragment part of url # the parameters to encode for the query and fragment part of url
query = {'db': cr.dbname} query = {'db': cr.dbname}
@ -78,6 +79,8 @@ class res_partner(osv.Model):
fragment['view_type'] = view_type fragment['view_type'] = view_type
if menu_id: if menu_id:
fragment['menu_id'] = menu_id fragment['menu_id'] = menu_id
if model:
fragment['model'] = model
if res_id: if res_id:
fragment['id'] = res_id fragment['id'] = res_id

View File

@ -397,7 +397,8 @@ class crm_lead(base_stage, format_address, osv.osv):
search_domain += [('|')] * len(section_ids) search_domain += [('|')] * len(section_ids)
for section_id in section_ids: for section_id in section_ids:
search_domain.append(('section_ids', '=', section_id)) search_domain.append(('section_ids', '=', section_id))
search_domain.append(('case_default', '=', True)) else:
search_domain.append(('case_default', '=', True))
# AND with cases types # AND with cases types
search_domain.append(('type', 'in', types)) search_domain.append(('type', 'in', types))
# AND with the domain in parameter # AND with the domain in parameter
@ -423,15 +424,15 @@ class crm_lead(base_stage, format_address, osv.osv):
def case_mark_lost(self, cr, uid, ids, context=None): def case_mark_lost(self, cr, uid, ids, context=None):
""" Mark the case as lost: state=cancel and probability=0 """ """ Mark the case as lost: state=cancel and probability=0 """
for lead in self.browse(cr, uid, ids): for lead in self.browse(cr, uid, ids):
stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0)], context=context) stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 0.0),('on_change','=',True)], context=context)
if stage_id: if stage_id:
self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context) self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
return True return True
def case_mark_won(self, cr, uid, ids, context=None): def case_mark_won(self, cr, uid, ids, context=None):
""" Mark the case as lost: state=done and probability=100 """ """ Mark the case as won: state=done and probability=100 """
for lead in self.browse(cr, uid, ids): for lead in self.browse(cr, uid, ids):
stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0)], context=context) stage_id = self.stage_find(cr, uid, [lead], lead.section_id.id or False, [('probability', '=', 100.0),('on_change','=',True)], context=context)
if stage_id: if stage_id:
self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context) self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
return True return True
@ -677,29 +678,23 @@ class crm_lead(base_stage, format_address, osv.osv):
contact_id = False contact_id = False
if customer: if customer:
contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default'] contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
if not section_id: if not section_id:
section_id = lead.section_id and lead.section_id.id or False section_id = lead.section_id and lead.section_id.id or False
val = {
if section_id:
stage_ids = crm_stage.search(cr, uid, [('sequence', '>=', 1), ('section_ids', '=', section_id), ('probability', '>', 0)])
else:
stage_ids = crm_stage.search(cr, uid, [('sequence', '>=', 1), ('probability', '>', 0)])
stage_id = stage_ids and stage_ids[0] or False
return {
'planned_revenue': lead.planned_revenue, 'planned_revenue': lead.planned_revenue,
'probability': lead.probability, 'probability': lead.probability,
'name': lead.name, 'name': lead.name,
'partner_id': customer and customer.id or False, 'partner_id': customer and customer.id or False,
'user_id': (lead.user_id and lead.user_id.id), 'user_id': (lead.user_id and lead.user_id.id),
'type': 'opportunity', 'type': 'opportunity',
'stage_id': stage_id or False,
'date_action': fields.datetime.now(), 'date_action': fields.datetime.now(),
'date_open': fields.datetime.now(), 'date_open': fields.datetime.now(),
'email_from': customer and customer.email or lead.email_from, 'email_from': customer and customer.email or lead.email_from,
'phone': customer and customer.phone or lead.phone, 'phone': customer and customer.phone or lead.phone,
} }
if not lead.stage_id or lead.stage_id.type=='lead':
val['stage_id'] = self.stage_find(cr, uid, [lead], section_id, [('state', '=', 'draft'),('type', 'in', ('opportunity','both'))], context=context)
return val
def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None): def convert_opportunity(self, cr, uid, ids, partner_id, user_ids=False, section_id=False, context=None):
customer = False customer = False
@ -966,6 +961,11 @@ class crm_lead(base_stage, format_address, osv.osv):
# Mail Gateway # Mail Gateway
# ---------------------------------------- # ----------------------------------------
def message_get_reply_to(self, cr, uid, ids, context=None):
""" Override to get the reply_to of the parent project. """
return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
for lead in self.browse(cr, uid, ids, context=context)]
def message_new(self, cr, uid, msg, custom_values=None, context=None): def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway """ Overrides mail_thread message_new that is called by the mailgateway
through message_process. through message_process.

View File

@ -65,6 +65,7 @@
<field name="name">Lost</field> <field name="name">Lost</field>
<field eval="1" name="case_default"/> <field eval="1" name="case_default"/>
<field eval="True" name="fold"/> <field eval="True" name="fold"/>
<field eval="1" name="on_change"/>
<field name="state">cancel</field> <field name="state">cancel</field>
<field eval="'0'" name="probability"/> <field eval="'0'" name="probability"/>
<field eval="'17'" name="sequence"/> <field eval="'17'" name="sequence"/>

View File

@ -514,7 +514,7 @@
<field name="type_id" invisible="1"/> <field name="type_id" invisible="1"/>
<field name="stage_id"/> <field name="stage_id"/>
<field name="planned_revenue" sum="Expected Revenues"/> <field name="planned_revenue" sum="Expected Revenues"/>
<field name="probability" widget="progressbar" avg="Avg. of Probability"/> <field name="probability" avg="Avg. of Probability"/>
<field name="section_id" invisible="context.get('invisible_section', True)"/> <field name="section_id" invisible="context.get('invisible_section', True)"/>
<field name="user_id"/> <field name="user_id"/>
<field name="priority" invisible="1"/> <field name="priority" invisible="1"/>

View File

@ -53,7 +53,7 @@
<label for="content" string="Template"/> <label for="content" string="Template"/>
that will be used as a content template for all new page of this category. that will be used as a content template for all new page of this category.
</div> </div>
<field name="content" placeholder="e.g. Once upon a time..." class="oe_edit_only"/> <field name="content" placeholder="e.g. Once upon a time..." class="oe_edit_only" widget="html"/>
<div class="oe_document_page"> <div class="oe_document_page">
<field name="display_content" widget="html" class="oe_view_only" options='{"safe": True}'/> <field name="display_content" widget="html" class="oe_view_only" options='{"safe": True}'/>
</div> </div>

View File

@ -483,15 +483,18 @@ class fleet_vehicle_log_fuel(osv.Model):
#make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
#liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
#of 3.0/2=1.5) #of 3.0/2=1.5)
#If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
#onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
#computation to 2 decimal
liter = float(liter) liter = float(liter)
price_per_liter = float(price_per_liter) price_per_liter = float(price_per_liter)
amount = float(amount) amount = float(amount)
if liter > 0 and price_per_liter > 0: if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
return {'value' : {'amount' : liter * price_per_liter,}} return {'value' : {'amount' : round(liter * price_per_liter,2),}}
elif liter > 0 and amount > 0: elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
return {'value' : {'price_per_liter' : amount / liter,}} return {'value' : {'price_per_liter' : round(amount / liter,2),}}
elif price_per_liter > 0 and amount > 0: elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
return {'value' : {'liter' : amount / price_per_liter,}} return {'value' : {'liter' : round(amount / price_per_liter,2),}}
else : else :
return {} return {}
@ -500,15 +503,18 @@ class fleet_vehicle_log_fuel(osv.Model):
#make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
#liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
#of 3.0/2=1.5) #of 3.0/2=1.5)
#If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
#onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
#computation to 2 decimal
liter = float(liter) liter = float(liter)
price_per_liter = float(price_per_liter) price_per_liter = float(price_per_liter)
amount = float(amount) amount = float(amount)
if price_per_liter > 0 and liter > 0: if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
return {'value' : {'amount' : liter * price_per_liter,}} return {'value' : {'amount' : round(liter * price_per_liter,2),}}
elif price_per_liter > 0 and amount > 0: elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
return {'value' : {'liter' : amount / price_per_liter,}} return {'value' : {'liter' : round(amount / price_per_liter,2),}}
elif liter > 0 and amount > 0: elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
return {'value' : {'price_per_liter' : amount / liter,}} return {'value' : {'price_per_liter' : round(amount / liter,2),}}
else : else :
return {} return {}
@ -517,16 +523,20 @@ class fleet_vehicle_log_fuel(osv.Model):
#make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per #make any difference between 3.0 and 3). This cause a problem if you encode, for example, 2 liters at 1.5 per
#liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead #liter => total is computed as 3.0, then trigger an onchange that recomputes price_per_liter as 3/2=1 (instead
#of 3.0/2=1.5) #of 3.0/2=1.5)
#If there is no change in the result, we return an empty dict to prevent an infinite loop due to the 3 intertwine
#onchange. And in order to verify that there is no change in the result, we have to limit the precision of the
#computation to 2 decimal
liter = float(liter) liter = float(liter)
price_per_liter = float(price_per_liter) price_per_liter = float(price_per_liter)
amount = float(amount) amount = float(amount)
if amount > 0 and liter > 0: if amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
return {'value': {'price_per_liter': amount / liter,}} return {'value': {'price_per_liter': round(amount / liter,2),}}
elif amount > 0 and price_per_liter > 0: elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
return {'value': {'liter': amount / price_per_liter,}} return {'value': {'liter': round(amount / price_per_liter,2),}}
elif liter > 0 and price_per_liter > 0: elif liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
return {'value': {'amount': liter * price_per_liter,}} return {'value': {'amount': round(liter * price_per_liter,2),}}
return {} else :
return {}
def _get_default_service_type(self, cr, uid, context): def _get_default_service_type(self, cr, uid, context):
try: try:

View File

@ -212,7 +212,6 @@ class hr_expense_expense(osv.osv):
if analytic_journal_ids: if analytic_journal_ids:
account_journal.write(cr, uid, [journal.id], {'analytic_journal_id': analytic_journal_ids[0]}, context=context) account_journal.write(cr, uid, [journal.id], {'analytic_journal_id': analytic_journal_ids[0]}, context=context)
voucher_id = voucher_obj.create(cr, uid, voucher, context=context) voucher_id = voucher_obj.create(cr, uid, voucher, context=context)
wkf_service.trg_validate(uid, 'account.voucher', voucher_id, 'proforma_voucher', cr)
self.write(cr, uid, [exp.id], {'voucher_id': voucher_id, 'state': 'done'}, context=context) self.write(cr, uid, [exp.id], {'voucher_id': voucher_id, 'state': 'done'}, context=context)
return True return True

View File

@ -283,7 +283,14 @@ class hr_holidays(osv.osv):
result['value']['number_of_days_temp'] = 0 result['value']['number_of_days_temp'] = 0
return result return result
def create(self, cr, uid, values, context=None):
""" Override to avoid automatic logging of creation """
if context is None:
context = {}
context = dict(context, mail_create_nolog=True)
return super(hr_holidays, self).create(cr, uid, values, context=context)
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):
check_fnct = self.pool.get('hr.holidays.status').check_access_rights check_fnct = self.pool.get('hr.holidays.status').check_access_rights
for holiday in self.browse(cr, uid, ids, context=context): for holiday in self.browse(cr, uid, ids, context=context):
@ -430,7 +437,7 @@ class hr_holidays(osv.osv):
def holidays_first_validate_notificate(self, cr, uid, ids, context=None): def holidays_first_validate_notificate(self, cr, uid, ids, context=None):
for obj in self.browse(cr, uid, ids, context=context): for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id], self.message_post(cr, uid, [obj.id],
_("Request <b>approved</b>, waiting second validation."), context=context) _("Request approved, waiting second validation."), context=context)
class resource_calendar_leaves(osv.osv): class resource_calendar_leaves(osv.osv):
_inherit = "resource.calendar.leaves" _inherit = "resource.calendar.leaves"

View File

@ -49,7 +49,7 @@
<record id="mt_holidays_confirmed" model="mail.message.subtype"> <record id="mt_holidays_confirmed" model="mail.message.subtype">
<field name="name">To Approve</field> <field name="name">To Approve</field>
<field name="res_model">hr.holidays</field> <field name="res_model">hr.holidays</field>
<field name="description">Request confirmed, waiting confirmation</field> <field name="description">Request created and waiting confirmation</field>
</record> </record>
<record id="mt_holidays_approved" model="mail.message.subtype"> <record id="mt_holidays_approved" model="mail.message.subtype">
<field name="name">Approved</field> <field name="name">Approved</field>

View File

@ -75,13 +75,6 @@ class mail_notification(osv.Model):
if not cr.fetchone(): if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)') cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
def create(self, cr, uid, vals, context=None):
""" Override of create to check that we can not create a notification
for a message the user can not read. """
if self.pool.get('mail.message').check_access_rights(cr, uid, 'read'):
return super(mail_notification, self).create(cr, uid, vals, context=context)
return False
def get_partners_to_notify(self, cr, uid, message, context=None): def get_partners_to_notify(self, cr, uid, message, context=None):
""" Return the list of partners to notify, based on their preferences. """ Return the list of partners to notify, based on their preferences.

View File

@ -21,10 +21,13 @@
import base64 import base64
import logging import logging
from openerp import tools from urllib import urlencode
from urlparse import urljoin
from openerp import tools
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.osv.orm import except_orm
from openerp.tools.translate import _ from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -158,7 +161,43 @@ class mail_mail(osv.Model):
:param browse_record mail: mail.mail browse_record :param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner :param browse_record partner: specific recipient partner
""" """
return mail.body_html body = mail.body_html
# partner is a user, link to a related document (incentive to install portal)
if partner and partner.user_ids and mail.model and mail.res_id \
and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
related_user = partner.user_ids[0]
try:
self.pool.get(mail.model).check_access_rule(cr, related_user.id, [mail.res_id], 'read', context=context)
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# the parameters to encode for the query and fragment part of url
query = {'db': cr.dbname}
fragment = {
'login': related_user.login,
'model': mail.model,
'id': mail.res_id,
}
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment)))
text = _("""<p>Access this document <a href="%s">directly in OpenERP</a></p>""") % url
body = tools.append_content_to_html(body, ("<div><p>%s</p></div>" % text), plaintext=False)
except except_orm, e:
pass
return body
def send_get_mail_reply_to(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method
is to be inherited by Portal, to add a link for signing in, in
each notification email a partner receives.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
if mail.reply_to:
return mail.reply_to
if not mail.model or not mail.res_id:
return False
if not hasattr(self.pool.get(mail.model), 'message_get_reply_to'):
return False
return self.pool.get(mail.model).message_get_reply_to(cr, uid, [mail.res_id], context=context)[0]
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
""" Return a dictionary for specific email values, depending on a """ Return a dictionary for specific email values, depending on a
@ -169,6 +208,7 @@ class mail_mail(osv.Model):
""" """
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context) subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
reply_to = self.send_get_mail_reply_to(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body) body_alternative = tools.html2plaintext(body)
email_to = [partner.email] if partner else tools.email_split(mail.email_to) email_to = [partner.email] if partner else tools.email_split(mail.email_to)
return { return {
@ -176,6 +216,7 @@ class mail_mail(osv.Model):
'body_alternative': body_alternative, 'body_alternative': body_alternative,
'subject': subject, 'subject': subject,
'email_to': email_to, 'email_to': email_to,
'reply_to': reply_to,
} }
def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None): def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None):
@ -219,7 +260,7 @@ class mail_mail(osv.Model):
body = email.get('body'), body = email.get('body'),
body_alternative = email.get('body_alternative'), body_alternative = email.get('body_alternative'),
email_cc = tools.email_split(mail.email_cc), email_cc = tools.email_split(mail.email_cc),
reply_to = mail.reply_to, reply_to = email.get('reply_to'),
attachments = attachments, attachments = attachments,
message_id = mail.message_id, message_id = mail.message_id,
references = mail.references, references = mail.references,

View File

@ -25,6 +25,7 @@ import dateutil
import email import email
import logging import logging
import pytz import pytz
import re
import time import time
import xmlrpclib import xmlrpclib
from email.message import Message from email.message import Message
@ -39,7 +40,7 @@ _logger = logging.getLogger(__name__)
def decode_header(message, header, separator=' '): def decode_header(message, header, separator=' '):
return separator.join(map(decode, message.get_all(header, []))) return separator.join(map(decode, filter(None, message.get_all(header, []))))
class mail_thread(osv.AbstractModel): class mail_thread(osv.AbstractModel):
@ -387,6 +388,18 @@ class mail_thread(osv.AbstractModel):
return [('message_unread', '=', True)] return [('message_unread', '=', True)]
return [] return []
#------------------------------------------------------
# Email specific
#------------------------------------------------------
def message_get_reply_to(self, cr, uid, ids, context=None):
if not self._inherits.get('mail.alias'):
return [False for id in ids]
return ["%s@%s" % (record['alias_name'], record['alias_domain'])
if record.get('alias_domain') and record.get('alias_name')
else False
for record in self.read(cr, uid, ids, ['alias_name', 'alias_domain'], context=context)]
#------------------------------------------------------ #------------------------------------------------------
# Mail gateway # Mail gateway
#------------------------------------------------------ #------------------------------------------------------
@ -635,14 +648,14 @@ class mail_thread(osv.AbstractModel):
""" """
if context is None: if context is None:
context = {} context = {}
data = {}
if isinstance(custom_values, dict):
data = custom_values.copy()
model = context.get('thread_model') or self._name model = context.get('thread_model') or self._name
model_pool = self.pool.get(model) model_pool = self.pool.get(model)
fields = model_pool.fields_get(cr, uid, context=context) fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'): if 'name' in fields and not data.get('name'):
data['name'] = msg_dict.get('subject', '') data['name'] = msg_dict.get('subject', '')
if custom_values and isinstance(custom_values, dict):
data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context) res_id = model_pool.create(cr, uid, data, context=context)
return res_id return res_id
@ -807,6 +820,41 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.") "now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context) self.message_post(cr, uid, [id], message, context=context)
def message_create_partners_from_emails(self, cr, uid, emails, context=None):
""" Convert a list of emails into a list partner_ids and a list
new_partner_ids. The return value is non conventional because
it is meant to be used by the mail widget.
:return dict: partner_ids and new_partner_ids
"""
partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message')
partner_ids = []
new_partner_ids = []
for email in emails:
m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
name = m.group(2) or m.group(0)
email = m.group(3)
ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context)
if ids:
partner_ids.append(ids[0])
else:
partner_id = partner_obj.create(cr, uid, {
'name': name or email,
'email': email,
}, context=context)
new_partner_ids.append(partner_id)
# link mail with this from mail to the new partner id
message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
if message_ids:
mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context)
return {
'partner_ids': partner_ids,
'new_partner_ids': new_partner_ids,
}
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs): subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
""" Post a new message in an existing thread, returning the new """ Post a new message in an existing thread, returning the new
@ -895,7 +943,7 @@ 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_user_api(self, cr, uid, thread_id, body='', parent_id=False, def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
attachment_ids=None, extra_emails=None, content_subtype='plaintext', attachment_ids=None, content_subtype='plaintext',
context=None, **kwargs): context=None, **kwargs):
""" Wrapper on message_post, used for user input : """ Wrapper on message_post, used for user input :
- mail gateway - mail gateway
@ -908,26 +956,12 @@ class mail_thread(osv.AbstractModel):
- type and subtype: comment and mail.mt_comment by default - type and subtype: comment and mail.mt_comment by default
- attachment_ids: supposed not attached to any document; attach them - attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter. to the related document. Should only be set by Chatter.
- extra_email: [ 'Fabien <fpi@openerp.com>', 'al@openerp.com' ]
""" """
partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message') mail_message_obj = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment') ir_attachment = self.pool.get('ir.attachment')
extra_emails = extra_emails or []
# 1.A.1: pre-process partners and incoming extra_emails # 1.A.1: add recipients of parent message
partner_ids = set([]) partner_ids = set([])
for email in extra_emails:
partner_id = partner_obj.find_or_create(cr, uid, email, context=context)
# link mail with this from mail to the new partner id
partner_msg_ids = mail_message_obj.search(cr, SUPERUSER_ID, [('email_from', '=', email), ('author_id', '=', False)], context=context)
if partner_id and partner_msg_ids:
mail_message_obj.write(cr, SUPERUSER_ID, partner_msg_ids, {'email_from': None, 'author_id': partner_id}, context=context)
partner_ids.add((4, partner_id))
if partner_ids:
self.message_subscribe(cr, uid, [thread_id], [item[1] for item in partner_ids], context=context)
# 1.A.2: add recipients of parent message
if parent_id: if parent_id:
parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context) parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids]) partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
@ -935,8 +969,21 @@ class mail_thread(osv.AbstractModel):
if self._name == 'mail.thread' and parent_message.author_id.id: if self._name == 'mail.thread' and parent_message.author_id.id:
partner_ids.add((4, parent_message.author_id.id)) partner_ids.add((4, parent_message.author_id.id))
# 1.A.3: add specified recipients # 1.A.2: add specified recipients
partner_ids |= set(kwargs.pop('partner_ids', [])) param_partner_ids = set()
for item in kwargs.pop('partner_ids', []):
if isinstance(item, (list)):
param_partner_ids.add((item[0], item[1]))
elif isinstance(item, (int, long)):
param_partner_ids.add((4, item))
else:
param_partner_ids.add(item)
partner_ids |= param_partner_ids
# 1.A.3: add parameters recipients as follower
# TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
if param_partner_ids and self._name != 'mail.thread':
self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
# 1.B: handle body, message_type and message_subtype # 1.B: handle body, message_type and message_subtype
if content_subtype == 'plaintext': if content_subtype == 'plaintext':

View File

@ -253,7 +253,14 @@ openerp.mail = function (session) {
} else { } else {
this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid); this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
} }
if (this.author_id) {
var email = this.author_id[1].match(/(.*)<(.*@.*)>/);
if (!email) {
this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]);
} else {
this.author_id.push(_.str.escapeHTML(email[0]), _.str.trim(email[1]), email[2]);
}
}
}, },
@ -370,6 +377,7 @@ openerp.mail = function (session) {
this.show_compact_message = false; this.show_compact_message = false;
this.show_delete_attachment = true; this.show_delete_attachment = true;
this.emails_from = []; this.emails_from = [];
this.partners_from = [];
}, },
start: function () { start: function () {
@ -494,43 +502,43 @@ openerp.mail = function (session) {
this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete); this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from); this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from);
this.$(".oe_partners_from").on('change', 'input', this.on_checked_partner_from);
}, },
on_compose_fullmail: function (default_composition_mode) { on_compose_fullmail: function (default_composition_mode) {
var self = this;
if(!this.do_check_attachment_upload()) { if(!this.do_check_attachment_upload()) {
return false; return false;
} }
if (default_composition_mode == 'reply') { // create list of new partners
this.check_recipient_partners().done(function (partner_ids) {
var context = { var context = {
'default_composition_mode': default_composition_mode, 'default_composition_mode': default_composition_mode,
'default_parent_id': this.id, 'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''), 'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': this.attachment_ids, 'default_attachment_ids': self.attachment_ids,
'default_partner_ids': partner_ids,
}; };
} else { if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
var context = { context.default_model = self.context.default_model;
'default_model': this.context.default_model, context.default_res_id = self.context.default_res_id;
'default_res_id': this.context.default_res_id, }
'default_composition_mode': default_composition_mode,
'default_parent_id': this.id, var action = {
'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''), type: 'ir.actions.act_window',
'default_attachment_ids': this.attachment_ids, res_model: 'mail.compose.message',
}; view_mode: 'form',
} view_type: 'form',
var action = { views: [[false, 'form']],
type: 'ir.actions.act_window', target: 'new',
res_model: 'mail.compose.message', context: context,
view_mode: 'form', };
view_type: 'form',
views: [[false, 'form']], self.do_action(action);
target: 'new', self.on_cancel();
context: context, });
};
this.do_action(action);
this.on_cancel();
}, },
reinit: function() { reinit: function() {
@ -562,63 +570,65 @@ openerp.mail = function (session) {
} }
}, },
check_recipient_partners: function (emails) { check_recipient_partners: function () {
var self = this; var self = this;
var deferreds = []; var partners_from = [];
for (var i = 0; i < emails.length; i++) { var emails = [];
deferreds.push($.Deferred()); _.each(this.emails_from, function (email_from) {
} if (email_from[1] && !_.find(emails, function (email) {return email == email_from[0][4];})) {
var ds_partner = new session.web.DataSetSearch(this, 'res.partner'); emails.push(email_from[0][1]);
_.each(emails, function (email) { }
ds_partner.call('search', [[['email', 'ilike', email]]]).then(function (partner_ids) { });
var deferred = deferreds[_.indexOf(emails, email)]; var deferred_check = $.Deferred();
if (!partner_ids.length) { self.parent_thread.ds_thread._model.call('message_create_partners_from_emails', [emails]).then(function (partners) {
var pop = new session.web.form.FormOpenPopup(this); partners_from = _.clone(partners.partner_ids);
pop.show_element( var deferreds = [];
'res.partner', _.each(partners.new_partner_ids, function (id) {
0, var deferred = $.Deferred()
{ deferreds.push(deferred);
'default_email': email, var pop = new session.web.form.FormOpenPopup(this);
'force_email': true, pop.show_element(
'ref': "compound_context", 'res.partner',
}, id,
{ {
title: _t("Please complete partner's informations"), 'force_email': true,
} 'ref': "compound_context",
); },
pop.on('write_completed, closed', self, function () { {
deferred.resolve(); title: _t("Please complete partner's informations"),
}); }
} );
else { pop.on('closed', self, function () {
deferred.resolve(); deferred.resolve();
} });
return deferred; partners_from.push(id);
});
$.when.apply( $, deferreds ).then(function () {
deferred_check.resolve(partners_from);
}); });
}); });
return $.when.apply( $, deferreds ).done(); return deferred_check;
}, },
on_message_post: function (event) { on_message_post: function (event) {
var self = this; var self = this;
if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) { if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
// create list of new partners // create list of new partners
var extra_email = _.map(_.filter(this.emails_from, function (f) {return f[1]}), function (f) {return f[0]}); this.check_recipient_partners().done(function (partner_ids) {
this.check_recipient_partners(extra_email).done(function () { self.do_send_message_post(partner_ids);
self.do_send_message_post();
}); });
} }
}, },
/*do post a message and fetch the message*/ /*do post a message and fetch the message*/
do_send_message_post: function () { do_send_message_post: function (partner_ids) {
var self = this; var self = this;
this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], { this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], {
'body': this.$('textarea').val(), 'body': this.$('textarea').val(),
'subject': false, 'subject': false,
'parent_id': this.context.default_parent_id, 'parent_id': this.context.default_parent_id,
'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}), 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
'extra_emails': _.map(_.filter(this.emails_from, function (f) {return f[1]}), function (f) {return f[0]}), 'partner_ids': partner_ids,
'context': this.parent_thread.context, 'context': this.parent_thread.context,
}).done(function (message_id) { }).done(function (message_id) {
var thread = self.parent_thread; var thread = self.parent_thread;
@ -678,26 +688,24 @@ openerp.mail = function (session) {
_.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());}); _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
} }
var emails_from = _.map(_.filter(messages, _.each(messages, function (thread) {
function (thread) {return thread.author_id && !thread.author_id[0];}), if (thread.author_id && !thread.author_id[0] &&
function (thread) {return thread.author_id[1];}); !_.find(self.emails_from, function (from) {return from[0][4] == thread.author_id[4];})) {
self.emails_from.push([thread.author_id, true]);
return _.each(emails_from, function (email_from) {
if (!_.find(self.emails_from, function (from) {return from[0] == email_from;})) {
self.emails_from.push([email_from, true]);
} }
}); });
return self.emails_from;
}, },
on_checked_email_from: function (event) { on_checked_email_from: function (event) {
var $input = $(event.target); var $input = $(event.target);
var email = $input.attr("data"); var email = $input.attr("data");
_.each(this.emails_from, function (email_from) { _.each(this.emails_from, function (email_from) {
if (email_from[0] == email) { if (email_from[0][4] == email) {
email_from[1] = $input.is(":checked"); email_from[1] = $input.is(":checked");
} }
}); });
} },
}); });
/** /**
@ -1690,7 +1698,6 @@ openerp.mail = function (session) {
this.defaults[key.replace(/^search_default_/, '')] = this.context[key]; this.defaults[key.replace(/^search_default_/, '')] = this.context[key];
} }
} }
this.action.params = _.extend({ this.action.params = _.extend({
'display_indented_thread': 1, 'display_indented_thread': 1,
'show_reply_button': true, 'show_reply_button': true,

View File

@ -221,11 +221,14 @@ openerp_mail_followers = function(session, mail) {
var nb_subtype = 0; var nb_subtype = 0;
_(records).each(function (record) {nb_subtype++;}); _(records).each(function (record) {nb_subtype++;});
if (nb_subtype > 1) { if (nb_subtype > 1) {
this.$('hr').show();
_(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.$('.oe_subtype_list') ); $(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
}); });
} else {
this.$('hr').hide();
} }
}, },

View File

@ -132,8 +132,8 @@
<div class="oe_emails_from"> <div class="oe_emails_from">
<t t-foreach='widget.emails_from' t-as='email_from'> <t t-foreach='widget.emails_from' t-as='email_from'>
<label title="Add them into recipients and followers"> <label title="Add them into recipients and followers">
<input type="checkbox" t-att-checked="email_from[1] ? 'checked' : undefind" t-att-data="email_from[0]"/> <input type="checkbox" t-att-checked="email_from[1] ? 'checked' : undefind" t-att-data="email_from[0][4]"/>
<t t-raw="email_from[0]"/> <t t-raw="email_from[0][2]"/>
</label> </label>
</t> </t>
</div> </div>
@ -243,8 +243,8 @@
<t t-if="widget.attachment_ids.length > 0"> <t t-if="widget.attachment_ids.length > 0">
<div class="oe_msg_attachment_list"></div> <div class="oe_msg_attachment_list"></div>
</t> </t>
<a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[1]"/></a> <a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[2]"/></a>
<span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-raw="widget.author_id[1]"/></span> <span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-raw="widget.author_id[2]"/></span>
<span class='oe_subtle'></span> <span class='oe_subtle'></span>
<span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span> <span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span>
<span t-if="!widget.options.readonly" class='oe_subtle'></span> <span t-if="!widget.options.readonly" class='oe_subtle'></span>

View File

@ -76,8 +76,6 @@ class invite_wizard(osv.osv_memory):
'subject': 'Invitation to follow %s' % document.name_get()[0][1], 'subject': 'Invitation to follow %s' % document.name_get()[0][1],
'body_html': '%s' % wizard.message, 'body_html': '%s' % wizard.message,
'auto_delete': True, 'auto_delete': True,
'res_id': False,
'model': False,
}, context=context) }, context=context)
mail_mail.send(cr, uid, [mail_id], recipient_ids=[follower_id], context=context) mail_mail.send(cr, uid, [mail_id], recipient_ids=[follower_id], context=context)
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}

View File

@ -13,7 +13,7 @@
<field name="res_id" invisible="1"/> <field name="res_id" invisible="1"/>
<field name="partner_ids" widget="many2many_tags_email" <field name="partner_ids" widget="many2many_tags_email"
placeholder="Add contacts to notify..." placeholder="Add contacts to notify..."
context="{'force_email':True}"/> context="{'force_email':True, 'show_email':True}"/>
<field name="message"/> <field name="message"/>
</group> </group>
<footer> <footer>

View File

@ -168,6 +168,7 @@ class mail_compose_message(osv.TransientModel):
reply_subject = "%s %s" % (re_prefix, reply_subject) reply_subject = "%s %s" % (re_prefix, reply_subject)
# 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 []
partner_ids += context.get('default_partner_ids', [])
# update the result # update the result
result = { result = {

View File

@ -74,11 +74,9 @@
} }
.etherpad_readonly ul, .etherpad_readonly ol { .etherpad_readonly ul, .etherpad_readonly ol {
margin-before: 1em; margin: 0;
margin-after: 1em; margin-left: 1.5em;
margin-start: 0px; padding: 0;
margin-end: 0px;
padding-start: 40px !important;
} }
.etherpad_readonly ul li{ .etherpad_readonly ul li{
list-style-type: disc; list-style-type: disc;
@ -92,8 +90,8 @@
.etherpad_readonly{ .etherpad_readonly{
font-family: arial, sans-serif; font-family: arial, sans-serif;
font-size: 13px; font-size: 15px;
line-height: 18px; line-height: 19px;
} }
.openerp .oe_form_nomargin .etherpad_readonly{ .openerp .oe_form_nomargin .etherpad_readonly{
@ -109,3 +107,12 @@
.etherpad_readonly ol ol ol ol ol ol li{ list-style-type: lower-roman !important; } .etherpad_readonly ol ol ol ol ol ol li{ list-style-type: lower-roman !important; }
.etherpad_readonly ol ol ol ol ol ol ol li{ list-style-type: decimal !important; } .etherpad_readonly ol ol ol ol ol ol ol li{ list-style-type: decimal !important; }
.etherpad_readonly ol ol ol ol ol ol ol ol li{ list-style-type: lower-latin !important; } .etherpad_readonly ol ol ol ol ol ol ol ol li{ list-style-type: lower-latin !important; }
.etherpad_readonly ul li { list-style-type: disc !important; }
.etherpad_readonly ul ul li { list-style-type: circle !important; }
.etherpad_readonly ul ul ul li { list-style-type: square !important; }
.etherpad_readonly ul ul ul ul li { list-style-type: disc !important; }
.etherpad_readonly ul ul ul ul ul li { list-style-type: circle !important; }
.etherpad_readonly ul ul ul ul ul ul li { list-style-type: square !important; }
.etherpad_readonly ul ul ul ul ul ul ul li { list-style-type: disc !important; }
.etherpad_readonly ul ul ul ul ul ul ul ul li { list-style-type: circle !important; }
.etherpad_readonly ul ul ul ul ul ul ul ul ul li { list-style-type: square !important; }

View File

@ -21,6 +21,7 @@
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.osv import osv from openerp.osv import osv
from openerp.osv.orm import except_orm
from openerp.tools import append_content_to_html from openerp.tools import append_content_to_html
from openerp.tools.translate import _ from openerp.tools.translate import _
@ -35,10 +36,21 @@ class mail_mail(osv.Model):
:param partner: browse_record of the specific recipient partner :param partner: browse_record of the specific recipient partner
:return: the resulting body_html :return: the resulting body_html
""" """
body = super(mail_mail, self).send_get_mail_body(cr, uid, mail, partner, context=context) partner_obj = self.pool.get('res.partner')
body = mail.body_html
if partner: if partner:
context = dict(context or {}, signup_valid=True) contex_signup = dict(context or {}, signup_valid=True)
partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, partner.id, context=context) partner = partner_obj.browse(cr, SUPERUSER_ID, partner.id, context=contex_signup)
text = _("""Access your personal documents through <a href="%s">our Customer Portal</a>""") % partner.signup_url text = _("""<p>Access your messages and personal documents through <a href="%s">our Customer Portal</a></p>""") % partner.signup_url
# partner is an user: add a link to the document if read access
if partner.user_ids and mail.model and mail.res_id \
and self.check_access_rights(cr, partner.user_ids[0].id, 'read', raise_exception=False):
related_user = partner.user_ids[0]
try:
self.pool.get(mail.model).check_access_rule(cr, related_user.id, [mail.res_id], 'read', context=context)
url = partner_obj._get_signup_url_for_action(cr, related_user.id, [partner.id], action='', res_id=mail.res_id, model=mail.model, context=context)[partner.id]
text = _("""<p>Access this document <a href="%s">directly in OpenERP</a></p>""") % url
except except_orm, e:
pass
body = append_content_to_html(body, ("<div><p>%s</p></div>" % text), plaintext=False) body = append_content_to_html(body, ("<div><p>%s</p></div>" % text), plaintext=False)
return body return body

View File

@ -44,8 +44,9 @@ openerp.portal_anonymous = function(instance) {
start: function() { start: function() {
var self = this; var self = this;
return $.when(this._super()).then(function() { return $.when(this._super()).then(function() {
var params = $.deparam($.param.querystring());
var dblist = self.db_list || []; var dblist = self.db_list || [];
if (!self.session.session_is_valid() && dblist.length === 1 && _.isEmpty(self.params)) { if (!self.session.session_is_valid() && dblist.length === 1 && (!params.token || !params.login)) {
self.remember_credentials = false; self.remember_credentials = false;
// XXX get login/pass from server (via a rpc call) ? // XXX get login/pass from server (via a rpc call) ?
return self.do_login(dblist[0], 'anonymous', 'anonymous'); return self.do_login(dblist[0], 'anonymous', 'anonymous');

View File

@ -1166,6 +1166,11 @@ class task(base_stage, osv.osv):
# Mail gateway # Mail gateway
# --------------------------------------------------- # ---------------------------------------------------
def message_get_reply_to(self, cr, uid, ids, context=None):
""" Override to get the reply_to of the parent project. """
return [task.project_id.message_get_reply_to()[0] if task.project_id else False
for task in self.browse(cr, uid, ids, context=context)]
def message_new(self, cr, uid, msg, custom_values=None, context=None): def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Override to updates the document according to the email. """ """ Override to updates the document according to the email. """
if custom_values is None: custom_values = {} if custom_values is None: custom_values = {}
@ -1198,7 +1203,7 @@ class task(base_stage, osv.osv):
act = 'do_%s' % res.group(2).lower() act = 'do_%s' % res.group(2).lower()
if act: if act:
getattr(self,act)(cr, uid, ids, context=context) getattr(self,act)(cr, uid, ids, context=context)
return super(task,self).message_update(cr, uid, msg, update_vals=update_vals, context=context) return super(task,self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
def project_task_reevaluate(self, cr, uid, ids, context=None): def project_task_reevaluate(self, cr, uid, ids, context=None):
if self.pool.get('res.users').has_group(cr, uid, 'project.group_time_work_estimation_tasks'): if self.pool.get('res.users').has_group(cr, uid, 'project.group_time_work_estimation_tasks'):

View File

@ -481,6 +481,11 @@ class project_issue(base_stage, osv.osv):
# Mail gateway # Mail gateway
# ------------------------------------------------------- # -------------------------------------------------------
def message_get_reply_to(self, cr, uid, ids, context=None):
""" Override to get the reply_to of the parent project. """
return [issue.project_id.message_get_reply_to()[0] if issue.project_id else False
for issue in self.browse(cr, uid, ids, context=context)]
def message_new(self, cr, uid, msg, custom_values=None, context=None): def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway """ Overrides mail_thread message_new that is called by the mailgateway
through message_process. through message_process.

View File

@ -31,7 +31,7 @@ Collects web application usage with Google Analytics.
""", """,
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'website': 'http://openerp.com', 'website': 'http://openerp.com',
'depends': ['web', 'auth_signup'], 'depends': ['web'],
'data': [], 'data': [],
'installable': True, 'installable': True,
'active': False, 'active': False,

View File

@ -3,12 +3,13 @@ var _gaq = _gaq || []; // asynchronous stack used by google analytics
openerp.web_analytics = function(instance) { openerp.web_analytics = function(instance) {
/** The Google Analytics Module inserts the Google Analytics JS Snippet /*
* at the top of the page, and sends to google an url each time the * The Web Analytics Module inserts the Google Analytics JS Snippet
* openerp url is changed. * at the top of the page, and sends to google an url each time the
* The pushes of the urls is made by triggering the 'state_pushed' event in the * openerp url is changed.
* web_client.do_push_state() method which is responsible of changing the openerp current url * The pushes of the urls is made by triggering the 'state_pushed' event in the
*/ * web_client.do_push_state() method which is responsible of changing the openerp current url
*/
// Google Analytics Code snippet // Google Analytics Code snippet
(function() { (function() {
@ -20,138 +21,219 @@ openerp.web_analytics = function(instance) {
s.parentNode.insertBefore(ga,s); s.parentNode.insertBefore(ga,s);
})(); })();
instance.web_analytics.Tracker = instance.web.Class.extend({
/*
* This method initializes the tracker
*/
init: function() {
/* Comment this lines when going on production, only used for testing on localhost
_gaq.push(['_setAccount', 'UA-35793871-1']);
_gaq.push(['_setDomainName', 'none']);
*/
var init_tracker = function() { /* Uncomment this lines when going on production */
// initialize tracker _gaq.push(['_setAccount', 'UA-7333765-1']);
_gaq.push(['_setAccount', 'UA-7333765-1']); _gaq.push(['_setDomainName', '.openerp.com']); // Allow multi-domain
//_gaq.push(['_setAccount', 'UA-36797757-1']); // Debug code /**/
_gaq.push(['_setDomainName', 'none']); // Change for the real domain },
/*
// Track user types * This method MUST be overriden by saas_demo and saas_trial in order to
if (instance.session.uid !== 1) { * set the correct user type. By default, the user connected is local to the DB.
if ((/\.demo.openerp.com/).test(instance.session.server)) { */
_gaq.push(['_setCustomVar', 1, 'User Type', 'Demo User', 1]); _get_user_type: function() {
return 'Local User';
},
/*
* This method gets the user access level, to be used as CV in GA
*/
_get_user_access_level: function() {
if (instance.session.uid === 1) {
return 'Admin User';
// Make the difference between portal users and anonymous users
} else if (instance.session.username.indexOf('@') !== -1) {
if (instance.session.username.indexOf('anonymous') === -1) {
return 'Portal User';
} else {
return 'Anonymous User';
}
} else if (instance.session.username.indexOf('anonymous') !== -1) {
return 'Anonymous User';
} else { } else {
_gaq.push(['_setCustomVar', 1, 'User Type', 'Normal User', 1]); return 'Normal User';
} }
} else { },
_gaq.push(['_setCustomVar', 1, 'User Type', 'Admin User', 1]); /*
} * This method contains the initialization of all user-related custom variables
* stored in GA. Also other modules can override it to add new custom variables
*/
initialize_custom: function() {
// Track User Access Level, Custom Variable 4 in GA with visitor level scope
// Values: 'Admin User', 'Normal User', 'Portal User', 'Anonymous User'
_gaq.push(['_setCustomVar', 4, 'User Access Level', this.user_access_level, 1]);
// Track object usage // Track User Type Conversion, Custom Variable 3 in GA with session level scope
_gaq.push(['_setCustomVar', 2, 'Object', 'no_model', 3]); // Values: 'Visitor', 'Demo', 'Online Trial', 'Online Paying', 'Local User'
// Tack view usage _gaq.push(['_setCustomVar', 1, 'User Type Conversion', this._get_user_type(), 2]);
_gaq.push(['_setCustomVar', 3, 'View Type', 'default', 3]);
_gaq.push(['_trackPageview']); return instance.session.rpc("/web/webclient/version_info", {})
}; .done(function(res) {
_gaq.push(['_setCustomVar', 5, 'Version', res.server_version, 3]);
var on_state_pushed = function(state) { _gaq.push(['_trackPageview']);
// Track only pages corresponding to a 'normal' view of OpenERP, views return;
// related to client actions are tracked by the action manager
if (state.model && state.view_type) {
// Track object usage
_gaq.push(['_setCustomVar', 2, 'Object', state.model, 3]);
// Tack view usage
_gaq.push(['_setCustomVar', 3, 'View Type', state.view_type, 3]);
// Track the page
var url = instance.web_analytics.generateUrl({'model': state.model, 'view_type': state.view_type});
_gaq.push(['_trackPageview', url]);
}
};
var include_tracker = function() {
// include the tracker into views and managers
// Track the events related with the creation and the modification of records
instance.web.FormView = instance.web.FormView.extend({
init: function(parent, dataset, view_id, options) {
this._super.apply(this, arguments);
var self = this;
this.on('record_created', self, function(r) {
var url = instance.web_analytics.generateUrl({'model': this.model, 'view_type': 'form'});
_gaq.push(['_trackEvent', this.model, 'on_button_create_save', url]);
}); });
this.on('record_saved', self, function(r) { },
var url = instance.web_analytics.generateUrl({'model': this.model, 'view_type': 'form'}); /*
_gaq.push(['_trackEvent', this.model, 'on_button_edit_save', url]); * Method called in order to send _trackEvent to GA
*/
_push_event: function(options) {
_gaq.push(['_trackEvent',
options.category,
options.action,
options.label,
options.value,
options.noninteraction
]);
},
/*
* Method called in order to send ecommerce transactions to GA
*/
_push_ecommerce: function(trans_data, item_list) {
_gaq.push(['_addTrans',
trans_data.order_id,
trans_data.store_name,
trans_data.total,
trans_data.tax,
trans_data.shipping,
trans_data.city,
trans_data.state,
trans_data.country,
]);
_.each(item_list, function(item) {
_gaq.push(['_addItem',
item.order_id,
item.sku,
item.name,
item.category,
item.price,
item.quantity,
]);
});
_gaq.push(['_trackTrans']);
},
/*
* This method contains the initialization of the object and view type
* as an event in GA.
*/
on_state_pushed: function(state) {
// Track only pages corresponding to a 'normal' view of OpenERP, views
// related to client actions are tracked by the action manager
if (state.model && state.view_type) {
// Track the page
var url = instance.web_analytics.generateUrl({'model': state.model, 'view_type': state.view_type});
this._push_event({
'category': state.model,
'action': state.view_type,
'label': url,
}); });
} }
}); },
/*
// Track client actions * This method includes the tracker into views and managers. It can be overriden
instance.web.ActionManager.include({ * by other modules in order to extend tracking functionalities
ir_actions_client: function (action, options) { */
var url = instance.web_analytics.generateUrl({'action': action.tag}); include_tracker: function() {
_gaq.push(['_trackPageview', url]); var t = this;
return this._super.apply(this, arguments); // Track the events related with the creation and the modification of records,
}, // the view type is always form
}); instance.web.FormView.include({
init: function(parent, dataset, view_id, options) {
// Track button events this._super.apply(this, arguments);
instance.web.View.include({ var self = this;
do_execute_action: function(action_data, dataset, record_id, on_closed) { this.on('record_created', self, function(r) {
console.log(action_data); var url = instance.web_analytics.generateUrl({'model': r.model, 'view_type': 'form'});
var category = this.model || dataset.model || ''; t._push_event({
var action; 'category': r.model,
if (action_data.name && _.isNaN(action_data.name-0)) { 'action': 'form',
action = action_data.name; 'label': url,
} else { });
action = action_data.string || action_data.special || ''; });
this.on('record_saved', self, function(r) {
var url = instance.web_analytics.generateUrl({'model': r.model, 'view_type': 'form'});
t._push_event({
'category': r.model,
'action': 'form',
'label': url,
});
});
} }
var label = instance.web_analytics.generateUrl({'model': category, 'view_type': this.view_type}); });
_gaq.push(['_trackEvent', category, action, label]);
return this._super.apply(this, arguments);
},
});
// Track error events // Track client actions
instance.web.CrashManager.include({ instance.web.ActionManager.include({
show_error: function(error) { ir_actions_client: function (action, options) {
var hash = window.location.hash; var url = instance.web_analytics.generateUrl({'action': action.tag});
var params = $.deparam(hash.substr(hash.indexOf('#')+1)); var category = action.res_model || action.type;
var options = {}; t._push_event({
if (params.model && params.view_type) { 'category': action.res_model || action.type,
options = {'model': params.model, 'view_type': params.view_type}; 'action': action.name || action.tag,
} else { 'label': url,
options = {'action': params.action}; });
} return this._super.apply(this, arguments);
var label = instance.web_analytics.generateUrl(options); },
if (error.code) { });
_gaq.push(['_trackEvent', error.message, error.data.fault_code, label, ,true]);
} else {
_gaq.push(['_trackEvent', error.type, error.data.debug, label, ,true]);
}
this._super.apply(this, arguments);
},
});
};
// Track button events
instance.web.View.include({
do_execute_action: function(action_data, dataset, record_id, on_closed) {
var category = this.model || dataset.model || '';
var action;
if (action_data.name && _.isNaN(action_data.name-0)) {
action = action_data.name;
} else {
action = action_data.string || action_data.special || '';
}
var url = instance.web_analytics.generateUrl({'model': category, 'view_type': this.view_type});
t._push_event({
'category': category,
'action': action,
'label': url,
});
return this._super.apply(this, arguments);
},
});
if (instance.client instanceof instance.web.WebClient) { // not for embedded clients // Track error events
init_tracker(); instance.web.CrashManager.include({
show_error: function(error) {
// Set the account and domain to start tracking var hash = window.location.hash;
instance.client.on('state_pushed', this, on_state_pushed); var params = $.deparam(hash.substr(hash.indexOf('#')+1));
var options = {};
include_tracker(); if (params.model && params.view_type) {
} else if (!instance.client) { options = {'model': params.model, 'view_type': params.view_type};
// client does not already exists, we are in monodb mode } else {
options = {'action': params.action};
instance.web.WebClient.include({ }
init: function() { var url = instance.web_analytics.generateUrl(options);
init_tracker(); if (error.code) {
return this._super.apply(this, arguments); t._push_event({
}, 'category': error.message,
start: function() { 'action': error.data.fault_code,
var self = this; 'label': url,
return this._super.apply(this, arguments).done(function() { 'noninteraction': true,
self.on('state_pushed', self, on_state_pushed); });
include_tracker(); } else {
}); t._push_event({
} 'category': error.type,
}); 'action': error.data.debug,
} 'label': url,
'noninteraction': true,
});
}
this._super.apply(this, arguments);
},
});
},
});
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// utility functions // utility functions
@ -165,4 +247,37 @@ openerp.web_analytics = function(instance) {
return url; return url;
}; };
instance.web_analytics.setupTracker = function(wc) {
var t = wc.tracker;
return $.when(t._get_user_access_level()).then(function(r) {
t.user_access_level = r;
t.initialize_custom().then(function() {
wc.on('state_pushed', t, t.on_state_pushed);
t.include_tracker();
});
});
};
// Set correctly the tracker in the current instance
if (instance.client instanceof instance.web.WebClient) { // not for embedded clients
instance.webclient.tracker = new instance.web_analytics.Tracker();
instance.web_analytics.setupTracker(instance.webclient);
} else if (!instance.client) {
// client does not already exists, we are in monodb mode
instance.web.WebClient.include({
start: function() {
var d = this._super.apply(this, arguments);
this.tracker = new instance.web_analytics.Tracker();
return d;
},
show_application: function() {
var self = this;
$.when(this.subscribe_deferred).then(function() {
instance.web_analytics.setupTracker(self);
});
this._super();
},
});
}
}; };