diff --git a/MANIFEST.in b/MANIFEST.in index 46e071c97c8..d4e3af91152 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include README.md recursive-include odoo *.css recursive-include odoo *.csv recursive-include odoo *.doc +recursive-include odoo *.docx recursive-include odoo *.eml recursive-include odoo *.eot recursive-include odoo *.gif @@ -16,6 +17,8 @@ recursive-include odoo *.less recursive-include odoo *.md recursive-include odoo *.mp3 recursive-include odoo *.ogg +recursive-include odoo *.ods +recursive-include odoo *.odt recursive-include odoo *.otf recursive-include odoo *.pdf recursive-include odoo *.png @@ -35,8 +38,10 @@ recursive-include odoo *.wsdl recursive-include odoo *.xls recursive-include odoo *.xsd recursive-include odoo *.xsl +recursive-include odoo *.xlsx recursive-include odoo *.xml recursive-include odoo *.yml +recursive-include odoo *.zip recursive-include odoo/addons/l10n_mx_edi *.xslt *.key *.cer *.txt recursive-include odoo/addons/l10n_mx_reports *.xslt recursive-exclude * *.py[co] diff --git a/addons/account/data/payment_receipt_data.xml b/addons/account/data/payment_receipt_data.xml index 3190edfb151..d3c02413732 100644 --- a/addons/account/data/payment_receipt_data.xml +++ b/addons/account/data/payment_receipt_data.xml @@ -14,7 +14,7 @@ ${object.partner_id.lang}

Dear ${object.partner_id.name},

-

Thank you for your payment.
Here is your payment receipt ${(object.name or '').replace('/','-')} amounting to ${object.amount} ${object.currency_id.name} from ${object.company_id.name}.

+

Thank you for your payment.
Here is your payment receipt ${(object.name or '').replace('/','-')} amounting to ${format_amount(object.amount, object.currency_id)} from ${object.company_id.name}.

If you have any questions, please do not hesitate to contact us.

Best regards, % if user and user.signature: diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 048b5d7e937..baa0aed666e 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -443,7 +443,7 @@ class AccountJournal(models.Model): for journal in self: if journal.sequence_id and journal.sequence_number_next: sequence = journal.sequence_id._get_current_sequence() - sequence.number_next = journal.sequence_number_next + sequence.sudo().number_next = journal.sequence_number_next @api.multi # do not depend on 'refund_sequence_id.date_range_ids', because @@ -525,6 +525,8 @@ class AccountJournal(models.Model): if ('company_id' in vals and journal.company_id.id != vals['company_id']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) + if self.bank_account_id: + self.bank_account_id.company_id = vals['company_id'] if ('code' in vals and journal.code != vals['code']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its short name.')) @@ -538,6 +540,8 @@ class AccountJournal(models.Model): self.default_debit_account_id.currency_id = vals['currency_id'] if not 'default_credit_account_id' in vals and self.default_credit_account_id: self.default_credit_account_id.currency_id = vals['currency_id'] + if self.bank_account_id: + self.bank_account_id.currency_id = vals['currency_id'] if 'bank_account_id' in vals and not vals.get('bank_account_id'): raise UserError(_('You cannot remove the bank account from the journal once set.')) result = super(AccountJournal, self).write(vals) diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 1ed9b16669c..77823fbe8c8 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -340,7 +340,7 @@ class AccountBankStatement(models.Model): def link_bank_to_partner(self): for statement in self: for st_line in statement.line_ids: - if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id != st_line.partner_id: + if st_line.bank_account_id and st_line.partner_id and not st_line.bank_account_id.partner_id: st_line.bank_account_id.partner_id = st_line.partner_id diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index e66a6ab068e..a769a3f8d17 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -341,12 +341,13 @@ class account_journal(models.Model): ctx.update({ 'journal_type': self.type, 'default_journal_id': self.id, - 'search_default_journal_id': self.id, 'default_type': invoice_type, 'type': invoice_type }) [action] = self.env.ref('account.%s' % action_name).read() + if not self.env.context.get('use_domain'): + ctx['search_default_journal_id'] = self.id action['context'] = ctx action['domain'] = self._context.get('use_domain', []) account_invoice_filter = self.env.ref('account.view_account_invoice_filter', False) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8d0c8d27bcd..c477ca91d77 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -106,7 +106,7 @@ class AccountMove(models.Model): default=lambda self: self.env.user.company_id) matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") # Dummy Account field to search on account.move by account_id - dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False) + dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False, readonly=True) tax_cash_basis_rec_id = fields.Many2one( 'account.partial.reconcile', string='Tax Cash Basis Entry of', @@ -1361,7 +1361,7 @@ class AccountMoveLine(models.Model): record.payment_id.state = 'reconciled' result = super(AccountMoveLine, self).write(vals) - if self._context.get('check_move_validity', True): + if self._context.get('check_move_validity', True) and any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')): move_ids = set() for line in self: if line.move_id.id not in move_ids: diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index c750dbff7c6..711c62e91a6 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -576,9 +576,9 @@ class account_payment(models.Model): writeoff_line['amount_currency'] = amount_currency_wo writeoff_line['currency_id'] = currency_id writeoff_line = aml_obj.create(writeoff_line) - if counterpart_aml['debit'] or writeoff_line['credit']: + if counterpart_aml['debit'] or (writeoff_line['credit'] and not counterpart_aml['credit']): counterpart_aml['debit'] += credit_wo - debit_wo - if counterpart_aml['credit'] or writeoff_line['debit']: + if counterpart_aml['credit'] or (writeoff_line['debit'] and not counterpart_aml['debit']): counterpart_aml['credit'] += debit_wo - credit_wo counterpart_aml['amount_currency'] -= amount_currency_wo diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 60cba94fffc..9894dc6a9b4 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -526,17 +526,37 @@ var StatementModel = BasicModel.extend({ */ togglePartialReconcile: function (handle) { var line = this.getLine(handle); - var props = _.filter(line.reconciliation_proposition, {'invalid': false}); - var prop = props[0]; - if (props.length !== 1 || Math.abs(line.st_line.amount) >= Math.abs(prop.amount)) { + + // Retrieve the toggle proposition + var selected; + _.each(line.reconciliation_proposition, function (prop) { + if (!prop.invalid) { + if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && line.st_line.amount > 0 && line.st_line.amount < prop.amount) || + ((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && line.st_line.amount < 0 && line.st_line.amount > prop.amount)) { + selected = prop; + return false; + } + } + }); + + // If no toggled proposition found, reject it + if (selected == null) return $.Deferred().reject(); - } - prop.partial_reconcile = !prop.partial_reconcile; - if (!prop.partial_reconcile) { + + // Inverse partial_reconcile value + selected.partial_reconcile = !selected.partial_reconcile; + if (!selected.partial_reconcile) { return this._computeLine(line); } + + // Compute the write_off + var format_options = { currency_id: line.st_line.currency_id }; + selected.write_off_amount = selected.amount + line.balance.amount; + selected.write_off_amount_str = field_utils.format.monetary(Math.abs(selected.write_off_amount), {}, format_options); + selected.write_off_amount_str = selected.write_off_amount_str.replace(' ', ' '); + return this._computeLine(line).then(function () { - if (prop.partial_reconcile) { + if (selected.partial_reconcile) { line.balance.amount = 0; line.balance.type = 1; line.mode = 'inactive'; @@ -646,6 +666,7 @@ var StatementModel = BasicModel.extend({ 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). @@ -1078,8 +1099,9 @@ var StatementModel = BasicModel.extend({ // Do not forward port in master. @CSN will change this var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount; if (prop.partial_reconcile === true) { - amount = -line.st_line.amount; + amount = -prop.write_off_amount; } + var result = { name : prop.label, debit : amount > 0 ? amount : 0, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 731d70c58e3..e86b59ec88f 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -353,7 +353,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { // reconciliation_proposition var $props = this.$('.accounting_view tbody').empty(); - var props = _.filter(state.reconciliation_proposition, {'display': true}); + + // loop state propositions + var props = []; + var nb_debit_props = 0; + var nb_credit_props = 0; + _.each(state.reconciliation_proposition, function (prop) { + if (prop.display) { + props.push(prop); + if (prop.amount < 0) + nb_debit_props += 1; + else if (prop.amount > 0) + nb_credit_props += 1; + } + }); + _.each(props, function (line) { var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); if (!isNaN(line.id)) { @@ -361,18 +375,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { .appendTo($line.find('.cell_info_popover')) .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); } - - if ((state.balance.amount_currency !== 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)) - ) { + if (line.already_paid === false && + ((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1 + && line.amount > 0 && state.st_line.amount > 0 && state.st_line.amount < line.amount) || + ((state.balance.amount_currency > 0 || line.partial_reconcile) && nb_debit_props == 1 + && line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) { var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left'); var text; if (line.partial_reconcile) { text = _t("Undo the partial reconciliation."); - $cell.text(state.st_line.amount_str); + $cell.text(line.write_off_amount_str); } else { text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open."); } @@ -414,20 +426,24 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { this._renderCreate(state); } var data = this.model.get(this.handleCreateRecord).data; - - this.model.notifyChanges(this.handleCreateRecord, state.createForm); - this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}); - _.each(state.createForm.analytic_tag_ids, function (tag) { - self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}}); - }); - - var record = this.model.get(this.handleCreateRecord); - _.each(this.fields, function (field, fieldName) { - if (self._avoidFieldUpdate[fieldName]) return; - if (fieldName === "partner_id") return; - if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { - field.reset(record); - } + this.model.notifyChanges(this.handleCreateRecord, state.createForm).then(function () { + // FIXME can't it directly written REPLACE_WITH ids=state.createForm.analytic_tag_ids + this.model.notifyChanges(this.handleCreateRecord, {analytic_tag_ids: {operation: 'REPLACE_WITH', ids: []}}).then(function (){ + var defs = []; + _.each(state.createForm.analytic_tag_ids, function (tag) { + defs.push(self.model.notifyChanges(self.handleCreateRecord, {analytic_tag_ids: {operation: 'ADD_M2M', ids: tag}})); + }); + $.when.apply($, defs).then(function () { + var record = self.model.get(self.handleCreateRecord); + _.each(self.fields, function (field, fieldName) { + if (self._avoidFieldUpdate[fieldName]) return; + if (fieldName === "partner_id") return; + if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { + field.reset(record); + } + }); + }); + }); }); } this.$('.create .add_line').toggle(!!state.balance.amount_currency); @@ -639,7 +655,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { return; } if(event.keyCode === 13) { - if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) { + var created_lines = _.findWhere(this.model.lines, {mode: 'create'}); + if (created_lines && created_lines.balance.amount) { this._onCreateProposition(); } return; diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 5a3e6dba6c2..dc5ec8d95aa 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -34,17 +34,22 @@ var db = { fields: { id: {string: "ID", type: 'integer'}, code: {string: "code", type: 'integer'}, - display_name: {string: "Displayed name", type: 'char'}, + name: {string: "Displayed name", type: 'char'}, }, records: [ - {id: 282, code: 100000, display_name: "100000 Fixed Asset Account"}, - {id: 283, code: 101000, display_name: "101000 Current Assets"}, - {id: 284, code: 101110, display_name: "101110 Stock Valuation Account"}, - {id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)"}, - {id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)"}, - {id: 287, code: 101200, display_name: "101200 Account Receivable"}, - {id: 288, code: 101300, display_name: "101300 Tax Paid"}, - {id: 308, code: 101401, display_name: "101401 Bank"}, + {id: 282, code: 100000, name: "100000 Fixed Asset Account"}, + {id: 283, code: 101000, name: "101000 Current Assets"}, + {id: 284, code: 101110, name: "101110 Stock Valuation Account"}, + {id: 285, code: 101120, name: "101120 Stock Interim Account (Received)"}, + {id: 286, code: 101130, name: "101130 Stock Interim Account (Delivered)"}, + {id: 287, code: 101200, name: "101200 Account Receivable"}, + {id: 288, code: 101300, name: "101300 Tax Paid"}, + {id: 308, code: 101401, name: "101401 Bank"}, + {id: 500, code: 500, name: "500 Account"}, + {id: 501, code: 501, name: "501 Account"}, + {id: 502, code: 502, name: "502 Account"}, + {id: 503, code: 503, name: "503 Account"}, + {id: 504, code: 504, name: "504 Account"}, ], mark_as_reconciled: function () { return $.when(); @@ -776,13 +781,7 @@ QUnit.module('account', { partner_id: false, counterpart_aml_dicts:[], payment_aml_ids: [392], - new_aml_dicts: [ - { - "credit": 343.42, - "debit": 0, - "name": "Bank fees : Open balance" - } - ], + new_aml_dicts: [], }] ], "should call process_reconciliations with partial reconcile values"); } @@ -811,14 +810,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, 0, "should not display the partial reconciliation alert"); + assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should 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, 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.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert"); + assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_true'), "should 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'), "match", "should be inactive mode"); + assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode"); widget.$('button.btn-default:not(hidden)').trigger('click'); clientAction.destroy(); @@ -1036,13 +1035,77 @@ QUnit.module('account', { clientAction.destroy(); }); + QUnit.test('Reconciliation create line (many2one test)', function (assert) { + assert.expect(5); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + var def = $.Deferred(); + + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + archs: { + "account.account,false,list": '', + "account.account,false,search": '', + }, + mockRPC: function (route, args) { + if (args.method === 'name_get') { + return def.then(this._super.bind(this, route, args)); + } + return this._super(route, args); + }, + }); + + clientAction.prependTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + // open the first line in write-off mode + widget.$('.accounting_view tfoot td:first').trigger('click'); + + // select an account with the many2one (drop down) + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101200)').trigger('mouseenter').trigger('click'); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Display the selected account"); + assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "101200", "Display the code of the selected account"); + + // use the many2one select dialog to change the account + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(Search)').trigger('mouseenter').trigger('click'); + // select the account who does not appear in the drop drown + $('.modal tr.o_data_row:contains(502)').click(); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "101200 Account Receivable", "Selected account does not change"); + // wait the name_get to render the changes + def.resolve(); + assert.strictEqual(widget.$('.create .create_account_id input').val(), "502 Account", "Display the selected account"); + assert.strictEqual(widget.$('tbody:first .cell_account_code').text(), "502", "Display the code of the selected account"); + clientAction.destroy(); + }); + QUnit.test('Reconciliation create line with taxes', function (assert) { assert.expect(13); 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')); @@ -1054,26 +1117,26 @@ QUnit.module('account', { widget.$('.create .create_label input').val('test1').trigger('input'); widget.$('.create .create_amount input').val('1100').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.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(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines"); widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); - assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values"); 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(), "145.00", "should display 'Create Write-off' with 145.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Create Write-off' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); clientAction.destroy(); @@ -1085,7 +1148,7 @@ QUnit.module('account', { var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, + data: this.params.data, }); clientAction.appendTo($('#qunit-fixture')); diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index 3e0e1c8961d..b527efa6be3 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -361,3 +361,53 @@ class TestPayment(AccountingTestCase): {'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False}, {'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False}, ]) + + def test_payment_and_writeoff_in_other_currency(self): + # Use case: + # Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 8.65, 'credit': 0.0, 'amount_currency': 13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 25.0, 'amount_currency': -38.22, 'currency_id': self.currency_usd_id}, + ]) + # Use case: + # Company is in EUR, create a vendor bill for 25 EUR and register payment of 25 USD. + # Mark invoice as fully paid with a write_off + # Check that all the aml are correctly created. + invoice = self.create_invoice(amount=25, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id) + # register payment on invoice + payment = self.payment_model.create({'payment_type': 'inbound', + 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'supplier', + 'partner_id': self.partner_agrolait.id, + 'amount': 25, + 'currency_id': self.currency_usd_id, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'writeoff_account_id': self.account_payable.id, + 'journal_id': self.bank_journal_euro.id, + 'invoice_ids': [(4, invoice.id, None)] + }) + payment.post() + self.check_journal_items(payment.move_line_ids, [ + {'account_id': self.account_eur.id, 'debit': 16.35, 'credit': 0.0, 'amount_currency': 25.0, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id}, + {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id}, + ]) diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 114668fb14f..03ebd4fdaa9 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -537,10 +537,8 @@ - - - - + + @@ -574,33 +572,25 @@ + account.invoice.select.invoices account.invoice primary - - - Not Draft - - - + + account.invoice.select.credit.notes account.invoice primary - - - Not Draft - - - + @@ -671,7 +661,7 @@ [('type','=','out_invoice')] {'type':'out_invoice', 'journal_type': 'sale'} - +

Create a customer invoice @@ -706,7 +696,7 @@ [('type','=','out_refund')] {'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'} - +

Create a credit note @@ -744,7 +734,7 @@ [('type','=','in_invoice')] {'default_type': 'in_invoice', 'type': 'in_invoice', 'journal_type': 'purchase'} - +

Record a new vendor bill @@ -780,7 +770,7 @@ [('type','=','in_refund')] {'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'} - +

Create a vendor credit note diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index dfa08fbadce..9c7f1aca551 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -117,16 +117,12 @@ View

- - Invoices - Bills - + Invoices + Bills
- - Credit Notes - Credit Notes - + Credit Notes + Credit Notes
Payments Matching diff --git a/addons/account/views/account_report.xml b/addons/account/views/account_report.xml index 06e077e7d8c..2ba116e884a 100644 --- a/addons/account/views/account_report.xml +++ b/addons/account/views/account_report.xml @@ -56,6 +56,7 @@ id="action_report_financial" model="account.financial.report" string="Financial report" + menu="False" report_type="qweb-pdf" name="account.report_financial" file="account.report_financial" diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index dddaacd1ead..712bfe7b1f9 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -16,7 +16,7 @@ - + diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py index ac9c6e57be3..77f9b5fcad8 100644 --- a/addons/account_analytic_default/models/account_analytic_default.py +++ b/addons/account_analytic_default/models/account_analytic_default.py @@ -66,7 +66,7 @@ class AccountInvoiceLine(models.Model): @api.onchange('product_id') def _onchange_product_id(self): res = super(AccountInvoiceLine, self)._onchange_product_id() - rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.partner_id.id, self.env.uid, + rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid, fields.Date.today(), company_id=self.company_id.id) self.account_analytic_id = rec.analytic_id.id self.analytic_tag_ids = rec.analytic_tag_ids.ids @@ -75,7 +75,7 @@ class AccountInvoiceLine(models.Model): def _set_additional_fields(self, invoice): if not self.account_analytic_id or not self.analytic_tag_ids: rec = self.env['account.analytic.default'].account_get( - self.product_id.id, self.invoice_id.partner_id.id, self.env.uid, + self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid, fields.Date.today(), company_id=self.company_id.id) if rec: if self.account_analytic_id: diff --git a/addons/account_invoicing/__init__.py b/addons/account_invoicing/__init__.py index 91206cf6094..371975a409a 100644 --- a/addons/account_invoicing/__init__.py +++ b/addons/account_invoicing/__init__.py @@ -11,3 +11,12 @@ def uninstall_hook(cr, registry): ('module', '=', 'account') ]).mapped('res_id') env['ir.ui.menu'].browse(res_ids).update({'active': False}) + + +def post_init_hook(cr, registry): + env = Environment(cr, SUPERUSER_ID, {}) + res_ids = env['ir.model.data'].search([ + ('model', '=', 'ir.ui.menu'), + ('module', '=', 'account'), + ]).mapped('res_id') + env['ir.ui.menu'].browse(res_ids).update({'active': True}) diff --git a/addons/account_invoicing/__manifest__.py b/addons/account_invoicing/__manifest__.py index faa54037c69..880c5b899c3 100644 --- a/addons/account_invoicing/__manifest__.py +++ b/addons/account_invoicing/__manifest__.py @@ -28,4 +28,5 @@ You could use this simplified accounting in case you work with an (external) acc ], 'application': True, 'uninstall_hook': 'uninstall_hook', + 'post_init_hook': 'post_init_hook', } diff --git a/addons/auth_signup/models/__init__.py b/addons/auth_signup/models/__init__.py index 44dc1ee6b11..a25160266e7 100644 --- a/addons/auth_signup/models/__init__.py +++ b/addons/auth_signup/models/__init__.py @@ -4,3 +4,4 @@ from . import res_config_settings from . import ir_http from . import res_partner from . import res_users +from . import ir_model_fields diff --git a/addons/auth_signup/models/ir_model_fields.py b/addons/auth_signup/models/ir_model_fields.py new file mode 100644 index 00000000000..828b9de8346 --- /dev/null +++ b/addons/auth_signup/models/ir_model_fields.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details + +from odoo import models + + +# This is a nasty hack, targeted for V11 only +class IrModelFields(models.Model): + _inherit = 'ir.model.fields' + + def unlink(self): + # Prevent the deletion of some `shared` fields... -_- + self = self.filtered( + lambda rec: not ( + rec.model == 'res.config.settings' + and rec.name == 'auth_signup_uninvited' + ) + ) + return super(IrModelFields, self).unlink() diff --git a/addons/base_gengo/wizard/base_gengo_translations.py b/addons/base_gengo/wizard/base_gengo_translations.py index 43d2e81ca33..3b260dd5a61 100644 --- a/addons/base_gengo/wizard/base_gengo_translations.py +++ b/addons/base_gengo/wizard/base_gengo_translations.py @@ -63,7 +63,7 @@ class BaseGengoTranslations(models.TransientModel): 'view_type': 'form', 'view_mode': 'form', 'res_model': 'res.config.settings', - 'target': 'current', + 'target': 'inline', 'context': {'module' : 'general_settings'}, } diff --git a/addons/base_iban/models/res_partner_bank.py b/addons/base_iban/models/res_partner_bank.py index d1f6d8d9221..456d520a53a 100644 --- a/addons/base_iban/models/res_partner_bank.py +++ b/addons/base_iban/models/res_partner_bank.py @@ -89,6 +89,7 @@ _map_iban_template = { 'bg': 'BGkk BBBB SSSS DDCC CCCC CC', # Bulgaria 'bh': 'BHkk BBBB CCCC CCCC CCCC CC', # Bahrain 'br': 'BRkk BBBB BBBB SSSS SCCC CCCC CCCT N', # Brazil + 'by': 'BYkk BBBB AAAA CCCC CCCC CCCC CCCC', # Belarus 'ch': 'CHkk BBBB BCCC CCCC CCCC C', # Switzerland 'cr': 'CRkk BBBC CCCC CCCC CCCC C', # Costa Rica 'cy': 'CYkk BBBS SSSS CCCC CCCC CCCC CCCC', # Cyprus diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index 7021f880917..f5af6b89483 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -302,7 +302,8 @@ class Import(models.TransientModel): try: thousand_separator = decimal_separator = False for val in preview_values: - if val == '': + val = val.strip() + if not val: continue # value might have the currency symbol left or right from the value val = self._remove_currency_symbol(val) @@ -587,6 +588,7 @@ class Import(models.TransientModel): thousand_separator = options.get('float_thousand_separator', ' ') decimal_separator = options.get('float_decimal_separator', '.') for line in data: + line[index] = line[index].strip() if not line[index]: continue line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.') diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py index aaa1e69946a..d9c3d4092a2 100644 --- a/addons/base_import_module/models/ir_module.py +++ b/addons/base_import_module/models/ir_module.py @@ -145,12 +145,13 @@ def _is_studio_custom(path): Returns True if any of the records contains a context with the key studio in it, False if none of the records do """ - path = os.path.join(path, 'data') - filenames = next(iter(os.walk(path)))[2] - filenames = [f for f in filenames if f.lower().endswith('.xml')] + filepaths = [] + for level in os.walk(path): + filepaths += [os.path.join(level[0], fn) for fn in level[2]] + filepaths = [fp for fp in filepaths if fp.lower().endswith('.xml')] - for filename in filenames: - root = lxml.etree.parse(os.path.join(path, filename)).getroot() + for fp in filepaths: + root = lxml.etree.parse(fp).getroot() for record in root: # there might not be a context if it's a non-studio module diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py index a944c9d6b71..c2ca5a60735 100644 --- a/addons/crm/models/res_config_settings.py +++ b/addons/crm/models/res_config_settings.py @@ -10,7 +10,7 @@ class ResConfigSettings(models.TransientModel): crm_alias_prefix = fields.Char('Default Alias Name for Leads') generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails', config_parameter='crm.generate_lead_from_alias') group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead') - module_crm_phone_validation = fields.Boolean("Phone Validation") + module_crm_phone_validation = fields.Boolean("Phone Formatting") module_web_clearbit = fields.Boolean("Customer Autocomplete") def _find_default_lead_alias_id(self): @@ -33,7 +33,7 @@ class ResConfigSettings(models.TransientModel): @api.onchange('generate_lead_from_alias') def _onchange_generate_lead_from_alias(self): - self.crm_alias_prefix = 'info' if self.generate_lead_from_alias else False + self.crm_alias_prefix = (self.crm_alias_prefix or 'info') if self.generate_lead_from_alias else False @api.model def get_values(self): diff --git a/addons/crm/views/res_config_settings_views.xml b/addons/crm/views/res_config_settings_views.xml index 582063ae90a..4b421d928f1 100644 --- a/addons/crm/views/res_config_settings_views.xml +++ b/addons/crm/views/res_config_settings_views.xml @@ -62,7 +62,7 @@
diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index fdc568725d6..41ea4cc7682 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -210,8 +210,11 @@ class DeliveryCarrier(models.Model): carrier.product_id.list_price = carrier.fixed_price def fixed_rate_shipment(self, order): + price = self.fixed_price + if self.company_id.currency_id.id != order.currency_id.id: + price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price) return {'success': True, - 'price': self.fixed_price, + 'price': price, 'error_message': False, 'warning_message': False} diff --git a/addons/event/data/email_template_data.xml b/addons/event/data/email_template_data.xml index f2a742d9318..b7788cba9a3 100644 --- a/addons/event/data/email_template_data.xml +++ b/addons/event/data/email_template_data.xml @@ -84,18 +84,6 @@ - - - - -
-

- Don't forget to add it to your calendar: -

- Google - Outlook - Yahoo -
@@ -165,6 +153,16 @@
% endif + + + + +
+ Add this event to your calendar + Google + Outlook + Yahoo +
diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index da5d9fb69b1..213c5716059 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -247,9 +247,11 @@ class FleetVehicle(models.Model): @return: the costs log view """ self.ensure_one() + copy_context = dict(self.env.context) + copy_context.pop('group_by', None) res = self.env['ir.actions.act_window'].for_xml_id('fleet', 'fleet_vehicle_costs_action') res.update( - context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True), + context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True), domain=[('vehicle_id', '=', self.id)] ) return res diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py index 726e5efe375..c9d14c47495 100644 --- a/addons/hr_payroll/models/hr_payslip.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -190,7 +190,8 @@ class HrPayslip(models.Model): leave_time = (interval[1] - interval[0]).seconds / 3600 current_leave_struct['number_of_hours'] += leave_time work_hours = contract.employee_id.get_day_work_hours_count(interval[0].date(), calendar=contract.resource_calendar_id) - current_leave_struct['number_of_days'] += leave_time / work_hours + if work_hours: + current_leave_struct['number_of_days'] += leave_time / work_hours # compute worked days work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id) diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py index 0d182afcc72..46c0748f5fa 100644 --- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py +++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py @@ -95,7 +95,7 @@ class HrContract(models.Model): + 12.0 * contract.internet \ + 12.0 * (contract.mobile + contract.mobile_plus) \ + 12.0 * contract.transport_employer_cost \ - + (1.326 / 1.05 * 12.0) * contract.commission_on_target \ + + contract.warrants_cost \ + 220.0 * contract.meal_voucher_paid_by_employer contract.final_yearly_costs = yearly_cost / (1.0 - (contract.holidays - 20.0) / 231.0) contract.wage = contract._get_gross_from_employer_costs(contract.final_yearly_costs) @@ -116,8 +116,8 @@ class HrContract(models.Model): @api.depends('commission_on_target') def _compute_warrants_cost(self): for contract in self: - contract.warrants_cost = contract.commission_on_target * 1.05 * 12.0 - contract.warrant_value_employee = contract.commission_on_target * (1.00 - 0.535) * 12.0 + contract.warrants_cost = contract.commission_on_target * 1.326 * 1.05 * 12.0 + contract.warrant_value_employee = contract.commission_on_target * 1.326 * (1.00 - 0.535) * 12.0 @api.depends('wage', 'fuel_card', 'representation_fees', 'transport_employer_cost', 'internet', 'mobile', 'mobile_plus') diff --git a/addons/l10n_fr/data/account_chart_template_data.xml b/addons/l10n_fr/data/account_chart_template_data.xml index db836b0fd3b..aac0137d150 100644 --- a/addons/l10n_fr/data/account_chart_template_data.xml +++ b/addons/l10n_fr/data/account_chart_template_data.xml @@ -2836,98 +2836,98 @@ Valeurs mobilières de placement - Parts dans entreprises liées 501 - + Valeurs mobilières de placement - Actions propres 502 - + Valeurs mobilières de placement - Titres cotés 5031 - + Valeurs mobilières de placement - Titres non cotés 5035 - + Valeurs mobilières de placement - Autres titres conférant un droit de propriété 504 - + Obligations et bons émis par la société et rachetés par elle 505 - + Obligations cotés 5061 - + Obligations non cotés 5065 - + Bons du Trésor et bons de caisse à court terme 507 - + Autres valeurs mobilières de placement 5081 - + Bons de souscription 5082 - + Intérêts courus sur obligations, bons et valeurs assimilées 5088 - + Versements restant à effectuer sur valeurs mobilières de placement non libérées 509 - + Coupons échus à l'encaissement 5111 - + @@ -2935,7 +2935,7 @@ Chèques à encaisser 5112 - + @@ -2943,7 +2943,7 @@ Effets à l'encaissement 5113 - + @@ -2951,7 +2951,7 @@ Effets à l'escompte 5114 - + @@ -2980,56 +2980,56 @@ Sociétés de bourse 516 - + Autres organismes financiers 517 - + Intérêts courus à payer 5181 - + Intérêts courus à recevoir 5188 - + Concours bancaires courants - Crédit de mobilisation de créances commerciales (CMCC) 5191 - + Concours bancaires courants - Mobilisation de créances nées à l'étranger 5193 - + Concours bancaires courants - Intérêts courus sur concours bancaires courants 5198 - + Instruments de trésorerie 52 - + @@ -3064,35 +3064,35 @@ Régies d'avances et accréditifs 54 - + Provisions pour dépréciation des actions 5903 - + Provisions pour dépréciation des autres titres conférant un droit de propriété 5904 - + Provisions pour dépréciation des obligations 5906 - + Provisions pour dépréciation des autres valeurs mobilières de placement et créances assimilées (provisions) 5908 - + diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index 8675040c026..168b1955609 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -18,6 +18,10 @@ DIOT: 16% taxes + + DIOT: 16% IMP + taxes + DIOT: 0% taxes diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml index afd0cf91146..ac648e3c738 100644 --- a/addons/mail/data/mail_shortcode_data.xml +++ b/addons/mail/data/mail_shortcode_data.xml @@ -5,18 +5,25 @@ :) - 😊 - <img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/> + 🙂 + <img src='/mail/static/src/img/smiley/smile.png' alt='🙂'/> Smile image :D - 😄 - <img src='/mail/static/src/img/smiley/smile.png' alt='😄'/> + 😀 + <img src='/mail/static/src/img/smiley/smile_open_mouth.png' alt='😀'/> Smile with open mouth image + + ^^ + 😊 + <img src='/mail/static/src/img/smiley/normal_smile.png' alt='😊'/> + Smile with with smiling eyes + image + xD 😆 @@ -25,7 +32,7 @@ image - :-) + :joy 😂 <img src='/mail/static/src/img/smiley/tears_of_joy.png' alt='😂'/> Tears of joy diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 8042cab9b1d..48a4c7d091f 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -56,7 +56,7 @@ class Channel(models.Model): ('channel', 'Channel')], 'Channel Type', default='channel') description = fields.Text('Description') - uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4()) + uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4(), copy=False) email_send = fields.Boolean('Send messages by email', default=False) # multi users channel channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') diff --git a/addons/mail/static/src/img/smiley/smile_open_mouth.png b/addons/mail/static/src/img/smiley/smile_open_mouth.png new file mode 100644 index 00000000000..7dcdd3f55fe Binary files /dev/null and b/addons/mail/static/src/img/smiley/smile_open_mouth.png differ diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index 8de8a242e68..3a7b9c320e1 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -591,7 +591,7 @@ var BasicComposer = Widget.extend({ */ _onKeydown: function (event) { switch(event.which) { - // UP, DOWN: prevent moving cursor if navigation in mention propositions + // UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions case $.ui.keyCode.UP: case $.ui.keyCode.DOWN: case $.ui.keyCode.TAB: diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js index 2fa34b613ba..0aac950cf5b 100644 --- a/addons/mail/static/src/js/document_viewer.js +++ b/addons/mail/static/src/js/document_viewer.js @@ -17,6 +17,7 @@ var DocumentViewer = Widget.extend({ 'click .o_viewer_video': '_onVideoClicked', 'click .move_next': '_onNext', 'click .move_previous': '_onPrevious', + 'click .o_rotate': '_onRotate', 'click .o_zoom_in': '_onZoomIn', 'click .o_zoom_out': '_onZoomOut', 'click .o_close_btn, .o_viewer_img_wrapper': '_onClose', @@ -103,6 +104,30 @@ var DocumentViewer = Widget.extend({ this.$('.o_viewer_img').load(_.bind(this._onImageLoaded, this)); this._reset(); }, + /** + * Get CSS transform property based on scale and angle + * + * @private + * @param {float} scale + * @param {float} angle + */ + _getTransform: function(scale, angle) { + return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)' + }, + /** + * Rotate image clockwise by provided angle + * + * @private + * @param {float} angle + */ + _rotate: function (angle) { + this._reset(); + var new_angle = (this.angle || 0) + angle; + this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle)); + this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%'); + this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%'); + this.angle = new_angle; + }, /** * Zoom in/out image by provided scale * @@ -111,7 +136,7 @@ var DocumentViewer = Widget.extend({ */ _zoom: function (scale) { if (scale > 0.5) { - this.$('.o_viewer_img').css('transform', 'scale3d(' + scale + ', ' + scale + ', 1)'); + this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0)); this.scale = scale; } }, @@ -280,6 +305,14 @@ var DocumentViewer = Widget.extend({ videoElement.pause(); } }, + /** + * @private + * @param {MouseEvent} e + */ + _onRotate: function (e) { + e.preventDefault(); + this._rotate(90); + }, /** * @private * @param {MouseEvent} e diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index 9df25c895a3..e2fae8b106e 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -264,6 +264,7 @@ img { width: @o-mail-attachment-image-size; height: @o-mail-attachment-image-size; + image-orientation: from-image; // Only supported in Firefox } .o_image_overlay{ display: none; @@ -365,6 +366,9 @@ .o-flex-display(); justify-content: center; align-items: center; + img { + image-orientation: from-image; // Only supported in Firefox + } img, video { max-width: 100%; max-height: 100%; diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 4a7ba086ebf..cf80ed32995 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -50,6 +50,11 @@