diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-20 14:16:48 +0000 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-20 14:33:54 +0000 |
| commit | 58a83d12228fbbd4e9dde542e133d264204ee0f3 (patch) | |
| tree | 60aaf45e3281e88fddc58fea8fe75de0ad4e2f46 /addons | |
| parent | f6da4d57bcc8e19ae0bd6d571926706bee46e08f (diff) | |
| parent | 4a1321bc99dd7d8d153aceb2d0076161ce28bcd7 (diff) | |
[MERGE] forward port branch saas-12.4 up to 4a1321bc99d
closes odoo/odoo#37127
Signed-off-by: Christophe Simonis <chs@odoo.com>
Diffstat (limited to 'addons')
50 files changed, 1007 insertions, 102 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 967656fd90e..e2301af91d3 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -317,13 +317,13 @@ class AccountAccount(models.Model): if opening_move_line: if amount: # modify the line - setattr(opening_move_line.with_context({'check_move_validity': False}), field, amount) + opening_move_line.with_context(check_move_validity=False)[field] = amount elif counter_part_map[field]: # delete the line (no need to keep a line with value = 0) - opening_move_line.with_context({'check_move_validity': False}).unlink() + opening_move_line.with_context(check_move_validity=False).unlink() elif amount: # create a new line, as none existed before - self.env['account.move.line'].with_context({'check_move_validity': False}).create({ + self.env['account.move.line'].with_context(check_move_validity=False).create({ 'name': _('Opening balance'), field: amount, 'move_id': opening_move.id, @@ -794,7 +794,7 @@ class AccountJournal(models.Model): return { 'alias_defaults': {'type': type == 'purchase' and 'in_invoice' or 'out_invoice', 'company_id': self.company_id.id, 'journal_id': self.id}, 'alias_parent_thread_id': self.id, - 'alias_name': re.sub(r'[^\w]+', '-', alias_name) + 'alias_name': alias_name, } def unlink(self): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 222727f614a..2f23d4c923c 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -133,13 +133,13 @@ class AccountMove(models.Model): amount_residual = fields.Monetary(string='Amount Due', store=True, compute='_compute_amount') amount_untaxed_signed = fields.Monetary(string='Untaxed Amount Signed', store=True, readonly=True, - compute='_compute_amount') + compute='_compute_amount', currency_field='company_currency_id') amount_tax_signed = fields.Monetary(string='Tax Signed', store=True, readonly=True, - compute='_compute_amount') + compute='_compute_amount', currency_field='company_currency_id') amount_total_signed = fields.Monetary(string='Total Signed', store=True, readonly=True, - compute='_compute_amount') + compute='_compute_amount', currency_field='company_currency_id') amount_residual_signed = fields.Monetary(string='Amount Due Signed', store=True, - compute='_compute_amount') + compute='_compute_amount', currency_field='company_currency_id') amount_by_group = fields.Binary(string="Tax amount by group", compute='_compute_invoice_taxes_by_group', help="technical field used in report and in invoice form view with a widget to display the detail of taxes (grouped by tax group) under the subtotal") @@ -2767,7 +2767,7 @@ class AccountMoveLine(models.Model): reconciled = True line.reconciled = reconciled - line.amount_residual = line.move_id.company_id.currency_id.round(amount * sign) + line.amount_residual = line.move_id.company_id.currency_id.round(amount * sign) if line.move_id.company_id else amount * sign line.amount_residual_currency = line.currency_id and line.currency_id.round(amount_residual_currency * sign) or 0.0 @api.depends('tax_repartition_line_id.invoice_tax_id', 'tax_repartition_line_id.refund_tax_id') diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 25ddf2a3544..6b4dd3a1078 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -319,15 +319,17 @@ class account_payment(models.Model): move_currency = self.env['res.currency'].browse(res['currency_id']) if move_currency == currency and move_currency != company.currency_id: total += res['residual_currency'] - elif move_currency == currency == company.currency_id: - total += res['amount_residual'] else: - total += move_currency._convert(res['amount_residual'], currency, company, date) + total += company.currency_id._convert(res['amount_residual'], currency, company, date) return total def name_get(self): return [(payment.id, payment.name or _('Draft Payment')) for payment in self] + @api.model + def _get_move_name_transfer_separator(self): + return 'ยงยง' + @api.depends('move_line_ids.reconciled') def _get_move_reconciled(self): for payment in self: @@ -448,7 +450,6 @@ class account_payment(models.Model): move.unlink() rec.write({ 'state': 'cancelled', - 'move_name': False, }) def unlink(self): @@ -484,6 +485,7 @@ class account_payment(models.Model): all_move_vals = [] for payment in self: company_currency = payment.company_id.currency_id + move_names = payment.move_name.split(payment._get_move_name_transfer_separator()) if payment.move_name else None # Compute amounts. write_off_amount = payment.payment_difference_handling == 'reconcile' and -payment.payment_difference or 0.0 @@ -591,6 +593,9 @@ class account_payment(models.Model): 'payment_id': payment.id, })) + if move_names: + move_vals['name'] = move_names[0] + all_move_vals.append(move_vals) # ==== 'transfer' ==== @@ -634,6 +639,9 @@ class account_payment(models.Model): ], } + if move_names and len(move_names) == 2: + transfer_move_vals['name'] = move_names[1] + all_move_vals.append(transfer_move_vals) return all_move_vals @@ -677,7 +685,8 @@ class account_payment(models.Model): moves.filtered(lambda move: move.journal_id.post_at != 'bank_rec').post() # Update the state / move before performing any reconciliation. - rec.write({'state': 'posted', 'move_name': moves[0].name}) + move_name = self._get_move_name_transfer_separator().join(moves.mapped('name')) + rec.write({'state': 'posted', 'move_name': move_name}) if rec.payment_type in ('inbound', 'outbound'): # ==== 'inbound' / 'outbound' ==== diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 3ad49898420..dfa524f5cf6 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -238,7 +238,7 @@ class AccountChartTemplate(models.Model): }) # Set the transfer account on the company - company.transfer_account_id = self.env['account.account'].search([('code', '=like', self.transfer_account_code_prefix + '%')])[0] + company.transfer_account_id = self.env['account.account'].search([('code', '=like', self.transfer_account_code_prefix + '%')])[:1] # Create Bank journals self._create_bank_journals(company, acc_template_ref) @@ -265,7 +265,7 @@ class AccountChartTemplate(models.Model): """ model_to_check = ['account.move', 'account.payment', 'account.bank.statement'] for model in model_to_check: - if len(self.env[model].search([('company_id', '=', company_id.id)])) > 0: + if self.env[model].sudo().search([('company_id', '=', company_id.id)], limit=1): return True return False diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index ef0f030a5ee..e3884d5c975 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -225,7 +225,7 @@ class ResPartner(models.Model): @api.depends_context('force_company') def _credit_debit_get(self): - tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.company.id)._query_get() + tables, where_clause, where_params = self.env['account.move.line'].with_context(state='posted', company_id=self.env.company.id)._query_get() where_params = [tuple(self.ids)] + where_params if where_clause: where_clause = 'AND ' + where_clause @@ -266,9 +266,11 @@ class ResPartner(models.Model): SELECT partner.id FROM res_partner partner LEFT JOIN account_move_line aml ON aml.partner_id = partner.id + JOIN account_move move ON move.id = aml.move_id RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s AND NOT acc.deprecated AND acc.company_id = %s + AND move.state == 'posted' GROUP BY partner.id HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand)) res = self._cr.fetchall() diff --git a/addons/account/models/res_users.py b/addons/account/models/res_users.py index 1bc6ee1c813..6b379668fa3 100644 --- a/addons/account/models/res_users.py +++ b/addons/account/models/res_users.py @@ -12,14 +12,11 @@ class Users(models.Model): def _check_one_user_type(self): super(Users, self)._check_one_user_type() - users_with_both_groups = self.filtered(lambda user: - user.has_group('account.group_show_line_subtotals_tax_included') and - user.has_group('account.group_show_line_subtotals_tax_excluded') - ) - if users_with_both_groups: - names = ", ".join(users_with_both_groups.mapped('name')) + g1 = self.env.ref('account.group_show_line_subtotals_tax_included') + g2 = self.env.ref('account.group_show_line_subtotals_tax_excluded') + + if self._has_multiple_groups([g1.id, g2.id]): raise ValidationError(_("A user cannot have both Tax B2B and Tax B2C.\n" - "Problematic user(s): %s\n" "You should go in General Settings, and choose to display Product Prices\n" "either in 'Tax-Included' or in 'Tax-Excluded' mode\n" - "(or switch twice the mode if you are already in the desired one).") % names) + "(or switch twice the mode if you are already in the desired one).")) diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index ade019768ea..557a482d34f 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -33,6 +33,8 @@ class TestPayment(AccountingTestCase): self.bank_journal_euro = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'}) self.account_eur = self.bank_journal_euro.default_debit_account_id + self.cash_journal_euro = self.env['account.journal'].create({'name': 'Cash', 'type': 'cash', 'code': 'CASH'}) + self.bank_journal_usd = self.env['account.journal'].create({'name': 'Bank US', 'type': 'bank', 'code': 'BNK68', 'currency_id': self.currency_usd_id}) self.account_usd = self.bank_journal_usd.default_debit_account_id @@ -425,3 +427,119 @@ class TestPayment(AccountingTestCase): # The invoice should now be paid self.assertEqual(invoice.invoice_payment_state, 'paid', "Invoice should be in 'paid' state after having reconciled the two payments with a bank statement") + + def test_payment_cancel_keep_name(self): + self.bank_journal_euro.update_posted = True + + payment = self.payment_model.create({ + 'payment_type': 'inbound', + 'payment_method_id': self.payment_method_manual_in.id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 90, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'journal_id': self.bank_journal_euro.id, + }) + + payment.post() + self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 1) + name = payment.move_line_ids.mapped('move_id').name + self.assertTrue(name) + + payment.cancel() + self.assertFalse(payment.move_line_ids.mapped('move_id')) + payment.action_draft() + + payment.post() + self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 1) + self.assertEqual(name, payment.move_line_ids.mapped('move_id').name) + + def test_payment_transfer_cancel_keep_names(self): + self.bank_journal_euro.update_posted = True + self.cash_journal_euro.update_posted = True + + payment = self.payment_model.create({ + 'payment_type': 'transfer', + 'payment_method_id': self.payment_method_manual_out.id, + 'amount': 90, + 'payment_date': time.strftime('%Y') + '-07-15', + 'journal_id': self.bank_journal_euro.id, + 'destination_journal_id': self.cash_journal_euro.id, + }) + + payment.post() + self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 2) + + all_moves = payment.move_line_ids.mapped('move_id') + move = all_moves.filtered(lambda m: m.journal_id == self.bank_journal_euro) + transfer_move = all_moves - move + self.assertEqual(transfer_move.journal_id, self.cash_journal_euro) + + name = move.name + transfer_name = transfer_move.name + self.assertTrue(name) + self.assertTrue(transfer_name) + self.assertNotEqual(name, transfer_name) + + reconciled_lines = payment.move_line_ids.filtered(lambda l: l.reconciled) + self.assertEqual(len(reconciled_lines), 2) + self.assertEqual(reconciled_lines.mapped('move_id'), all_moves) + + reconciled_lines.remove_move_reconcile() + payment.cancel() + self.assertFalse(payment.move_line_ids.mapped('move_id')) + payment.action_draft() + + payment.post() + self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 2) + + all_moves = payment.move_line_ids.mapped('move_id') + move = all_moves.filtered(lambda m: m.journal_id == self.bank_journal_euro) + transfer_move = all_moves - move + self.assertEqual(transfer_move.journal_id, self.cash_journal_euro) + + self.assertEqual(name, move.name) + self.assertEqual(transfer_name, transfer_move.name) + + def test_payment_cancel_to_transfer(self): + self.bank_journal_euro.update_posted = True + + payment = self.payment_model.create({ + 'payment_type': 'inbound', + 'payment_method_id': self.payment_method_manual_in.id, + 'partner_type': 'customer', + 'partner_id': self.partner_agrolait.id, + 'amount': 90, + 'payment_date': time.strftime('%Y') + '-07-15', + 'payment_difference_handling': 'reconcile', + 'journal_id': self.bank_journal_euro.id, + }) + + payment.post() + self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 1) + name = payment.move_line_ids.mapped('move_id').name + self.assertTrue(name) + + payment.cancel() + self.assertFalse(payment.move_line_ids.mapped('move_id')) + payment.action_draft() + + payment.write({ + 'payment_type': 'transfer', + 'payment_method_id': self.payment_method_manual_out.id, + 'partner_id': False, + 'destination_journal_id': self.cash_journal_euro.id, + }) + + payment.post() + self.assertEqual(len(payment.move_line_ids.mapped('move_id')), 2) + + all_moves = payment.move_line_ids.mapped('move_id') + move = all_moves.filtered(lambda m: m.journal_id == self.bank_journal_euro) + transfer_move = all_moves - move + self.assertEqual(transfer_move.journal_id, self.cash_journal_euro) + + self.assertEqual(name, move.name) + self.assertTrue(transfer_move.name) + self.assertNotEqual(name, transfer_move.name) diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index bcae241be67..11114cf893b 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -334,9 +334,9 @@ class TestReconciliationMatchingRules(AccountingTestCase): # Check first line has been well reconciled. self.assertRecordValues(self.bank_line_1.journal_entry_ids, [ - {'partner_id': self.partner_1.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax21.id]}, - {'partner_id': self.partner_1.id, 'debit': 21.0, 'credit': 0.0, 'tax_line_id': self.tax21.id}, - {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0}, + {'partner_id': self.partner_1.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax21.id], 'tax_line_id': False}, + {'partner_id': self.partner_1.id, 'debit': 21.0, 'credit': 0.0, 'tax_ids': [], 'tax_line_id': self.tax21.id}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0, 'tax_ids': [], 'tax_line_id': False}, ]) def test_reverted_move_matching(self): diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 4269adb3fa9..894976e827d 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -30,7 +30,7 @@ <strong>Invoice Date:</strong> <p class="m-0" t-field="o.invoice_date"/> </div> - <div class="col-auto mw-100 mb-2" t-if="o.invoice_date_due and o.type == 'out_invoice' and o.state in ('open', 'in_payment', 'paid')" name="due_date"> + <div class="col-auto mw-100 mb-2" t-if="o.invoice_date_due and o.type == 'out_invoice' and o.state == 'posted'" name="due_date"> <strong>Due Date:</strong> <p class="m-0" t-field="o.invoice_date_due"/> </div> diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml index 3da12afe644..19f692b229f 100644 --- a/addons/account/views/report_payment_receipt_templates.xml +++ b/addons/account/views/report_payment_receipt_templates.xml @@ -2,7 +2,7 @@ <odoo> <template id="report_payment_receipt_document"> <t t-call="web.external_layout"> - <t t-set="o" t-value="o.with_context({'lang': lang})"/> + <t t-set="o" t-value="o.with_context(lang=lang)"/> <div class="page"> <h3><strong>Payment Receipt: <span t-field="o.name"/></strong></h3> <div class="row mt64"> diff --git a/addons/delivery/models/stock_move.py b/addons/delivery/models/stock_move.py index 39d4707301a..c5b81f1cd51 100644 --- a/addons/delivery/models/stock_move.py +++ b/addons/delivery/models/stock_move.py @@ -8,7 +8,7 @@ from odoo import api, fields, models class StockMove(models.Model): _inherit = 'stock.move' - weight = fields.Float(compute='_cal_move_weight', digits='Stock Weight', store=True) + weight = fields.Float(compute='_cal_move_weight', digits='Stock Weight', store=True, compute_sudo=True) @api.depends('product_id', 'product_uom_qty', 'product_uom') def _cal_move_weight(self): diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index 2b2da5777b0..9b46344a9b1 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -79,7 +79,7 @@ class StockPicking(models.Model): delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True) carrier_id = fields.Many2one("delivery.carrier", string="Carrier", check_company=True) volume = fields.Float(copy=False) - weight = fields.Float(compute='_cal_weight', digits='Stock Weight', store=True, help="Total weight of the products in the picking.") + weight = fields.Float(compute='_cal_weight', digits='Stock Weight', store=True, help="Total weight of the products in the picking.", compute_sudo=True) carrier_tracking_ref = fields.Char(string='Tracking Reference', copy=False) carrier_tracking_url = fields.Char(string='Tracking URL', compute='_compute_carrier_tracking_url') weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom) diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index 0b9d5aa3513..ae3f84a4b13 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -96,7 +96,7 @@ class EventTicket(models.Model): def _compute_is_expired(self): for record in self: if record.deadline: - current_date = fields.Date.context_today(record.with_context({'tz': record.event_id.date_tz})) + current_date = fields.Date.context_today(record.with_context(tz=record.event_id.date_tz)) record.is_expired = record.deadline < current_date else: record.is_expired = False diff --git a/addons/l10n_de/static/src/scss/report_din5008.scss b/addons/l10n_de/static/src/scss/report_din5008.scss index cdecd43eda7..801ef16637c 100644 --- a/addons/l10n_de/static/src/scss/report_din5008.scss +++ b/addons/l10n_de/static/src/scss/report_din5008.scss @@ -21,6 +21,7 @@ top: 17.7mm; width: 85mm; height: 27.3mm; + line-height: 1.15; overflow-y: hidden; word-break: break-word; float: left; diff --git a/addons/l10n_fr/data/account_tax_data.xml b/addons/l10n_fr/data/account_tax_data.xml index f8938c2f145..3f72c43b63e 100644 --- a/addons/l10n_fr/data/account_tax_data.xml +++ b/addons/l10n_fr/data/account_tax_data.xml @@ -1683,7 +1683,7 @@ <record model="account.tax.template" id="tva_export_0"> <field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/> <field name="name">TVA 0% export (vente)</field> - <field name="description">TVA 0% Import</field> + <field name="description">TVA 0% Export</field> <field name="amount" eval="0.00"/> <field name="amount_type">percent</field> <field name="sequence" eval="10"/> diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py index 3fbdd494a5e..0e7b9d3a156 100644 --- a/addons/l10n_multilang/models/l10n_multilang.py +++ b/addons/l10n_multilang/models/l10n_multilang.py @@ -120,6 +120,9 @@ class AccountChartTemplate(models.Model): in_xml_ids = {xml_id.name: xml_id for xml_id in in_xml_ids} for name, xml_id in expected_in_xml_id_names.items(): + # ignore nonconforming customized data + if name not in in_xml_ids: + continue in_records += self.env[model + '.template'].browse(in_xml_ids[name].res_id) out_records += self.env[model].browse(xml_id.res_id) diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js index 80e7747cbdd..b16dee08915 100644 --- a/addons/lunch/static/tests/lunch_kanban_tests.js +++ b/addons/lunch/static/tests/lunch_kanban_tests.js @@ -261,6 +261,55 @@ QUnit.module('LunchKanbanView', { kanban.destroy(); }); + QUnit.test('search panel domain location false: fetch products in all locations', async function (assert) { + assert.expect(10); + const regularInfos = _.extend({}, this.regularInfos); + + const kanban = await createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: function (route, args) { + assert.step(route); + + if (route.startsWith('/lunch')) { + return mockLunchRPC({ + infos: regularInfos, + userLocation: false, + }).apply(this, arguments); + } + if (args.method === 'search_panel_select_multi_range') { + assert.deepEqual(args.kwargs.search_domain, [], + 'The domain should not exist since the location is false.'); + } + if (route === '/web/dataset/search_read') { + assert.deepEqual(args.domain, [], + 'The domain for fetching actual data should be correct'); + } + return this._super.apply(this, arguments); + } + }); + assert.verifySteps([ + '/lunch/user_location_get', + '/web/dataset/call_kw/product/search_panel_select_multi_range', + '/web/dataset/call_kw/product/search_panel_select_multi_range', + '/web/dataset/search_read', + '/lunch/infos', + '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id', + ]) + + kanban.destroy(); + }); + QUnit.test('non-empty cart', async function (assert) { assert.expect(17); diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index 15447921645..4cc8234ba23 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -240,7 +240,7 @@ class AliasMixin(models.AbstractModel): for record in child_model.search([('alias_id', '=', False)]): # create the alias, and link it to the current record alias = alias_model.create(record.get_alias_values()) - record.with_context({'mail_notrack': True}).alias_id = alias + record.with_context(mail_notrack=True).alias_id = alias _logger.info('Mail alias created for %s %s (id %s)', record._name, record.display_name, record.id) diff --git a/addons/mail_bot/models/mail_bot.py b/addons/mail_bot/models/mail_bot.py index 2e0ab860255..10686b14ba3 100644 --- a/addons/mail_bot/models/mail_bot.py +++ b/addons/mail_bot/models/mail_bot.py @@ -30,7 +30,7 @@ class MailBot(models.AbstractModel): if answer: message_type = values.get('message_type', 'comment') subtype_id = values.get('subtype_id', self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')) - record.with_context({"mail_create_nosubscribe": True}).sudo().message_post(body=answer, author_id=odoobot_id, message_type=message_type, subtype_id=subtype_id) + record.with_context(mail_create_nosubscribe=True).sudo().message_post(body=answer, author_id=odoobot_id, message_type=message_type, subtype_id=subtype_id) def _get_answer(self, record, body, values, command=False): # onboarding diff --git a/addons/mail_bot/models/mail_channel.py b/addons/mail_bot/models/mail_channel.py index efada26b88a..0fc6c6f056b 100644 --- a/addons/mail_bot/models/mail_channel.py +++ b/addons/mail_bot/models/mail_channel.py @@ -16,7 +16,7 @@ class Channel(models.Model): if self.env.user.odoobot_state == 'not_initialized': partner = self.env.user.partner_id odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("base.partner_root") - channel = self.with_context({"mail_create_nosubscribe": True}).create({ + channel = self.with_context(mail_create_nosubscribe=True).create({ 'channel_partner_ids': [(4, partner.id), (4, odoobot_id)], 'public': 'private', 'channel_type': 'chat', diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py index 9af9b84d9d6..da15e757cb3 100644 --- a/addons/mrp_account/models/product.py +++ b/addons/mrp_account/models/product.py @@ -11,11 +11,13 @@ class ProductTemplate(models.Model): def action_bom_cost(self): templates = self.filtered(lambda t: t.product_variant_count == 1 and t.bom_count > 0) - return templates.mapped('product_variant_id').action_bom_cost() + if templates: + return templates.mapped('product_variant_id').action_bom_cost() def button_bom_cost(self): templates = self.filtered(lambda t: t.product_variant_count == 1 and t.bom_count > 0) - return templates.mapped('product_variant_id').button_bom_cost() + if templates: + return templates.mapped('product_variant_id').button_bom_cost() class ProductProduct(models.Model): @@ -50,6 +52,8 @@ class ProductProduct(models.Model): def _compute_bom_price(self, bom, boms_to_recompute=False): self.ensure_one() + if not bom: + return 0 if not boms_to_recompute: boms_to_recompute = [] total = 0 diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index 2d29278cd7d..be081a4e34f 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -83,7 +83,7 @@ class PaymentProcessing(http.Controller): # populate the returned dictionnary with the transactions data for tx in payment_transaction_ids: message_to_display = tx.acquirer_id[tx.state + '_msg'] if tx.state in ['done', 'pending', 'cancel'] else None - result['transactions'].append({ + tx_info = { 'reference': tx.reference, 'state': tx.state, 'return_url': tx.return_url, @@ -93,7 +93,9 @@ class PaymentProcessing(http.Controller): 'amount': tx.amount, 'currency': tx.currency_id.name, 'acquirer_provider': tx.acquirer_id.provider, - }) + } + tx_info.update(tx._get_processing_info()) + result['transactions'].append(tx_info) tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False) try: diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index f9d703972d8..d23658ffa5d 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -644,6 +644,10 @@ class PaymentTransaction(models.Model): transactions = self.filtered(lambda t: t.state != 'draft') return transactions and transactions[0] or transactions + def _get_processing_info(self): + """ Extensible method for providers if they need specific fields/info regarding a tx in the payment processing page. """ + return dict() + def _get_payment_transaction_sent_message(self): self.ensure_one() if self.payment_token_id: diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js index 53b688be06a..f52d34f6a8c 100644 --- a/addons/payment/static/src/js/payment_processing.js +++ b/addons/payment/static/src/js/payment_processing.js @@ -94,7 +94,7 @@ odoo.define('payment.processing', function (require) { return nbTx; } // if there's only one tx to manage - if(countTxInState(['tx_done', 'tx_error']) === 1) { + if(countTxInState(['tx_done', 'tx_error', 'tx_pending']) === 1) { var tx = render_values['tx_done'][0] || render_values['tx_error'][0]; if (tx) { window.location = tx.return_url; diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index f347973ad02..dc2ea54e81c 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -76,14 +76,16 @@ class PaypalController(http.Controller): if resp in ['VERIFIED', 'SUCCESS']: _logger.info('Paypal: validated data') res = request.env['payment.transaction'].sudo().form_feedback(post, 'paypal') - if not res: + if not res and tx: tx.sudo()._set_transaction_error('Validation error occured. Please contact your administrator.') elif resp in ['INVALID', 'FAIL']: _logger.warning('Paypal: answered INVALID/FAIL on data verification') - tx.sudo()._set_transaction_error('Invalid response from Paypal. Please contact your administrator.') + if tx: + tx.sudo()._set_transaction_error('Invalid response from Paypal. Please contact your administrator.') else: _logger.warning('Paypal: unrecognized paypal answer, received %s instead of VERIFIED/SUCCESS or INVALID/FAIL (validation: %s)' % (resp, 'PDT' if pdt_request else 'IPN/DPN')) - tx.sudo()._set_transaction_error('Unrecognized error from Paypal. Please contact your administrator.') + if tx: + tx.sudo()._set_transaction_error('Unrecognized error from Paypal. Please contact your administrator.') return res @http.route('/payment/paypal/ipn/', type='http', auth='public', methods=['POST'], csrf=False) diff --git a/addons/payment_payumoney/models/payment.py b/addons/payment_payumoney/models/payment.py index 202e4bd1d72..b8f60673031 100644 --- a/addons/payment_payumoney/models/payment.py +++ b/addons/payment_payumoney/models/payment.py @@ -26,7 +26,7 @@ class PaymentAcquirerPayumoney(models.Model): if environment == 'prod': return {'payumoney_form_url': 'https://secure.payu.in/_payment'} else: - return {'payumoney_form_url': 'https://test.payu.in/_payment'} + return {'payumoney_form_url': 'https://sandboxsecure.payu.in/_payment'} def _payumoney_generate_sign(self, inout, values): """ Generate the shasign for incoming or outgoing communications. diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 8a6eb09624c..54932140049 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -357,6 +357,9 @@ td { height:100%; display: -webkit-flex; display: flex; + overflow : hidden; + overflow-x: auto; + -webkit-overflow-scrolling: touch; } .pos .pos-rightheader > * { border-right: 1px solid #292929; @@ -437,8 +440,6 @@ td { display: flex; -webkit-flex: 1; flex: 1; - max-width: -moz-available; - max-width: -webkit-fill-available; } .pos .orders { display: -webkit-flex; @@ -446,8 +447,6 @@ td { vertical-align: top; margin-left: 0px; overflow: hidden; - overflow-x: auto; - -webkit-overflow-scrolling: touch; } /* c) The session buttons */ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 011bcce36fc..0e5fb975786 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1757,7 +1757,7 @@ var ReceiptScreenWidget = ScreenWidget.extend({ var invoiced = self.pos.push_and_invoice_order(order); self.invoicing = true; - invoiced.fail(self._handleFailedPushForInvoice.bind(self, order, true)); // refresh + invoiced.catch(self._handleFailedPushForInvoice.bind(self, order, true)); // refresh invoiced.then(function(){ self.invoicing = false; diff --git a/addons/product/models/product.py b/addons/product/models/product.py index fe3fac09f76..6e0fdaa206e 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -710,6 +710,7 @@ class SupplierInfo(models.Model): 'res.partner', 'Vendor', ondelete='cascade', required=True, help="Vendor of this product", check_company=True) + active = fields.Boolean(related='name.active', readonly=True) product_name = fields.Char( 'Vendor Product Name', help="This vendor's product name will be used when printing a request for quotation. Keep empty to use the internal one.") diff --git a/addons/product/report/product_pricelist_templates.xml b/addons/product/report/product_pricelist_templates.xml index 0f7d54db714..41ba6092f31 100644 --- a/addons/product/report/product_pricelist_templates.xml +++ b/addons/product/report/product_pricelist_templates.xml @@ -53,9 +53,9 @@ </td> <t t-foreach="data['quantities']" t-as="quantity"> <td><strong t-esc="categ_data['prices'][product.id][quantity]" - t-esc-options="{ - 'widget': 'monetary', - 'display_currency': data['pricelist'].currency_id}"/> + t-options="{ + 'widget': 'float', + 'decimal_precision': 'Product Price'}"/> </td> </t> </tr> diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index d6bb0f9ce30..f081041715e 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -457,7 +457,7 @@ class SaleOrder(models.Model): self.ensure_one() journal = self.env['account.move'].with_context(force_company=self.company_id.id, default_type='out_invoice')._get_default_journal() if not journal: - raise UserError(_('Please define an accounting sales journal for the company %s (%s).') % (self.company_id.name, self.company_id.id)) + raise UserError(_('Please define an accounting sales journal for the company %s (%s).') % (self.company_id.name, self.company_id.id)) invoice_vals = { 'ref': self.client_order_ref or '', diff --git a/addons/sale_mrp/tests/test_sale_mrp_procurement.py b/addons/sale_mrp/tests/test_sale_mrp_procurement.py index 25753211eca..e9c7a52ae62 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_procurement.py +++ b/addons/sale_mrp/tests/test_sale_mrp_procurement.py @@ -69,3 +69,105 @@ class TestSaleMrpProcurement(TransactionCase): # I verify that a manufacturing order has been generated, and that its name and reference are correct mo = self.env['mrp.production'].search([('origin', 'like', sale_order_so0.name)], limit=1) self.assertTrue(mo, 'Manufacturing order has not been generated') + + def test_sale_mrp_pickings(self): + """ Test sale of multiple mrp products in MTO + to avoid generating multiple deliveries + to the customer location + """ + + # Create warehouse + self.customer_location = self.env['ir.model.data'].xmlid_to_res_id('stock.stock_location_customers') + warehouse_form = Form(self.env['stock.warehouse']) + warehouse_form.name = 'Test Warehouse' + warehouse_form.code = 'TWH' + self.warehouse = warehouse_form.save() + + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Stick' + product_form.type = 'product' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.raw_product = product_form.save() + + # Create manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Stick' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + product_form.type = 'product' + product_form.route_ids.clear() + product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) + product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) + self.finished_product = product_form.save() + + # Create manifactured product which uses another manifactured + product_form = Form(self.env['product.product']) + product_form.name = 'Arrow' + product_form.type = 'product' + product_form.route_ids.clear() + product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) + product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) + self.complex_product = product_form.save() + + ## Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Iron' + product_form.type = 'product' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.raw_product_2 = product_form.save() + + # Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.finished_product + bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id + bom_product_form.product_qty = 1.0 + bom_product_form.type = 'normal' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.raw_product + bom_line.product_qty = 2.0 + + self.bom = bom_product_form.save() + + ## Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.complex_product + bom_product_form.product_tmpl_id = self.complex_product.product_tmpl_id + with bom_product_form.bom_line_ids.new() as line: + line.product_id = self.finished_product + line.product_qty = 1.0 + with bom_product_form.bom_line_ids.new() as line: + line.product_id = self.raw_product_2 + line.product_qty = 1.0 + + self.complex_bom = bom_product_form.save() + + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm_sam' + + so_form = Form(self.env['sale.order']) + so_form.partner_id = self.env.ref('base.res_partner_4') + with so_form.order_line.new() as line: + line.product_id = self.complex_product + line.price_unit = 1 + line.product_uom_qty = 1 + with so_form.order_line.new() as line: + line.product_id = self.finished_product + line.price_unit = 1 + line.product_uom_qty = 1 + sale_order_so0 = so_form.save() + + sale_order_so0.action_confirm() + + pickings = sale_order_so0.picking_ids + + # One delivery... + self.assertEqual(len(pickings), 1) + + # ...with two products + move_lines = pickings[0].move_lines + self.assertEqual(len(move_lines), 2) diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py index 96b1a91e4c4..b74e2a42abc 100644 --- a/addons/snailmail/models/snailmail_letter.py +++ b/addons/snailmail/models/snailmail_letter.py @@ -217,7 +217,7 @@ class SnailmailLetter(models.Model): else: # adding the web logo from the company for future possible customization document.update({ - 'company_logo': letter.company_id.logo_web.decode('utf-8'), + 'company_logo': letter.company_id.logo_web and letter.company_id.logo_web.decode('utf-8') or False, }) attachment = letter._fetch_attachment() if attachment: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index a22a29b40f8..ec78b56eca9 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -49,7 +49,7 @@ class StockMove(models.Model): description_picking = fields.Text('Description of Picking') product_qty = fields.Float( 'Real Quantity', compute='_compute_product_qty', inverse='_set_product_qty', - digits=0, store=True, + digits=0, store=True, compute_sudo=True, help='Quantity in the default UoM of the product') product_uom_qty = fields.Float( 'Initial Demand', diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 36acbf4d000..eaeca84b039 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -741,7 +741,10 @@ class Warehouse(models.Model): if not change_to_multiple: # If single delivery we should create the necessary MTO rules for the resupply routings = [self.Routing(self.lot_stock_id, location, self.out_type_id, 'pull') for location in rules.mapped('location_id')] - mto_rule_vals = self._get_rule_values(routings) + mto_vals = self._get_global_route_rules_values().get('mto_pull_id') + values = mto_vals['create_values'] + mto_rule_vals = self._get_rule_values(routings, values, name_suffix='MTO') + for mto_rule_val in mto_rule_vals: Rule.create(mto_rule_val) else: diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index a2f4917b699..c1d01022c68 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2550,11 +2550,28 @@ var FieldProgressBar = AbstractField.extend({ if (this.recordData[this.nodeOptions.current_value]) { this.value = this.recordData[this.nodeOptions.current_value]; } - this.max_value = this.recordData[this.nodeOptions.max_value] || 100; + + // The few next lines determine if the widget can write on the record or not + this.editable_readonly = !!this.nodeOptions.editable_readonly; + // "hard" readonly this.readonly = this.nodeOptions.readonly || !this.nodeOptions.editable; - this.edit_max_value = this.nodeOptions.edit_max_value || false; + + this.canWrite = !this.readonly && ( + this.mode === 'edit' || + (this.editable_readonly && this.mode === 'readonly') || + (this.viewType === 'kanban') // Keep behavior before commit + ); + + // Boolean to toggle if we edit the numerator (value) or the denominator (max_value) + this.edit_max_value = !!this.nodeOptions.edit_max_value; + this.max_value = this.recordData[this.nodeOptions.max_value] || 100; + this.title = _t(this.attrs.title || this.nodeOptions.title) || ''; - this.edit_on_click = !this.nodeOptions.edit_max_value || false; + + // Ability to edit the field through the bar + // /!\ this feature is disabled + this.enableBarAsInput = false; + this.edit_on_click = this.enableBarAsInput && this.mode === 'readonly' && !this.edit_max_value; this.write_mode = false; }, @@ -2562,19 +2579,19 @@ var FieldProgressBar = AbstractField.extend({ var self = this; this._render_value(); - if (!this.readonly) { + if (this.canWrite) { if (this.edit_on_click) { this.$el.on('click', '.o_progress', function (e) { var $target = $(e.currentTarget); - self.value = Math.floor((e.pageX - $target.offset().left) / $target.outerWidth() * self.max_value); + var numValue = Math.floor((e.pageX - $target.offset().left) / $target.outerWidth() * self.max_value); + self.on_update(numValue); self._render_value(); - self.on_update(self.value); }); } else { this.$el.on('click', function () { if (!self.write_mode) { var $input = $('<input>', {type: 'text', class: 'o_progressbar_value o_input'}); - $input.on('blur', _.bind(self.on_change_input, self)); + $input.on('blur', self.on_change_input.bind(self)); self.$('.o_progressbar_value').replaceWith($input); self.write_mode = true; self._render_value(); @@ -2584,24 +2601,25 @@ var FieldProgressBar = AbstractField.extend({ } return this._super(); }, + /** + * Updates the widget with value + * + * @param {Number} value + */ on_update: function (value) { - if (!isNaN(value)) { - if (this.edit_max_value) { - try { - this.max_value = this._parseValue(value); - this._isValid = true; - } catch (e) { - this._isValid = false; - } - var changes = {}; - changes[this.nodeOptions.max_value] = this.max_value; - this.trigger_up('field_changed', { - dataPointID: this.dataPointID, - changes: changes, - }); - } else { - this._setValue(value); - } + if (this.edit_max_value) { + this.max_value = value; + this._isValid = true; + var changes = {}; + changes[this.nodeOptions.max_value] = this.max_value; + this.trigger_up('field_changed', { + dataPointID: this.dataPointID, + changes: changes, + }); + } else { + // _setValues accepts string and will parse it + var formattedValue = this._formatValue(value); + this._setValue(formattedValue); } }, on_change_input: function (e) { @@ -2609,29 +2627,42 @@ var FieldProgressBar = AbstractField.extend({ if (e.type === 'change' && !$input.is(':focus')) { return; } - if (isNaN($input.val())) { - this.do_warn(_t("Wrong value entered!"), _t("Only Integer Value should be valid.")); - } else { - if (e.type === 'input') { - this._render_value($input.val()); - if (parseFloat($input.val()) === 0) { + + var parsedValue; + try { + // Cover all numbers with parseFloat + parsedValue = field_utils.parse.float($input.val()); + } catch (error) { + this.do_warn(_t("Wrong value entered!"), _t("Only Integer or Float Value should be valid.")); + } + + if (parsedValue !== undefined) { + if (e.type === 'input') { // ensure what has just been typed in the input is a number + // returns NaN if not a number + this._render_value(parsedValue); + if (parsedValue === 0) { $input.select(); } - } else { + } else { // Implicit type === 'blur': we commit the value if (this.edit_max_value) { - this.max_value = $(e.target).val(); - } else { - this.value = $(e.target).val() || 0; + parsedValue = parsedValue || 100; } + var $div = $('<div>', {class: 'o_progressbar_value'}); this.$('.o_progressbar_value').replaceWith($div); this.write_mode = false; + this.on_update(parsedValue); this._render_value(); - this.on_update(this.edit_max_value ? this.max_value : this.value); } } }, + /** + * Renders the value + * + * @private + * @param {Number} v + */ _render_value: function (v) { var value = this.value; var max_value = this.max_value; diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index cb83301c14d..43a485fb009 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -19,6 +19,8 @@ var config = require('web.config'); var core = require('web.core'); var mvc = require('web.mvc'); +var session = require('web.session'); + var QWeb = core.qweb; var AbstractController = mvc.Controller.extend(ActionMixin, { @@ -519,6 +521,7 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { this.dp.add(this._rpc({ model: data.model, method: data.method, + context: session.user_context, })).then(function (action) { if (action !== undefined) { self.do_action(action, options); diff --git a/addons/web/static/src/js/views/control_panel/search/search_bar.js b/addons/web/static/src/js/views/control_panel/search/search_bar.js index 3f58fcdce2a..42b156beed7 100644 --- a/addons/web/static/src/js/views/control_panel/search/search_bar.js +++ b/addons/web/static/src/js/views/control_panel/search/search_bar.js @@ -9,6 +9,8 @@ var Widget = require('web.Widget'); var SearchBar = Widget.extend({ template: 'SearchView.SearchBar', events: _.extend({}, Widget.prototype.events, { + 'compositionend .o_searchview_input': '_onCompositionendInput', + 'compositionstart .o_searchview_input': '_onCompositionstartInput', 'keydown': '_onKeydown', }), /** @@ -32,6 +34,7 @@ var SearchBar = Widget.extend({ this.autoCompleteSources = []; this.searchFacets = []; + this._isInputComposing = false; }, /** * @override @@ -200,10 +203,27 @@ var SearchBar = Widget.extend({ } }, /** + * @rivate + * @param {CompositionEvent} ev + */ + _onCompositionendInput: function () { + this._isInputComposing = false; + }, + /** + * @rivate + * @param {CompositionEvent} ev + */ + _onCompositionstartInput: function () { + this._isInputComposing = true; + }, + /** * @private * @param {KeyEvent} e */ _onKeydown: function (e) { + if (this._isInputComposing) { + return; + } switch(e.which) { case $.ui.keyCode.LEFT: this._focusPreceding(); diff --git a/addons/web/static/src/js/views/control_panel/search/search_facet.js b/addons/web/static/src/js/views/control_panel/search/search_facet.js index 917d42b7e41..22d957c030b 100644 --- a/addons/web/static/src/js/views/control_panel/search/search_facet.js +++ b/addons/web/static/src/js/views/control_panel/search/search_facet.js @@ -10,6 +10,8 @@ var SearchFacet = Widget.extend({ template: 'SearchView.SearchFacet', events: _.extend({}, Widget.prototype.events, { 'click .o_facet_remove': '_onFacetRemove', + 'compositionend': '_onCompositionend', + 'compositionstart': '_onCompositionstart', 'keydown': '_onKeydown', }), /** @@ -26,6 +28,7 @@ var SearchFacet = Widget.extend({ }); this.separator = this._getSeparator(); this.icon = this._getIcon(); + this._isComposing = false; }, //-------------------------------------------------------------------------- @@ -130,6 +133,20 @@ var SearchFacet = Widget.extend({ /** * @private + * @param {CompositionEvent} ev + */ + _onCompositionend: function (ev) { + this._isComposing = false; + }, + /** + * @private + * @param {CompositionEvent} ev + */ + _onCompositionstart: function (ev) { + this._isComposing = true; + }, + /** + * @private */ _onFacetRemove: function () { this.trigger_up('facet_removed', {group: this.facet}); @@ -139,6 +156,9 @@ var SearchFacet = Widget.extend({ * @param {KeyboardEvent} ev */ _onKeydown: function (ev) { + if (this._isComposing) { + return; + } switch (ev.which) { case $.ui.keyCode.BACKSPACE: this.trigger_up('facet_removed', {group: this.facet}); diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js index d222a5642cf..4f8c42325db 100644 --- a/addons/web/static/src/js/views/search_panel.js +++ b/addons/web/static/src/js/views/search_panel.js @@ -288,8 +288,9 @@ var SearchPanel = Widget.extend({ }); _.map(values, function (value) { var value = category.values[value.id]; - if (value.parentId) { - category.values[value.parentId].childrenIds.push(value.id); + var parentCategoryId = value.parentId; + if (parentCategoryId && parentCategoryId in category.values) { + category.values[parentCategoryId].childrenIds.push(value.id); } }); category.rootIds = _.filter(_.map(values, function (value) { diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js index 1bb00be8d38..30c38690d17 100644 --- a/addons/web/static/src/js/widgets/auto_complete.js +++ b/addons/web/static/src/js/widgets/auto_complete.js @@ -29,16 +29,23 @@ return Widget.extend({ this.searching = true; this.search_string = ''; this.current_search = null; + this._isInputComposing = false; }, start: function () { var self = this; + this.$input.on('compositionend', function (ev) { + self._isInputComposing = false; + }); + this.$input.on('compositionstart', function (ev) { + self._isInputComposing = true; + }); this.$input.on('keyup', function (ev) { - if (ev.which === $.ui.keyCode.RIGHT) { + if (ev.which === $.ui.keyCode.RIGHT && !self._isInputComposing) { self.searching = true; ev.preventDefault(); return; } - if (ev.which === $.ui.keyCode.ENTER) { + if (ev.which === $.ui.keyCode.ENTER && !self._isInputComposing) { if (self.search_string.length) { self.select_item(ev); } @@ -68,6 +75,9 @@ return Widget.extend({ } }); this.$input.on('keydown', function (ev) { + if (self._isInputComposing) { + return; + } switch (ev.which) { case $.ui.keyCode.ENTER: diff --git a/addons/web/static/src/scss/banner.scss b/addons/web/static/src/scss/banner.scss index d7237203897..be011d70698 100644 --- a/addons/web/static/src/scss/banner.scss +++ b/addons/web/static/src/scss/banner.scss @@ -1,8 +1,9 @@ .o_has_banner { .o_view_nocontent { top: 30%; - @media screen and (max-height: map-get($grid-breakpoints, 'md')) { + @include media-breakpoint-down(sm) { position: relative; + margin: auto; } } } diff --git a/addons/web/static/src/scss/modal.scss b/addons/web/static/src/scss/modal.scss index f33aec59431..e7243042d35 100644 --- a/addons/web/static/src/scss/modal.scss +++ b/addons/web/static/src/scss/modal.scss @@ -96,6 +96,11 @@ } } +body.modal-open { + // Allow vertical scrolling in modals on iOS + -webkit-overflow-scrolling: touch; +} + // Temporary fix for modals which are not instantiated thanks to the Dialog // JS classes (deprecated case) (see bootstrap_overridden.scss) + Frontend. // diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 8abe04107ff..aa86ff925e4 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -11,6 +11,7 @@ var KanbanView = require('web.KanbanView'); var ListView = require('web.ListView'); var session = require('web.session'); var testUtils = require('web.test_utils'); +var testUtilsDom = require('web.test_utils_dom'); var field_registry = require('web.field_registry'); var createView = testUtils.createView; @@ -5955,7 +5956,7 @@ QUnit.module('basic_fields', { QUnit.module('FieldProgressBar'); QUnit.test('Field ProgressBar: max_value should update', async function (assert) { - assert.expect(2); + assert.expect(3); this.data.partner.records = this.data.partner.records.slice(0,1); this.data.partner.records[0].qux = 2; @@ -5980,6 +5981,15 @@ QUnit.module('basic_fields', { viewOptions: { mode: 'edit', }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.deepEqual( + args.args[1], + {int_field: 999, qux: 5, display_name: 'new name'}, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } }); assert.strictEqual(form.$('.o_progressbar_value').text(), '10 / 2', @@ -5991,6 +6001,435 @@ QUnit.module('basic_fields', { assert.strictEqual(form.$('.o_progressbar_value').text(), '999 / 5', 'The value of the progress bar should be correct after the update'); + await testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: value should not update in readonly mode when sliding the bar', async function (assert) { + assert.expect(4); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + assert.step(route); + return this._super.apply(this, arguments); + } + }); + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'Initial value should be correct') + + var $progressBarEl = form.$('.o_progress'); + var top = $progressBarEl.offset().top + 5; + var left = $progressBarEl.offset().left + 5; + try { + testUtils.triggerPositionalMouseEvent(left, top, "click"); + } catch (e) { + form.destroy(); + $view.remove(); + throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); + } + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'New value should be different than initial after click'); + + assert.verifySteps(["/web/dataset/call_kw/partner/read"]); + + form.destroy(); + $view.remove(); + }); + + QUnit.test('Field ProgressBar: value should not update in edit mode when sliding the bar', async function (assert) { + assert.expect(6); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + assert.step(route); + return this._super.apply(this, arguments); + } + }); + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'Initial value should be correct') + + var $progressBarEl = form.$('.o_progress'); + var top = $progressBarEl.offset().top + 5; + var left = $progressBarEl.offset().left + 5; + try { + testUtils.triggerPositionalMouseEvent(left, top, "click"); + } catch (e) { + form.destroy(); + $view.remove(); + throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); + } + assert.strictEqual(form.$('.o_progressbar_value.o_input').val(), "99", + 'Value of input is not changed'); + await testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'New value should be different than initial after click'); + + assert.verifySteps(["/web/dataset/call_kw/partner/read"]); + + form.destroy(); + $view.remove(); + }); + + QUnit.test('Field ProgressBar: value should update in edit mode when typing in input', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].int_field, 69, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']); + + await testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '69%', + 'New value should be different than initial after click'); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: value should update in edit mode when typing in input with field max value', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="qux" invisible="1" />' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\'}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].int_field, 69, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']); + + await testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '69 / 0', + 'New value should be different than initial after click'); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: max value should update in edit mode when typing in input with field max value', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="qux" invisible="1" />' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].qux, 69, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), "0.44444", 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']); + + await testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 69', + 'New value should be different than initial after click'); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: Standard readonly mode is readonly', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="qux" invisible="1" />' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true}" />' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + assert.step(route); + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + assert.containsNone(form, '.o_progressbar_value.o_input', 'no input in readonly mode'); + + assert.verifySteps(["/web/dataset/call_kw/partner/read"]); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: max value should update in readonly mode with right parameter when typing in input with field max value', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="qux" invisible="1" />' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true, \'editable_readonly\': true}" />' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].qux, 69, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), "0.44444", 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 69', + 'New value should be different than initial after changing it'); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: value should update in readonly mode with right parameter when typing in input with field value', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'editable_readonly\': true}" />' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].int_field, 69, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), "99", 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, '69.6', ['input', 'blur']); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '69%', + 'New value should be different than initial after changing it'); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: write float instead of int works, in locale', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + translateParameters: { + thousands_sep: "#", + decimal_point: ":", + }, + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.strictEqual(args.args[1].int_field, 1037, + 'New value of progress bar saved'); + } + return this._super.apply(this, arguments); + } + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, '1#037:9', ['input', 'blur']); + + await testUtilsDom.click(form.$buttons.find('.o_form_button_save')); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '1k%', + 'New value should be different than initial after click'); + + form.destroy(); + }); + + QUnit.test('Field ProgressBar: write gibbrish instead of int throws warning', async function (assert) { + assert.expect(5); + this.data.partner.records[0].int_field = 99; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + interceptsPropagate: { + call_service: function (ev) { + if (ev.data.service === 'notification') { + assert.strictEqual(ev.data.method, 'notify'); + assert.strictEqual(ev.data.args[0].title, 'Wrong value entered!'); + } + } + }, + }); + + assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '99%', + 'Initial value should be correct'); + + await testUtilsDom.click(form.$('.o_progress')); + + var $valInput = form.$('.o_progressbar_value.o_input'); + assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct'); + + await testUtils.fields.editAndTrigger($valInput, 'trente sept virgule neuf', ['input']); + form.destroy(); }); diff --git a/addons/web/static/tests/views/search_panel_tests.js b/addons/web/static/tests/views/search_panel_tests.js index 233fa88d355..632158d9b1e 100644 --- a/addons/web/static/tests/views/search_panel_tests.js +++ b/addons/web/static/tests/views/search_panel_tests.js @@ -571,6 +571,64 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('category has been archived', async function (assert) { + assert.expect(2); + + this.data.company.fields.active = {type: 'boolean', string: 'Archived'}; + this.data.company.records = [ + { + name: 'Company 5', + id: 5, + active: true, + }, { + name: 'child of 5 archived', + parent_id: 5, + id: 666, + active: false, + }, { + name: 'child of 666', + parent_id: 666, + id: 777, + active: true, + } + ]; + + var kanban = await createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div> + <field name="foo"/> + </div> + </t> + </templates> + </kanban>`, + archs: { + 'partner,false,search': `<search><searchpanel><field name="company_id"/></searchpanel></search>`, + }, + mockRPC: async function (route, args) { + if (route === '/web/dataset/call_kw/partner/search_panel_select_range') { + var results = await this._super.apply(this, arguments); + results.values = results.values.filter(rec => rec.active !== false); + return Promise.resolve(results); + } + return this._super.apply(this, arguments); + }, + }); + + assert.containsN(kanban, '.o_search_panel_category_value', 2, + 'The number of categories should be 2: All and Company 5'); + + assert.containsNone(kanban, '.o_toggle_fold', + 'None of the categories should have children'); + + kanban.destroy(); + }); + QUnit.test('use two categories to refine search', async function (assert) { assert.expect(14); diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index c9571f59c51..b94c7108122 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -77,7 +77,16 @@ var SnippetEditor = Widget.extend({ return $clone; }, start: this._onDragAndDropStart.bind(this), - stop: this._onDragAndDropStop.bind(this), + stop: function () { + // Delay our stop handler so that some summernote handlers + // which occur on mouseup (and are themself delayed) are + // executed first (this prevents the library to crash + // because our stop handler may change the DOM). + var args = arguments; + setTimeout(function () { + self._onDragAndDropStop.apply(self, args); + }, 0); + }, }); } else { this.$('.o_move_handle').addClass('d-none'); diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 5b493dfef0d..eec85058ebc 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -700,6 +700,17 @@ registry.colorpicker = SnippetOption.extend({ this.$el.find('we-collapse').append($pt); } + + // TODO refactor in master + // The primary and secondary are hardcoded here (but marked as hidden) + // so they can be removed from snippets when selecting another color. + // Normally, the chosable colors do not contain them, which prevents + // them to be removed. Indeed, normally, the 'alpha' and 'beta' colors + // (which are the same) are displayed instead... but not for all themes. + var $colorpicker = this.$el.find('.colorpicker'); + $colorpicker.append($('<button/>', {'class': 'd-none', 'data-color': 'primary'})); + $colorpicker.append($('<button/>', {'class': 'd-none', 'data-color': 'secondary'})); + var classes = []; this.$el.find('.colorpicker button').each(function () { var $color = $(this); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index a6a9fb675fd..c291a00a0c7 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -1139,7 +1139,7 @@ var VideoWidget = MediaWidget.extend({ $video.attr('src', vinMatch[0] + '/embed/simple'); videoType = 'vin'; } else if (vimMatch && vimMatch[3].length) { - $video.attr('src', '//player.vimeo.com/video/' + vimMatch[3] + autoplay); + $video.attr('src', '//player.vimeo.com/video/' + vimMatch[3] + autoplay.replace('mute', 'muted')); videoType = 'vim'; } else if (dmMatch && dmMatch[2].length) { var justId = dmMatch[2].replace('video/', ''); diff --git a/addons/website_google_map/controllers/main.py b/addons/website_google_map/controllers/main.py index 092fab26a03..afa7dd784f4 100644 --- a/addons/website_google_map/controllers/main.py +++ b/addons/website_google_map/controllers/main.py @@ -38,7 +38,7 @@ class GoogleMap(http.Controller): "counter": len(partners), "partners": [] } - for partner in partners.with_context({'show_address': True}): + for partner in partners.with_context(show_address=True): # TODO in master, do not use `escape` but `t-esc` in the qweb template. partner_data["partners"].append({ 'id': partner.id, diff --git a/addons/website_twitter/static/src/js/website.twitter.animation.js b/addons/website_twitter/static/src/js/website.twitter.animation.js index e0c54bcb9d7..3bea076d7bd 100644 --- a/addons/website_twitter/static/src/js/website.twitter.animation.js +++ b/addons/website_twitter/static/src/js/website.twitter.animation.js @@ -9,6 +9,7 @@ var qweb = core.qweb; publicWidget.registry.twitter = publicWidget.Widget.extend({ selector: '.twitter', xmlDependencies: ['/website_twitter/static/src/xml/website.twitter.xml'], + disabledInEditableMode: false, events: { 'mouseenter .wrap-row': '_onEnterRow', 'mouseleave .wrap-row': '_onLeaveRow', |
