summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-20 14:16:48 +0000
committerChristophe Simonis <chs@odoo.com>2019-09-20 14:33:54 +0000
commit58a83d12228fbbd4e9dde542e133d264204ee0f3 (patch)
tree60aaf45e3281e88fddc58fea8fe75de0ad4e2f46 /addons
parentf6da4d57bcc8e19ae0bd6d571926706bee46e08f (diff)
parent4a1321bc99dd7d8d153aceb2d0076161ce28bcd7 (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')
-rw-r--r--addons/account/models/account.py8
-rw-r--r--addons/account/models/account_move.py10
-rw-r--r--addons/account/models/account_payment.py19
-rw-r--r--addons/account/models/chart_template.py4
-rw-r--r--addons/account/models/partner.py4
-rw-r--r--addons/account/models/res_users.py13
-rw-r--r--addons/account/tests/test_payment.py118
-rw-r--r--addons/account/tests/test_reconciliation_matching_rules.py6
-rw-r--r--addons/account/views/report_invoice.xml2
-rw-r--r--addons/account/views/report_payment_receipt_templates.xml2
-rw-r--r--addons/delivery/models/stock_move.py2
-rw-r--r--addons/delivery/models/stock_picking.py2
-rw-r--r--addons/event_sale/models/event.py2
-rw-r--r--addons/l10n_de/static/src/scss/report_din5008.scss1
-rw-r--r--addons/l10n_fr/data/account_tax_data.xml2
-rw-r--r--addons/l10n_multilang/models/l10n_multilang.py3
-rw-r--r--addons/lunch/static/tests/lunch_kanban_tests.js49
-rw-r--r--addons/mail/models/mail_alias.py2
-rw-r--r--addons/mail_bot/models/mail_bot.py2
-rw-r--r--addons/mail_bot/models/mail_channel.py2
-rw-r--r--addons/mrp_account/models/product.py8
-rw-r--r--addons/payment/controllers/portal.py6
-rw-r--r--addons/payment/models/payment_acquirer.py4
-rw-r--r--addons/payment/static/src/js/payment_processing.js2
-rw-r--r--addons/payment_paypal/controllers/main.py8
-rw-r--r--addons/payment_payumoney/models/payment.py2
-rw-r--r--addons/point_of_sale/static/src/css/pos.css7
-rw-r--r--addons/point_of_sale/static/src/js/screens.js2
-rw-r--r--addons/product/models/product.py1
-rw-r--r--addons/product/report/product_pricelist_templates.xml6
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale_mrp/tests/test_sale_mrp_procurement.py102
-rw-r--r--addons/snailmail/models/snailmail_letter.py2
-rw-r--r--addons/stock/models/stock_move.py2
-rw-r--r--addons/stock/models/stock_warehouse.py5
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js101
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js3
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_bar.js20
-rw-r--r--addons/web/static/src/js/views/control_panel/search/search_facet.js20
-rw-r--r--addons/web/static/src/js/views/search_panel.js5
-rw-r--r--addons/web/static/src/js/widgets/auto_complete.js14
-rw-r--r--addons/web/static/src/scss/banner.scss3
-rw-r--r--addons/web/static/src/scss/modal.scss5
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js441
-rw-r--r--addons/web/static/tests/views/search_panel_tests.js58
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js11
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js11
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/media.js2
-rw-r--r--addons/website_google_map/controllers/main.py2
-rw-r--r--addons/website_twitter/static/src/js/website.twitter.animation.js1
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',