summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-10-13 13:13:11 +0200
committerChristophe Simonis <chs@odoo.com>2017-10-13 13:13:11 +0200
commit1da064a1d34b48ff23d8cdf5a1c189c8bbb4eb64 (patch)
treed832553531790ca342cda3b03e6c44097717ff43
parent119ae9b5dcaea577bfd91b36565b764990b9de02 (diff)
parent8ee9024a4c888647df9e18861fe5e58b68947469 (diff)
[MERGE] forward port branch saas-16 up to 8ee9024a4c8
-rw-r--r--addons/account/models/partner.py2
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js22
-rw-r--r--addons/account/static/tests/reconciliation_tests.js77
-rw-r--r--addons/account/views/account_view.xml23
-rw-r--r--addons/hr_expense/models/hr_expense.py2
-rw-r--r--addons/hr_org_chart/controllers/hr_org_chart.py5
-rw-r--r--addons/pos_cache/models/pos_cache.py4
-rw-r--r--addons/sale/models/account_invoice.py5
-rw-r--r--addons/sale/models/sale.py4
-rw-r--r--addons/stock/models/stock_quant.py3
-rw-r--r--addons/web/static/src/js/fields/field_utils.js41
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js4
-rw-r--r--addons/web/static/src/js/views/abstract_view.js3
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js29
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js2
-rw-r--r--addons/web/static/src/less/fields.less10
-rw-r--r--addons/web/static/src/less/form_view.less17
-rw-r--r--addons/web/static/src/less/list_view.less3
-rw-r--r--addons/web/static/src/xml/base.xml4
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js62
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js81
-rw-r--r--addons/web/static/tests/views/form_tests.js31
-rw-r--r--addons/website_quote/models/sale_order.py7
-rw-r--r--addons/website_sale/static/src/css/website_sale.css4
-rw-r--r--addons/website_sale/static/src/css/website_sale.sass2
-rw-r--r--addons/website_sale/views/templates.xml8
-rw-r--r--doc/cla/corporate/jarsa.md5
-rw-r--r--doc/cla/individual/carlosdauden.md9
-rw-r--r--odoo/addons/base/ir/ir_qweb/qweb.py52
-rw-r--r--odoo/addons/base/res/res.lang.csv2
-rw-r--r--odoo/fields.py45
-rw-r--r--odoo/models.py17
32 files changed, 439 insertions, 146 deletions
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index db8c4918b81..e48aa2e3b38 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -268,7 +268,7 @@ class ResPartner(models.Model):
all_partner_ids = []
for partner in self:
# price_total is in the company currency
- all_partners_and_children[partner] = self.search([('id', 'child_of', partner.id)]).ids
+ all_partners_and_children[partner] = self.with_context(active_test=False).search([('id', 'child_of', partner.id)]).ids
all_partner_ids += all_partners_and_children[partner]
# searching account.invoice.report via the orm is comparatively expensive
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 877dae2c9e0..880619411fe 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -3,6 +3,8 @@ odoo.define('account.ReconciliationModel', function (require) {
var BasicModel = require('web.BasicModel');
var field_utils = require('web.field_utils');
+var utils = require('web.utils');
+var session = require('web.session');
var CrashManager = require('web.CrashManager');
var core = require('web.core');
var _t = core._t;
@@ -582,7 +584,7 @@ var StatementModel = BasicModel.extend({
}));
}
ids.push(line.id);
- values.push({
+ var values_dict = {
"partner_id": line.st_line.partner_id,
"counterpart_aml_dicts": _.map(_.filter(props, function (prop) {
return !isNaN(prop.id) && !prop.already_paid;
@@ -593,7 +595,23 @@ var StatementModel = BasicModel.extend({
"new_aml_dicts": _.map(_.filter(props, function (prop) {
return isNaN(prop.id);
}), self._formatToProcessReconciliation.bind(self, line)),
- });
+ };
+ // If the lines are not fully balanced, create an unreconciled amount.
+ // line.st_line.currency_id is never false here because its equivalent to
+ // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side).
+ // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details
+ var currency = session.get_currency(line.st_line.currency_id);
+ var balance = line.balance.amount;
+ if (!utils.float_is_zero(balance, currency.digits[1])) {
+ var unreconciled_amount_dict = {
+ 'account_id': line.st_line.open_balance_account_id,
+ 'credit': balance > 0 ? balance : 0,
+ 'debit': balance < 0 ? -balance : 0,
+ 'name': line.st_line.name + ' : ' + _t("Open balance"),
+ };
+ values_dict['new_aml_dicts'].push(unreconciled_amount_dict);
+ }
+ values.push(values_dict);
line.reconciled = true;
self.valuenow++;
});
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 30aec5679c8..a8fa2c6f233 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -548,7 +548,16 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
});
clientAction.appendTo($('#qunit-fixture'));
@@ -574,7 +583,16 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
});
clientAction.appendTo($('#qunit-fixture'));
@@ -626,7 +644,7 @@ QUnit.module('account', {
"credit": 525,
"debit": 0,
"name": "INV/2017/0003"
- }],
+ }],
payment_aml_ids: [], new_aml_dicts: []}]],
"Should call process_reconciliations with ids");
});
@@ -651,12 +669,27 @@ QUnit.module('account', {
partner_id: false,
counterpart_aml_dicts:[],
payment_aml_ids: [392],
- new_aml_dicts: []
+ new_aml_dicts: [
+ {
+ "credit": 343.42,
+ "debit": 0,
+ "name": "Bank fees : Open balance"
+ }
+ ],
}]
], "should call process_reconciliations with partial reconcile values");
}
return this._super(route, args);
},
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
});
clientAction.appendTo($('body'));
@@ -813,11 +846,20 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
});
clientAction.appendTo($('#qunit-fixture'));
- assert.strictEqual(clientAction.$('.accounting_view tfoot .cell_right, .accounting_view tfoot .cell_left').text().replace(/[$, ]+/g, ''), "1175.0032.582000.00", "should display the open balance values");
+ assert.strictEqual(clientAction.$('.accounting_view tfoot .cell_right, .accounting_view tfoot .cell_left').text().replace(/[$, ]+/g, ''), " 1175.00 32.58 2000.00", "should display the open balance values");
var widget = clientAction.widgets[0];
@@ -849,8 +891,8 @@ QUnit.module('account', {
widget.$('.create .create_amount input').val('1100.00').trigger('input');
- assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text(), "1100.00", "should display the value 1100.00 in right column");
- assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' line because the rest to reconcile is 75.00");
+ assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text(), "$ 1100.00", "should display the value 1100.00 in right column");
+ assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$ 75.00", "should display 'Open Balance' line because the rest to reconcile is 75.00");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have ever only the created reconcile line");
assert.strictEqual(widget.$('.accounting_view tbody tr').text().replace(/[\n\r\s$,]+/g, ' '), " 101200 SAJ/2014/002 and SAJ/2014/003 1100.00 ",
"the new line should be update the amout");
@@ -862,9 +904,9 @@ QUnit.module('account', {
$('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click');
widget.$('.create .create_label input').val('test0').trigger('input');
- assert.strictEqual(widget.$('.accounting_view tbody .cell_left:last').text(), "100.00", "should display the value 100.00 in left column");
+ assert.strictEqual(widget.$('.accounting_view tbody .cell_left:last').text(), "$ 100.00", "should display the value 100.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'");
- assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "175.00", "should display 'Open Balance' line because the rest to reconcile is 175.00");
+ assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$ 175.00", "should display 'Open Balance' line because the rest to reconcile is 175.00");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines");
assert.strictEqual(widget.$('.accounting_view tbody tr:eq(1)').text().replace(/[\n\r\s$,]+/g, ' '), " 101000 test0 100.00 ",
"the new line should have the selected account, name and amout");
@@ -879,9 +921,9 @@ QUnit.module('account', {
$('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click');
widget.$('.create .create_label input').val('test1').trigger('input');
- assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "200.00", "should display the value 100.00 in left column");
+ assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column");
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'");
- assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "25.00", "should display 'Create Write-off' with 25.00 in left column");
+ assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Create Write-off' with 25.00 in left column");
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines");
clientAction.destroy();
@@ -1243,7 +1285,16 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
+ session: {
+ currencies: {
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ }
+ },
});
clientAction.appendTo($('#qunit-fixture'));
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 179043f60dd..325b7ef2ac5 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -2189,29 +2189,6 @@
<menuitem id="menu_account_financial_reports_tree" name="Account Reports" parent="menu_account_reports" action="action_account_financial_report_tree"/>
- <record id="view_account_report_tree_hierarchy" model="ir.ui.view">
- <field name="name">account.report.hierarchy</field>
- <field name="model">account.financial.report</field>
- <field name="field_parent">children_ids</field>
- <field name="arch" type="xml">
- <tree string="Account Reports Hierarchy">
- <field name="name"/>
- <field name="type"/>
- <field name="parent_id" invisible="1"/>
- <field name="account_report_id"/>
- </tree>
- </field>
- </record>
-
- <record id="action_account_report_tree_hierarchy" model="ir.actions.act_window">
- <field name="name">Financial Reports Hierarchy</field>
- <field name="res_model">account.financial.report</field>
- <field name="view_type">tree</field>
- <field name="view_id" ref="view_account_report_tree_hierarchy"/>
- <field name="domain">[('parent_id','=',False)]</field>
- </record>
-
- <menuitem id="menu_account_report_tree_hierarchy" name="Account Reports Hierarchy" parent="menu_account_reports" action="action_account_report_tree_hierarchy"/>
<!-- Manual reconciliation -->
<record id="action_manual_reconciliation" model="ir.actions.client">
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index a2044c4bd25..cfbd6e77967 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -292,7 +292,7 @@ class HrExpense(models.Model):
account_move.append(move_line)
# Calculate tax lines and adjust base line
- taxes = expense.tax_ids.compute_all(expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id)
+ taxes = expense.tax_ids.with_context(round=True).compute_all(expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id)
account_move[-1]['price'] = taxes['total_excluded']
account_move[-1]['tax_ids'] = [(6, 0, expense.tax_ids.ids)]
for tax in taxes['taxes']:
diff --git a/addons/hr_org_chart/controllers/hr_org_chart.py b/addons/hr_org_chart/controllers/hr_org_chart.py
index a088bb86663..5729997636f 100644
--- a/addons/hr_org_chart/controllers/hr_org_chart.py
+++ b/addons/hr_org_chart/controllers/hr_org_chart.py
@@ -10,12 +10,13 @@ class HrOrgChartController(http.Controller):
_managers_level = 2 # FP request
def _prepare_employee_data(self, employee):
+ job = employee.sudo().job_id
return dict(
id=employee.id,
name=employee.name,
link='/mail/view?model=hr.employee&res_id=%s' % employee.id,
- job_id=employee.job_id.id,
- job_name=employee.job_id.name or '',
+ job_id=job.id,
+ job_name=job.name or '',
direct_sub_count=len(employee.child_ids),
indirect_sub_count=employee.child_all_count,
)
diff --git a/addons/pos_cache/models/pos_cache.py b/addons/pos_cache/models/pos_cache.py
index bc6523c3eb5..10f617fe729 100644
--- a/addons/pos_cache/models/pos_cache.py
+++ b/addons/pos_cache/models/pos_cache.py
@@ -23,10 +23,10 @@ class pos_cache(models.Model):
@api.one
def refresh_cache(self):
- products = self.env['product.product'].search(self.get_product_domain())
+ Product = self.env['product.product'].sudo(self.compute_user_id.id)
+ products = Product.search(self.get_product_domain())
prod_ctx = products.with_context(pricelist=self.config_id.pricelist_id.id, display_default_code=False,
lang=self.compute_user_id.lang)
- prod_ctx = prod_ctx.sudo(self.compute_user_id.id)
res = prod_ctx.read(self.get_product_fields())
datas = {
'cache': base64.encodestring(json.dumps(res)),
diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py
index 1cf260da62e..9878e75ec04 100644
--- a/addons/sale/models/account_invoice.py
+++ b/addons/sale/models/account_invoice.py
@@ -111,7 +111,6 @@ class AccountInvoiceLine(models.Model):
'invoice_line_id', 'order_line_id',
string='Sales Order Lines', readonly=True, copy=False)
layout_category_id = fields.Many2one('sale.layout_category', string='Section')
- layout_category_sequence = fields.Integer(
- related='layout_category_id.sequence',
- string='Layout Sequence', store=True)
+ layout_category_sequence = fields.Integer(string='Layout Sequence')
+ # TODO: remove layout_category_sequence in master or make it work properly
price_total = fields.Monetary(compute='_compute_total_price', string='Total Amount', store=True)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 4c8bee5cd81..43fd9942388 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -890,8 +890,8 @@ class SaleOrderLine(models.Model):
amt_invoiced = fields.Monetary(string='Amount Invoiced', compute='_compute_invoice_amount', store=True)
layout_category_id = fields.Many2one('sale.layout_category', string='Section')
- layout_category_sequence = fields.Integer(related='layout_category_id.sequence', string='Layout Sequence', store=True)
- # Store is intentionally set in order to keep the "historic" order.
+ layout_category_sequence = fields.Integer(string='Layout Sequence')
+ # TODO: remove layout_category_sequence in master or make it work properly
@api.multi
def _prepare_invoice_line(self, qty):
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index ca60cb50239..c6d5700c366 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -407,7 +407,8 @@ class QuantPackage(models.Model):
def action_view_picking(self):
action = self.env.ref('stock.action_picking_tree_all').read()[0]
- pickings = self.env['stock.move.line'].search([('result_package_id', 'in', self.ids)]).mapped('picking_id')
+ domain = ['|', ('result_package_id', 'in', self.ids), ('package_id', 'in', self.ids)]
+ pickings = self.env['stock.move.line'].search(domain).mapped('picking_id')
action['domain'] = [('id', 'in', pickings.ids)]
return action
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 369dda9f037..7ba4122cccb 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -418,13 +418,36 @@ function parseDateTime(value, field, options) {
throw new Error(_.str.sprintf(core._t("'%s' is not a correct datetime"), value));
}
-function parseFloat(value) {
+/**
+ * Parse a String containing number in language formating
+ *
+ * @param {string} value
+ * The string to be parsed with the setting of thousands and
+ * decimal separator
+ * @returns {float|NaN} the number value contained in the string representation
+ */
+function parseNumber(value) {
if (core._t.database.parameters.thousands_sep) {
var escapedSep = _.str.escapeRegExp(core._t.database.parameters.thousands_sep);
value = value.replace(new RegExp(escapedSep, 'g'), '');
}
- value = value.replace(core._t.database.parameters.decimal_point, '.');
- var parsed = Number(value);
+ if (core._t.database.parameters.decimal_point) {
+ value = value.replace(core._t.database.parameters.decimal_point, '.');
+ }
+ return Number(value);
+}
+
+/**
+ * Parse a String containing float in language formating
+ *
+ * @param {string} value
+ * The string to be parsed with the setting of thousands and
+ * decimal separator
+ * @returns {float}
+ * @throws {Error} if no float is found respecting the language configuration
+ */
+function parseFloat(value) {
+ var parsed = parseNumber(value);
if (isNaN(parsed)) {
throw new Error(_.str.sprintf(core._t("'%s' is not a correct float"), value));
}
@@ -491,9 +514,17 @@ function parseFloatTime(value) {
return factor * (hours + (minutes / 60));
}
+/**
+ * Parse a String containing integer with language formating
+ *
+ * @param {string} value
+ * The string to be parsed with the setting of thousands and
+ * decimal separator
+ * @returns {integer}
+ * @throws {Error} if no integer is found respecting the language configuration
+ */
function parseInteger(value) {
- value = value.replace(new RegExp(core._t.database.parameters.thousands_sep, "g"), '');
- var parsed = Number(value);
+ var parsed = parseNumber(value);
// do not accept not numbers or float values
if (isNaN(parsed) || parsed % 1 || parsed < -2147483648 || parsed > 2147483647) {
throw new Error(_.str.sprintf(core._t("'%s' is not a correct integer"), value));
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index ed5f03b4f9b..c4422285ae7 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -423,7 +423,7 @@ var FieldMany2One = AbstractField.extend({
_updateExternalButton: function () {
var has_external_button = !this.nodeOptions.no_open && !this.floating && this.isSet();
this.$external_button.toggle(has_external_button);
- this.$el.toggleClass('o_with_button', has_external_button);
+ this.$el.toggleClass('o_with_button', has_external_button); // Should not be required anymore but kept for compatibility
},
@@ -1943,7 +1943,7 @@ var FieldSelection = AbstractField.extend({
for (var i = 0 ; i < this.values.length ; i++) {
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
- html: this.values[i][1]
+ text: this.values[i][1]
}));
}
var value = this.value;
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 594a4507963..a6cfea6e3be 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -226,7 +226,8 @@ var AbstractView = Class.extend({
return;
}
- attrs.limit = attrs.mode === "tree" ? 80 : 40;
+ attrs.limit = attrs.mode === "tree" ? 80 :
+ (attrs.widget === 'many2many_tags' ? 1000 : 40);
if (attrs.Widget.prototype.useSubview && !attrs.__no_fetch && !attrs.views[attrs.mode]) {
var context = {};
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 5c6f5d2d851..6884442039e 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -2450,7 +2450,12 @@ var BasicModel = AbstractModel.extend({
_generateChanges: function (record, options) {
options = options || {};
var viewType = options.viewType || record.viewType;
- var changes = _.extend({}, record._changes);
+ var changes;
+ if ('changesOnly' in options && !options.changesOnly) {
+ changes = _.extend({}, record.data, record._changes);
+ } else {
+ changes = _.extend({}, record._changes);
+ };
var withReadonly = options.withReadonly || false;
var commands = this._generateX2ManyCommands(record, {
changesOnly: 'changesOnly' in options ? options.changesOnly : true,
@@ -2496,10 +2501,14 @@ var BasicModel = AbstractModel.extend({
* current value of the parent record.
*
* @param {Object} record
+ * @param {Object} [options] This option object will be given to the private
+ * method _generateX2ManyCommands. In particular, it is useful to be able
+ * to send changesOnly:true to get all data, not only the current changes.
* @returns {Object} the data
*/
- _generateOnChangeData: function (record) {
- var commands = this._generateX2ManyCommands(record, {withReadonly: true});
+ _generateOnChangeData: function (record, options) {
+ options = _.extend({}, options || {}, {withReadonly: true});
+ var commands = this._generateX2ManyCommands(record, options);
var data = _.extend(this.get(record.id, {raw: true}).data, commands);
// 'display_name' is automatically added to the list of fields to fetch,
// when fetching a record, even if it doesn't appear in the view. However,
@@ -2618,6 +2627,7 @@ var BasicModel = AbstractModel.extend({
relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]});
changes = relRecord ? this._generateChanges(relRecord, options) : {};
if (!_.isEmpty(changes)) {
+ delete changes.id;
command = x2ManyCommands.update(relRecord.res_id, changes);
didChange = true;
} else {
@@ -3194,16 +3204,7 @@ var BasicModel = AbstractModel.extend({
if (value[0] === 6) {
// REPLACE_WITH
_.each(value[2], function (res_id) {
- r = self._makeDataPoint({
- modelName: x2manyList.model,
- context: x2manyList.context,
- fieldsInfo: fieldsInfo,
- fields: fields,
- parentID: x2manyList.id,
- res_id: res_id,
- viewType: viewType,
- });
- x2manyList._changes.push({operation: 'ADD', id: r.id});
+ x2manyList._changes.push({operation: 'ADD', resID: res_id});
});
var def = self._readUngroupedList(x2manyList).then(function () {
return $.when(
@@ -3315,7 +3316,7 @@ var BasicModel = AbstractModel.extend({
options.fieldName = fields;
}
var context = this._getContext(record, options);
- var currentData = this._generateOnChangeData(record);
+ var currentData = this._generateOnChangeData(record, {changesOnly: false});
return self._rpc({
model: record.model,
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index d25d1a423c9..ab5e4beb6b5 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -808,7 +808,7 @@ var FormRenderer = BasicRenderer.extend({
_.each(this.allFieldWidgets[this.state.id], function (widget) {
var idForLabel = self.idsForLabels[widget.name];
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
- if (core.debug || widget.attrs.help || widget.field.help) {
+ if (config.debug || widget.attrs.help || widget.field.help) {
self._addFieldTooltip(widget, $label);
}
if (widget.attrs.widget === 'upgrade_boolean') {
diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less
index de4675d6701..9450147e7e8 100644
--- a/addons/web/static/src/less/fields.less
+++ b/addons/web/static/src/less/fields.less
@@ -49,7 +49,7 @@
// Dropdowns input (many2one, ...)
.o_input_dropdown {
- .o-flex(1, 0, auto);
+ .o-flex(1, 1, auto);
position: relative;
width: 100%;
@@ -178,14 +178,6 @@
color: @brand-success;
}
- // Many2One
- &.o_field_many2one {
- .o-inline-flex-display();
- > span, > button {
- .o-flex(0, 0, auto);
- }
- }
-
// Handle widget
&.o_row_handle {
cursor: ns-resize;
diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less
index a73e89317f5..9bfbd6c535e 100644
--- a/addons/web/static/src/less/form_view.less
+++ b/addons/web/static/src/less/form_view.less
@@ -64,12 +64,6 @@
.o-align-self(center);
}
}
-
- .o_row .o_field_many2one, .o_field_many2one.oe_inline {
- &.o_with_button:not(:last-child) { // If invisible element as last child, this line has no effect but that's not very important
- margin-right: (35px + (@odoo-form-spacing-unit/2));
- }
- }
}
.o_row > div > .o_field_widget {
@@ -358,6 +352,17 @@
}
}
+ :not(.o_row) > .o_field_widget,
+ .o_row > .o_field_widget:last-child { // Note: this does not take care
+ // of an invisible last-child but
+ // it does not really matter
+ // Makes extra buttons (e.g. m2o external button) overflow on the
+ // right padding of the parent element
+ .o_input_dropdown {
+ .o-flex(1, 0, auto);
+ }
+ }
+
&.o_label_nowrap .o_form_label {
white-space: nowrap;
}
diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less
index d541bc8cfc3..82debf1a035 100644
--- a/addons/web/static/src/less/list_view.less
+++ b/addons/web/static/src/less/list_view.less
@@ -178,9 +178,6 @@
.o_field_widget {
min-width: 0;
}
- .o_with_button .o_input_dropdown {
- width: 1%; // Hack so that the sibling button is not overlapped
- }
.o_input_dropdown, .o_datepicker {
> .o_input {
padding-right: 15px; // To prevent caret overlapping
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 4ffffd37984..a4773488d30 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -35,11 +35,11 @@
</li>
<li t-if="widget.attrs.context || widget.field.context" data-item="context">
<span class="oe_tooltip_technical_title">Context:</span>
- <t t-esc="widget.attrs.context || widget.field.context"/>
+ <t t-esc="widget.attrs.context || JSON.stringify(widget.field.context)"/>
</li>
<li t-if="widget.attrs.domain || widget.field.domain" data-item="domain">
<span class="oe_tooltip_technical_title">Domain:</span>
- <t t-esc="widget.attrs.domain || widget.field.domain"/>
+ <t t-esc="widget.attrs.domain || JSON.stringify(widget.field.domain)"/>
</li>
<li t-if="widget.attrs.modifiers and widget.attrs.modifiers != '{}'" data-item="modifiers">
<span class="oe_tooltip_technical_title">Modifiers:</span>
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
index a0284798553..1f8c27e7192 100644
--- a/addons/web/static/tests/fields/field_utils_tests.js
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -128,7 +128,15 @@ QUnit.test('format one2many', function(assert) {
});
QUnit.test('parse float', function(assert) {
- assert.expect(7);
+ assert.expect(10);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: '.',
+ thousands_sep: ','
+ });
assert.strictEqual(fieldUtils.parse.float(""), 0);
assert.strictEqual(fieldUtils.parse.float("0"), 0);
@@ -136,16 +144,64 @@ QUnit.test('parse float', function(assert) {
assert.strictEqual(fieldUtils.parse.float("-100.00"), -100);
assert.strictEqual(fieldUtils.parse.float("1,000.00"), 1000);
assert.strictEqual(fieldUtils.parse.float("1,000,000.00"), 1000000);
+ assert.strictEqual(fieldUtils.parse.float('1,234.567'), 1234.567);
+ assert.throws(function () {
+ fieldUtils.parse.float("1.000.000");
+ }, "Throw an exception if it's not a valid number");
- var originalParameters = $.extend(true, {}, core._t.database.parameters);
_.extend(core._t.database.parameters, {
grouping: [3, 0],
decimal_point: ',',
thousands_sep: '.'
});
+
assert.strictEqual(fieldUtils.parse.float('1.234,567'), 1234.567);
+ assert.throws(function () {
+ fieldUtils.parse.float("1,000,000");
+ }, "Throw an exception if it's not a valid number");
- core._t.database.parameters = originalParameters;
+ _.extend(core._t.database.parameters, originalParameters);
+});
+
+QUnit.test('parse integer', function(assert) {
+ assert.expect(11);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: '.',
+ thousands_sep: ','
+ });
+
+ assert.strictEqual(fieldUtils.parse.integer(""), 0);
+ assert.strictEqual(fieldUtils.parse.integer("0"), 0);
+ assert.strictEqual(fieldUtils.parse.integer("100"), 100);
+ assert.strictEqual(fieldUtils.parse.integer("-100"), -100);
+ assert.strictEqual(fieldUtils.parse.integer("1,000"), 1000);
+ assert.strictEqual(fieldUtils.parse.integer("1,000,000"), 1000000);
+ assert.throws(function () {
+ fieldUtils.parse.integer("1.000.000");
+ }, "Throw an exception if it's not a valid number");
+ assert.throws(function () {
+ fieldUtils.parse.integer("1,234.567");
+ }, "Throw an exception if the number is a float");
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+
+ assert.strictEqual(fieldUtils.parse.integer("1.000.000"), 1000000);
+ assert.throws(function () {
+ fieldUtils.parse.integer("1,000,000");
+ }, "Throw an exception if it's not a valid number");
+ assert.throws(function () {
+ fieldUtils.parse.integer("1.234,567");
+ }, "Throw an exception if the number is a float");
+
+ _.extend(core._t.database.parameters, originalParameters);
});
QUnit.test('parse monetary', function(assert) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 7a0937084c3..bc24146f253 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1063,6 +1063,44 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('list in form: default_get with x2many create and onchange', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges.turtles = function (obj) {
+ assert.deepEqual(
+ obj.turtles,
+ [
+ [0, false, {turtle_foo: 'blip', id: 2}],
+ [0, false, {turtle_foo: 'kawa', id: 3}]
+ ],
+ "should have properly created the x2many command list");
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({turtles: [[6, 0, [2,3]]]});
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.destroy();
+ });
+
QUnit.test('list in form: call button in sub view', function (assert) {
assert.expect(6);
@@ -7498,6 +7536,26 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('field selection with many2ones and special characters', function (assert) {
+ assert.expect(1);
+
+ // edit the partner with id=4
+ this.data.partner.records[2].display_name = '<span>hey</span>';
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu" widget="selection"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ });
+ assert.strictEqual(form.$('select option[value="4"]').text(), '<span>hey</span>');
+
+ form.destroy();
+ });
+
QUnit.test('widget selection on a many2one: domain updated by an onchange', function (assert) {
assert.expect(4);
@@ -7891,6 +7949,29 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2many_tags can load more than 40 records', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'};
+ this.data.partner.records[0].partner_ids = [];
+ for (var i = 15; i < 115; i++) {
+ this.data.partner.records.push({id: i, display_name: 'walter' + i});
+ this.data.partner.records[0].partner_ids.push(i);
+ }
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('.o_field_widget[name="partner_ids"] > span').length, 100,
+ 'should have rendered 100 tags');
+ form.destroy();
+ });
+
QUnit.test('field many2many_tags keeps focus when being edited', function (assert) {
assert.expect(7);
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 6a3fdb0fbea..5d2010907d5 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -5977,5 +5977,36 @@ QUnit.module('Views', {
form.destroy();
});
+
+ QUnit.test('proper context stringification in debug mode tooltip', function (assert) {
+ assert.expect(2);
+
+ var initialDebugMode = config.debug;
+ config.debug = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="product_id" context="{\'lang\': \'en_US\'}"/>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ var $field = form.$('[name="product_id"]');
+ $field.tooltip('show', true);
+ $field.trigger($.Event('mouseenter'));
+ assert.strictEqual($('.oe_tooltip_technical>li[data-item="context"]').length,
+ 1, 'context should be present for this field');
+ assert.strictEqual($('.oe_tooltip_technical>li[data-item="context"]')[0].lastChild.wholeText.trim(),
+ "{'lang': 'en_US'}", "context should be properly stringified");
+
+ config.debug = initialDebugMode;
+ form.destroy();
+ });
+
+
});
});
diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py
index a0c99d0b3f5..1ef01d517a3 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/website_quote/models/sale_order.py
@@ -106,15 +106,20 @@ class SaleOrder(models.Model):
order_lines = [(5, 0, 0)]
for line in template.quote_line:
+ discount = 0
if self.pricelist_id:
price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
+ if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit:
+ discount = (line.price_unit - price) / line.price_unit * 100
+ price = line.price_unit
+
else:
price = line.price_unit
data = {
'name': line.name,
'price_unit': price,
- 'discount': line.discount,
+ 'discount': 100 - ((100 - discount) * (100 - line.discount)/100),
'product_uom_qty': line.product_uom_qty,
'product_id': line.product_id.id,
'layout_category_id': line.layout_category_id,
diff --git a/addons/website_sale/static/src/css/website_sale.css b/addons/website_sale/static/src/css/website_sale.css
index 0343c9dfa43..66735871e06 100644
--- a/addons/website_sale/static/src/css/website_sale.css
+++ b/addons/website_sale/static/src/css/website_sale.css
@@ -341,6 +341,10 @@
min-width: 76px;
}
+.oe_cart > .oe_structure {
+ clear: both;
+}
+
div#payment_method div.list-group {
margin-left: 40px;
}
diff --git a/addons/website_sale/static/src/css/website_sale.sass b/addons/website_sale/static/src/css/website_sale.sass
index 0846c6d55fd..3b9ceb7fdd9 100644
--- a/addons/website_sale/static/src/css/website_sale.sass
+++ b/addons/website_sale/static/src/css/website_sale.sass
@@ -299,6 +299,8 @@
.oe_cart table td:first-child
min-width: 76px
+.oe_cart > .oe_structure
+ clear: both
div#payment_method
div.list-group
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index a27e1a339c4..0df77aaecf5 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -88,7 +88,7 @@
</div>
<div class="oe_product_image">
<a itemprop="url" t-att-href="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))">
- <span itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'resize': None if product_image_big else '300x300', 'zoom': 'image'}" t-att-alt="product.name" />
+ <span itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.template/{{product.id}}/image" t-field="product.image" t-options="{'widget': 'image', 'resize': None if product_image_big else '300x300', 'zoom': 'image'}" t-att-alt="product.name" />
</a>
</div>
<t t-if="show_publish">
@@ -103,7 +103,7 @@
<section>
<h5>
<strong>
- <a itemprop="name" t-att-href="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))" t-field="product.name" />
+ <a itemprop="name" t-att-href="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']&gt;1 else None))" t-att-content="product.name" t-field="product.name" />
</strong>
</h5>
<div itemprop="offers" itemscope="itemscope" itemtype="http://schema.org/Offer" class="product_price" t-if="product.product_variant_ids">
@@ -413,7 +413,7 @@
</div>
<div class="row">
<div class="col-sm-7">
- <span itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image'}" groups="!website_sale.group_website_multi_image"/>
+ <span itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.template/{{product.id}}/image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image'}" groups="!website_sale.group_website_multi_image"/>
<t groups="website_sale.group_website_multi_image">
<t t-set="variant_img" t-value="product.product_variant_id.image_variant"/>
<t t-set="image_ids" t-value="product.product_image_ids"/>
@@ -457,7 +457,7 @@
</div>
<div class="col-sm-5 col-lg-4 col-lg-offset-1" id="product_details">
<h1 itemprop="name" t-field="product.name">Product Name</h1>
- <span itemprop="url" style="display:none;" t-esc="'/shop/product/%s' % slug(product)" />
+ <span itemprop="url" style="display:none;" t-esc="'%sshop/product/%s' % (request.httprequest.url_root, slug(product))"/>
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product)"/>
<form action="/shop/cart/update" class="js_add_cart_variants" t-att-data-attribute_value_ids="attribute_value_ids" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
diff --git a/doc/cla/corporate/jarsa.md b/doc/cla/corporate/jarsa.md
index 105359f0bb6..b82292ff2d1 100644
--- a/doc/cla/corporate/jarsa.md
+++ b/doc/cla/corporate/jarsa.md
@@ -13,3 +13,8 @@ List of contributors:
Alan Ramos alan.ramos@jarsa.com.mx https://github.com/alan196
Luis Triana luis.triana@jarsa.com.mx https://github.com/xluiisx
Luis Triana luistriana.28@gmail.com https://github.com/xluiisx
+Oscar Garza oscar.garza@jarsa.com.mx https://github.com/ougc27
+Alexis Reza alexis.reza@jarsa.com.mx https://github.com/AlexisReza
+Isabel Esparza isabel.esparza@jarsa.com.mx https://github.com/IsabelEsparza
+Miguel Ruiz miguel.ruiz@jarsa.com.mx https://github.com/lmgr0312
+Sarai Osorio sarai.osorio@jarsa.com.mx https://github.com/SaraiOsorio
diff --git a/doc/cla/individual/carlosdauden.md b/doc/cla/individual/carlosdauden.md
new file mode 100644
index 00000000000..a672fcaf988
--- /dev/null
+++ b/doc/cla/individual/carlosdauden.md
@@ -0,0 +1,9 @@
+Spain, 2015-05-28
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Carlos Dauden carlos.dauden@tecnativa.com https://github.com/carlosdauden
diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py
index 43c10fe19f0..a773daf600e 100644
--- a/odoo/addons/base/ir/ir_qweb/qweb.py
+++ b/odoo/addons/base/ir/ir_qweb/qweb.py
@@ -1135,7 +1135,26 @@ class QWeb(object):
def _compile_directive_esc(self, el, options):
field_options = self._compile_widget_options(el, 'esc')
- content = self._compile_widget(el, el.attrib.pop('t-esc'), field_options, 'escape')
+ content = self._compile_widget(el, el.attrib.pop('t-esc'), field_options)
+ if not field_options:
+ # if content is not False and if content is not None:
+ # content = escape(pycompat.to_text(content))
+ content.append(self._if_content_is_not_Falsy([
+ ast.Assign(
+ targets=[ast.Name(id='content', ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Name(id='escape', ctx=ast.Load()),
+ args=[ast.Call(
+ func=ast.Name(id='to_text', ctx=ast.Load()),
+ args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ )],
+ keywords=[],
+ starargs=None, kwargs=None
+ )
+ )],
+ []
+ ))
return content + self._compile_widget_value(el, options)
def _compile_directive_raw(self, el, options):
@@ -1143,33 +1162,8 @@ class QWeb(object):
content = self._compile_widget(el, el.attrib.pop('t-raw'), field_options)
return content + self._compile_widget_value(el, options)
+ # escape attribute is deprecated and will remove after v11
def _compile_widget(self, el, expression, field_options, escape=None):
- # if isinstance(value, string_types):
- # value = escape(to_text(value))
- escaped = ast.If(
- test=ast.Call(
- func=ast.Name(id='isinstance', ctx=ast.Load()),
- args=[
- ast.Name(id='content', ctx=ast.Load()),
- ast.Name(id='string_types', ctx=ast.Load())
- ], keywords=[],
- starargs=None, kwargs=None
- ),
- body=[ast.Assign(
- targets=[ast.Name(id='content', ctx=ast.Store())],
- value=ast.Call(
- func=ast.Name(id=escape, ctx=ast.Load()),
- args=[ast.Call(
- func=ast.Name(id='to_text', ctx=ast.Load()),
- args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
- starargs=None, kwargs=None
- )], keywords=[],
- starargs=None, kwargs=None
- ) if escape else ast.Name(id='content', ctx=ast.Load())
- )],
- orelse=[]
- )
-
if field_options:
return [
# value = t-(esc|raw)
@@ -1177,7 +1171,6 @@ class QWeb(object):
targets=[ast.Name(id='content', ctx=ast.Store())],
value=self._compile_expr0(expression)
),
- escaped,
# t_attrs, content, force_display = self._get_widget(value, expression, tagName, field options, template options, values)
ast.Assign(
targets=[ast.Tuple(elts=[
@@ -1221,8 +1214,7 @@ class QWeb(object):
self._compile_expr0(expression),
ast.Name(id='None', ctx=ast.Load()),
], ctx=ast.Load())
- ),
- escaped
+ )
]
# for backward compatibility to remove after v10
diff --git a/odoo/addons/base/res/res.lang.csv b/odoo/addons/base/res/res.lang.csv
index 44b1406b753..e04af8271b7 100644
--- a/odoo/addons/base/res/res.lang.csv
+++ b/odoo/addons/base/res/res.lang.csv
@@ -21,7 +21,7 @@
"base.lang_fr_BE","French (BE) / Français (BE)","fr_BE","fr_BE","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True"
"base.lang_fr_CA","French (CA) / Français (CA)","fr_CA","fr_CA","Left-to-Right","[3,0]",","," ","%Y-%m-%d","%H:%M:%S","True"
"base.lang_fr_CH","French (CH) / Français (CH)","fr_CH","fr_CH","Left-to-Right","[3,0]",".","'","%d. %m. %Y","%H:%M:%S","True"
-"base.lang_fr","French / Français","fr_FR","fr","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True"
+"base.lang_fr","French / Français","fr_FR","fr","Left-to-Right","[3,0]",","," ","%d/%m/%Y","%H:%M:%S","True"
"base.lang_gl_ES","Galician / Galego","gl_ES","gl_ES","Left-to-Right","[]",",",,"%d/%m/%Y","%H:%M:%S","True"
"base.lang_ka_GE","Georgian / ქართული ენა","ka_GE","ka_GE","Left-to-Right","[3,0]",",",".","%m/%d/%Y","%H:%M:%S","True"
"base.lang_de","German / Deutsch","de_DE","de","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","True"
diff --git a/odoo/fields.py b/odoo/fields.py
index f04c4139b6f..4c41c938637 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -52,6 +52,10 @@ def copy_cache(records, env):
if value and field.type in ('many2one', 'one2many', 'many2many', 'reference'):
todo.update(field.convert_to_record(value, record))
+def first(records):
+ """ Return the first record in ``records``, with the same prefetching. """
+ return next(iter(records)) if len(records) > 1 else records
+
def resolve_mro(model, name, predicate):
""" Return the list of successively overridden values of attribute ``name``
@@ -558,12 +562,41 @@ class Field(MetaField('DummyField', (object,), {})):
""" Compute the related field ``self`` on ``records``. """
# when related_sudo, bypass access rights checks when reading values
others = records.sudo() if self.related_sudo else records
- for record, other in pycompat.izip(records, others):
- if not record.id and record.env != other.env:
- # draft records: copy record's cache to other's cache first
- copy_cache(record, other.env)
- other, field = self.traverse_related(other)
- record[self.name] = other[field.name]
+ # copy the cache of draft records into others' cache
+ if records.env != others.env:
+ copy_cache(records - records.filtered('id'), others.env)
+ #
+ # Traverse fields one by one for all records, in order to take advantage
+ # of prefetching for each field access. In order to clarify the impact
+ # of the algorithm, consider traversing 'foo.bar' for records a1 and a2,
+ # where 'foo' is already present in cache for a1, a2. Initially, both a1
+ # and a2 are marked for prefetching. As the commented code below shows,
+ # traversing all fields one record at a time will fetch 'bar' one record
+ # at a time.
+ #
+ # b1 = a1.foo # mark b1 for prefetching
+ # v1 = b1.bar # fetch/compute bar for b1
+ # b2 = a2.foo # mark b2 for prefetching
+ # v2 = b2.bar # fetch/compute bar for b2
+ #
+ # On the other hand, traversing all records one field at a time ensures
+ # maximal prefetching for each field access.
+ #
+ # b1 = a1.foo # mark b1 for prefetching
+ # b2 = a2.foo # mark b2 for prefetching
+ # v1 = b1.bar # fetch/compute bar for b1, b2
+ # v2 = b2.bar # value already in cache
+ #
+ # This difference has a major impact on performance, in particular in
+ # the case where 'bar' is a computed field that takes advantage of batch
+ # computation.
+ #
+ values = list(others)
+ for name in self.related[:-1]:
+ values = [first(value[name]) for value in values]
+ # assign final values to records
+ for record, value in pycompat.izip(records, values):
+ record[self.name] = value[self.related_field.name]
def _inverse_related(self, records):
""" Inverse the related field ``self`` on ``records``. """
diff --git a/odoo/models.py b/odoo/models.py
index 7a056c326e5..dea095cd4c9 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -3377,14 +3377,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
id_new, = cr.fetchone()
self = self.browse(id_new)
- if self.env.lang and self.env.lang != 'en_US':
- # add translations for self.env.lang
- for name, val in vals.items():
- field = self._fields[name]
- if field.store and field.column_type and field.translate is True:
- tname = "%s,%s" % (self._name, name)
- self.env['ir.translation']._set_ids(tname, 'model', self.env.lang, self.ids, val, val)
-
if self._parent_store and not self._context.get('defer_parent_store_computation'):
if self.pool._init:
self.pool._init_parent[self._name] = True
@@ -3446,6 +3438,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self.recompute()
self.check_access_rule('create')
+
+ if self.env.lang and self.env.lang != 'en_US':
+ # add translations for self.env.lang
+ for name, val in vals.items():
+ field = self._fields[name]
+ if field.store and field.column_type and field.translate is True:
+ tname = "%s,%s" % (self._name, name)
+ self.env['ir.translation']._set_ids(tname, 'model', self.env.lang, self.ids, val, val)
+
return id_new
# TODO: ameliorer avec NULL