diff options
| author | Thibault Delavallée <tde@openerp.com> | 2013-01-10 13:51:29 +0100 |
|---|---|---|
| committer | Thibault Delavallée <tde@openerp.com> | 2013-01-10 13:51:29 +0100 |
| commit | fb3dd0ebc2ae4db6a19d6c781d0b243281c8dfaf (patch) | |
| tree | 28be4e39a3992cb508ec37978020d6cac997f83e | |
| parent | f51c31bbfdb0b396628b29880276f26cbb43f818 (diff) | |
| parent | d9faa7c095718e405e0cf043090526a259d527b8 (diff) | |
[MERGE] Forward porting of the 7.0 addons branch inside trunk until rev 8561.
bzr revid: tde@openerp.com-20130110125129-9g99h3heldcnspfh
29 files changed, 557 insertions, 301 deletions
diff --git a/addons/account/account.py b/addons/account/account.py index 5f20c858cf8..a14c44405b9 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -1463,6 +1463,7 @@ class account_move(osv.osv): line_id = self.pool.get('account.move.line').create(cr, uid, { 'name': _(mode.capitalize()+' Centralisation'), 'centralisation': mode, + 'partner_id': False, 'account_id': account_id, 'move_id': move.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, { 'name': _('Currency Adjustment'), 'centralisation': 'currency', + 'partner_id': False, 'account_id': account_id, 'move_id': move.id, 'journal_id': move.journal_id.id, diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index c9cf90789cb..250a0446534 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1484,11 +1484,10 @@ class account_invoice_line(osv.osv): new_price = res_final['value']['price_unit'] * currency.rate res_final['value']['price_unit'] = new_price - if result['uos_id'] != res.uom_id.id: - selected_uom = self.pool.get('product.uom_id').browse(cr, uid, result['uos_id'], context=context) - if res.uom_id.category_id.id == selected_uom.category_id.id: - new_price = res_final['value']['price_unit'] * uom_id.factor_inv - res_final['value']['price_unit'] = new_price + if result['uos_id'] and result['uos_id'] != res.uom_id.id: + selected_uom = self.pool.get('product.uom').browse(cr, uid, result['uos_id'], context=context) + new_price = self.pool.get('product.uom')._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uos_id']) + res_final['value']['price_unit'] = new_price 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): diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index c8246a187f5..5e072c3d258 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -97,7 +97,7 @@ <form string="Account Period" version="7.0"> <header> <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"/> </header> <sheet> diff --git a/addons/account/wizard/account_open_closed_fiscalyear_view.xml b/addons/account/wizard/account_open_closed_fiscalyear_view.xml index d4cfd398920..16ddfc1f1ac 100644 --- a/addons/account/wizard/account_open_closed_fiscalyear_view.xml +++ b/addons/account/wizard/account_open_closed_fiscalyear_view.xml @@ -5,8 +5,8 @@ <field name="name">account.open.closed.fiscalyear.form</field> <field name="model">account.open.closed.fiscalyear</field> <field name="arch" type="xml"> - <form string="Cancel Fiscal Year Opening Entries" version="7.0"> - <separator string="Cancel Fiscal Year Opening Entries"/> + <form string="Cancel Fiscal Year Closing Entries" version="7.0"> + <separator string="Cancel Fiscal Year Closing Entries"/> <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. </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)]"/> </group> <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 <button string="Discard" class="oe_link" special="cancel"/> </footer> @@ -23,7 +23,7 @@ This wizard will remove the end of year journal entries of selected fiscal year. </record> <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="view_type">form</field> <field name="view_mode">tree,form</field> 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..2fb81a005c0 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -397,7 +397,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 +424,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 +678,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 +961,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. 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 @@ <field name="name">Lost</field> <field eval="1" name="case_default"/> <field eval="True" name="fold"/> + <field eval="1" name="on_change"/> <field name="state">cancel</field> <field eval="'0'" name="probability"/> <field eval="'17'" name="sequence"/> 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 @@ <field name="type_id" invisible="1"/> <field name="stage_id"/> <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="user_id"/> <field name="priority" invisible="1"/> 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 @@ <label for="content" string="Template"/> that will be used as a content template for all new page of this category. </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"> <field name="display_content" widget="html" class="oe_view_only" options='{"safe": True}'/> </div> 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 <b>approved</b>, 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 @@ <record id="mt_holidays_confirmed" model="mail.message.subtype"> <field name="name">To Approve</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 id="mt_holidays_approved" model="mail.message.subtype"> <field name="name">Approved</field> 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 = _("""<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): """ 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_thread.py b/addons/mail/mail_thread.py index be34153e316..f2cea9b4538 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): @@ -388,6 +389,18 @@ class mail_thread(osv.AbstractModel): 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,41 @@ 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]) + 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 +943,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 +956,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 <fpi@openerp.com>', '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 +969,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/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index f8643e94bb3..e08afdad217 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, + 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, }; - } - 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, - }; - this.do_action(action); - this.on_cancel(); + self.do_action(action); + self.on_cancel(); + }); + }, reinit: function() { @@ -562,63 +570,65 @@ 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 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 { + 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(); + 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 +688,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 +1698,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 @@ <div class="oe_emails_from"> <t t-foreach='widget.emails_from' t-as='email_from'> <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]"/> - <t t-raw="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][2]"/> </label> </t> </div> @@ -243,8 +243,8 @@ <t t-if="widget.attachment_ids.length > 0"> <div class="oe_msg_attachment_list"></div> </t> - <a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&id=#{widget.author_id[0]}"><t t-raw="widget.author_id[1]"/></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> + <a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&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[2]"/></span> <span class='oe_subtle'>•</span> <span t-att-title="widget.date"><t t-raw="widget.timerelative"/></span> <span t-if="!widget.options.readonly" class='oe_subtle'>•</span> 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 @@ <field name="res_id" invisible="1"/> <field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..." - context="{'force_email':True}"/> + context="{'force_email':True, 'show_email':True}"/> <field name="message"/> </group> <footer> diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 0062b5e01bd..ec0c2a8de06 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -168,6 +168,7 @@ class mail_compose_message(osv.TransientModel): reply_subject = "%s %s" % (re_prefix, reply_subject) # get partner_ids from original message partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else [] + partner_ids += context.get('default_partner_ids', []) # update the result result = { diff --git a/addons/pad/static/src/css/etherpad.css b/addons/pad/static/src/css/etherpad.css index 381b4eeffec..ad47be32b02 100644 --- a/addons/pad/static/src/css/etherpad.css +++ b/addons/pad/static/src/css/etherpad.css @@ -74,11 +74,9 @@ } .etherpad_readonly ul, .etherpad_readonly ol { - margin-before: 1em; - margin-after: 1em; - margin-start: 0px; - margin-end: 0px; - padding-start: 40px !important; + margin: 0; + margin-left: 1.5em; + padding: 0; } .etherpad_readonly ul li{ list-style-type: disc; @@ -92,8 +90,8 @@ .etherpad_readonly{ font-family: arial, sans-serif; - font-size: 13px; - line-height: 18px; + font-size: 15px; + line-height: 19px; } .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 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 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; } diff --git a/addons/portal/mail_mail.py b/addons/portal/mail_mail.py index a18c71759f8..5d2a91e691f 100644 --- a/addons/portal/mail_mail.py +++ b/addons/portal/mail_mail.py @@ -21,6 +21,7 @@ from openerp import SUPERUSER_ID from openerp.osv import osv +from openerp.osv.orm import except_orm from openerp.tools import append_content_to_html from openerp.tools.translate import _ @@ -35,10 +36,21 @@ class mail_mail(osv.Model): :param partner: browse_record of the specific recipient partner :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: - context = dict(context or {}, signup_valid=True) - partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, partner.id, context=context) - text = _("""Access your personal documents through <a href="%s">our Customer Portal</a>""") % partner.signup_url + contex_signup = dict(context or {}, signup_valid=True) + partner = partner_obj.browse(cr, SUPERUSER_ID, partner.id, context=contex_signup) + 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) return body diff --git a/addons/portal_anonymous/static/src/js/portal_anonymous.js b/addons/portal_anonymous/static/src/js/portal_anonymous.js index 55326225403..92060aa6f99 100644 --- a/addons/portal_anonymous/static/src/js/portal_anonymous.js +++ b/addons/portal_anonymous/static/src/js/portal_anonymous.js @@ -44,8 +44,9 @@ openerp.portal_anonymous = function(instance) { start: function() { var self = this; return $.when(this._super()).then(function() { + var params = $.deparam($.param.querystring()); 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; // XXX get login/pass from server (via a rpc call) ? return self.do_login(dblist[0], 'anonymous', 'anonymous'); diff --git a/addons/project/project.py b/addons/project/project.py index e98254d2fa1..3c7c180ebb3 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -1166,6 +1166,11 @@ class task(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 [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): """ Override to updates the document according to the email. """ if custom_values is None: custom_values = {} @@ -1198,7 +1203,7 @@ class task(base_stage, osv.osv): act = 'do_%s' % res.group(2).lower() if act: 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): if self.pool.get('res.users').has_group(cr, uid, 'project.group_time_work_estimation_tasks'): diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 8b2afb130e1..5a757fdac39 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -481,6 +481,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. 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..75bcf29a575 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,219 @@ 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. + */ + _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]); - } - - // 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}); - _gaq.push(['_trackPageview', url]); - } - }; + }, + /* + * 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]); - var include_tracker = function() { - // include the tracker into views and managers + // 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', this._get_user_type(), 2]); - // 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]); + return instance.session.rpc("/web/webclient/version_info", {}) + .done(function(res) { + _gaq.push(['_setCustomVar', 5, 'Version', res.server_version, 3]); + _gaq.push(['_trackPageview']); + return; }); - 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, + }); + }); + 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 - 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, + }); + 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 +247,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(); + }, + }); + } + }; |
