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 @@ - - account.report.hierarchy - account.financial.report - children_ids - - - - - - - - - - - - Financial Reports Hierarchy - account.financial.report - tree - - [('parent_id','=',False)] - - - 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($('', { 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 @@ Context: - + Domain: - + Modifiers: 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); - core._t.database.parameters = originalParameters; + 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"); + + _.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: '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + 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 = 'hey'; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '', + res_id: 1, + viewOptions: {mode: 'edit'}, + }); + assert.strictEqual(form.$('select option[value="4"]').text(), 'hey'); + + 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: '' + + '' + + '', + 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: '' + + '' + + '' + + '' + + '', + }); + + 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 @@ - + @@ -103,7 +103,7 @@ - + @@ -413,7 +413,7 @@ - + @@ -457,7 +457,7 @@ Product Name - + 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