diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 4d7add6879a..85bed6a05ad 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -335,6 +335,7 @@ class AccountBankStatement(models.Model): 'st_lines_ids': st_lines_left.ids, 'notifications': [], 'statement_name': len(statements) == 1 and statements[0].name or False, + 'journal_id': statements and statements[0].journal_id.id or False, 'num_already_reconciled_lines': 0, } @@ -470,7 +471,7 @@ class AccountBankStatementLine(models.Model): 'details': { 'name': _("Automatically reconciled items"), 'model': 'account.move', - 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').ids + 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').mapped('move_id').ids } }] return { @@ -586,7 +587,7 @@ class AccountBankStatementLine(models.Model): if str: str_domain = self.env['account.move.line'].domain_move_lines_for_reconciliation(str=str) if not partner_id: - str_domain = expression.OR([str_domain, ('partner_id.name', 'ilike', str)]) + str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]]) domain = expression.AND([domain, str_domain]) if excluded_ids: domain = expression.AND([[('id', 'not in', excluded_ids)], domain]) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index b2c79471929..51175be7794 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -29,6 +29,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, { validate: '_onValidate', validate_all_balanced: '_onValidate', change_name: '_onChangeName', + close_statement: '_onCloseStatement', }, config: { // used to instanciate the model @@ -202,7 +203,26 @@ var StatementAction = Widget.extend(ControlPanelMixin, { }); }); }, - + /** + * call 'closeStatement' model method + * + * @private + * @param {OdooEvent} event + */ + _onCloseStatement: function (event) { + var self = this; + return this.model.closeStatement().then(function (result) { + self.do_action({ + name: 'Bank Statements', + res_model: 'account.bank.statement', + res_id: result, + views: [[false, 'form']], + type: 'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + }); + }); + }, /** * call 'validate' or 'autoReconciliation' model method then destroy the * validated lines and update the action renderer with the new status bar @@ -222,6 +242,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, { 'title': self.title, 'time': Date.now()-self.time, 'notifications': result.notifications, + 'context': self.model.getContext(), }); _.each(result.handles, function (handle) { self._getWidget(handle).destroy(); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 11e4f4bb55f..e12f8f21009 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -230,11 +230,31 @@ var StatementModel = BasicModel.extend({ var line = this.getLine(handle); line.st_line.partner_id = partner && partner.id; line.st_line.partner_name = partner && partner.display_name || ''; - return this._performMoveLine(handle).then(function () { - if (line.mode === 'create') { - return self.createProposition(handle); - } - }); + return this._changePartner(handle, partner.id) + .then(function() { + line.reconciliation_proposition = []; + return self._performMoveLine(handle) + }) + .then(function () { + if (line.mode === 'create') { + return self.createProposition(handle); + } + }); + }, + /** + * close the statement + * @returns {Deferred} resolves to the res_id of the closed statements + */ + closeStatement: function () { + var self = this; + return this._rpc({ + model: 'account.bank.statement', + method: 'button_confirm_bank', + args: [self.bank_statement_id.id], + }) + .then(function () { + return self.bank_statement_id.id + }); }, /** * @@ -256,6 +276,13 @@ var StatementModel = BasicModel.extend({ line.createForm = _.pick(prop, this.quickCreateFields); return this._computeLine(line); }, + /** + * Return context information and journal_id + * @returns {Object} context + */ + getContext: function () { + return this.context; + }, /** * get the line data for this handle * @@ -282,6 +309,8 @@ var StatementModel = BasicModel.extend({ if (!statement_ids) { return $.when(); } + this.context = context; + var def_statement = this._rpc({ model: 'account.bank.statement', method: 'reconciliation_widget_preprocess', @@ -292,6 +321,7 @@ var StatementModel = BasicModel.extend({ self.bank_statement_id = statement_ids.length === 1 ? {id: statement_ids[0], display_name: statement.statement_name} : false; self.valuenow = 0; self.valuemax = statement.st_lines_ids.length; + self.context.journal_id = statement.journal_id; _.each(statement.st_lines_ids, function (id) { self.lines[_.uniqueId('rline')] = { 'id': id, @@ -484,14 +514,19 @@ var StatementModel = BasicModel.extend({ _.each(handles, function (handle) { var line = self.getLine(handle); var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.is_tax && !prop.invalid;}); + if (props.length === 0) { + props.push(self._formatQuickCreate(line, { + account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]], + })); + } ids.push(line.id); values.push({ "partner_id": line.st_line.partner_id, "counterpart_aml_dicts": _.map(_.filter(props, function (prop) { - return !isNaN(prop.id) && prop.partial_reconcile; + return !isNaN(prop.id) && !prop.already_paid; }), self._formatToProcessReconciliation.bind(self, line)), "payment_aml_ids": _.pluck(_.filter(props, function (prop) { - return !isNaN(prop.id) && !prop.partial_reconcile; + return !isNaN(prop.id) && prop.already_paid; }), 'id'), "new_aml_dicts": _.map(_.filter(props, function (prop) { return isNaN(prop.id); @@ -557,6 +592,29 @@ var StatementModel = BasicModel.extend({ }); return this._computeLine(line); }, + /** + * When changing partner, read property_account_receivable and payable + * of that partner because the counterpart account might cahnge depending + * on the partner + * + * @private + * @param {string} handle + * @param {integer} partner_id + * @returns {Deferred} + */ + _changePartner: function (handle, partner_id) { + var self = this; + return this._rpc({ + model: 'res.partner', + method: 'read', + args: [partner_id, ["property_account_receivable_id", "property_account_payable_id"]], + }).then(function (result) { + if (result.length > 0) { + var line = self.getLine(handle); + self.lines[handle].st_line.open_balance_account_id = line.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0]; + } + }); + }, /** * Calculates the balance; format each proposition amount_str and mark as * invalid the line with empty account_id, amount or label @@ -573,7 +631,7 @@ var StatementModel = BasicModel.extend({ // compute taxes var tax_defs = []; var reconciliation_proposition = []; - var format_options = { + var formatOptions = { currency_id: line.st_line.currency_id, }; _.each(line.reconciliation_proposition, function (prop) { @@ -610,10 +668,10 @@ var StatementModel = BasicModel.extend({ }); prop.amount = tax.base; - prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, format_options); + prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions); prop.invalid = !self._isValid(prop); - tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, format_options); + tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions); tax_prop.invalid = prop.invalid; reconciliation_proposition.push(tax_prop); @@ -644,7 +702,7 @@ var StatementModel = BasicModel.extend({ total = Math.round(total*1000)/1000 || 0; line.balance = { 'amount': total, - 'amount_str': field_utils.format.monetary(Math.abs(total), {}, format_options), + 'amount_str': field_utils.format.monetary(Math.abs(total), {}, formatOptions), 'account_code': self.accounts[line.st_line.open_balance_account_id], }; line.balance.type = line.balance.amount ? (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1; @@ -759,6 +817,9 @@ var StatementModel = BasicModel.extend({ _formatQuickCreate: function (line, values) { values = values || {}; var account = this._formatNameGet(values.account_id); + var formatOptions = { + currency_id: line.st_line.currency_id, + }; var prop = { 'id': _.uniqueId('createLine'), 'label': values.label || line.st_line.name, @@ -771,7 +832,7 @@ var StatementModel = BasicModel.extend({ 'credit': 0, 'base_amount': values.amount_type !== "percentage" ? (values.amount || line.balance.amount) : - line.st_line.amount * values.amount / 100, + line.balance.amount * values.amount / 100, 'percent': values.amount_type === "percentage" ? values.amount : null, 'link': values.link, 'display': true, @@ -781,9 +842,10 @@ var StatementModel = BasicModel.extend({ '__focus': '__focus' in values ? values.__focus : true, }; if (prop.base_amount) { + // Call to format and parse needed to round the value to the currency precision var sign = prop.base_amount < 0 ? -1 : 1; - var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, line); - prop.base_amount = sign * field_utils.parse.monetary(amount); + var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions); + prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions); } prop.amount = prop.base_amount; return prop; @@ -843,7 +905,10 @@ var StatementModel = BasicModel.extend({ * @returns {object} */ _formatToProcessReconciliation: function (line, prop) { - var amount = prop.partial_reconcile ? -line.st_line.amount : prop.amount; + var amount = -prop.amount; + if (prop.partial_reconcile === true) { + amount = -line.st_line.amount; + } var result = { name : prop.label, debit : amount > 0 ? amount : 0, @@ -854,7 +919,7 @@ var StatementModel = BasicModel.extend({ } else { result.account_id = prop.account_id.id; if (prop.journal_id) { - result.journal_id = prop.account_id.id; + result.journal_id = prop.journal_id.id; } } if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id; diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index bd8d8e52982..e0ecf61d354 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -22,6 +22,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { 'click div:first h1.statement_name': '_onClickStatementName', 'click div:first h1.statement_name_edition button': '_onValidateName', "click *[rel='do_action']": "_onDoAction", + 'click button.button_back_to_statement': '_onGoToBankStatement', + 'click button.button_close_statement': '_onCloseBankStatement', }, /** * @override @@ -107,7 +109,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { var $done = $(qweb.render("reconciliation.done", { 'duration': moment(dt).utc().format(time.strftime_to_moment_format(_t.database.parameters.time_format)), 'number': state.valuenow, - 'timePerTransaction': Math.round(dt/1000/state.valuemax) + 'timePerTransaction': Math.round(dt/1000/state.valuemax), + 'context': state.context, })); this.$el.children().hide(); // display rainbowman after full reconciliation @@ -165,11 +168,12 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { }, /** * @private + * Click on close bank statement button, this will + * close and then open form view of bank statement + * @param {MouseEvent} event */ - _onValidateName: function () { - var name = this.model.get(this.handleNameRecord).data.name; - this.trigger_up('change_name', {'data': name}); - this.$('.statement_name, .statement_name_edition').toggle(); + _onCloseBankStatement: function (e) { + this.trigger_up('close_statement'); }, /** * @private @@ -190,6 +194,34 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { view_mode: "list" }); }, + /** + * Open the list view for account.bank.statement model + * @private + * @param {MouseEvent} event + */ + _onGoToBankStatement: function (e) { + var journalId = $(e.target).attr('data_journal_id'); + if (journalId) { + journalId = parseInt(journalId); + } + this.do_action({ + name: 'Bank Statements', + res_model: 'account.bank.statement', + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + context: {search_default_journal_id: journalId}, + view_type: 'list', + view_mode: 'form', + }); + }, + /** + * @private + */ + _onValidateName: function () { + var name = this.model.get(this.handleNameRecord).data.name; + this.trigger_up('change_name', {'data': name}); + this.$('.statement_name, .statement_name_edition').toggle(); + }, }); @@ -312,6 +344,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { } if ((state.balance.amount !== 0 || line.partial_reconcile) && props.length === 1 && + line.already_paid === false && ( (state.st_line.amount > 0 && state.st_line.amount < props[0].amount) || (state.st_line.amount < 0 && state.st_line.amount > props[0].amount)) @@ -372,6 +405,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { // Private //-------------------------------------------------------------------------- + /** + * @private + * @param {jQueryElement} $el + */ + _destroyPopover: function ($el) { + var popover = $el.data('bs.popover'); + if (popover) { + popover.destroy(); + } + }, /** * @private * @param {integer} partnerID @@ -557,16 +600,20 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { * @param {MouseEvent} event */ _onSelectMoveLine: function (event) { - var mv_line_id = $(event.target).closest('.mv_line').data('line-id'); - this.trigger_up('add_proposition', {'data': mv_line_id}); + var $el = $(event.target) + this._destroyPopover($el); + var moveLineId = $el.closest('.mv_line').data('line-id'); + this.trigger_up('add_proposition', {'data': moveLineId}); }, /** * @private * @param {MouseEvent} event */ _onSelectProposition: function (event) { - var mv_line_id = $(event.target).closest('.mv_line').data('line-id'); - this.trigger_up('remove_proposition', {'data': mv_line_id}); + var $el = $(event.target) + this._destroyPopover($el); + var moveLineId = $el.closest('.mv_line').data('line-id'); + this.trigger_up('remove_proposition', {'data': moveLineId}); }, /** * @private diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 9f6ec95142a..e73a42c785b 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -48,6 +48,16 @@

Congrats, you're all done!

You reconciled transactions in .
That's on average seconds per transaction.

+ +

+ + + + + + +

+
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 79841262c4f..0eeb805112c 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -9,6 +9,9 @@ var data = { image: {string: "image", type: 'integer'}, customer: {string: "customer", type: 'boolean'}, supplier: {string: "supplier", type: 'boolean'}, + parent_id: {string: "Parent", type: 'boolean'}, + property_account_receivable_id: {string: 'Account receivable', type: 'many2one', relation: 'account.account'}, + property_account_payable_id: {string: 'Account payable', type: 'many2one', relation: 'account.account'}, }, records: [ {id: 1, display_name: "partner 1", image: 'AAA', customer: true}, @@ -16,7 +19,7 @@ var data = { {id: 3, display_name: "partner 3", image: 'CCC', customer: true}, {id: 4, display_name: "partner 4", image: 'DDD', customer: true}, {id: 8, display_name: "Agrolait", image: 'EEE', customer: true}, - {id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true}, + {id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true, property_account_receivable_id: 287, property_account_payable_id: 287}, ], mark_as_reconciled: function () { return $.when(); @@ -339,7 +342,10 @@ var mv_lines = { '[]': [], '[5,"",0,6]': [ {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0002", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 109, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_code': "101200", 'ref': "", 'already_paid': false}, - {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 112, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false} + {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 112, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false}, + {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0012", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 134, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false}, + {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 4,610.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 106, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 4,610.00", 'debit': 4610.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false}, + {'account_type': "payable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 10,000.00", 'partner_id': 12, 'account_name': "Account Payable", 'name': "BILL/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 114, 'credit': 10000.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 10,000.00", 'debit': 0.0, 'account_id': [284, "101110 Stock Valuation Account"], 'account_code': "111100", 'ref': "", 'already_paid': false} ], '[5,"b",0,6]': [ {'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 100.00", 'partner_id': 8, 'account_name': "Bank", 'name': "BNK1/2017/0003: CUST.IN/2017/0001", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 394, 'credit': 0.0, 'journal_id': "Bank", 'amount_str': "$ 100.00", 'debit': 100.0, 'account_code': "101401", 'ref': "", 'already_paid': true}, @@ -602,7 +608,19 @@ QUnit.module('account', { testUtils.intercept(clientAction, 'call_service', function (event) { assert.deepEqual(event.data.args[1].args, - [[5],[{partner_id: 8, counterpart_aml_dicts: [], payment_aml_ids: [109,112], new_aml_dicts: []}]], + [[5],[{partner_id: 8, counterpart_aml_dicts: [{ + "counterpart_aml_id": 109, + "credit": 650, + "debit": 0, + "name": "INV/2017/0002" + }, + { + "counterpart_aml_id": 112, + "credit": 525, + "debit": 0, + "name": "INV/2017/0003" + }], + payment_aml_ids: [], new_aml_dicts: []}]], "Should call process_reconciliations with ids"); }); @@ -625,13 +643,8 @@ QUnit.module('account', { [6], [{ partner_id: false, - counterpart_aml_dicts:[{ - name: "BNK1/2017/0002: SUPP.OUT/2017/0002", - debit: 32.58, - credit: 0, - counterpart_aml_id: 392 - }], - payment_aml_ids: [], + counterpart_aml_dicts:[], + payment_aml_ids: [392], new_aml_dicts: [] }] ], "should call process_reconciliations with partial reconcile values"); @@ -652,14 +665,14 @@ QUnit.module('account', { assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert"); widget.$('.accounting_view thead td:first').trigger('click'); widget.$('.match .cell_account_code:first').trigger('click'); - assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert"); + assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 0, "should not display the partial reconciliation alert"); assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button"); assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); widget.$('.accounting_view .cell_left .line_info_button').trigger('click'); - assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert"); - assert.ok(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should display the partial reconciliation information"); + assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 0, "should not display a partial reconciliation alert"); + assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information"); assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button"); - assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode"); + assert.strictEqual( widget.$el.data('mode'), "match", "should be inactive mode"); widget.$('button.btn-default:not(hidden)').trigger('click'); clientAction.destroy(); @@ -722,6 +735,29 @@ QUnit.module('account', { clientAction.destroy(); }); + QUnit.test('Reconciliation change partner', function (assert) { + assert.expect(4); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + }); + + clientAction.appendTo($('#qunit-fixture')); + var widget = clientAction.widgets[0]; + assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait"); + assert.strictEqual(widget.$('.match table tr').length, 2, "agrolait should have 2 propositions for reconciliation"); + + // Simulate changing partner + widget.$('.o_input_dropdown input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(Camptocamp)').trigger('mouseenter').trigger('click'); + clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'Camptocamp', id: 12}}, stopped: false}); + assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); + assert.strictEqual(widget.$('.match table tr').length, 3, "camptocamp should have 3 propositions for reconciliation"); + + clientAction.destroy(); + }); QUnit.test('Reconciliation create line', function (assert) { assert.expect(23); diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 0a763810604..85fb49924f6 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -615,7 +615,7 @@ class Import(models.TransientModel): for num, line in enumerate(data): if line[index]: try: - line[index] = dt.strftime(dt.strptime(ustr(line[index]).encode('utf-8'), user_format), server_format) + line[index] = dt.strftime(dt.strptime(ustr(line[index].strip()).encode('utf-8'), user_format), server_format) except ValueError as e: raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e)) except Exception as e: diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js index 52e213ddb7d..d266147bcc7 100644 --- a/addons/mail/static/src/js/many2many_tags_email.js +++ b/addons/mail/static/src/js/many2many_tags_email.js @@ -1,52 +1,130 @@ odoo.define('mail.many2manytags', function (require) { "use strict"; -// FIXME: apply https://github.com/odoo/odoo/commit/1217ae914b313df7fe8511c138871e585bb21c81 - +var BasicModel = require('web.BasicModel'); var core = require('web.core'); var form_common = require('web.view_dialogs'); var field_registry = require('web.field_registry'); var relational_fields = require('web.relational_fields'); +var M2MTags = relational_fields.FieldMany2ManyTags; var _t = core._t; -var FieldMany2ManyTags = relational_fields.FieldMany2ManyTags; +BasicModel.include({ -var FieldMany2ManyTagsEmail = FieldMany2ManyTags.extend({ - init: function() { - this.values_checking = []; - this._super.apply(this, arguments); - }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- - add_id: function (id) { + /** + * @private + * @param {Object} record - an element from the localData + * @param {string} fieldName + * @return {Deferred} the deferred is resolved with the + * invalidPartnerIds + */ + _setInvalidMany2ManyTagsEmail: function (record, fieldName) { var self = this; - var _super = this._super.bind(this); - // check partner has email - this.trigger_up('perform_model_rpc', { - model: 'res.partner', - method: 'search', - args: [[ - ["id", "=", id], - ["email", "=", false], - ]], - on_success: function(partner_id) { - if (partner_id.length) { - // invalid partner - var pop = new form_common.FormViewDialog(self, { - res_model: 'res.partner', - res_id: partner_id[0], - title: _t("Please complete partner's informations and Email"), - }).open(); - pop.on('write_completed', self, function () { - // self.values_checking = _.without(self.values_checking, id); - _super.apply(self, [id]); - }); - } else { - // valid partner - _super.apply(self, [id]); - } + var localID = (record._changes && fieldName in record._changes) ? + record._changes[fieldName] : + record.data[fieldName]; + var list = this.localData[localID]; + var invalidPartnerIds = []; + _.each(list.data, function (id) { + var record = self.localData[id]; + if (!record.data.email) { + invalidPartnerIds.push(record); } }); + var def; + if (invalidPartnerIds) { + // remove invalid partners + var changes = {operation: 'REMOVE', ids: _.pluck(invalidPartnerIds, 'id')}; + def = this._applyX2ManyChange(record, fieldName, changes); + } + return $.when(def).then(function () { + return $.when({ + invalidPartnerIds: _.pluck(invalidPartnerIds, 'res_id'), + }); + }); + }, +}); + +var FieldMany2ManyTagsEmail = M2MTags.extend({ + fieldsToFetch: _.extend({}, M2MTags.prototype.fieldsToFetch, { + email: {type: 'char'}, + }), + specialData: "_setInvalidMany2ManyTagsEmail", + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Open a popup for each invalid partners (without email) to fill the email. + * + * @private + * @returns {Deferred} + */ + _checkEmailPopup: function () { + var self = this; + + var popupDefs = []; + var validPartners = []; + + // propose the user to correct invalid partners + _.each(this.record.specialData[this.name].invalidPartnerIds, function (resID) { + var def = $.Deferred(); + popupDefs.push(def); + + var pop = new form_common.FormViewDialog(self, { + res_model: self.field.relation, + res_id: resID, + title: _t("Please complete partner's informations and Email"), + on_saved: function (record) { + if (record.data.email) { + validPartners.push(record.res_id); + } + }, + }).open(); + pop.on('closed', self, function () { + def.resolve(); + }); + }); + return $.when.apply($, popupDefs).then(function() { + // All popups have been processed for the given ids + // It is now time to set the final value with valid partners ids. + validPartners = _.uniq(validPartners); + if (validPartners.length) { + var values = _.map(validPartners, function (id) { + return {id: id}; + }); + self._setValue({ + operation: 'ADD_M2M', + ids: values, + }); + } + }); + }, + /** + * Override to check if all many2many values have an email set before + * rendering the widget. + * + * @override + * @private + */ + _render: function () { + var self = this; + var def = $.Deferred(); + var _super = this._super.bind(this); + if (this.record.specialData[this.name].invalidPartnerIds.length) { + def = this._checkEmailPopup(); + } else { + def.resolve(); + } + return def.then(function () { + return _super.apply(self, arguments); + }); }, }); diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js index 481dc49af36..0d7635cae8b 100644 --- a/addons/mail/static/src/js/thread_field.js +++ b/addons/mail/static/src/js/thread_field.js @@ -21,7 +21,7 @@ var ThreadField = AbstractField.extend(chat_mixin, { this.msgIDs = this.value.res_ids; }, willStart: function () { - return this._chatReady(); + return this.alive(this._chatReady()); }, start: function () { var self = this; @@ -43,7 +43,7 @@ var ThreadField = AbstractField.extend(chat_mixin, { var def1 = this.thread.appendTo(this.$el); var def2 = this._super.apply(this, arguments); - return $.when(def1, def2).then(function () { + return this.alive($.when(def1, def2)).then(function () { // unwrap the thread to remove an unnecessary level on div self.setElement(self.thread.$el); diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 2bb5742e378..f268851463d 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -11,7 +11,9 @@ var testUtils = require('web.test_utils'); var BasicComposer = Composers.BasicComposer; +var createAsyncView = testUtils.createAsyncView; var createView = testUtils.createView; +var createAsyncView = testUtils.createAsyncView; QUnit.module('mail', {}, function () { @@ -862,5 +864,150 @@ QUnit.test('followers widget: do not display follower duplications', function (a form.destroy(); }); +QUnit.test('does not render and crash when destroyed before chat system is ready', function (assert) { + assert.expect(0); + + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '
', + res_id: 2, + mockRPC: function (route, args) { + if (route === '/mail/read_followers') { + return $.when({ + followers: [], + subtypes: [], + }); + } + return this._super(route, args); + }, + intercepts: { + chat_manager_ready: function (event) { + // we delay the return of the chat_manager ready event + event.data.callback(def); + }, + get_messages: function (event) { + event.stopPropagation(); + event.data.callback($.when([{ + attachment_ids: [], + body: "", + date: moment("2016-12-20 09:35:40"), + id: 34, + res_id: 3, + author_id: ["3", "Fu Ck Mil Grom"], + }])); + }, + get_bus: function (event) { + event.stopPropagation(); + event.data.callback(new Bus()); + }, + get_session: function (event) { + event.stopPropagation(); + event.data.callback({uid: 1}); + }, + }, + }); + + form.destroy(); + // here, the chat_manager system is ready, and the chatter can try to render + // itself. We simply make sure here that no crashes occur (since the form + // view is destroyed, all rpcs will be dropped, and many other mechanisms + // relying on events will not work, such as _getBus) + def.resolve(); +}); + +QUnit.module('FieldMany2ManyTagsEmail', { + beforeEach: function () { + this.data = { + partner: { + fields: { + display_name: { string: "Displayed name", type: "char" }, + timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'}, + }, + records: [{ + id: 1, + display_name: "first record", + timmy: [], + }], + }, + partner_type: { + fields: { + name: {string: "Partner Type", type: "char"}, + email: {string: "Email", type: "char"}, + }, + records: [ + {id: 12, display_name: "gold", email: 'coucou@petite.perruche'}, + {id: 14, display_name: "silver", email: ''}, + ] + }, + }; + }, +}); + +QUnit.test('fieldmany2many tags email', function (assert) { + assert.expect(11); + var done = assert.async(); + + this.data.partner.records[0].timmy = [12, 14]; + + // the modals need to be closed before the form view rendering + createAsyncView({ + View: FormView, + model: 'partner', + data: this.data, + res_id: 1, + arch:'
' + + '' + + '' + + '' + + '' + + '
', + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + if (args.method ==='read' && args.model === 'partner_type') { + assert.step(args.args[0]); + assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email"); + } + return this._super.apply(this, arguments); + }, + archs: { + 'partner_type,false,form': '
', + }, + }).then(function (form) { + // should read it 3 times (1 with the form view, one with the form dialog and one after save) + assert.verifySteps([[12, 14], [14], [14]]); + assert.strictEqual(form.$('.o_field_many2manytags[name="timmy"] span.o_tag_color_10').length, 2, + "the second tag should be present"); + + form.destroy(); + done(); + }); + + assert.strictEqual($('.modal-body.o_act_window').length, 1, + "there should be one modal opened to edit the empty email"); + assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver", + "the opened modal should be a form view dialog with the partner_type 14"); + assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1, + "there should be an email field in the modal"); + + // set the email and save the modal (will render the form view) + $('.modal-body.o_act_window input[name="email"]').val('coucou@petite.perruche').trigger('input'); + $('.modal-footer .btn-primary').click(); +}); + }); }); diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 7142cb5967c..44993bdc907 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -559,7 +559,7 @@ class MassMailing(models.Model): def retry_failed_mail(self): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() - failed_mails.unlink() + failed_mails.sudo().unlink() self.write({'state': 'in_queue'}) #------------------------------------------------------ diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py index 49d87c2fe3c..58226b2e9e8 100644 --- a/addons/mrp/models/mrp_workcenter.py +++ b/addons/mrp/models/mrp_workcenter.py @@ -16,7 +16,7 @@ class MrpWorkcenter(models.Model): # resource name = fields.Char(related='resource_id.name', store=True) - time_efficiency = fields.Float('Time Efficiency', related='resource_id.time_efficiency', store=True) + time_efficiency = fields.Float('Time Efficiency', related='resource_id.time_efficiency', default=100, store=True) active = fields.Boolean('Active', related='resource_id.active', default=True, store=True) code = fields.Char('Code', copy=False) diff --git a/addons/point_of_sale/i18n/point_of_sale.pot b/addons/point_of_sale/i18n/point_of_sale.pot index 77c4a996ec8..0d7ea224acf 100644 --- a/addons/point_of_sale/i18n/point_of_sale.pot +++ b/addons/point_of_sale/i18n/point_of_sale.pot @@ -3512,6 +3512,18 @@ msgid "You have to select a pricelist in the sale form !\n" "Please set one before choosing a product." msgstr "" +#. module: point_of_sale +#: code:addons/point_of_sale/wizard/pos_details.py:48 +#, python-format +msgid "You have to set a logo for your company." +msgstr "" + +#. module: point_of_sale +#: code:addons/point_of_sale/wizard/pos_details.py:50 +#, python-format +msgid "You have to set your reports's header and footer layout." +msgstr "" + #. module: point_of_sale #: code:addons/point_of_sale/models/pos_session.py:24 #, python-format diff --git a/addons/point_of_sale/wizard/pos_details.py b/addons/point_of_sale/wizard/pos_details.py index c4691daec3a..1e8fed01a25 100644 --- a/addons/point_of_sale/wizard/pos_details.py +++ b/addons/point_of_sale/wizard/pos_details.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -44,5 +44,9 @@ class PosDetails(models.TransientModel): @api.multi def generate_report(self): + if (not self.env.user.company_id.logo): + raise UserError(_("You have to set a logo or a layout for your company.")) + elif (not self.env.user.company_id.external_report_layout): + raise UserError(_("You have to set your reports's header and footer layout.")) data = {'date_start': self.start_date, 'date_stop': self.end_date, 'config_ids': self.pos_config_ids.ids} return self.env.ref('point_of_sale.sale_details_report').report_action([], data=data) diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 49ea8241f95..41e9b79110b 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -108,7 +108,7 @@ class Pricelist(models.Model): """ self.ensure_one() if not date: - date = self._context.get('date', fields.Date.today()) + date = self._context.get('date') or fields.Date.today() if not uom_id and self._context.get('uom'): uom_id = self._context['uom'] if uom_id: diff --git a/addons/resource/i18n/resource.pot b/addons/resource/i18n/resource.pot index 30b510ade92..aa093e5292e 100644 --- a/addons/resource/i18n/resource.pot +++ b/addons/resource/i18n/resource.pot @@ -441,6 +441,12 @@ msgstr "" msgid "This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes." msgstr "" +#. module: resource +#: code:addons/resource/models/resource.py:667 +#, python-format +msgid "The efficiency factor cannot be equal to 0." +msgstr "" + #. module: resource #: selection:resource.calendar.attendance,dayofweek:0 msgid "Thursday" diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 7c86cfd232c..cd26abaabc8 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -673,6 +673,13 @@ class ResourceResource(models.Model): ('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'), ] + @api.multi + @api.constrains('time_efficiency') + def _check_time_efficiency(self): + for record in self: + if record.time_efficiency == 0: + raise ValidationError(_('The efficiency factor cannot be equal to 0.')) + @api.model def create(self, values): if values.get('company_id') and not values.get('calendar_id'): diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index a22b6e7c42c..4ff43dd1b35 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -923,6 +923,21 @@ class SaleOrderLine(models.Model): uom=self.product_uom.id ) + result = {'domain': domain} + + title = False + message = False + warning = {} + if product.sale_line_warn != 'no-message': + title = _("Warning for %s") % product.name + message = product.sale_line_warn_msg + warning['title'] = title + warning['message'] = message + result = {'warning': warning} + if product.sale_line_warn == 'block': + self.product_id = False + return result + name = product.name_get()[0][1] if product.description_sale: name += '\n' + product.description_sale @@ -934,22 +949,11 @@ class SaleOrderLine(models.Model): vals['price_unit'] = self.env['account.tax']._fix_tax_included_price(self._get_display_price(product), product.taxes_id, self.tax_id) self.update(vals) - title = False - message = False - warning = {} - if product.sale_line_warn != 'no-message': - title = _("Warning for %s") % product.name - message = product.sale_line_warn_msg - warning['title'] = title - warning['message'] = message - if product.sale_line_warn == 'block': - self.product_id = False - return {'warning': warning} - return {'domain': domain} + return result @api.onchange('product_uom', 'product_uom_qty') def product_uom_change(self): - if not self.product_uom: + if not self.product_uom or not self.product_id: self.price_unit = 0.0 return if self.order_id.pricelist_id and self.order_id.partner_id: diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py index 502409b7144..9b3932ef5fb 100644 --- a/addons/sale_timesheet/tests/__init__.py +++ b/addons/sale_timesheet/tests/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -#from . import test_sale_timesheet -#from . import test_sale_service +from . import test_sale_timesheet +from . import test_sale_service from . import test_timesheet_revenue diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 9f05f2d3861..fc2bcdc5b53 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -315,8 +315,8 @@ class StockMove(models.Model): 'product_uom_id': self.product_id.uom_id.id, 'ref': self.picking_id.name, 'partner_id': partner_id, - 'debit': debit_value, - 'credit': 0, + 'debit': debit_value if debit_value > 0 else 0, + 'credit': -debit_value if debit_value < 0 else 0, 'account_id': debit_account_id, } credit_line_vals = { @@ -326,8 +326,8 @@ class StockMove(models.Model): 'product_uom_id': self.product_id.uom_id.id, 'ref': self.picking_id.name, 'partner_id': partner_id, - 'credit': credit_value, - 'debit': 0, + 'credit': credit_value if credit_value > 0 else 0, + 'debit': -credit_value if credit_value < 0 else 0, 'account_id': credit_account_id, } res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)] diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index 280a90cec8d..5d4a44ba420 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -420,16 +420,20 @@ var ActionManager = Widget.extend({ return this.inner_widget; }, history_back: function() { - var nb_views = this.inner_action.get_nb_views(); - if (nb_views > 1) { + var nbViews = this.inner_action.get_nb_views(); + if (nbViews > 1) { // Stay on this action, but select the previous view - return this.select_action(this.inner_action, nb_views - 2); + return this.select_action(this.inner_action, nbViews - 2); } - if (this.action_stack.length > 1) { + var nbActions = this.action_stack.length; + if (nbActions > 1) { // Select the previous action - var action = this.action_stack[this.action_stack.length - 2]; - nb_views = action.get_nb_views(); - return this.select_action(action, nb_views - 1); + var action = this.action_stack[nbActions - 2]; + nbViews = action.get_nb_views(); + return this.select_action(action, nbViews - 1); + } + else if (nbActions === 1 && nbViews === 1) { + return this.select_action(this.action_stack[0], 0); } return $.Deferred().reject(); }, diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js index 14ed4bdebc7..9b431d3e57a 100644 --- a/addons/web/static/src/js/chrome/view_manager.js +++ b/addons/web/static/src/js/chrome/view_manager.js @@ -561,7 +561,8 @@ var ViewManager = Widget.extend(ControlPanelMixin, { var self = this; var result_handler = on_closed || function () {}; var context = new Context(this.env.context, action_data.context || {}); - var record_id = res_ids && res_ids[0]; + // OR NULL hereunder: pyeval waits specifically for a null value, different from undefined + var record_id = res_ids && res_ids[0] || null; // response handler var handler = function (action) { @@ -651,18 +652,10 @@ var ViewManager = Widget.extend(ControlPanelMixin, { * will call this very view manager to activate the previous view. * @todo: directly switch to previous view * - * A special case is done in the case that there is no previous view. When - * that happens, we simply rerender the current view. - * * @private */ _onSwitchToPreviousView: function () { - if (this.view_stack.length === 1) { - var currentView = this.view_stack[0].controller; - currentView.update({}, {reload: false}); - } else { - this.do_action('history_back'); - } + this.trigger_up('history_back'); } }); diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 567c54abcb1..14512c02912 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -485,8 +485,16 @@ var FieldMany2One = AbstractField.extend({ }, /** * @private + * + * @param {OdooEvent} ev */ - _onInputKeyup: function () { + _onInputKeyup: function (ev) { + if (ev.which === $.ui.keyCode.ENTER) { + // If we pressed enter, we want to prevent _onInputFocusout from + // executing since it would open a M2O dialog to request + // confirmation that the many2one is not properly set. + return; + } if (this.$input.val() === "") { this.reinitialize(false); } else if (this._getDisplayName(this.m2o_value) !== this.$input.val()) { @@ -1909,7 +1917,7 @@ var FieldRadio = FieldSelection.extend({ this._super.apply(this, arguments); if (this.mode === 'edit') { this.tagName = 'div'; - this.className += this.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical'; + this.className += this.nodeOptions.horizontal ? ' o_horizontal' : ' o_vertical'; } this.unique_id = _.uniqueId("radio"); this._setValues(); diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 82dc6f90d26..cc72816a85d 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -250,7 +250,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { on_fail: function (reason) { reload().always(function() { def.reject(reason); - }) + }); }, on_success: def.resolve.bind(def), }); 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 906464a5a26..c562fa388f0 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1187,7 +1187,8 @@ var BasicModel = AbstractModel.extend({ } var self = this; - var list = this.localData[record._changes[fieldName] || record.data[fieldName]]; + var localID = (record._changes && record._changes[fieldName]) || record.data[fieldName]; + var list = this.localData[localID]; var field = record.fields[fieldName]; var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index d4d3a6307f9..aacbb744252 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -30,6 +30,7 @@ QUnit.module('relational_fields', { type: "selection", selection: [['red', "Red"], ['black', "Black"]], default: 'red', + string: "Color", }, date: {string: "Some Date", type: "date"}, datetime: {string: "Datetime Field", type: 'datetime'}, @@ -1140,6 +1141,52 @@ QUnit.module('relational_fields', { }); }); + QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should not trigger M2ODialog', function (assert) { + var done = assert.async(); + assert.expect(1); + + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + archs: { + 'partner,false,form': '
', + }, + }); + + var $input = form.$('.o_field_many2one input'); + $input.val("Something that does not exist").trigger('input'); + $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter'); + concurrency.delay(0).then(function() { + $input.trigger($.Event('keydown', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + $input.trigger($.Event('keypress', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + $input.trigger($.Event('keyup', { + which: $.ui.keyCode.ENTER, + keyCode: $.ui.keyCode.ENTER, + })); + concurrency.delay(0).then(function() { + $input.blur(); + assert.strictEqual($('.modal').length, 1, + "should have one modal in body"); + form.destroy(); + done(); + }); + }); + }); + QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', function (assert) { assert.expect(6); var done = assert.async(); @@ -1472,11 +1519,11 @@ QUnit.module('relational_fields', { }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", - "should have the 3 rows in the correct order") + "should have the 3 rows in the correct order"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", - "should still have the 3 rows in the correct order") + "should still have the 3 rows in the correct order"); // Drag and drop the fourth line in second position testUtils.dragAndDrop( @@ -1489,11 +1536,11 @@ QUnit.module('relational_fields', { "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", - "should have the 3 rows in the new order") + "should have the 3 rows in the new order"); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", - "should still have the 3 rows in the new order") + "should still have the 3 rows in the new order"); form.destroy(); }); @@ -6412,6 +6459,31 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('fieldradio widget has o_horizontal or o_vertical class', function (assert) { + assert.expect(2); + + this.data.partner.fields.color2 = this.data.partner.fields.color; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + }); + + var btn1 = form.$('div.o_field_radio.o_vertical'); + var btn2 = form.$('div.o_field_radio.o_horizontal'); + + assert.strictEqual(btn1.length, 1, "should have o_vertical class"); + assert.strictEqual(btn2.length, 1, "should have o_horizontal class"); + form.destroy(); + }); + QUnit.test('fieldradio widget with numerical keys encoded as strings', function (assert) { assert.expect(5); diff --git a/addons/website/static/src/js/website.backendDashboard.js b/addons/website/static/src/js/website.backendDashboard.js index f1dcdf6a6ec..16b1281f222 100644 --- a/addons/website/static/src/js/website.backendDashboard.js +++ b/addons/website/static/src/js/website.backendDashboard.js @@ -59,8 +59,8 @@ var Dashboard = Widget.extend(ControlPanelMixin, { fetch_data: function() { var self = this; return ajax.jsonRpc('/website/fetch_dashboard_data', 'call', { - 'date_from': this.date_from.format('YYYY-MM-DD'), - 'date_to': this.date_to.format('YYYY-MM-DD'), + 'date_from': this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(), + 'date_to': this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(), }).done(function(result) { self.data = result; self.dashboards_data = result.dashboards; diff --git a/addons/website_crm_partner_assign/report/crm_lead_report.py b/addons/website_crm_partner_assign/report/crm_lead_report.py index e72a1b3dedf..07676117473 100644 --- a/addons/website_crm_partner_assign/report/crm_lead_report.py +++ b/addons/website_crm_partner_assign/report/crm_lead_report.py @@ -67,7 +67,7 @@ class CrmLeadReportAssign(models.Model): p.grade_id, p.date as partner_date, c.planned_revenue*(c.probability/100) as probable_revenue, - 1 as nbr, + 1 as nbr_cases, c.create_date as create_date, extract('epoch' from (c.write_date-c.create_date))/(3600*24) as delay_close, extract('epoch' from (c.date_deadline - c.date_closed))/(3600*24) as delay_expected, diff --git a/doc/howtos/backend/exercise-dashboard b/doc/howtos/backend/exercise-dashboard index eda27131096..d876e37c12e 100644 --- a/doc/howtos/backend/exercise-dashboard +++ b/doc/howtos/backend/exercise-dashboard @@ -9,12 +9,13 @@ Index: addons/openacademy/__manifest__.py 'version': '0.1', # any module necessary for this one to work correctly -- 'depends': ['base'], -+ 'depends': ['base', 'board'], +- 'depends': ['base', 'report'], ++ 'depends': ['base', 'report', 'board'], # always loaded 'data': [ -@@ -31,6 +31,7 @@ +@@ -30,6 +30,7 @@ + 'templates.xml', 'views/openacademy.xml', 'views/partner.xml', + 'views/session_board.xml', diff --git a/doc/howtos/backend/exercise-report b/doc/howtos/backend/exercise-report index 2f8b5dfe5e8..4492eb0f087 100644 --- a/doc/howtos/backend/exercise-report +++ b/doc/howtos/backend/exercise-report @@ -1,11 +1,24 @@ # HG changeset patch # Parent c140f0a861a08881d8737bca0ffb83904a2059a3 +Index: addons/openacademy/__manifest__.py +=================================================================== +--- addons.orig/openacademy/__manifest__.py 2014-08-26 17:26:21.535783052 +0200 ++++ addons/openacademy/__manifest__.py 2014-08-26 17:26:21.531783052 +0200 +@@ -21,7 +21,7 @@ + 'version': '0.1', + + # any module necessary for this one to work correctly +- 'depends': ['base'], ++ 'depends': ['base', 'report'], + + # always loaded + 'data': [ Index: addons/openacademy/__manifest__.py =================================================================== --- addons.orig/openacademy/__manifest__.py 2014-08-29 08:39:43.975536806 +0200 +++ addons/openacademy/__manifest__.py 2014-08-29 08:39:52.000000000 +0200 -@@ -31,6 +31,7 @@ +@@ -30,6 +30,7 @@ 'templates.xml', 'views/openacademy.xml', 'views/partner.xml', diff --git a/doc/howtos/backend/exercise-state-basic b/doc/howtos/backend/exercise-state-basic deleted file mode 100644 index f70a41b4dae..00000000000 --- a/doc/howtos/backend/exercise-state-basic +++ /dev/null @@ -1,55 +0,0 @@ -# HG changeset patch -# Parent 8c721171aa16a41e94059f53d6780c67b5ef2dfc -# Parent 8d2ca42b5be2031ea9624896df53f09f7ca131be - -diff --git a/openacademy/models.py b/openacademy/models.py ---- a/openacademy/models.py -+++ b/openacademy/models.py -@@ -66,6 +66,24 @@ class Session(models.Model): - attendees_count = fields.Integer( - string="Attendees count", compute='_get_attendees_count', store=True) - -+ state = fields.Selection([ -+ ('draft', "Draft"), -+ ('confirmed', "Confirmed"), -+ ('done', "Done"), -+ ], default='draft') -+ -+ @api.multi -+ def action_draft(self): -+ self.state = 'draft' -+ -+ @api.multi -+ def action_confirm(self): -+ self.state = 'confirmed' -+ -+ @api.multi -+ def action_done(self): -+ self.state = 'done' -+ - @api.depends('seats', 'attendee_ids') - def _taken_seats(self): - for r in self: -diff --git a/openacademy/views/openacademy.xml b/openacademy/views/openacademy.xml ---- a/openacademy/views/openacademy.xml -+++ b/openacademy/views/openacademy.xml -@@ -96,6 +96,19 @@ - openacademy.session - -
-+
-+
-+ - - - diff --git a/doc/howtos/backend/exercise-translations b/doc/howtos/backend/exercise-translations index 8ade655253d..e7dfe53ebb7 100644 --- a/doc/howtos/backend/exercise-translations +++ b/doc/howtos/backend/exercise-translations @@ -28,7 +28,7 @@ diff --git a/openacademy/models.py b/openacademy/models.py default['name'] = new_name return super(Course, self).copy(default) -@@ -97,15 +97,15 @@ class Session(models.Model): +@@ -79,15 +79,15 @@ class Session(models.Model): if self.seats < 0: return { 'warning': { @@ -48,7 +48,7 @@ diff --git a/openacademy/models.py b/openacademy/models.py }, } -@@ -151,4 +151,4 @@ class Session(models.Model): +@@ -133,4 +133,4 @@ class Session(models.Model): def _check_instructor_not_in_attendees(self): for r in self: if r.instructor_id and r.instructor_id in r.attendee_ids: diff --git a/doc/howtos/backend/exercise-wizard-action b/doc/howtos/backend/exercise-wizard-action index 3bc20624ee9..23d6a0f2667 100644 --- a/doc/howtos/backend/exercise-wizard-action +++ b/doc/howtos/backend/exercise-wizard-action @@ -2,7 +2,7 @@ Index: addons/openacademy/views/openacademy.xml =================================================================== --- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:19.255004611 +0200 +++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:19.251004612 +0200 -@@ -248,6 +248,12 @@ +@@ -234,6 +234,12 @@ diff --git a/doc/howtos/backend/exercise-wizard-launch b/doc/howtos/backend/exercise-wizard-launch index 49b1c09191e..7b5d65c8074 100644 --- a/doc/howtos/backend/exercise-wizard-launch +++ b/doc/howtos/backend/exercise-wizard-launch @@ -17,7 +17,7 @@ Index: addons/openacademy/views/openacademy.xml =================================================================== --- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:07.879004780 +0200 +++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:07.871004780 +0200 -@@ -237,5 +237,26 @@ +@@ -224,5 +224,26 @@ diff --git a/doc/howtos/backend/exercise-wizard-multi b/doc/howtos/backend/exercise-wizard-multi index dc99dba2e31..f8ece675090 100644 --- a/doc/howtos/backend/exercise-wizard-multi +++ b/doc/howtos/backend/exercise-wizard-multi @@ -2,7 +2,7 @@ Index: addons/openacademy/views/openacademy.xml =================================================================== --- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:28.583004473 +0200 +++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:28.579004473 +0200 -@@ -245,7 +245,7 @@ +@@ -231,7 +231,7 @@ diff --git a/doc/howtos/backend/series b/doc/howtos/backend/series index 36627e4eadd..e48d05914e9 100644 --- a/doc/howtos/backend/series +++ b/doc/howtos/backend/series @@ -24,7 +24,6 @@ exercise-searchview exercise-gantt exercise-graph exercise-kanban -exercise-state-basic exercise-access-rights exercise-access-rules exercise-wizard diff --git a/doc/setup/install.rst b/doc/setup/install.rst index afc9b0c8f14..8e25e5e3ded 100644 --- a/doc/setup/install.rst +++ b/doc/setup/install.rst @@ -546,7 +546,7 @@ default db to serve on localhost:8069 http://www.enterprisedb.com/products-services-training/pgdownload .. _Quilt: http://en.wikipedia.org/wiki/Quilt_(software) .. _saas: https://www.odoo.com/page/start -.. _the wkhtmltopdf download page: http://wkhtmltopdf.org/downloads.html +.. _the wkhtmltopdf download page: https://github.com/wkhtmltopdf/wkhtmltopdf/releases/tag/0.12.1 .. _UAC: http://en.wikipedia.org/wiki/User_Account_Control .. _wkhtmltopdf: http://wkhtmltopdf.org .. _pip: https://pip.pypa.io