-
Units Done
+
+ Units Consumed
Units Remaining
diff --git a/addons/account_followup/account_followup.py b/addons/account_followup/account_followup.py
index 9821030df0b..285a3529ee7 100644
--- a/addons/account_followup/account_followup.py
+++ b/addons/account_followup/account_followup.py
@@ -166,8 +166,8 @@ class res_partner(osv.osv):
action_text = partner.latest_followup_level_id_without_lit.manual_action_note or ''
#Check date: put the minimum date if it existed already
- action_date = (partner.payment_next_action_date and min(partner.payment_next_action_date, fields.date.context_today(cr, uid, context))
- ) or fields.date.context_today(cr, uid, context)
+ action_date = (partner.payment_next_action_date and min(partner.payment_next_action_date, fields.date.context_today(self, cr, uid, context=context))
+ ) or fields.date.context_today(self, cr, uid, context=context)
# Check responsible: if partner has not got a responsible already, take from follow-up
responsible_id = False
@@ -218,9 +218,9 @@ class res_partner(osv.osv):
unknown_mails = unknown_mails + 1
action_text = _("Email not sent because of email address of partner not filled in")
if partner.payment_next_action_date:
- payment_action_date = min(fields.date.context_today(cr, uid, ctx), partner.payment_next_action_date)
+ payment_action_date = min(fields.date.context_today(self, cr, uid, context=ctx), partner.payment_next_action_date)
else:
- payment_action_date = fields.date.context_today(cr, uid, ctx)
+ payment_action_date = fields.date.context_today(self, cr, uid, context=ctx)
if partner.payment_next_action:
payment_next_action = partner.payment_next_action + " \n " + action_text
else:
@@ -242,7 +242,7 @@ class res_partner(osv.osv):
followup_table = ''
if partner.unreconciled_aml_ids:
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
- current_date = fields.date.context_today(cr, uid, context)
+ current_date = fields.date.context_today(self, cr, uid, context=context)
rml_parse = account_followup_print.report_rappel(cr, uid, "followup_rml_parser")
final_res = rml_parse._lines_get_with_partner(partner, company.id)
@@ -311,7 +311,7 @@ class res_partner(osv.osv):
'''
res = {}
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
- current_date = fields.date.context_today(cr, uid, context)
+ current_date = fields.date.context_today(self, cr, uid, context=context)
for partner in self.browse(cr, uid, ids, context=context):
worst_due_date = False
amount_due = amount_overdue = 0.0
diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py
index 5c558ca68d7..b35ff274a6d 100644
--- a/addons/auth_signup/res_users.py
+++ b/addons/auth_signup/res_users.py
@@ -52,7 +52,7 @@ class res_partner(osv.Model):
(not partner.signup_expiration or dt <= partner.signup_expiration)
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
the url state components (menu_id, id, view_type) """
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
if context and context.get('signup_valid') and not partner.user_ids:
self.signup_prepare(cr, uid, [partner.id], context=context)
+ partner.refresh()
# the parameters to encode for the query and fragment part of url
query = {'db': cr.dbname}
@@ -78,6 +79,8 @@ class res_partner(osv.Model):
fragment['view_type'] = view_type
if menu_id:
fragment['menu_id'] = menu_id
+ if model:
+ fragment['model'] = model
if res_id:
fragment['id'] = res_id
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 3fae85f311d..4455663ca9a 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -87,12 +87,13 @@ class crm_lead(base_stage, format_address, osv.osv):
def create(self, cr, uid, vals, context=None):
if context is None:
context = {}
- if not vals.get('stage_id') and vals.get('section_id'):
+ if not vals.get('stage_id'):
ctx = context.copy()
- ctx['default_section_id'] = vals['section_id']
+ if vals.get('section_id'):
+ ctx['default_section_id'] = vals['section_id']
+ if vals.get('type'):
+ ctx['default_type'] = vals['type']
vals['stage_id'] = self._get_default_stage_id(cr, uid, context=ctx)
- elif not vals.get('stage_id') and context.get('default_section_id'):
- vals['stage_id'] = self._get_default_stage_id(cr, uid, context=context)
return super(crm_lead, self).create(cr, uid, vals, context=context)
def _get_default_section_id(self, cr, uid, context=None):
@@ -397,7 +398,8 @@ class crm_lead(base_stage, format_address, osv.osv):
search_domain += [('|')] * len(section_ids)
for section_id in section_ids:
search_domain.append(('section_ids', '=', section_id))
- search_domain.append(('case_default', '=', True))
+ else:
+ search_domain.append(('case_default', '=', True))
# AND with cases types
search_domain.append(('type', 'in', types))
# AND with the domain in parameter
@@ -423,15 +425,15 @@ class crm_lead(base_stage, format_address, osv.osv):
def case_mark_lost(self, cr, uid, ids, context=None):
""" Mark the case as lost: state=cancel and probability=0 """
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:
self.case_set(cr, uid, [lead.id], values_to_update={'probability': 0.0}, new_stage_id=stage_id, context=context)
return True
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):
- 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:
self.case_set(cr, uid, [lead.id], values_to_update={'probability': 100.0}, new_stage_id=stage_id, context=context)
return True
@@ -677,29 +679,23 @@ class crm_lead(base_stage, format_address, osv.osv):
contact_id = False
if customer:
contact_id = self.pool.get('res.partner').address_get(cr, uid, [customer.id])['default']
-
if not section_id:
section_id = lead.section_id and lead.section_id.id or False
-
- 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 {
+ val = {
'planned_revenue': lead.planned_revenue,
'probability': lead.probability,
'name': lead.name,
'partner_id': customer and customer.id or False,
'user_id': (lead.user_id and lead.user_id.id),
'type': 'opportunity',
- 'stage_id': stage_id or False,
'date_action': fields.datetime.now(),
'date_open': fields.datetime.now(),
'email_from': customer and customer.email or lead.email_from,
'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):
customer = False
@@ -966,6 +962,11 @@ class crm_lead(base_stage, format_address, osv.osv):
# 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):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
@@ -974,16 +975,17 @@ class crm_lead(base_stage, format_address, osv.osv):
if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
- custom_values.update({
+ defaults = {
'name': msg.get('subject') or _("No Subject"),
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
- })
+ }
if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES):
- custom_values['priority'] = msg.get('priority')
- return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ defaults['priority'] = msg.get('priority')
+ defaults.update(custom_values)
+ return super(crm_lead, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
""" Overrides mail_thread message_update that is called by the mailgateway
diff --git a/addons/crm/crm_lead_data.xml b/addons/crm/crm_lead_data.xml
index da677972b8b..bb590f49d73 100644
--- a/addons/crm/crm_lead_data.xml
+++ b/addons/crm/crm_lead_data.xml
@@ -65,6 +65,7 @@
Lost
+
cancel
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index 24837cff586..3d7bd45a209 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -514,7 +514,7 @@
-
+
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index 5aaa6582e34..a724579f1e8 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -190,15 +190,16 @@ class crm_claim(base_stage, osv.osv):
"""
if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
- custom_values.update({
+ defaults = {
'name': msg.get('subject') or _("No Subject"),
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
- })
+ }
if msg.get('priority'):
- custom_values['priority'] = msg.get('priority')
- return super(crm_claim,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ defaults['priority'] = msg.get('priority')
+ defaults.update(custom_values)
+ return super(crm_claim,self).message_new(cr, uid, msg, custom_values=defaults, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
""" Overrides mail_thread message_update that is called by the mailgateway
diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py
index a707787491d..5ed1c040627 100644
--- a/addons/crm_helpdesk/crm_helpdesk.py
+++ b/addons/crm_helpdesk/crm_helpdesk.py
@@ -100,14 +100,15 @@ class crm_helpdesk(base_state, base_stage, osv.osv):
"""
if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
- custom_values.update({
+ defaults = {
'name': msg.get('subject') or _("No Subject"),
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
- })
- return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ }
+ defaults.update(custom_values)
+ return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=defaults, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
""" Overrides mail_thread message_update that is called by the mailgateway
diff --git a/addons/document_page/document_page_view.xml b/addons/document_page/document_page_view.xml
index d8daaada967..08c77647d17 100644
--- a/addons/document_page/document_page_view.xml
+++ b/addons/document_page/document_page_view.xml
@@ -53,7 +53,7 @@
that will be used as a content template for all new page of this category.
-
+
diff --git a/addons/fleet/fleet.py b/addons/fleet/fleet.py
index 83f96bd71b5..7c5e01415e1 100644
--- a/addons/fleet/fleet.py
+++ b/addons/fleet/fleet.py
@@ -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
#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)
+ #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)
price_per_liter = float(price_per_liter)
amount = float(amount)
- if liter > 0 and price_per_liter > 0:
- return {'value' : {'amount' : liter * price_per_liter,}}
- elif liter > 0 and amount > 0:
- return {'value' : {'price_per_liter' : amount / liter,}}
- elif price_per_liter > 0 and amount > 0:
- return {'value' : {'liter' : amount / price_per_liter,}}
+ if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
+ return {'value' : {'amount' : round(liter * price_per_liter,2),}}
+ elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
+ return {'value' : {'price_per_liter' : round(amount / liter,2),}}
+ elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
+ return {'value' : {'liter' : round(amount / price_per_liter,2),}}
else :
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
#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)
+ #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)
price_per_liter = float(price_per_liter)
amount = float(amount)
- if price_per_liter > 0 and liter > 0:
- return {'value' : {'amount' : liter * price_per_liter,}}
- elif price_per_liter > 0 and amount > 0:
- return {'value' : {'liter' : amount / price_per_liter,}}
- elif liter > 0 and amount > 0:
- return {'value' : {'price_per_liter' : amount / liter,}}
+ if liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
+ return {'value' : {'amount' : round(liter * price_per_liter,2),}}
+ elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
+ return {'value' : {'liter' : round(amount / price_per_liter,2),}}
+ elif amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
+ return {'value' : {'price_per_liter' : round(amount / liter,2),}}
else :
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
#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)
+ #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)
price_per_liter = float(price_per_liter)
amount = float(amount)
- if amount > 0 and liter > 0:
- return {'value': {'price_per_liter': amount / liter,}}
- elif amount > 0 and price_per_liter > 0:
- return {'value': {'liter': amount / price_per_liter,}}
- elif liter > 0 and price_per_liter > 0:
- return {'value': {'amount': liter * price_per_liter,}}
- return {}
+ if amount > 0 and liter > 0 and round(amount/liter,2) != price_per_liter:
+ return {'value': {'price_per_liter': round(amount / liter,2),}}
+ elif amount > 0 and price_per_liter > 0 and round(amount/price_per_liter,2) != liter:
+ return {'value': {'liter': round(amount / price_per_liter,2),}}
+ elif liter > 0 and price_per_liter > 0 and round(liter*price_per_liter,2) != amount:
+ return {'value': {'amount': round(liter * price_per_liter,2),}}
+ else :
+ return {}
def _get_default_service_type(self, cr, uid, context):
try:
diff --git a/addons/hr_expense/hr_expense.py b/addons/hr_expense/hr_expense.py
index 7557bb1abfe..f32479c2ae2 100644
--- a/addons/hr_expense/hr_expense.py
+++ b/addons/hr_expense/hr_expense.py
@@ -212,7 +212,6 @@ class hr_expense_expense(osv.osv):
if analytic_journal_ids:
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)
- 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)
return True
diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py
index ab078766edc..c7d7867911d 100644
--- a/addons/hr_holidays/hr_holidays.py
+++ b/addons/hr_holidays/hr_holidays.py
@@ -283,7 +283,14 @@ class hr_holidays(osv.osv):
result['value']['number_of_days_temp'] = 0
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):
check_fnct = self.pool.get('hr.holidays.status').check_access_rights
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):
for obj in self.browse(cr, uid, ids, context=context):
self.message_post(cr, uid, [obj.id],
- _("Request
approved, waiting second validation."), context=context)
+ _("Request approved, waiting second validation."), context=context)
class resource_calendar_leaves(osv.osv):
_inherit = "resource.calendar.leaves"
diff --git a/addons/hr_holidays/hr_holidays_data.xml b/addons/hr_holidays/hr_holidays_data.xml
index 365423427c5..2b370f08821 100644
--- a/addons/hr_holidays/hr_holidays_data.xml
+++ b/addons/hr_holidays/hr_holidays_data.xml
@@ -49,7 +49,7 @@
To Approve
hr.holidays
- Request confirmed, waiting confirmation
+ Request created and waiting confirmation
Approved
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index 33e32d94762..44d8912c5f1 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -347,16 +347,17 @@ class hr_applicant(base_stage, osv.Model):
"""
if custom_values is None: custom_values = {}
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
- custom_values.update({
+ defaults = {
'name': msg.get('subject') or _("No Subject"),
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
- })
+ }
if msg.get('priority'):
- custom_values['priority'] = msg.get('priority')
- return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ defaults['priority'] = msg.get('priority')
+ defaults.update(custom_values)
+ return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=defaults, context=context)
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
""" Override mail_thread message_update that is called by the mailgateway
diff --git a/addons/lunch/lunch.py b/addons/lunch/lunch.py
index f61ecc8eb7d..0f9033fce38 100644
--- a/addons/lunch/lunch.py
+++ b/addons/lunch/lunch.py
@@ -107,6 +107,7 @@ class lunch_order(osv.Model):
elif alert.alter_type == 'week':
#the alert is activated during some days of the week
return self.check_day(alert)
+ return True # alter_type == 'days' (every day)
def _default_alerts_get(self, cr, uid, context=None):
"""
diff --git a/addons/mail/mail_alias.py b/addons/mail/mail_alias.py
index ef204d7a8cb..d9c49d833ab 100644
--- a/addons/mail/mail_alias.py
+++ b/addons/mail/mail_alias.py
@@ -66,7 +66,7 @@ class mail_alias(osv.Model):
'alias_name': fields.char('Alias', required=True,
help="The name of the email alias, e.g. 'jobs' "
"if you want to catch emails for ",),
- 'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True,
+ 'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade",
help="The model (OpenERP Document Kind) to which this alias "
"corresponds. Any incoming email that does not reply to an "
"existing record will cause the creation of a new record "
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index 66da33d35f0..ec9bc86d6c5 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -75,13 +75,6 @@ class mail_notification(osv.Model):
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)')
- 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):
""" Return the list of partners to notify, based on their preferences.
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index d6cb2a2cf7e..7b8ec0b3579 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -21,10 +21,13 @@
import base64
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.osv import fields, osv
+from openerp.osv.orm import except_orm
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
@@ -158,7 +161,43 @@ class mail_mail(osv.Model):
:param browse_record mail: mail.mail browse_record
: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 = _("""Access this document directly in OpenERP
""") % url
+ body = tools.append_content_to_html(body, ("" % 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):
""" 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)
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)
email_to = [partner.email] if partner else tools.email_split(mail.email_to)
return {
@@ -176,6 +216,7 @@ class mail_mail(osv.Model):
'body_alternative': body_alternative,
'subject': subject,
'email_to': email_to,
+ 'reply_to': reply_to,
}
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_alternative = email.get('body_alternative'),
email_cc = tools.email_split(mail.email_cc),
- reply_to = mail.reply_to,
+ reply_to = email.get('reply_to'),
attachments = attachments,
message_id = mail.message_id,
references = mail.references,
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index 600c3c6d832..16587ae24fd 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -78,7 +78,7 @@ class mail_message(osv.Model):
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
if not message.get('model') or not message.get('res_id') or not self.pool.get(message['model']):
continue
- 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.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
return result
def _get_to_read(self, cr, uid, ids, name, arg, context=None):
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index be34153e316..d75a9199369 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -25,6 +25,7 @@ import dateutil
import email
import logging
import pytz
+import re
import time
import xmlrpclib
from email.message import Message
@@ -39,7 +40,7 @@ _logger = logging.getLogger(__name__)
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):
@@ -387,6 +388,18 @@ class mail_thread(osv.AbstractModel):
return [('message_unread', '=', True)]
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
#------------------------------------------------------
@@ -635,14 +648,14 @@ class mail_thread(osv.AbstractModel):
"""
if context is None:
context = {}
+ data = {}
+ if isinstance(custom_values, dict):
+ data = custom_values.copy()
model = context.get('thread_model') or self._name
model_pool = self.pool.get(model)
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'):
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)
return res_id
@@ -807,6 +820,42 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.")
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])
+ partner_id = 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',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
""" Post a new message in an existing thread, returning the new
@@ -895,7 +944,7 @@ class mail_thread(osv.AbstractModel):
return mail_message.create(cr, uid, values, context=context)
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):
""" Wrapper on message_post, used for user input :
- mail gateway
@@ -908,26 +957,12 @@ class mail_thread(osv.AbstractModel):
- type and subtype: comment and mail.mt_comment by default
- attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter.
- - extra_email: [ 'Fabien ', 'al@openerp.com' ]
"""
- partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message')
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([])
- 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:
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])
@@ -935,8 +970,21 @@ class mail_thread(osv.AbstractModel):
if self._name == 'mail.thread' and parent_message.author_id.id:
partner_ids.add((4, parent_message.author_id.id))
- # 1.A.3: add specified recipients
- partner_ids |= set(kwargs.pop('partner_ids', []))
+ # 1.A.2: add specified recipients
+ 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
if content_subtype == 'plaintext':
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 3f33f1dbca5..41b0a46c2b4 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -62,16 +62,6 @@ class res_users(osv.Model):
# create aliases for all users and avoid constraint errors
self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(res_users, self)._auto_init,
self._columns['alias_id'], 'login', alias_force_key='id', context=context)
- # make already existing users follow themselves, using SQL to avoid using the ORM during the auto_init
- cr.execute(""" SELECT p.id FROM res_partner p
- LEFT JOIN mail_followers n
- ON (n.partner_id = p.id AND n.res_model = 'res.partner' AND n.res_id = p.id)
- WHERE n.id IS NULL
- """)
- params = [(res[0], res[0]) for res in cr.fetchall()]
- cr.executemany(""" INSERT INTO mail_followers (partner_id, res_model, res_id)
- VALUES (%s, 'res.partner', %s)
- """, params)
def create(self, cr, uid, data, context=None):
# create default alias same as the login
@@ -81,12 +71,11 @@ class res_users(osv.Model):
mail_alias = self.pool.get('mail.alias')
alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context)
data['alias_id'] = alias_id
- data.pop('alias_name', None) # prevent errors during copy()
+ data.pop('alias_name', None) # prevent errors during copy()
- # create user that follows its related partner
+ # create user
user_id = super(res_users, self).create(cr, uid, data, context=context)
user = self.browse(cr, uid, user_id, context=context)
- self.pool.get('res.partner').message_subscribe(cr, uid, [user.partner_id.id], [user.partner_id.id], context=context)
# alias
mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context)
# create a welcome message
@@ -150,6 +139,9 @@ class res_users(osv.Model):
self.pool.get('res.partner').message_subscribe(cr, uid, [partner_id], partner_ids, subtype_ids=subtype_ids, context=context)
return True
+ def message_create_partners_from_emails(self, cr, uid, emails, context=None):
+ return self.pool.get('res.partner').message_create_partners_from_emails(cr, uid, emails, context=context)
+
class res_users_mail_group(osv.Model):
""" Update of res.users class
diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js
index f8643e94bb3..e3986a390d6 100644
--- a/addons/mail/static/src/js/mail.js
+++ b/addons/mail/static/src/js/mail.js
@@ -253,7 +253,14 @@ openerp.mail = function (session) {
} else {
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_delete_attachment = true;
this.emails_from = [];
+ this.partners_from = [];
},
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_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) {
-
+ var self = this;
if(!this.do_check_attachment_upload()) {
return false;
}
- if (default_composition_mode == 'reply') {
+ // create list of new partners
+ this.check_recipient_partners().done(function (partner_ids) {
var context = {
'default_composition_mode': default_composition_mode,
- 'default_parent_id': this.id,
- 'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
- 'default_attachment_ids': this.attachment_ids,
+ 'default_parent_id': self.id,
+ 'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
+ 'default_attachment_ids': self.attachment_ids,
+ 'default_partner_ids': partner_ids,
};
- } else {
- var context = {
- 'default_model': this.context.default_model,
- 'default_res_id': this.context.default_res_id,
- 'default_composition_mode': default_composition_mode,
- 'default_parent_id': this.id,
- 'default_body': mail.ChatterUtils.get_text2html(this.$el ? (this.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
- 'default_attachment_ids': this.attachment_ids,
- };
- }
- var action = {
- type: 'ir.actions.act_window',
- res_model: 'mail.compose.message',
- view_mode: 'form',
- view_type: 'form',
- views: [[false, 'form']],
- target: 'new',
- context: context,
- };
+ if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
+ context.default_model = self.context.default_model;
+ context.default_res_id = self.context.default_res_id;
+ }
+
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: context,
+ };
+
+ self.do_action(action);
+ self.on_cancel();
+ });
- this.do_action(action);
- this.on_cancel();
},
reinit: function() {
@@ -562,63 +570,68 @@ openerp.mail = function (session) {
}
},
- check_recipient_partners: function (emails) {
+ check_recipient_partners: function () {
var self = this;
- var deferreds = [];
- for (var i = 0; i < emails.length; i++) {
- deferreds.push($.Deferred());
+ var partners_from = [];
+ var emails = [];
+ _.each(this.emails_from, function (email_from) {
+ if (email_from[1] && !_.find(emails, function (email) {return email == email_from[0][4];})) {
+ emails.push(email_from[0][1]);
+ }
+ });
+ var deferred_check = $.Deferred();
+ if (emails.length == 0) {
+ return deferred_check.resolve(partners_from);
}
- var ds_partner = new session.web.DataSetSearch(this, 'res.partner');
- _.each(emails, function (email) {
- ds_partner.call('search', [[['email', 'ilike', email]]]).then(function (partner_ids) {
- var deferred = deferreds[_.indexOf(emails, email)];
- if (!partner_ids.length) {
- var pop = new session.web.form.FormOpenPopup(this);
- pop.show_element(
- 'res.partner',
- 0,
- {
- 'default_email': email,
- 'force_email': true,
- 'ref': "compound_context",
- },
- {
- title: _t("Please complete partner's informations"),
- }
- );
- pop.on('write_completed, closed', self, function () {
- deferred.resolve();
- });
- }
- else {
+ self.parent_thread.ds_thread._model.call('message_create_partners_from_emails', [emails]).then(function (partners) {
+ partners_from = _.clone(partners.partner_ids);
+ var deferreds = [];
+ _.each(partners.new_partner_ids, function (id) {
+ var deferred = $.Deferred()
+ deferreds.push(deferred);
+ var pop = new session.web.form.FormOpenPopup(this);
+ pop.show_element(
+ 'res.partner',
+ id,
+ {
+ 'force_email': true,
+ 'ref': "compound_context",
+ },
+ {
+ title: _t("Please complete partner's informations"),
+ }
+ );
+ pop.on('closed', self, function () {
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) {
var self = this;
if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
// 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(extra_email).done(function () {
- self.do_send_message_post();
+ this.check_recipient_partners().done(function (partner_ids) {
+ self.do_send_message_post(partner_ids);
});
}
},
/*do post a message and fetch the message*/
- do_send_message_post: function () {
+ do_send_message_post: function (partner_ids) {
var self = this;
this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], {
'body': this.$('textarea').val(),
'subject': false,
'parent_id': this.context.default_parent_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,
}).done(function (message_id) {
var thread = self.parent_thread;
@@ -678,26 +691,24 @@ openerp.mail = function (session) {
_.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
}
- var emails_from = _.map(_.filter(messages,
- function (thread) {return thread.author_id && !thread.author_id[0];}),
- function (thread) {return thread.author_id[1];});
-
- 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]);
+ _.each(messages, function (thread) {
+ if (thread.author_id && !thread.author_id[0] &&
+ !_.find(self.emails_from, function (from) {return from[0][4] == thread.author_id[4];})) {
+ self.emails_from.push([thread.author_id, true]);
}
});
+ return self.emails_from;
},
on_checked_email_from: function (event) {
var $input = $(event.target);
var email = $input.attr("data");
_.each(this.emails_from, function (email_from) {
- if (email_from[0] == email) {
+ if (email_from[0][4] == email) {
email_from[1] = $input.is(":checked");
}
});
- }
+ },
});
/**
@@ -1690,7 +1701,6 @@ openerp.mail = function (session) {
this.defaults[key.replace(/^search_default_/, '')] = this.context[key];
}
}
-
this.action.params = _.extend({
'display_indented_thread': 1,
'show_reply_button': true,
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 1fc93781039..9f88116acc7 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -221,11 +221,14 @@ openerp_mail_followers = function(session, mail) {
var nb_subtype = 0;
_(records).each(function (record) {nb_subtype++;});
if (nb_subtype > 1) {
+ this.$('hr').show();
_(records).each(function (record, record_name) {
record.name = record_name;
record.followed = record.followed || undefined;
$(session.web.qweb.render('mail.followers.subtype', {'record': record})).appendTo( self.$('.oe_subtype_list') );
});
+ } else {
+ this.$('hr').hide();
}
},
diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml
index c1a32a2fe89..63f8a10658f 100644
--- a/addons/mail/static/src/xml/mail.xml
+++ b/addons/mail/static/src/xml/mail.xml
@@ -132,8 +132,8 @@
@@ -243,8 +243,8 @@
-
-
+
+
•
•
diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py
index 39bb99007ee..07a87a16910 100644
--- a/addons/mail/tests/test_mail_message.py
+++ b/addons/mail/tests/test_mail_message.py
@@ -365,11 +365,3 @@ class test_mail_access_rights(TestMailBase):
{'subject': 'Subject', 'body': 'Body text'},
{'default_composition_mode': 'reply', 'default_parent_id': pigs_msg_id})
mail_compose.send_mail(cr, user_raoul_id, [compose_id])
-
- # Do: Raoul writes on its own profile, ok because follower of its partner
- self.res_users.message_post(cr, user_raoul_id, user_raoul_id, body='I love Raoul')
- self.res_partner.message_post(cr, user_raoul_id, partner_raoul_id, body='I love Raoul')
- compose_id = mail_compose.create(cr, user_raoul_id,
- {'subject': 'Subject', 'body': 'Body text', 'partner_ids': []},
- {'default_composition_mode': 'comment', 'default_model': 'res.users', 'default_res_id': self.user_raoul_id})
- mail_compose.send_mail(cr, user_raoul_id, [compose_id])
diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py
index bcb60ba44c0..5fddd574182 100644
--- a/addons/mail/wizard/invite.py
+++ b/addons/mail/wizard/invite.py
@@ -76,8 +76,6 @@ class invite_wizard(osv.osv_memory):
'subject': 'Invitation to follow %s' % document.name_get()[0][1],
'body_html': '%s' % wizard.message,
'auto_delete': True,
- 'res_id': False,
- 'model': False,
}, context=context)
mail_mail.send(cr, uid, [mail_id], recipient_ids=[follower_id], context=context)
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/mail/wizard/invite_view.xml b/addons/mail/wizard/invite_view.xml
index 5b3b679ae9f..e957d546d37 100644
--- a/addons/mail/wizard/invite_view.xml
+++ b/addons/mail/wizard/invite_view.xml
@@ -13,7 +13,7 @@
+ context="{'force_email':True, 'show_email':True}"/>
-
- project.task.search.form
- project.task
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
account.analytic.account.form.inherit
account.analytic.account
@@ -593,11 +610,7 @@
Tasks
project.task
- form
kanban,tree,form,calendar,gantt,graph
-
-
- {}
diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py
index 8b2afb130e1..b0b7193b7aa 100644
--- a/addons/project_issue/project_issue.py
+++ b/addons/project_issue/project_issue.py
@@ -67,12 +67,11 @@ class project_issue(base_stage, osv.osv):
def create(self, cr, uid, vals, context=None):
if context is None:
context = {}
- if not vals.get('stage_id') and vals.get('project_id'):
+ if not vals.get('stage_id'):
ctx = context.copy()
- ctx['default_project_id'] = vals['project_id']
+ if vals.get('project_id'):
+ ctx['default_project_id'] = vals['project_id']
vals['stage_id'] = self._get_default_stage_id(cr, uid, context=ctx)
- elif not vals.get('stage_id') and context.get('default_project_id'):
- vals['stage_id'] = self._get_default_stage_id(cr, uid, context=context)
return super(project_issue, self).create(cr, uid, vals, context=context)
def _get_default_project_id(self, cr, uid, context=None):
@@ -481,6 +480,11 @@ class project_issue(base_stage, osv.osv):
# 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):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
@@ -492,17 +496,18 @@ class project_issue(base_stage, osv.osv):
desc = html2plaintext(msg.get('body')) if msg.get('body') else ''
- custom_values.update({
+ defaults = {
'name': msg.get('subject') or _("No Subject"),
'description': desc,
'email_from': msg.get('from'),
'email_cc': msg.get('cc'),
'user_id': False,
- })
+ }
if msg.get('priority'):
- custom_values['priority'] = msg.get('priority')
+ defaults['priority'] = msg.get('priority')
- res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ defaults.update(custom_values)
+ res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
return res_id
def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
diff --git a/addons/sale/res_config.py b/addons/sale/res_config.py
index f87841e1338..44733be3e39 100644
--- a/addons/sale/res_config.py
+++ b/addons/sale/res_config.py
@@ -98,13 +98,20 @@ Example: Product: this product is deprecated, do not purchase more than 5.
wizard = self.browse(cr, uid, ids)[0]
if wizard.time_unit:
- product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant')
- product.write({'uom_id': wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
+ product = False
+ try:
+ product = ir_model_data.get_object(cr, uid, 'product', 'product_product_consultant')
+ except:
+ #product with xml_id product_product_consultant has not been found. Don't do anything except logging the exception
+ import logging
+ _logger = logging.getLogger(__name__)
+ _logger.warning("Warning, product with xml_id 'product_product_consultant' hasn't been found")
+ if product:
+ product.write({'uom_id': wizard.time_unit.id, 'uom_po_id': wizard.time_unit.id})
if wizard.module_project and wizard.time_unit:
user = self.pool.get('res.users').browse(cr, uid, uid, context)
user.company_id.write({'project_time_mode_id': wizard.time_unit.id})
-
return {}
def onchange_task_work(self, cr, uid, ids, task_work, context=None):
diff --git a/addons/web_analytics/__openerp__.py b/addons/web_analytics/__openerp__.py
index a15e770b120..829bd0ddff7 100644
--- a/addons/web_analytics/__openerp__.py
+++ b/addons/web_analytics/__openerp__.py
@@ -31,7 +31,7 @@ Collects web application usage with Google Analytics.
""",
'author': 'OpenERP SA',
'website': 'http://openerp.com',
- 'depends': ['web', 'auth_signup'],
+ 'depends': ['web'],
'data': [],
'installable': True,
'active': False,
diff --git a/addons/web_analytics/static/src/js/web_analytics.js b/addons/web_analytics/static/src/js/web_analytics.js
index 01c7cc2a7a3..26171cb32aa 100644
--- a/addons/web_analytics/static/src/js/web_analytics.js
+++ b/addons/web_analytics/static/src/js/web_analytics.js
@@ -3,12 +3,13 @@ var _gaq = _gaq || []; // asynchronous stack used by google analytics
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
- * openerp url is changed.
- * 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
- */
+ /*
+ * The Web Analytics Module inserts the Google Analytics JS Snippet
+ * at the top of the page, and sends to google an url each time the
+ * openerp url is changed.
+ * 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
(function() {
@@ -20,138 +21,229 @@ openerp.web_analytics = function(instance) {
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() {
- // initialize tracker
- _gaq.push(['_setAccount', 'UA-7333765-1']);
- //_gaq.push(['_setAccount', 'UA-36797757-1']); // Debug code
- _gaq.push(['_setDomainName', 'none']); // Change for the real domain
-
- // Track user types
- if (instance.session.uid !== 1) {
- if ((/\.demo.openerp.com/).test(instance.session.server)) {
- _gaq.push(['_setCustomVar', 1, 'User Type', 'Demo User', 1]);
+ /* Uncomment this lines when going on production */
+ _gaq.push(['_setAccount', 'UA-7333765-1']);
+ _gaq.push(['_setDomainName', '.openerp.com']); // Allow multi-domain
+ /**/
+ },
+ /*
+ * This method MUST be overriden by saas_demo and saas_trial in order to
+ * set the correct user type. By default, the user connected is local to
+ * the DB (like in accounts).
+ */
+ _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 {
- _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() {
+ var self = this;
+ return instance.session.rpc("/web/webclient/version_info", {})
+ .done(function(res) {
+ _gaq.push(['_setCustomVar', 5, 'Version', res.server_version, 3]);
+ // 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', self.user_access_level, 1]);
- // Track object usage
- _gaq.push(['_setCustomVar', 2, 'Object', 'no_model', 3]);
- // Tack view usage
- _gaq.push(['_setCustomVar', 3, 'View Type', 'default', 3]);
-
- _gaq.push(['_trackPageview']);
- };
-
- var 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 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});
+ // Track User Type Conversion, Custom Variable 3 in GA with session level scope
+ // Values: 'Visitor', 'Demo', 'Online Trial', 'Online Paying', 'Local User'
+ _gaq.push(['_setCustomVar', 1, 'User Type Conversion', self._get_user_type(), 2]);
+ _gaq.push(['_trackPageview']);
+ return;
+ });
+ },
+ /*
+ * Method called in order to send _trackPageview to GA
+ */
+ _push_pageview: function(url) {
_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
- instance.web.ActionManager.include({
- ir_actions_client: function (action, options) {
- var url = instance.web_analytics.generateUrl({'action': action.tag});
- _gaq.push(['_trackPageview', url]);
- return this._super.apply(this, arguments);
- },
- });
-
- // Track button events
- instance.web.View.include({
- do_execute_action: function(action_data, dataset, record_id, on_closed) {
- console.log(action_data);
- 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 || '';
+ },
+ /*
+ * This method includes the tracker into views and managers. It can be overriden
+ * by other modules in order to extend tracking functionalities
+ */
+ include_tracker: function() {
+ var t = this;
+ // 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) {
+ this._super.apply(this, arguments);
+ var self = this;
+ this.on('record_created', 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,
+ 'noninteraction': true,
+ });
+ });
+ 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,
+ 'noninteraction': true,
+ });
+ });
}
- 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
- instance.web.CrashManager.include({
- show_error: function(error) {
- var hash = window.location.hash;
- var params = $.deparam(hash.substr(hash.indexOf('#')+1));
- var options = {};
- if (params.model && params.view_type) {
- options = {'model': params.model, 'view_type': params.view_type};
- } else {
- options = {'action': params.action};
- }
- 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 client actions
+ instance.web.ActionManager.include({
+ ir_actions_client: function (action, options) {
+ var url = instance.web_analytics.generateUrl({'action': action.tag});
+ var category = action.res_model || action.type;
+ t._push_event({
+ 'category': action.res_model || action.type,
+ 'action': action.name || action.tag,
+ 'label': url,
+ });
+ return 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,
+ 'noninteraction': true,
+ });
+ return this._super.apply(this, arguments);
+ },
+ });
- if (instance.client instanceof instance.web.WebClient) { // not for embedded clients
- init_tracker();
-
- // Set the account and domain to start tracking
- instance.client.on('state_pushed', this, on_state_pushed);
-
- include_tracker();
- } else if (!instance.client) {
- // client does not already exists, we are in monodb mode
-
- instance.web.WebClient.include({
- init: function() {
- init_tracker();
- return this._super.apply(this, arguments);
- },
- start: function() {
- var self = this;
- return this._super.apply(this, arguments).done(function() {
- self.on('state_pushed', self, on_state_pushed);
- include_tracker();
- });
- }
- });
- }
+ // Track error events
+ instance.web.CrashManager.include({
+ show_error: function(error) {
+ var hash = window.location.hash;
+ var params = $.deparam(hash.substr(hash.indexOf('#')+1));
+ var options = {};
+ if (params.model && params.view_type) {
+ options = {'model': params.model, 'view_type': params.view_type};
+ } else {
+ options = {'action': params.action};
+ }
+ var url = instance.web_analytics.generateUrl(options);
+ if (error.code) {
+ t._push_event({
+ 'category': error.message,
+ 'action': error.data.fault_code,
+ 'label': url,
+ 'noninteraction': true,
+ });
+ } else {
+ t._push_event({
+ 'category': error.type,
+ 'action': error.data.debug,
+ 'label': url,
+ 'noninteraction': true,
+ });
+ }
+ this._super.apply(this, arguments);
+ },
+ });
+ },
+ });
// ----------------------------------------------------------------
// utility functions
@@ -165,4 +257,37 @@ openerp.web_analytics = function(instance) {
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();
+ },
+ });
+ }
+
};