[MERGE] forward port of branch 8.0 up to 491372e
authorChristophe Simonis <chs@odoo.com>
Wed, 5 Nov 2014 19:30:40 +0000 (20:30 +0100)
committerChristophe Simonis <chs@odoo.com>
Wed, 5 Nov 2014 19:30:40 +0000 (20:30 +0100)
45 files changed:
1  2 
addons/account/account_invoice.py
addons/account/security/account_security.xml
addons/base_action_rule/base_action_rule.py
addons/crm/crm.py
addons/crm/crm_lead.py
addons/hr/hr.py
addons/mail/mail_mail.py
addons/mail/tests/test_mail_features.py
addons/marketing_campaign/marketing_campaign_view.xml
addons/mass_mailing/models/mass_mailing.py
addons/payment/models/payment_acquirer.py
addons/point_of_sale/point_of_sale.py
addons/point_of_sale/static/src/css/pos.css
addons/point_of_sale/static/src/js/screens.js
addons/product/product.py
addons/project_issue/project_issue.py
addons/purchase/purchase_view.xml
addons/report/static/src/js/qwebactionmanager.js
addons/share/wizard/share_wizard.py
addons/web/static/src/js/view_form.js
addons/web/static/src/js/views.js
addons/web/static/src/xml/base.xml
addons/web_kanban/static/src/js/kanban.js
addons/website/controllers/main.py
addons/website/models/ir_qweb.py
addons/website/models/ir_ui_view.py
addons/website/models/website.py
addons/website/static/src/css/website.css
addons/website/static/src/css/website.sass
addons/website/views/website_templates.xml
addons/website_blog/views/website_blog_templates.xml
addons/website_crm/controllers/main.py
addons/website_forum/controllers/main.py
addons/website_forum/models/forum.py
addons/website_sale/controllers/main.py
doc/conf.py
openerp/addons/base/ir/ir_model.py
openerp/addons/base/ir/ir_qweb.py
openerp/addons/base/ir/ir_ui_view.py
openerp/addons/base/res/res_partner.py
openerp/addons/base/res/res_users.py
openerp/models.py
openerp/service/server.py
openerp/tools/convert.py
openerp/tools/misc.py

Simple merge
@@@ -141,11 -128,12 +141,11 @@@ class base_action_rule(osv.osv)
      def _process(self, cr, uid, action, record_ids, context=None):
          """ process the given action on the records """
          model = self.pool[action.model_id.model]
 -
          # modify records
          values = {}
-         if 'date_action_last' in model._all_columns:
+         if 'date_action_last' in model._fields:
              values['date_action_last'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
-         if action.act_user_id and 'user_id' in model._all_columns:
+         if action.act_user_id and 'user_id' in model._fields:
              values['user_id'] = action.act_user_id.id
          if values:
              model.write(cr, uid, record_ids, values, context=context)
Simple merge
Simple merge
diff --cc addons/hr/hr.py
Simple merge
@@@ -303,12 -298,22 +303,22 @@@ class mail_mail(osv.Model)
                          subtype='html',
                          subtype_alternative='plain',
                          headers=headers)
-                     res = ir_mail_server.send_email(cr, uid, msg,
+                     try:
+                         res = ir_mail_server.send_email(cr, uid, msg,
                                                      mail_server_id=mail.mail_server_id.id,
                                                      context=context)
+                     except AssertionError as error:
+                         if error.message == ir_mail_server.NO_VALID_RECIPIENT:
+                             # No valid recipient found for this particular
+                             # mail item -> ignore error to avoid blocking
+                             # delivery to next recipients, if any. If this is
+                             # the only recipient, the mail will show as failed.
+                             _logger.warning("Ignoring invalid recipients for mail.mail %s: %s",
+                                             mail.message_id, email.get('email_to'))
+                         else:
+                             raise
                  if res:
 -                    mail.write({'state': 'sent', 'message_id': res})
 +                    mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
                      mail_sent = True
  
                  # /!\ can't use mail.state here, as mail.refresh() will cause an error
Simple merge
@@@ -917,9 -926,25 +917,12 @@@ function openerp_pos_screens(instance, 
              this._super();
              var self = this;
  
 -            var print_button = this.add_action_button({
 -                    label: _t('Print'),
 -                    icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
 -                    click: function(){ self.print(); },
 -                });
 -
 -            var finish_button = this.add_action_button({
 -                    label: _t('Next Order'),
 -                    icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
 -                    click: function() { self.finishOrder(); },
 -                });
 -
              this.refresh();
-             this.print();
+             if (!this.pos.get('selectedOrder')._printed) {
+                 this.print();
+             }
  
 -            //
              // The problem is that in chrome the print() is asynchronous and doesn't
              // execute until all rpc are finished. So it conflicts with the rpc used
              // to send the orders to the backend, and the user is able to go to the next 
              // 2 seconds is the same as the default timeout for sending orders and so the dialog
              // should have appeared before the timeout... so yeah that's not ultra reliable. 
  
 -            finish_button.set_disabled(true);   
 +            this.lock_screen(true);  
              setTimeout(function(){
 -                finish_button.set_disabled(false);
 +                self.lock_screen(false);  
              }, 2000);
          },
 +        lock_screen: function(locked) {
 +            this._locked = locked;
 +            if (locked) {
 +                this.$('.next').removeClass('highlight');
 +            } else {
 +                this.$('.next').addClass('highlight');
 +            }
 +        },
          print: function() {
+             this.pos.get('selectedOrder')._printed = true;
              window.print();
          },
 -        finishOrder: function() {
 -            this.pos.get('selectedOrder').destroy();
 +        finish_order: function() {
 +            if (!this._locked) {
 +                this.pos.get_order().finalize();
 +            }
 +        },
 +        renderElement: function() {
 +            var self = this;
 +            this._super();
 +            this.$('.next').click(function(){
 +                self.finish_order();
 +            });
 +            this.$('.button.print').click(function(){
 +                self.print();
 +            });
          },
          refresh: function() {
 -            var order = this.pos.get('selectedOrder');
 -            $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
 +            var order = this.pos.get_order();
 +            this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
                      widget:this,
                      order: order,
                      orderlines: order.get('orderLines').models,
Simple merge
Simple merge
                                      <field name="name"/>
                                      <field name="date_planned"/>
                                      <field name="company_id" groups="base.group_multi_company" widget="selection"/>
 -                                    <field name="account_analytic_id" groups="purchase.group_analytic_accounting" domain="[('type','not in',('view','template'))]"/>
 +                                    <field name="account_analytic_id" context="{'default_partner_id':parent.partner_id}" groups="purchase.group_analytic_accounting" domain="[('type','not in',('view','template'))]"/>
-                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,parent.state,context)"/>
-                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,parent.state,context)"/>
+                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
+                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,parent.state,context)"/>
                                      <field name="price_unit"/>
                                      <field name="taxes_id" widget="many2many_tags" domain="[('parent_id','=',False),('type_tax_use','!=','sale')]"/>
                                      <field name="price_subtotal"/>
                      <sheet>
                          <group>
                              <group>
 -                                <field name="product_id" on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)"/>
 +                                <field name="product_id"
-                                     on_change="onchange_product_id(parent.pricelist_id,product_id,0,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,context)" context="{'partner_id': parent.partner_id}"/>
++                                    on_change="onchange_product_id(parent.pricelist_id,product_id,0,False,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,context)" context="{'partner_id': parent.partner_id}"/>
                                  <label for="product_qty"/>
                                  <div>
-                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)" class="oe_inline"/>
-                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,price_unit,'draft',context)" class="oe_inline"/>
+                                     <field name="product_qty" on_change="onchange_product_id(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id,parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)" class="oe_inline"/>
+                                     <field name="product_uom" groups="product.group_uom" on_change="onchange_product_uom(parent.pricelist_id,product_id,product_qty,product_uom,parent.partner_id, parent.date_order,parent.fiscal_position,date_planned,name,False,'draft',context)" class="oe_inline"/>
                                  </div>
                                  <field name="price_unit"/>
                              </group>
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -273,11 -271,10 +273,10 @@@ instance.web_kanban.KanbanView = instan
              self.dataset.ids = [];
              if (!groups.length) {
                  self.no_result();
 -                return false;
 +                return $.when();
              }
              self.nb_records = 0;
-             var remaining = groups.length - 1,
-                 groups_array = [];
+             var groups_array = [];
              return $.when.apply(null, _.map(groups, function (group, index) {
                  var def = $.when([]);
                  var dataset = new instance.web.DataSetSearch(self, self.dataset.model,
                  if(!self.nb_records) {
                      self.no_result();
                  }
-                 self.trigger('kanban_groups_processed');
+                 if (self.dataset.index >= self.nb_records){
+                     self.dataset.index = self.dataset.size() ? 0 : null;
+                 }
 -                return self.do_add_groups(groups_array);
++                return self.do_add_groups(groups_array).done(function() {
++                    self.trigger('kanban_groups_processed');
++                });
              });
          });
      },
Simple merge
Simple merge
Simple merge
Simple merge
  <template id="footer_custom" inherit_id="website.layout" name="Footer">
      <xpath expr="//div[@id='footer_container']" position="replace">
          <div class="oe_structure" id="footer">
-             <section class="mt16 mb16">
 -            <section data-snippet-id='three-columns'>
++            <section>
                  <div class="container">
                      <div class="row">
                          <div class="col-md-4">
@@@ -237,7 -286,7 +237,7 @@@ class WebsiteForum(http.Controller)
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/reopen', type='http', auth="user", methods=['POST'], website=True)
      def question_reopen(self, forum, question, **kwarg):
-         question.state = 'active'
 -        request.registry['forum.post'].reopen(request.cr, request.uid, [question.id], context=request.context)
++        question.reopen()
          return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
  
      @http.route('/forum/<model("forum.forum"):forum>/question/<model("forum.post"):question>/delete', type='http', auth="user", methods=['POST'], website=True)
@@@ -1,7 -1,6 +1,8 @@@
  # -*- coding: utf-8 -*-
  
  from datetime import datetime
++import logging
 +import math
  import uuid
  from werkzeug.exceptions import Forbidden
  
@@@ -12,7 -11,11 +13,8 @@@ from openerp import tool
  from openerp import SUPERUSER_ID
  from openerp.addons.website.models.website import slug
  from openerp.exceptions import Warning
 -from openerp.osv import osv, fields
 -from openerp.tools import html2plaintext
 -from openerp.tools.translate import _
  
+ _logger = logging.getLogger(__name__)
  
  class KarmaError(Forbidden):
      """ Karma-related error, used for forum and posts. """
@@@ -292,53 -362,80 +294,79 @@@ class Post(models.Model)
                  raise KarmaError('Not enough karma to accept or refuse an answer')
              # update karma except for self-acceptance
              mult = 1 if vals['is_correct'] else -1
 -            for post in self.browse(cr, uid, ids, context=context):
 -                if vals['is_correct'] != post.is_correct and post.create_uid.id != uid:
 -                    self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * mult, context=context)
 -                    self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * mult, context=context)
 -        if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id'] for key in vals.keys()) and any(not post.can_edit for post in posts):
 +            for post in self:
 +                if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
 +                    post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult)
 +                    self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult)
 +        if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id'] for key in vals.keys()) and any(not post.can_edit for post in self):
              raise KarmaError('Not enough karma to edit a post.')
  
 -        res = super(Post, self).write(cr, uid, ids, vals, context=context)
 +        res = super(Post, self).write(vals)
          # if post content modify, notify followers
          if 'content' in vals or 'name' in vals:
 -            for post in posts:
 +            for post in self:
                  if post.parent_id:
                      body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit'
 -                    obj_id = post.parent_id.id
 +                    obj_id = post.parent_id
                  else:
                      body, subtype = _('Question Edited'), 'website_forum.mt_question_edit'
 -                    obj_id = post.id
 -                self.message_post(cr, uid, obj_id, body=body, subtype=subtype, context=context)
 +                    obj_id = post
 +                obj_id.message_post(body=body, subtype=subtype)
          return res
  
 -
 -    def reopen(self, cr, uid, ids, context=None):
 -        if any(post.parent_id or post.state != 'close'
 -                    for post in self.browse(cr, uid, ids, context=context)):
 +    @api.multi
++    def reopen(self):
++        if any(post.parent_id or post.state != 'close' for post in self):
+             return False
 -        reason_offensive = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_7')
 -        reason_spam = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_8')
 -        for post in self.browse(cr, uid, ids, context=context):
 -            if post.closed_reason_id.id in (reason_offensive, reason_spam):
++        reason_offensive = self.env.ref('website_forum.reason_7')
++        reason_spam = self.env.ref('website_forum.reason_8')
++        for post in self:
++            if post.closed_reason_id in (reason_offensive, reason_spam):
+                 _logger.info('Upvoting user <%s>, reopening spam/offensive question',
+                              post.create_uid.login)
+                 # TODO: in master, consider making this a tunable karma parameter
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id],
 -                                                 post.forum_id.karma_gen_question_downvote * -5,
 -                                                 context=context)
 -        self.pool['forum.post'].write(cr, SUPERUSER_ID, ids, {'state': 'active'}, context=context)
++                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_downvote * -5)
 -    def close(self, cr, uid, ids, reason_id, context=None):
 -        if any(post.parent_id for post in self.browse(cr, uid, ids, context=context)):
++        self.sudo().write({'state': 'active'}}
++
++    @api.multi
 +    def close(self, reason_id):
 +        if any(post.parent_id for post in self):
              return False
 -        reason_offensive = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_7')
 -        reason_spam = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'website_forum.reason_8')
++        reason_offensive = self.env.ref('website_forum.reason_7').id
++        reason_spam = self.env.ref('website_forum.reason_8').id
+         if reason_id in (reason_offensive, reason_spam):
 -            for post in self.browse(cr, uid, ids, context=context):
++            for post in self:
+                 _logger.info('Downvoting user <%s> for posting spam/offensive contents',
+                              post.create_uid.login)
+                 # TODO: in master, consider making this a tunable karma parameter
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id],
 -                                                 post.forum_id.karma_gen_question_downvote * 5,
 -                                                 context=context)
++                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_question_downvote * 5)
 -        self.pool['forum.post'].write(cr, uid, ids, {
 +        self.write({
              'state': 'close',
 -            'closed_uid': uid,
 +            'closed_uid': self._uid,
              'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
              'closed_reason_id': reason_id,
 -        }, context=context)
 +        })
 +        return True
  
 -    def unlink(self, cr, uid, ids, context=None):
 -        posts = self.browse(cr, uid, ids, context=context)
 -        if any(not post.can_unlink for post in posts):
 +    @api.multi
 +    def unlink(self):
 +        if any(not post.can_unlink for post in self):
              raise KarmaError('Not enough karma to unlink a post')
          # if unlinking an answer with accepted answer: remove provided karma
 -        for post in posts:
 +        for post in self:
              if post.is_correct:
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [post.create_uid.id], post.forum_id.karma_gen_answer_accepted * -1, context=context)
 -                self.pool['res.users'].add_karma(cr, SUPERUSER_ID, [uid], post.forum_id.karma_gen_answer_accept * -1, context=context)
 -        return super(Post, self).unlink(cr, uid, ids, context=context)
 -
 -    def vote(self, cr, uid, ids, upvote=True, context=None):
 -        Vote = self.pool['forum.post.vote']
 -        vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
 +                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
 +                self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
 +        return super(Post, self).unlink()
 +
 +    @api.multi
 +    def vote(self, upvote=True):
 +        Vote = self.env['forum.post.vote']
 +        vote_ids = Vote.search([('post_id', 'in', self._ids), ('user_id', '=', self._uid)])
          new_vote = '1' if upvote else '-1'
          voted_forum_ids = set()
          if vote_ids:
@@@ -190,14 -196,6 +190,14 @@@ class website_sale(http.Controller)
          categories = category_obj.browse(cr, uid, category_ids, context=context)
          categs = filter(lambda x: not x.parent_id, categories)
  
 +        domain += [('public_categ_ids', 'in', category_ids)]
 +        product_obj = pool.get('product.template')
 +
 +        product_count = product_obj.search_count(cr, uid, domain, context=context)
 +        pager = request.website.pager(url=url, total=product_count, page=page, step=PPG, scope=7, url_args=post)
-         product_ids = product_obj.search(cr, uid, domain, limit=PPG+10, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
++        product_ids = product_obj.search(cr, uid, domain, limit=PPG, offset=pager['offset'], order='website_published desc, website_sequence desc', context=context)
 +        products = product_obj.browse(cr, uid, product_ids, context=context)
 +
          attributes_obj = request.registry['product.attribute']
          attributes_ids = attributes_obj.search(cr, uid, [], context=context)
          attributes = attributes_obj.browse(cr, uid, attributes_ids, context=context)
diff --cc doc/conf.py
Simple merge
Simple merge
@@@ -949,16 -935,16 +946,16 @@@ class Contact(orm.AbstractModel)
  
          val = {
              'name': value.split("\n")[0],
 -            'address': escape("\n".join(value.split("\n")[1:])),
 +            'address': escape("\n".join(value.split("\n")[1:])).strip(),
-             'phone': field_browse.phone,
-             'mobile': field_browse.mobile,
-             'fax': field_browse.fax,
-             'city': field_browse.city,
-             'country_id': field_browse.country_id.display_name,
-             'website': field_browse.website,
-             'email': field_browse.email,
+             'phone': value_rec.phone,
+             'mobile': value_rec.mobile,
+             'fax': value_rec.fax,
+             'city': value_rec.city,
+             'country_id': value_rec.country_id.display_name,
+             'website': value_rec.website,
+             'email': value_rec.email,
              'fields': opf,
-             'object': field_browse,
+             'object': value_rec,
              'options': options
          }
  
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge