[MERGE] Forward-port 8.0 up to 494ecc6
authorOlivier Dony <odo@openerp.com>
Fri, 1 Aug 2014 12:33:21 +0000 (14:33 +0200)
committerOlivier Dony <odo@openerp.com>
Fri, 1 Aug 2014 12:33:21 +0000 (14:33 +0200)
19 files changed:
addons/account/account_analytic_line.py
addons/account/account_invoice.py
addons/account_analytic_plans/account_analytic_plans.py
addons/base_action_rule/base_action_rule.py
addons/crm/base_partner_merge.py
addons/crm/crm_lead.py
addons/event/event.py
addons/mail/mail_thread.py
addons/mail/static/src/css/mail_group.css
addons/mass_mailing/controllers/main.py
addons/mass_mailing/models/mass_mailing.py
addons/web/static/src/css/base.css
addons/web/static/src/css/base.sass
addons/web/static/src/js/chrome.js
addons/web/static/src/js/core.js
addons/web/static/src/js/view_form.js
addons/web/static/src/xml/base.xml
addons/website_customer/controllers/main.py
openerp/models.py

index d5ec349..0c122b2 100644 (file)
@@ -74,13 +74,19 @@ class account_analytic_line(osv.osv):
         product_obj = self.pool.get('product.product')
         analytic_journal_obj =self.pool.get('account.analytic.journal')
         product_price_type_obj = self.pool.get('product.price.type')
+        product_uom_obj = self.pool.get('product.uom')
         j_id = analytic_journal_obj.browse(cr, uid, journal_id, context=context)
         prod = product_obj.browse(cr, uid, prod_id, context=context)
         result = 0.0
         if prod_id:
-            unit = prod.uom_id.id
+            unit_obj = False
+            if unit:
+                unit_obj = product_uom_obj.browse(cr, uid, unit, context=context)
+            if not unit_obj or prod.uom_id.category_id.id != unit_obj.category_id.id:
+                unit = prod.uom_id.id
             if j_id.type == 'purchase':
-                unit = prod.uom_po_id.id
+                if not unit_obj or prod.uom_po_id.category_id.id != unit_obj.category_id.id:
+                    unit = prod.uom_po_id.id
         if j_id.type <> 'sale':
             a = prod.property_account_expense.id
             if not a:
index f4b3ac2..a8be837 100644 (file)
@@ -379,7 +379,7 @@ class account_invoice(models.Model):
         assert len(self) == 1, 'This option should only be used for a single id at a time.'
         template = self.env.ref('account.email_template_edi_invoice', False)
         compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
-        ctx = dict(self._context,
+        ctx = dict(
             default_model='account.invoice',
             default_res_id=self.id,
             default_use_template=bool(template),
@@ -793,7 +793,10 @@ class account_invoice(models.Model):
                 continue
 
             ctx = dict(self._context, lang=inv.partner_id.lang)
-            date_invoice = inv.date_invoice or fields.Date.context_today(self)
+
+            if not inv.date_invoice:
+                inv.with_context(ctx).write({'date_invoice': fields.Date.context_today(self)})
+            date_invoice = inv.date_invoice
 
             company_currency = inv.company_id.currency_id
             # create the analytical lines, one move line per invoice line
@@ -906,7 +909,6 @@ class account_invoice(models.Model):
             move = account_move.with_context(ctx).create(move_vals)
             # make the invoice point to that move
             vals = {
-                'date_invoice': date_invoice,
                 'move_id': move.id,
                 'period_id': period.id,
                 'move_name': move.name,
index 5106323..8121f78 100644 (file)
@@ -392,7 +392,7 @@ class account_invoice(osv.osv):
                 if inv.type in ('in_invoice', 'in_refund'):
                     ref = inv.reference
                 else:
-                    ref = self._convert_ref(cr, uid, inv.number)
+                    ref = self._convert_ref(inv.number)
                 obj_move_line = acct_ins_obj.browse(cr, uid, il['analytics_id'], context=context)
                 ctx = context.copy()
                 ctx.update({'date': inv.date_invoice})
index b743603..d8fbec0 100644 (file)
@@ -156,6 +156,75 @@ class base_action_rule(osv.osv):
         """ Wrap the methods `create` and `write` of the models specified by
             the rules given by `ids` (or all existing rules if `ids` is `None`.)
         """
+        #
+        # Note: the patched methods create and write must be defined inside
+        # another function, otherwise their closure may be wrong. For instance,
+        # the function create refers to the outer variable 'create', which you
+        # expect to be bound to create itself. But that expectation is wrong if
+        # create is defined inside a loop; in that case, the variable 'create'
+        # is bound to the last function defined by the loop.
+        #
+
+        def make_create():
+            """ instanciate a create method that processes action rules """
+            def create(self, cr, uid, vals, context=None, **kwargs):
+                # avoid loops or cascading actions
+                if context and context.get('action'):
+                    return create.origin(self, cr, uid, vals, context=context)
+
+                # call original method with a modified context
+                context = dict(context or {}, action=True)
+                new_id = create.origin(self, cr, uid, vals, context=context, **kwargs)
+
+                # as it is a new record, we do not consider the actions that have a prefilter
+                action_model = self.pool.get('base.action.rule')
+                action_dom = [('model', '=', self._name),
+                              ('kind', 'in', ['on_create', 'on_create_or_write'])]
+                action_ids = action_model.search(cr, uid, action_dom, context=context)
+
+                # check postconditions, and execute actions on the records that satisfy them
+                for action in action_model.browse(cr, uid, action_ids, context=context):
+                    if action_model._filter(cr, uid, action, action.filter_id, [new_id], context=context):
+                        action_model._process(cr, uid, action, [new_id], context=context)
+                return new_id
+
+            return create
+
+        def make_write():
+            """ instanciate a write method that processes action rules """
+            def write(self, cr, uid, ids, vals, context=None, **kwargs):
+                # avoid loops or cascading actions
+                if context and context.get('action'):
+                    return write.origin(self, cr, uid, ids, vals, context=context)
+
+                # modify context
+                context = dict(context or {}, action=True)
+                ids = [ids] if isinstance(ids, (int, long, str)) else ids
+
+                # retrieve the action rules to possibly execute
+                action_model = self.pool.get('base.action.rule')
+                action_dom = [('model', '=', self._name),
+                              ('kind', 'in', ['on_write', 'on_create_or_write'])]
+                action_ids = action_model.search(cr, uid, action_dom, context=context)
+                actions = action_model.browse(cr, uid, action_ids, context=context)
+
+                # check preconditions
+                pre_ids = {}
+                for action in actions:
+                    pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, context=context)
+
+                # call original method
+                write.origin(self, cr, uid, ids, vals, context=context, **kwargs)
+
+                # check postconditions, and execute actions on the records that satisfy them
+                for action in actions:
+                    post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], context=context)
+                    if post_ids:
+                        action_model._process(cr, uid, action, post_ids, context=context)
+                return True
+
+            return write
+
         updated = False
         if ids is None:
             ids = self.search(cr, SUPERUSER_ID, [])
@@ -164,61 +233,8 @@ class base_action_rule(osv.osv):
             model_obj = self.pool[model]
             if not hasattr(model_obj, 'base_action_ruled'):
                 # monkey-patch methods create and write
-
-                def create(self, cr, uid, vals, context=None, **kwargs):
-                    # avoid loops or cascading actions
-                    if context and context.get('action'):
-                        return create.origin(self, cr, uid, vals, context=context)
-
-                    # call original method with a modified context
-                    context = dict(context or {}, action=True)
-                    new_id = create.origin(self, cr, uid, vals, context=context, **kwargs)
-
-                    # as it is a new record, we do not consider the actions that have a prefilter
-                    action_model = self.pool.get('base.action.rule')
-                    action_dom = [('model', '=', self._name),
-                                  ('kind', 'in', ['on_create', 'on_create_or_write'])]
-                    action_ids = action_model.search(cr, uid, action_dom, context=context)
-
-                    # check postconditions, and execute actions on the records that satisfy them
-                    for action in action_model.browse(cr, uid, action_ids, context=context):
-                        if action_model._filter(cr, uid, action, action.filter_id, [new_id], context=context):
-                            action_model._process(cr, uid, action, [new_id], context=context)
-                    return new_id
-
-                def write(self, cr, uid, ids, vals, context=None, **kwargs):
-                    # avoid loops or cascading actions
-                    if context and context.get('action'):
-                        return write.origin(self, cr, uid, ids, vals, context=context)
-
-                    # modify context
-                    context = dict(context or {}, action=True)
-                    ids = [ids] if isinstance(ids, (int, long, str)) else ids
-
-                    # retrieve the action rules to possibly execute
-                    action_model = self.pool.get('base.action.rule')
-                    action_dom = [('model', '=', self._name),
-                                  ('kind', 'in', ['on_write', 'on_create_or_write'])]
-                    action_ids = action_model.search(cr, uid, action_dom, context=context)
-                    actions = action_model.browse(cr, uid, action_ids, context=context)
-
-                    # check preconditions
-                    pre_ids = {}
-                    for action in actions:
-                        pre_ids[action] = action_model._filter(cr, uid, action, action.filter_pre_id, ids, context=context)
-
-                    # call original method
-                    write.origin(self, cr, uid, ids, vals, context=context, **kwargs)
-
-                    # check postconditions, and execute actions on the records that satisfy them
-                    for action in actions:
-                        post_ids = action_model._filter(cr, uid, action, action.filter_id, pre_ids[action], context=context)
-                        if post_ids:
-                            action_model._process(cr, uid, action, post_ids, context=context)
-                    return True
-
-                model_obj._patch_method('create', create)
-                model_obj._patch_method('write', write)
+                model_obj._patch_method('create', make_create())
+                model_obj._patch_method('write', make_write())
                 model_obj.base_action_ruled = True
                 updated = True
 
index 0331170..9749e7a 100644 (file)
@@ -377,11 +377,20 @@ class MergePartnerAutomatic(osv.TransientModel):
         return {'type': 'ir.actions.act_window_close'}
 
     def _generate_query(self, fields, maximum_group=100):
-        group_fields = ', '.join(fields)
+        sql_fields = []
+        for field in fields:
+            if field in ['email', 'name']:
+                sql_fields.append('lower(%s)' % field)
+            elif field in ['vat']:
+                sql_fields.append("replace(%s, ' ', '')" % field)
+            else:
+                sql_fields.append(field)
+
+        group_fields = ', '.join(sql_fields)
 
         filters = []
         for field in fields:
-            if field in ['email', 'name']:
+            if field in ['email', 'name', 'vat']:
                 filters.append((field, 'IS NOT', 'NULL'))
 
         criteria = ' AND '.join('%s %s %s' % (field, operator, value)
index b82f5a4..c4fd782 100644 (file)
@@ -29,6 +29,7 @@ from openerp import tools
 from openerp.addons.base.res.res_partner import format_address
 from openerp.osv import fields, osv, orm
 from openerp.tools.translate import _
+from openerp.tools import email_re
 
 CRM_LEAD_FIELDS_TO_MERGE = ['name',
     'partner_id',
@@ -1032,4 +1033,16 @@ class crm_lead(format_address, osv.osv):
             return {'value':{'country_id':country_id}}
         return {}
 
+    def message_partner_info_from_emails(self, cr, uid, id, emails, link_mail=False, context=None):
+        res = super(crm_lead, self).message_partner_info_from_emails(cr, uid, id, emails, link_mail=link_mail, context=context)
+        lead = self.browse(cr, uid, id, context=context)
+        for partner_info in res:
+            if not partner_info.get('partner_id') and (lead.partner_name or lead.contact_name):
+                emails = email_re.findall(partner_info['full_name'] or '')
+                email = emails and emails[0] or ''
+                if email and lead.email_from and email.lower() == lead.email_from.lower():
+                    partner_info['full_name'] = '%s <%s>' % (lead.partner_name or lead.contact_name, email)
+                    break
+        return res
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 325682f..0ee90ea 100644 (file)
@@ -147,9 +147,13 @@ class event_event(models.Model):
             ('done', 'Done')
         ], string='Status', default='draft', readonly=True, required=True, copy=False,
         help="If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'.")
-    email_registration_id = fields.Many2one('email.template', string='Registration Confirmation Email',
+    email_registration_id = fields.Many2one(
+        'email.template', string='Registration Confirmation Email',
+        domain=[('model', '=', 'event.registration')],
         help='This field contains the template of the mail that will be automatically sent each time a registration for this event is confirmed.')
-    email_confirmation_id = fields.Many2one('email.template', string='Event Confirmation Email',
+    email_confirmation_id = fields.Many2one(
+        'email.template', string='Event Confirmation Email',
+        domain=[('model', '=', 'event.registration')],
         help="If you set an email template, each participant will receive this email announcing the confirmation of the event.")
     reply_to = fields.Char(string='Reply-To Email',
         readonly=False, states={'done': [('readonly', True)]},
@@ -282,8 +286,8 @@ class event_event(models.Model):
 
 
 class event_registration(models.Model):
-    """Event Registration"""
-    _name= 'event.registration'
+    _name = 'event.registration'
+    _description = 'Event Registration'
     _inherit = ['mail.thread', 'ir.needaction_mixin']
     _order = 'name, create_date desc'
 
index 82b048b..9125b77 100644 (file)
@@ -935,7 +935,7 @@ class mail_thread(osv.AbstractModel):
 
         # 1. message is a reply to an existing message (exact match of message_id)
         ref_match = thread_references and tools.reference_re.search(thread_references)
-        msg_references = thread_references.split()
+        msg_references = mail_header_msgid_re.findall(thread_references)
         mail_message_ids = mail_msg_obj.search(cr, uid, [('message_id', 'in', msg_references)], context=context)
         if ref_match and mail_message_ids:
             original_msg = mail_msg_obj.browse(cr, SUPERUSER_ID, mail_message_ids[0], context=context)
@@ -978,7 +978,7 @@ class mail_thread(osv.AbstractModel):
                                 email_from, email_to, message_id, model, thread_id, custom_values, uid)
                             return [route]
 
-        # 2. Reply to a private message
+        # 3. Reply to a private message
         if in_reply_to:
             mail_message_ids = mail_msg_obj.search(cr, uid, [
                                 ('message_id', '=', in_reply_to),
@@ -995,7 +995,7 @@ class mail_thread(osv.AbstractModel):
                         email_from, email_to, message_id, mail_message.id, custom_values, uid)
                     return [route]
 
-        # 3. Look for a matching mail.alias entry
+        # 4. Look for a matching mail.alias entry
         # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
         # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
         rcpt_tos = \
@@ -1030,7 +1030,7 @@ class mail_thread(osv.AbstractModel):
                         routes.append(route)
                 return routes
 
-        # 4. Fallback to the provided parameters, if they work
+        # 5. Fallback to the provided parameters, if they work
         if not thread_id:
             # Legacy: fallback to matching [ID] in the Subject
             match = tools.res_re.search(decode_header(message, 'Subject'))
index 39cdf5c..20f10b6 100644 (file)
@@ -90,8 +90,6 @@
 }
 
 .openerp .oe_group_button {
-    position: absolute;
-    padding-bottom: 50px;
     bottom: 0;
 }
 
index 5eed0e3..bfe9750 100644 (file)
@@ -67,8 +67,7 @@ class MassMailController(http.Controller):
 
         contact_ids = Contacts.search(cr, SUPERUSER_ID, [('list_id', '=', int(list_id)), ('email', '=', email)], context=context)
         if not contact_ids:
-            contact_ng = Contacts.name_create(cr, SUPERUSER_ID, email, context=context)
-            Contacts.write(cr, SUPERUSER_ID, [contact_ng[0]], {'list_id': int(list_id)}, context=context)
+            Contacts.add_to_list(cr, SUPERUSER_ID, email, int(list_id), context=context)
         # add email to session
         request.session['mass_mailing_email'] = email
         return True
index 5844bcf..ae1a334 100644 (file)
@@ -52,15 +52,24 @@ class MassMailingContact(osv.Model):
         'list_id': _get_latest_list
     }
 
-    def name_create(self, cr, uid, name, context=None):
+    def get_name_email(self, name, context):
         name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
         if name and not email:
             email = name
         if email and not name:
             name = email
+        return name, email
+
+    def name_create(self, cr, uid, name, context=None):
+        name, email = self.get_name_email(name, context=context)
         rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
         return self.name_get(cr, uid, [rec_id], context)[0]
 
+    def add_to_list(self, cr, uid, name, list_id, context=None):
+        name, email = self.get_name_email(name, context=context)
+        rec_id = self.create(cr, uid, {'name': name, 'email': email, 'list_id': list_id}, context=context)
+        return self.name_get(cr, uid, [rec_id], context)[0]
+
     def message_get_default_recipients(self, cr, uid, ids, context=None):
         res = {}
         for record in self.browse(cr, uid, ids, context=context):
index b76bee2..00a1959 100644 (file)
@@ -8,6 +8,7 @@
   font-weight: normal;
   font-style: normal;
 }
+
 @font-face {
   font-family: "EntypoRegular";
   src: url("/web/static/src/font/entypo-webfont.eot") format("eot");
@@ -18,6 +19,7 @@
   font-weight: normal;
   font-style: normal;
 }
+
 #oe_main_menu_navbar {
   min-height: 34px;
   z-index: 1001;
   vertical-align: top;
 }
 .openerp .oe_title {
-  width: 38%;
   float: left;
 }
 .openerp .oe_title:after {
 .openerp .oe_form_dirty button.oe_highlight_on_dirty:hover {
   background: #ed6f6a;
 }
+.openerp .oe_warning_redirect {
+  border: none !important;
+  padding: 0 !important;
+  margin-left: 20px !important;
+  background: #f5f7f9 !important;
+  box-shadow: none !important;
+}
+.openerp .oe_warning_redirect:hover {
+  text-decoration: underline !important;
+}
 .openerp .oe_stat_button {
   font-weight: normal;
   width: 132px !important;
   display: inline;
   vertical-align: middle;
 }
-.openerp .oe_warning_redirect {
-  border: none !important;
-  padding: 0 !important;
-  margin-left: 20px !important;
-  background: #f5f7f9 !important;
-  box-shadow: none !important;
-}
-.openerp .oe_warning_redirect:hover {
-  text-decoration: underline !important;
-}
 .openerp .oe_stat_button:hover {
   background: #7c7bad;
   color: white;
     top: 0px;
   }
 }
+
 .kitten-mode-activated {
   background-size: cover;
   background-attachment: fixed;
index f1f9481..29eec8f 100644 (file)
@@ -239,8 +239,7 @@ $sheet-padding: 16px
     td
         vertical-align: top
     .oe_title
-        width: 38%
-        float: left    
+        float: left
     .oe_title:after
         content: "."
         display: block
index b0e22f9..c2b0af4 100644 (file)
@@ -115,6 +115,7 @@ instance.web.Dialog = instance.web.Widget.extend({
             this.init_dialog();
         }
         this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
+        $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal is opened
         //add to list of currently opened modal
         opened_modal.push(this.$dialog_box);
         return this;
@@ -1156,7 +1157,7 @@ instance.web.Client = instance.web.Widget.extend({
             }, 0);
         });
         instance.web.bus.on('click', this, function(ev) {
-            $.fn.tooltip('destroy');
+            $('.tooltip').remove();
             if (!$(ev.target).is('input[type=file]')) {
                 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
             }
index 23645e2..47e2a30 100644 (file)
@@ -772,7 +772,7 @@ instance.web.unblockUI = function() {
 /* Bootstrap defaults overwrite */
 $.fn.tooltip.Constructor.DEFAULTS.placement = 'auto top';
 $.fn.tooltip.Constructor.DEFAULTS.html = true;
-$.fn.tooltip.Constructor.DEFAULTS.container = 'body';
+$.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover focus click';
 //overwrite bootstrap tooltip method to prevent showing 2 tooltip at the same time
 var bootstrap_show_function = $.fn.tooltip.Constructor.prototype.show;
 $.fn.tooltip.Constructor.prototype.show = function () {
@@ -786,6 +786,18 @@ $.fn.tooltip.Constructor.prototype.show = function () {
     if (e.isDefaultPrevented() || !inDom) return;
     return bootstrap_show_function.call(this);
 };
+//overwrite bootstrap tooltip init method in order to check if tooltip is in a modal or not and
+//if so it needs to have a container body in order to be visible
+var bootstrap_init_tooltip_fnct = $.fn.tooltip.Constructor.prototype.init;
+$.fn.tooltip.Constructor.prototype.init = function (type, element, options) {
+    options = options || {}
+    if ($('.modal[aria-hidden="false"]').length !== 0){
+        if (options && !options.container){
+            options = _.extend({container: 'body'},options);
+        }
+    }
+    return bootstrap_init_tooltip_fnct.call(this, type, element, options);
+}
 
 /**
  * Registry for all the client actions key: tag value: widget
index 5767ffe..66e5005 100644 (file)
@@ -1835,7 +1835,6 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi
         trigger = trigger || this.$el;
         options = _.extend({
                 delay: { show: 500, hide: 0 },
-                trigger: 'hover',
                 title: function() {
                     var template = widget.template + '.tooltip';
                     if (!QWeb.has_template(template)) {
index d77b8d3..2aeed9e 100644 (file)
@@ -40,7 +40,7 @@
                     <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                     <h3 class="modal-title"><t t-raw="title"/></h3>
                 </div>
-                <div class="modal-body" style="overflow-y: auto;">
+                <div class="modal-body">
                 </div>
             </div>
         </div>
index fcb2d0d..d4b996a 100644 (file)
@@ -42,11 +42,12 @@ class WebsiteCustomer(http.Controller):
         if country_id:
             domain += [('country_id', '=', country_id)]
             if not any(x['country_id'][0] == country_id for x in countries):
-                country = country_obj.browse(cr, uid, country_id, context)
-                countries.append({
-                    'country_id_count': 0,
-                    'country_id': (country_id, country.name)
-                })
+                country = country_obj.read(cr, uid, country_id, ['name'], context)
+                if country:
+                    countries.append({
+                        'country_id_count': 0,
+                        'country_id': (country_id, country['name'])
+                    })
                 countries.sort(key=lambda d: d['country_id'][1])
 
         countries.insert(0, {
index 8c86f2e..6a6f232 100644 (file)
@@ -4447,18 +4447,19 @@ class BaseModel(object):
         order_by = self._generate_order_by(order, query)
         from_clause, where_clause, where_clause_params = query.get_sql()
 
-        limit_str = limit and ' limit %d' % limit or ''
-        offset_str = offset and ' offset %d' % offset or ''
         where_str = where_clause and (" WHERE %s" % where_clause) or ''
-        query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str
 
         if count:
-            # /!\ the main query must be executed as a subquery, otherwise
-            # offset and limit apply to the result of count()!
-            cr.execute('SELECT count(*) FROM (%s) AS count' % query_str, where_clause_params)
+            # Ignore order, limit and offset when just counting, they don't make sense and could
+            # hurt performance
+            query_str = 'SELECT count(1) FROM ' + from_clause + where_str
+            cr.execute(query_str, where_clause_params)
             res = cr.fetchone()
             return res[0]
 
+        limit_str = limit and ' limit %d' % limit or ''
+        offset_str = offset and ' offset %d' % offset or ''
+        query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str
         cr.execute(query_str, where_clause_params)
         res = cr.fetchall()