[MERGE] forward port of branch 8.0 up to 262eb66
authorChristophe Simonis <chs@odoo.com>
Tue, 21 Oct 2014 12:59:56 +0000 (14:59 +0200)
committerChristophe Simonis <chs@odoo.com>
Tue, 21 Oct 2014 12:59:56 +0000 (14:59 +0200)
34 files changed:
addons/account/account_cash_statement.py
addons/account/res_config.py
addons/account/static/src/js/account_widgets.js
addons/auth_signup/res_users.py
addons/mail/mail_thread.py
addons/mrp/mrp.py
addons/mrp_byproduct/mrp_byproduct.py
addons/point_of_sale/point_of_sale.py
addons/point_of_sale/report/pos_order_report.py
addons/point_of_sale/static/src/js/models.js
addons/project/report/project_report.py
addons/purchase/purchase.py
addons/sale/wizard/sale_make_invoice_advance.py
addons/web/controllers/main.py
addons/website/models/ir_qweb.py
addons/website/static/src/js/website.editor.js
addons/website/views/snippets.xml
addons/website_forum/controllers/main.py
addons/website_forum/models/forum.py
addons/website_forum/views/website_forum.xml
addons/website_sale/models/product.py
addons/website_sale/static/src/js/website_sale.js
addons/website_sale/views/views.xml
addons/website_sale_options/static/src/js/website_sale.js
doc/_themes/odoodoc/layout.html
doc/conf.py
doc/images/view-on-github.png [new file with mode: 0644]
doc/index.rst
doc/modules.rst
doc/modules/mail.rst [deleted file]
doc/reference/cmdline.rst
openerp/addons/base/ir/ir_http.py
openerp/addons/base/res/res_bank.py
openerp/addons/base/res/res_partner.py

index d4c98b7..5b1952f 100644 (file)
@@ -23,6 +23,7 @@
 import time
 
 from openerp.osv import fields, osv
+from openerp.tools import float_compare
 from openerp.tools.translate import _
 import openerp.addons.decimal_precision as dp
 
@@ -80,7 +81,8 @@ class account_cash_statement(osv.osv):
             if (statement.journal_id.type not in ('cash',)):
                 continue
             if not statement.journal_id.cash_control:
-                if statement.balance_end_real <> statement.balance_end:
+                prec = self.pool['decimal.precision'].precision_get(cr, uid, 'Account')
+                if float_compare(statement.balance_end_real, statement.balance_end, precision_digits=prec):
                     statement.write({'balance_end_real' : statement.balance_end})
                 continue
             start = end = 0
index 4e870b7..c69eeb9 100644 (file)
@@ -241,8 +241,8 @@ class account_config_settings(osv.osv_memory):
                     })
             # update taxes
             ir_values = self.pool.get('ir.values')
-            taxes_id = ir_values.get_default(cr, uid, 'product.product', 'taxes_id', company_id=company_id)
-            supplier_taxes_id = ir_values.get_default(cr, uid, 'product.product', 'supplier_taxes_id', company_id=company_id)
+            taxes_id = ir_values.get_default(cr, uid, 'product.template', 'taxes_id', company_id=company_id)
+            supplier_taxes_id = ir_values.get_default(cr, uid, 'product.template', 'supplier_taxes_id', company_id=company_id)
             values.update({
                 'default_sale_tax': isinstance(taxes_id, list) and taxes_id[0] or taxes_id,
                 'default_purchase_tax': isinstance(supplier_taxes_id, list) and supplier_taxes_id[0] or supplier_taxes_id,
@@ -311,9 +311,9 @@ class account_config_settings(osv.osv_memory):
             raise openerp.exceptions.AccessError(_("Only administrators can change the settings"))
         ir_values = self.pool.get('ir.values')
         config = self.browse(cr, uid, ids[0], context)
-        ir_values.set_default(cr, SUPERUSER_ID, 'product.product', 'taxes_id',
+        ir_values.set_default(cr, SUPERUSER_ID, 'product.template', 'taxes_id',
             config.default_sale_tax and [config.default_sale_tax.id] or False, company_id=config.company_id.id)
-        ir_values.set_default(cr, SUPERUSER_ID, 'product.product', 'supplier_taxes_id',
+        ir_values.set_default(cr, SUPERUSER_ID, 'product.template', 'supplier_taxes_id',
             config.default_purchase_tax and [config.default_purchase_tax.id] or False, company_id=config.company_id.id)
 
     def set_chart_of_accounts(self, cr, uid, ids, context=None):
index 158805f..04189e3 100644 (file)
@@ -41,6 +41,7 @@ openerp.account = function (instance) {
             this.max_move_lines_displayed = 5;
             this.animation_speed = 100; // "Blocking" animations
             this.aestetic_animation_speed = 300; // eye candy
+            this.map_currency_id_rounding = {};
             this.map_tax_id_amount = {};
             this.presets = {};
             // We'll need to get the code of an account selected in a many2one (whose value is the id)
@@ -199,6 +200,13 @@ openerp.account = function (instance) {
                         _.each(data, function(o) { self.map_account_id_code[o.id] = o.code });
                     });
 
+                // Create a dict currency id -> rounding factor
+                new instance.web.Model("res.currency")
+                    .query(['id', 'rounding'])
+                    .all().then(function(data) {
+                        _.each(data, function(o) { self.map_currency_id_rounding[o.id] = o.rounding });
+                    });
+
                 // Create a dict tax id -> amount
                 new instance.web.Model("account.tax")
                     .query(['id', 'amount'])
@@ -347,10 +355,12 @@ openerp.account = function (instance) {
                                     self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed);
                                 });
                             });
-                    }
-                    // Congratulate the user if the work is done
-                    if (self.reconciled_lines === self.st_lines.length) {
+                    } else if (self.reconciled_lines === self.st_lines.length) {
+                        // Congratulate the user if the work is done
                         self.displayDoneMessage();
+                    } else {
+                        // Some lines weren't persisted because they were't valid
+                        self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed);
                     }
                 }).fail(function() {
                     self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed);
@@ -386,7 +396,7 @@ openerp.account = function (instance) {
         
             // Update children if needed
             _.each(self.getChildren(), function(child){
-                if (child.partner_id === partner_id && child !== source_child) {
+                if ((child.partner_id === partner_id || child.st_line.has_no_partner) && child !== source_child) {
                     if (contains_lines(child.get("mv_lines_selected"), line_ids)) {
                         child.set("mv_lines_selected", _.filter(child.get("mv_lines_selected"), function(o){ return line_ids.indexOf(o.id) === -1 }));
                     } else if (contains_lines(child.mv_lines_deselected, line_ids)) {
@@ -412,6 +422,8 @@ openerp.account = function (instance) {
             _.each(self.getChildren(), function(child){
                 if (child.partner_id === partner_id && child !== source_child && (child.get("mode") === "match" || child.$el.hasClass("no_match")))
                     child.updateMatches();
+                if (child.st_line.has_no_partner && child.get("mode") === "match" || child.$el.hasClass("no_match"))
+                    child.updateMatches();
             });
         },
     
@@ -639,12 +651,13 @@ openerp.account = function (instance) {
             this.model_bank_statement_line = new instance.web.Model("account.bank.statement.line");
             this.model_res_users = new instance.web.Model("res.users");
             this.model_tax = new instance.web.Model("account.tax");
+            this.map_currency_id_rounding = this.getParent().map_currency_id_rounding;
             this.map_account_id_code = this.getParent().map_account_id_code;
             this.map_tax_id_amount = this.getParent().map_tax_id_amount;
             this.presets = this.getParent().presets;
             this.is_valid = true;
             this.is_consistent = true; // Used to prevent bad server requests
-            this.total_move_lines_num = undefined; // Used for pagers
+            this.can_fetch_more_move_lines; // Tell if we can show more move lines
             this.filter = "";
             // In rare cases like when deleting a statement line's partner we don't want the server to
             // look for a reconciliation proposition (in this particular case it might find a move line
@@ -1039,16 +1052,15 @@ openerp.account = function (instance) {
         pagerControlLeftHandler: function() {
             var self = this;
             if (self.$(".pager_control_left").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ }
-            if (self.total_move_lines_num < 0) { return; }
+            if (self.get("pager_index") === 0) { return; }
             self.set("pager_index", self.get("pager_index")-1 );
         },
         
         pagerControlRightHandler: function() {
             var self = this;
-            var new_index = self.get("pager_index")+1;
             if (self.$(".pager_control_right").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ }
-            if ((new_index * self.max_move_lines_displayed) >= self.total_move_lines_num) { return; }
-            self.set("pager_index", new_index );
+            if (! self.can_fetch_more_move_lines) { return; }
+            self.set("pager_index", self.get("pager_index")+1 );
         },
     
         filterHandler: function() {
@@ -1222,7 +1234,7 @@ openerp.account = function (instance) {
                 self.$(".pager_control_left").addClass("disabled");
             else
                 self.$(".pager_control_left").removeClass("disabled");
-            if (self.total_move_lines_num <= ((self.get("pager_index")+1) * self.max_move_lines_displayed))
+            if (! self.can_fetch_more_move_lines)
                 self.$(".pager_control_right").addClass("disabled");
             else
                 self.$(".pager_control_right").removeClass("disabled");
@@ -1323,12 +1335,12 @@ openerp.account = function (instance) {
         mvLinesChanged: function() {
             var self = this;
             // If pager_index is out of range, set it to display the last page
-            if (self.get("pager_index") !== 0 && self.total_move_lines_num <= (self.get("pager_index") * self.max_move_lines_displayed)) {
-                self.set("pager_index", Math.ceil(self.total_move_lines_num/self.max_move_lines_displayed)-1);
+            if (self.get("pager_index") !== 0 && self.get("mv_lines").length === 0 && ! self.can_fetch_more_move_lines) {
+                self.set("pager_index", 0);
             }
         
             // If there is no match to display, disable match view and pass in mode inactive
-            if (self.total_move_lines_num + self.mv_lines_deselected.length === 0 && self.filter === "") {
+            if (self.get("mv_lines").length + self.mv_lines_deselected.length === 0 && !self.can_fetch_more_move_lines && self.filter === "") {
                 self.$el.addClass("no_match");
                 if (self.get("mode") === "match") {
                     self.set("mode", "inactive");
@@ -1406,7 +1418,6 @@ openerp.account = function (instance) {
                         }
                     );
                 } else {
-                    line_created_being_edited[0].amount = amount;
                     line_created_being_edited.length = 1;
                     deferred_tax.resolve();
                 }
@@ -1414,9 +1425,12 @@ openerp.account = function (instance) {
     
             $.when(deferred_tax).then(function(){
                 // Format amounts
+                var rounding = 1/self.map_currency_id_rounding[self.st_line.currency_id];
                 $.each(line_created_being_edited, function(index, val) {
-                    if (val.amount)
+                    if (val.amount) {
+                        line_created_being_edited[index].amount = Math.round(val.amount*rounding)/rounding;
                         line_created_being_edited[index].amount_str = self.formatCurrency(Math.abs(val.amount), val.currency_id);
+                    }
                 });
                 self.set("line_created_being_edited", line_created_being_edited);
                 self.createdLinesChanged(); // TODO For some reason, previous line doesn't trigger change handler
@@ -1534,48 +1548,38 @@ openerp.account = function (instance) {
         updateMatches: function() {
             var self = this;
             var deselected_lines_num = self.mv_lines_deselected.length;
-            var move_lines_num = 0;
             var offset = self.get("pager_index") * self.max_move_lines_displayed - deselected_lines_num;
             if (offset < 0) offset = 0;
             var limit = (self.get("pager_index")+1) * self.max_move_lines_displayed - deselected_lines_num;
             if (limit > self.max_move_lines_displayed) limit = self.max_move_lines_displayed;
-            var excluded_ids = self.getParent().excluded_move_lines_ids[self.partner_id];
             var excluded_ids = _.collect(self.get("mv_lines_selected").concat(self.mv_lines_deselected), function(o) { return o.id; });
-            var globally_excluded_ids = self.getParent().excluded_move_lines_ids[self.partner_id];
+            var globally_excluded_ids = [];
+            if (self.st_line.has_no_partner)
+                _.each(self.getParent().excluded_move_lines_ids, function(o) { globally_excluded_ids = globally_excluded_ids.concat(o) });
+            else
+                globally_excluded_ids = self.getParent().excluded_move_lines_ids[self.partner_id];
             if (globally_excluded_ids !== undefined)
                 for (var i=0; i<globally_excluded_ids.length; i++)
                     if (excluded_ids.indexOf(globally_excluded_ids[i]) === -1)
                         excluded_ids.push(globally_excluded_ids[i]);
             
-            var deferred_move_lines;
-            var move_lines = [];
+            limit += 1; // Let's fetch 1 more item than requested
             if (limit > 0) {
-                // Load move lines
-                deferred_move_lines = self.model_bank_statement_line
+                return self.model_bank_statement_line
                     .call("get_move_lines_for_reconciliation_by_statement_line_id", [self.st_line.id, excluded_ids, self.filter, offset, limit])
                     .then(function (lines) {
-                        _.each(lines, function(line) {
-                            self.decorateMoveLine(line, self.st_line.currency_id);
-                            move_lines.push(line);
-                        }, self);
+                        _.each(lines, function(line) { self.decorateMoveLine(line, self.st_line.currency_id) }, self);
+                        // If we could fetch 1 more item than what we'll display, that means there are move lines left to be displayed (so we enable the pager)
+                        self.can_fetch_more_move_lines = (lines.length === limit);
+                        self.set("mv_lines", lines.slice(0, limit-1));
                     });
+            } else {
+                self.set("mv_lines", []);
             }
-        
-            // Fetch the number of move lines corresponding to this statement line and this filter
-            var deferred_total_move_lines_num = self.model_bank_statement_line
-                .call("get_move_lines_for_reconciliation_by_statement_line_id", [self.st_line.id, excluded_ids, self.filter, 0, undefined, true])
-                .then(function(num){
-                    move_lines_num = num;
-                });
-        
-            return $.when(deferred_move_lines, deferred_total_move_lines_num).then(function(){
-                self.total_move_lines_num = move_lines_num + deselected_lines_num;
-                self.set("mv_lines", move_lines);
-            });
         },
 
         // Changes the partner_id of the statement_line in the DB and reloads the widget
-        changePartner: function(partner_id, callback) {
+        changePartner: function(partner_id) {
             var self = this;
             self.is_consistent = false;
             return self.model_bank_statement_line
@@ -1588,7 +1592,6 @@ openerp.account = function (instance) {
                         self.do_load_reconciliation_proposition = true;
                         self.is_consistent = true;
                         self.set("mode", "match");
-                        if (callback) callback();
                     });
                 });
         },
index 97bcea5..620298c 100644 (file)
@@ -64,7 +64,6 @@ class res_partner(osv.Model):
             # when required, make sure the partner has a valid signup token
             if context.get('signup_valid') and not partner.user_ids:
                 self.signup_prepare(cr, uid, [partner.id], context=context)
-                partner.refresh()
 
             route = 'login'
             # the parameters to encode for the query
index 495b841..4356ee6 100644 (file)
@@ -1612,7 +1612,9 @@ class mail_thread(osv.AbstractModel):
 
         # _mail_flat_thread: automatically set free messages to the first posted message
         if self._mail_flat_thread and model and not parent_id and thread_id:
-            message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model)], context=context, order="id ASC", limit=1)
+            message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model), ('type', '=', 'email')], context=context, order="id ASC", limit=1)
+            if not message_ids:
+                message_ids = message_ids = mail_message.search(cr, uid, ['&', ('res_id', '=', thread_id), ('model', '=', model)], context=context, order="id ASC", limit=1)
             parent_id = message_ids and message_ids[0] or False
         # we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
         elif parent_id:
index 62d433d..856b56d 100644 (file)
@@ -934,8 +934,8 @@ class mrp_production(osv.osv):
                 new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty_uom),
                                                          location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
                 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
-                if produce_product.product_id.id == production.product_id.id and new_moves:
-                    main_production_move = new_moves[0]
+                if produce_product.product_id.id == production.product_id.id:
+                    main_production_move = produce_product.id
 
         if production_mode in ['consume', 'consume_produce']:
             if wiz:
@@ -1035,8 +1035,11 @@ class mrp_production(osv.osv):
     
     def _make_production_produce_line(self, cr, uid, production, context=None):
         stock_move = self.pool.get('stock.move')
+        proc_obj = self.pool.get('procurement.order')
         source_location_id = production.product_id.property_stock_production.id
         destination_location_id = production.location_dest_id.id
+        procs = proc_obj.search(cr, uid, [('production_id', '=', production.id)], context=context)
+        procurement_id = procs and procs[0] or False
         data = {
             'name': production.name,
             'date': production.date_planned,
@@ -1048,6 +1051,7 @@ class mrp_production(osv.osv):
             'location_id': source_location_id,
             'location_dest_id': destination_location_id,
             'move_dest_id': production.move_prod_id.id,
+            'procurement_id': procurement_id,
             'company_id': production.company_id.id,
             'production_id': production.id,
             'origin': production.name,
index 5649604..fa7c078 100644 (file)
@@ -84,6 +84,7 @@ class mrp_production(osv.osv):
         """ Confirms production order and calculates quantity based on subproduct_type.
         @return: Newly generated picking Id.
         """
+        move_obj = self.pool.get('stock.move')
         picking_id = super(mrp_production,self).action_confirm(cr, uid, ids, context=context)
         product_uom_obj = self.pool.get('product.uom')
         for production in self.browse(cr, uid, ids):
@@ -113,10 +114,11 @@ class mrp_production(osv.osv):
                     'location_id': source,
                     'location_dest_id': production.location_dest_id.id,
                     'move_dest_id': production.move_prod_id.id,
-                    'state': 'waiting',
                     'production_id': production.id
                 }
-                self.pool.get('stock.move').create(cr, uid, data)
+                move_id = move_obj.create(cr, uid, data, context=context)
+                move_obj.action_confirm(cr, uid, [move_id], context=context)
+
         return picking_id
 
     def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
index c10a6c4..ae6be54 100644 (file)
@@ -112,10 +112,21 @@ class pos_config(osv.osv):
                 return False
         return True
 
+    def _check_company_payment(self, cr, uid, ids, context=None):
+        for config in self.browse(cr, uid, ids, context=context):
+            journal_ids = [j.id for j in config.journal_ids]
+            if self.pool['account.journal'].search(cr, uid, [
+                    ('id', 'in', journal_ids),
+                    ('company_id', '!=', config.company_id.id)
+                ], count=True, context=context):
+                return False
+        return True
+
     _constraints = [
         (_check_cash_control, "You cannot have two cash controls in one Point Of Sale !", ['journal_ids']),
         (_check_company_location, "The company of the stock location is different than the one of point of sale", ['company_id', 'stock_location_id']),
         (_check_company_journal, "The company of the sale journal is different than the one of point of sale", ['company_id', 'journal_id']),
+        (_check_company_payment, "The company of a payment method is different than the one of point of sale", ['company_id', 'journal_ids']),
     ]
 
     def name_get(self, cr, uid, ids, context=None):
index abdf384..aa25052 100644 (file)
@@ -58,7 +58,7 @@ class pos_order_report(osv.osv):
                     sum(l.qty * u.factor) as product_qty,
                     sum(l.qty * l.price_unit) as price_total,
                     sum((l.qty * l.price_unit) * (l.discount / 100)) as total_discount,
-                    (sum(l.qty*l.price_unit)/sum(l.qty * u.factor))::decimal(16,2) as average_price,
+                    (sum(l.qty*l.price_unit)/sum(l.qty * u.factor))::decimal as average_price,
                     sum(cast(to_char(date_trunc('day',s.date_order) - date_trunc('day',s.create_date),'DD') as int)) as delay_validation,
                     s.partner_id as partner_id,
                     s.state as state,
index 0ac9ebe..ba5f66f 100644 (file)
@@ -331,13 +331,14 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
                         c.height = height
                     var ctx = c.getContext('2d');
                         ctx.drawImage(self.company_logo,0,0, width, height);
-                    
+
                     self.company_logo_base64 = c.toDataURL();
                     logo_loaded.resolve();
                 };
                 self.company_logo.onerror = function(){
                     logo_loaded.reject();
                 };
+                    self.company_logo.crossOrigin = "anonymous";
                 self.company_logo.src = '/web/binary/company_logo' +'?_'+Math.random();
 
                 return logo_loaded;
index 63650f9..2010a21 100644 (file)
@@ -85,7 +85,7 @@ class report_project_task_user(osv.osv):
                     planned_hours as hours_planned,
                     (extract('epoch' from (t.write_date-t.create_date)))/(3600*24)  as closing_days,
                     (extract('epoch' from (t.date_start-t.create_date)))/(3600*24)  as opening_days,
-                    abs((extract('epoch' from (t.date_deadline-t.write_date)))/(3600*24))  as delay_endings_days
+                    (extract('epoch' from (t.date_deadline-now())))/(3600*24)  as delay_endings_days
               FROM project_task t
                 WHERE t.active = 'true'
                 GROUP BY
index e17b541..338437a 100644 (file)
@@ -701,7 +701,7 @@ class purchase_order(osv.osv):
         product_uom = self.pool.get('product.uom')
         price_unit = order_line.price_unit
         if order_line.product_uom.id != order_line.product_id.uom_id.id:
-            price_unit *= order_line.product_uom.factor
+            price_unit *= order_line.product_uom.factor / order_line.product_id.uom_id.factor
         if order.currency_id.id != order.company_id.currency_id.id:
             #we don't round the price_unit, as we may want to store the standard price with more digits than allowed by the currency
             price_unit = self.pool.get('res.currency').compute(cr, uid, order.currency_id.id, order.company_id.currency_id.id, price_unit, round=False, context=context)
index 12081fa..d18d4ec 100644 (file)
@@ -54,6 +54,9 @@ class sale_advance_payment_inv(osv.osv_memory):
         'product_id': _get_advance_product,
     }
 
+    def _translate_advance(self, cr, uid, percentage=False, context=None):
+        return _("Advance of %s %%") if percentage else _("Advance of %s %s")
+
     def onchange_method(self, cr, uid, ids, advance_payment_method, product_id, context=None):
         if advance_payment_method == 'percentage':
             return {'value': {'amount':0, 'product_id':False }}
@@ -100,16 +103,17 @@ class sale_advance_payment_inv(osv.osv_memory):
             if wizard.advance_payment_method == 'percentage':
                 inv_amount = sale.amount_total * wizard.amount / 100
                 if not res.get('name'):
-                    res['name'] = _("Advance of %s %%") % (wizard.amount)
+                    res['name'] = self._translate_advance(cr, uid, percentage=True, context=dict(context, lang=sale.partner_id.lang)) % (wizard.amount)
             else:
                 inv_amount = wizard.amount
                 if not res.get('name'):
                     #TODO: should find a way to call formatLang() from rml_parse
                     symbol = sale.pricelist_id.currency_id.symbol
                     if sale.pricelist_id.currency_id.position == 'after':
-                        res['name'] = _("Advance of %s %s") % (inv_amount, symbol)
+                        symbol_order = (inv_amount, symbol)
                     else:
-                        res['name'] = _("Advance of %s %s") % (symbol, inv_amount)
+                        symbol_order = (symbol, inv_amount)
+                    res['name'] = self._translate_advance(cr, uid, context=dict(context, lang=sale.partner_id.lang)) % symbol_order
 
             # determine taxes
             if res.get('invoice_line_tax_id'):
@@ -155,7 +159,6 @@ class sale_advance_payment_inv(osv.osv_memory):
         sale_obj.write(cr, uid, sale_id, {'invoice_ids': [(4, inv_id)]}, context=context)
         return inv_id
 
-
     def create_invoices(self, cr, uid, ids, context=None):
         """ create invoices for the active sales orders """
         sale_obj = self.pool.get('sale.order')
index 1bdaa3e..332151e 100644 (file)
@@ -1210,7 +1210,7 @@ class Binary(http.Controller):
         '/web/binary/company_logo',
         '/logo',
         '/logo.png',
-    ], type='http', auth="none")
+    ], type='http', auth="none", cors="*")
     def company_logo(self, dbname=None, **kw):
         imgname = 'logo.png'
         placeholder = functools.partial(get_module_resource, 'web', 'static', 'src', 'img')
index 7eba7c5..019eedd 100644 (file)
@@ -438,6 +438,9 @@ class Contact(orm.AbstractModel):
     _name = 'website.qweb.field.contact'
     _inherit = ['ir.qweb.field.contact', 'website.qweb.field.many2one']
 
+    def from_html(self, cr, uid, model, column, element, context=None):
+        return None
+
 class QwebView(orm.AbstractModel):
     _name = 'website.qweb.field.qweb'
     _inherit = ['ir.qweb.field.qweb']
index 4dda0ff..6eb7569 100644 (file)
                 this.changed($(e.target));
             },
             'click button.filepicker': function () {
-                this.$('input[type=file]').click();
+                var filepicker = this.$('input[type=file]');
+                if (!_.isEmpty(filepicker)){
+                    filepicker[0].click();
+                }
             },
             'click .js_disable_optimization': function () {
                 this.$('input[name="disable_optimization"]').val('1');
-                this.$('button.filepicker').click();
+                var filepicker = this.$('button.filepicker');
+                if (!_.isEmpty(filepicker)){
+                    filepicker[0].click();
+                }
             },
             'change input[type=file]': 'file_selection',
             'submit form': 'form_submit',
index 20db73d..f620a05 100644 (file)
@@ -30,9 +30,9 @@
                                         <a href="/page/website.contactus" class="btn btn-success btn-large">Contact us</a>
                                     </p>
                             </div>
-                            <span class="carousel-img col-md-6 hidden-sm hidden-xs">
+                            <div class="carousel-img col-md-6 hidden-sm hidden-xs">
                                 <img class="img-responsive" src="/website/static/src/img/banner/banner_picture.png" alt="Banner Odoo Image"/>
-                            </span> 
+                            </div>
                         </div>
                     </div>
                 </div>
index b7f4e0e..251faf8 100644 (file)
@@ -295,12 +295,10 @@ class WebsiteForum(http.Controller):
         cr, uid, context = request.cr, request.uid, request.context
         if kwargs.get('comment') and post.forum_id.id == forum.id:
             # TDE FIXME: check that post_id is the question or one of its answers
-            request.registry['forum.post'].message_post(
-                cr, uid, post.id,
+            request.registry['forum.post']._post_comment(
+                cr, uid, post,
                 body=kwargs.get('comment'),
-                type='comment',
-                subtype='mt_comment',
-                context=dict(context, mail_create_nosubcribe=True))
+                context=context)
         return werkzeug.utils.redirect("/forum/%s/question/%s" % (slug(forum), slug(question)))
 
     @http.route('/forum/<model("forum.forum"):forum>/post/<model("forum.post"):post>/toggle_correct', type='json', auth="public", website=True)
index 5b1baa4..e0b6061 100644 (file)
@@ -10,8 +10,9 @@ from openerp.osv import osv, fields
 from openerp.tools import html2plaintext
 from openerp.tools.translate import _
 
+from werkzeug.exceptions import Forbidden
 
-class KarmaError(ValueError):
+class KarmaError(Forbidden):
     """ Karma-related error, used for forum and posts. """
     pass
 
@@ -357,14 +358,14 @@ class Post(osv.Model):
             context = {}
         create_context = dict(context, mail_create_nolog=True)
         post_id = super(Post, self).create(cr, uid, vals, context=create_context)
-        post = self.browse(cr, SUPERUSER_ID, post_id, context=context)  # SUPERUSER_ID to avoid read access rights issues when creating
+        post = self.browse(cr, uid, post_id, context=context)
         # deleted or closed questions
         if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active == False):
             osv.except_osv(_('Error !'), _('Posting answer on [Deleted] or [Closed] question is prohibited'))
         # karma-based access
-        if post.parent_id and not post.can_ask:
+        if not post.parent_id and not post.can_ask:
             raise KarmaError('Not enough karma to create a new question')
-        elif not post.parent_id and not post.can_answer:
+        elif post.parent_id and not post.can_answer:
             raise KarmaError('Not enough karma to answer to a question')
         # messaging and chatter
         base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
@@ -554,6 +555,15 @@ class Post(osv.Model):
         res_id = post.parent_id and "%s#answer-%s" % (post.parent_id.id, post.id) or post.id
         return "/forum/%s/question/%s" % (post.forum_id.id, res_id)
 
+    def _post_comment(self, cr, uid, post, body, context=None):
+        context = dict(context or {}, mail_create_nosubcribe=True)
+        if not post.can_comment:
+            raise KarmaError('Not enough karma to comment')
+        return self.message_post(cr, uid, post.id,
+                                 body=body,
+                                 type='comment',
+                                 subtype='mt_comment',
+                                 context=context)
 
 class PostReason(osv.Model):
     _name = "forum.post.reason"
index 326c06c..5372f84 100644 (file)
                     <t t-raw="0"/>
                 </div>
                 <div class="col-sm-3" id="right-column">
-                    <div t-if="not header.get('ask_hide')" class="btn-group btn-block mb16">
+                    <div t-if="not header.get('ask_hide')" t-attf-class="btn-group btn-block mb16 #{user.karma &gt;= forum.karma_ask and '' or 'karma_required'}" t-attf-data-karma="#{forum.karma_ask}">
                         <a type="button" class="btn btn-primary btn-lg col-sm-10" t-attf-href="/forum/#{slug(forum)}/#{forum.default_allow}">
                             <t t-if="forum.default_allow == 'ask_question'">Ask a Question</t>
                             <t t-if="forum.default_allow == 'post_link'">Submit a Post</t>
             <br/>
             <input type="text" name="post_tags" placeholder="Tags" class="form-control load_tags"/>
             <br/>
-            <button class="btn btn-primary" id="btn_ask_your_question">Post Your Question</button>
+            <button t-attf-class="btn btn-primary #{(user.karma &lt;= forum.karma_ask) and 'karma_required' or ''}"
+                id="btn_ask_your_question" t-att-data-karma="forum.karma_ask">Post Your Question</button>
         </form>
     </t>
 </template>
     <form t-attf-action="/forum/#{ slug(forum) }/#{slug(question)}/reply" method="post" role="form">
         <input type="hidden" name="karma" t-attf-value="#{user.karma}" id="karma"/>
         <textarea name="content" t-attf-id="content-#{str(question.id)}" class="form-control load_editor" required="True"/>
-        <button class="btn btn-primary mt16" id="btn_ask_your_question">Post Your Reply</button>
+        <button t-attf-class="btn btn-primary mt16 #{not question.can_answer and 'karma_required' or ''}"
+                id="btn_ask_your_question" t-att-data-karma="question.karma_answer">Post Your Reply</button>
     </form>
 </template>
 
                         </div>
                         <ul class="list-inline" id="options">
                             <li t-if="question.type == 'question'">
-                                <a style="cursor: pointer" data-toggle="collapse"
+                                <a style="cursor: pointer" t-att-data-toggle="question.can_comment and 'collapse' or ''"
                                     t-attf-class="fa fa-comment-o #{not question.can_comment and 'karma_required text-muted' or ''}"
                                     t-attf-data-karma="#{not question.can_comment and question.karma_comment or 0}"
                                     t-attf-data-target="#comment#{ question._name.replace('.','') + '-' + str(question.id) }">
                     <li t-if="question.type == 'question'">
                         <a t-attf-class="fa fa-comment-o #{not answer.can_comment and 'karma_required text-muted' or ''}"
                             t-attf-data-karma="#{not answer.can_comment and answer.karma_comment or 0}"
-                            style="cursor: pointer" data-toggle="collapse"
+                            style="cursor: pointer" t-att-data-toggle="answer.can_comment and 'collapse' or ''"
                             t-attf-data-target="#comment#{ answer._name.replace('.','') + '-' + str(answer.id) }"> Comment
                         </a>
                     </li>
index 2c0156f..1ce85d5 100644 (file)
@@ -194,7 +194,7 @@ class product_product(osv.Model):
 class product_attribute(osv.Model):
     _inherit = "product.attribute"
     _columns = {
-        'type': fields.selection([('radio', 'Radio'), ('select', 'Select'), ('color', 'Color'), ('hidden', 'Hidden')], string="Type", type="char"),
+        'type': fields.selection([('radio', 'Radio'), ('select', 'Select'), ('color', 'Color'), ('hidden', 'Hidden')], string="Type"),
     }
     _defaults = {
         'type': lambda *a: 'radio',
index 338369b..34cf1fc 100644 (file)
@@ -94,6 +94,9 @@ $('.oe_website_sale').each(function () {
         var $parent = $(this).closest('.js_product');
         $parent.find(".oe_default_price:first .oe_currency_value").html( price_to_str(+$(this).data('lst_price')) );
         $parent.find(".oe_price:first .oe_currency_value").html(price_to_str(+$(this).data('price')) );
+
+        var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img, img.product_detail_img');
+        $img.attr("src", "/website/image/product.product/" + $(this).val() + "/image");
     });
 
     $(oe_website_sale).on('change', 'input.js_variant_change, select.js_variant_change', function (ev) {
@@ -126,7 +129,7 @@ $('.oe_website_sale').each(function () {
         }
 
         if (product_id) {
-            var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img');
+            var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img, img.product_detail_img');
             $img.attr("src", "/website/image/product.product/" + product_id + "/image");
             $img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
                 .data('oe-model', 'product.product').data('oe-id', product_id);
@@ -169,12 +172,15 @@ $('.oe_website_sale').each(function () {
         $select.find("option:not(:first)").hide();
         var nb = $select.find("option[data-country_id="+($(this).val() || 0)+"]").show().size();
         $select.parent().toggle(nb>1);
-    }).change();
+    });
+    $(oe_website_sale).find("select[name='country_id']").change();
+
     $(oe_website_sale).on('change', "select[name='shipping_country_id']", function () {
         var $select = $("select[name='shipping_state_id']");
         $select.find("option:not(:first)").hide();
         var nb = $select.find("option[data-country_id="+($(this).val() || 0)+"]").show().size();
         $select.parent().toggle(nb>1);
-    }).change();
+    });
+    $(oe_website_sale).find("select[name='shipping_country_id']").change();
 });
 });
index 8324114..9a3f24b 100644 (file)
             </field>
         </field>
     </record>
+    <record id="attribute_tree_view" model="ir.ui.view">
+        <field name="name">product.attribute.tree.type</field>
+        <field name="model">product.attribute</field>
+        <field name="inherit_id" ref="product.attribute_tree_view"></field>
+        <field name="arch" type="xml">
+            <field name="name" position="after">
+                <field name="type"/>
+            </field>
+        </field>
+    </record>
 
     <!-- Product Public Categories -->
     <record id="product_public_category_form_view" model="ir.ui.view">
index c0dba9e..72a4513 100644 (file)
@@ -5,15 +5,18 @@ $(document).ready(function () {
         .click(function (event) {
             var $form = $(this).closest('form');
             var quantity = parseFloat($form.find('input[name="add_qty"]').val() || 1);
+            var product_id = parseInt($form.find('input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked').first().val(),10);
             event.preventDefault();
             openerp.jsonRpc("/shop/modal", 'call', {
-                    'product_id': parseInt($form.find('input[name="product_id"]').val(),10),
+                    'product_id': product_id,
                     kwargs: {
                        context: openerp.website.get_context()
                     },
                 }).then(function (modal) {
                     var $modal = $(modal);
 
+                    $modal.find('img:first').attr("src", "/website/image/product.product/" + product_id + "/image");
+
                     $modal.appendTo($form)
                         .modal()
                         .on('hidden.bs.modal', function () {
index 3e75dad..1fcb690 100644 (file)
@@ -35,7 +35,7 @@
                  main_navbar=False, titles_only=False) }}
       {% if github_link %}
         <p><a href="{{ github_link() }}" class="github">
-          Edit on GitHub
+          View on GitHub
         </a></p>
       {% endif %}
     </div>
index f36c3b1..34f77c9 100644 (file)
@@ -20,6 +20,7 @@ needs_sphinx = '1.1'
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
+    'sphinx.ext.ifconfig',
     'sphinx.ext.todo',
     'sphinx.ext.autodoc',
     'sphinx.ext.intersphinx',
@@ -165,6 +166,9 @@ html_sidebars = {
 # base URL from which the finished HTML is served.
 #html_use_opensearch = ''
 
+# default must be set otherwise ifconfig blows up
+todo_include_todos = False
+
 intersphinx_mapping = {
     'python': ('https://docs.python.org/2/', None),
     'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
diff --git a/doc/images/view-on-github.png b/doc/images/view-on-github.png
new file mode 100644 (file)
index 0000000..afaabc4
Binary files /dev/null and b/doc/images/view-on-github.png differ
index f83680e..2bb0aaa 100644 (file)
@@ -2,7 +2,31 @@
 odoo developer documentation
 ============================
 
-.. TODO: replace or style
+Welcome to the Odoo developer documentation.
+
+This documentation is incomplete and may contain errors, if you wish to
+contribute, every page should have a :guilabel:`View on Github` link:
+
+.. image:: images/view-on-github.*
+    :align: center
+
+Through this link you can edit documents and submit changes for review using
+`github's web interface
+<https://help.github.com/articles/editing-files-in-your-repository/>`_.
+Contributions are welcome and appreciated.
+
+.. todo:: what's the documentation's license?
+
+The documentation is currently organized in four sections:
+
+* :doc:`tutorials`, aimed at introducing the primary areas of developing Odoo
+  modules
+* :doc:`guides`, didactic documents covering more specific and specialized
+  areas of Odoo, trying to solve more specific problems
+* :doc:`reference`, which ought be the complete and canonical documentation
+  for Odoo subsystems
+* :doc:`modules`, documenting useful specialized modules and integration
+  methods (and currently empty)
 
 .. hidden toctree w/o titlesonly otherwise the titlesonly "sticks" to
    in-document toctrees and we can't have a toctree showing both "sibling"
@@ -16,4 +40,8 @@ odoo developer documentation
     reference
     modules
 
-.. todolist::
+.. ifconfig:: todo_include_todos
+
+    .. rubric:: Things to add and fix
+
+    .. todolist::
index 7c90fce..16133d1 100644 (file)
@@ -5,4 +5,3 @@ Module Objects
 .. toctree::
     :titlesonly:
 
-    modules/mail
diff --git a/doc/modules/mail.rst b/doc/modules/mail.rst
deleted file mode 100644 (file)
index 734fecc..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-============
-Mail Threads
-============
-
-
index e3f2952..32c1ef4 100644 (file)
@@ -15,20 +15,32 @@ Running the server
 
     database used when installing or updating modules.
 
+.. option:: --db-filter=<filter>
+
+    hides databases that do not match ``<filter>``. The filter is a
+    `regular expression`_, with the additions that:
+
+    - ``%h`` is replaced by the whole hostname the request is made on.
+    - ``%d`` is replaced by the subdomain the request is made on, with the
+      exception of ``www`` (so domain ``odoo.com`` and ``www.odoo.com`` both
+      match the database ``odoo``)
+
 .. option:: -i <modules>, --init=<modules>
 
-    comma-separated list of modules to install before running the server.
+    comma-separated list of modules to install before running the server
+    (requires :option:`-d`).
 
 .. option:: -u <modules>, --update=<modules>
 
-    comma-separated list of modules to update before running the server.
+    comma-separated list of modules to update before running the server
+    (requires :option:`-d`).
 
-.. option:: --addons-path <directories>
+.. option:: --addons-path=<directories>
 
     comma-separated list of directories in which modules are stored. These
     directories are scanned for modules (nb: when and why?)
 
-.. option:: -c <config>, --config <config>
+.. option:: -c <config>, --config=<config>
 
     provide an alternate configuration file
 
@@ -85,3 +97,4 @@ can be overridden using :option:`--config <odoo.py -c>`. Specifying
 to that file.
 
 .. _jinja2: http://jinja.pocoo.org
+.. _regular expression: https://docs.python.org/2/library/re.html
index c490043..64e6636 100644 (file)
@@ -85,7 +85,7 @@ class ir_http(osv.AbstractModel):
                 except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
                     # All other exceptions mean undetermined status (e.g. connection pool full),
                     # let them bubble up
-                    request.session.logout()
+                    request.session.logout(keep_db=True)
             getattr(self, "_auth_method_%s" % auth_method)()
         except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
             raise
index 84529f0..91ede73 100644 (file)
@@ -128,7 +128,7 @@ class res_partner_bank(osv.osv):
             change_default=True, domain="[('country_id','=',country_id)]"),
         'company_id': fields.many2one('res.company', 'Company',
             ondelete='cascade', help="Only if this bank account belong to your company"),
-        'partner_id': fields.many2one('res.partner', 'Account Owner', ondelete='cascade', select=True),
+        'partner_id': fields.many2one('res.partner', 'Account Owner', ondelete='cascade', select=True, domain=['|',('is_company','=',True),('parent_id','=',False)]),
         'state': fields.selection(_bank_type_get, 'Bank Account Type', required=True,
             change_default=True),
         'sequence': fields.integer('Sequence'),
index b6410d3..27b8875 100644 (file)
@@ -233,6 +233,7 @@ class res_partner(osv.Model, format_address):
         'date': fields.date('Date', select=1),
         'title': fields.many2one('res.partner.title', 'Title'),
         'parent_id': fields.many2one('res.partner', 'Related Company', select=True),
+        'parent_name': fields.related('parent_id', 'name', type='char', readonly=True, string='Parent name'),
         'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
         'ref': fields.char('Internal Reference', select=1),
         'lang': fields.selection(_lang_get, 'Language',
@@ -591,7 +592,7 @@ class res_partner(osv.Model, format_address):
         for record in self.browse(cr, uid, ids, context=context):
             name = record.name
             if record.parent_id and not record.is_company:
-                name =  "%s, %s" % (record.parent_id.name, name)
+                name = "%s, %s" % (record.parent_name, name)
             if context.get('show_address_only'):
                 name = self._display_address(cr, uid, record, without_company=True, context=context)
             if context.get('show_address'):
@@ -791,7 +792,7 @@ class res_partner(osv.Model, format_address):
             'state_name': address.state_id.name or '',
             'country_code': address.country_id.code or '',
             'country_name': address.country_id.name or '',
-            'company_name': address.parent_id.name or '',
+            'company_name': address.parent_name or '',
         }
         for field in self._address_fields(cr, uid, context=context):
             args[field] = getattr(address, field) or ''