diff --git a/addons/account/__init__.py b/addons/account/__init__.py index 866c39de02e..86d81923e02 100644 --- a/addons/account/__init__.py +++ b/addons/account/__init__.py @@ -45,6 +45,8 @@ def _auto_install_l10n(cr, registry): 'PL', 'PT', 'RO', 'SI', 'TR', 'UK', 'VE', 'VN' ]: module_list.append('base_vat') + if country_code == 'MX': + module_list.append('l10n_mx_edi') # European countries will be using SEPA europe = env.ref('base.europe', raise_if_not_found=False) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index f2a5e0d785b..8d45a4518c2 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -70,6 +70,12 @@ class AccountInvoice(models.Model): self.amount_total_signed = self.amount_total * sign self.amount_untaxed_signed = amount_untaxed_signed * sign + def _compute_sign_taxes(self): + for invoice in self: + sign = invoice.type in ['in_refund', 'out_refund'] and -1 or 1 + invoice.amount_untaxed_invoice_signed = invoice.amount_untaxed * sign + invoice.amount_tax_signed = invoice.amount_tax * sign + @api.onchange('amount_total') def _onchange_amount_total(self): for inv in self: @@ -347,8 +353,12 @@ class AccountInvoice(models.Model): store=True, readonly=True, compute='_compute_amount', tracking=True) amount_untaxed_signed = fields.Monetary(string='Untaxed Amount in Company Currency', currency_field='company_currency_id', store=True, readonly=True, compute='_compute_amount') + amount_untaxed_invoice_signed = fields.Monetary(string='Untaxed Amount in Invoice Currency', currency_field='currency_id', + readonly=True, compute='_compute_sign_taxes') amount_tax = fields.Monetary(string='Tax', store=True, readonly=True, compute='_compute_amount') + amount_tax_signed = fields.Monetary(string='Tax in Invoice Currency', currency_field='currency_id', + readonly=True, compute='_compute_sign_taxes') amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_compute_amount') amount_total_signed = fields.Monetary(string='Total in Invoice Currency', currency_field='currency_id', @@ -785,7 +795,7 @@ class AccountInvoice(models.Model): # Subscribe internal users on the newly created bill partners = self.env['res.partner'].browse(seen_partner_ids) is_internal = lambda p: (p.user_ids and - all(p.user_ids.mapped(lambda u: u.user_has_groups('base.group_user')))) + all(p.user_ids.mapped(lambda u: u.has_group('base.group_user')))) partners_to_subscribe = partners.filtered(is_internal) if partners_to_subscribe: invoice.message_subscribe([p.id for p in partners_to_subscribe]) @@ -1840,14 +1850,14 @@ class AccountInvoiceLine(models.Model): def _set_taxes(self): """ Used in on_change to set taxes and price""" self.ensure_one() - if self.invoice_id.type in ('out_invoice', 'out_refund'): - taxes = self.product_id.taxes_id or self.account_id.tax_ids or self.invoice_id.company_id.account_sale_tax_id - else: - taxes = self.product_id.supplier_taxes_id or self.account_id.tax_ids or self.invoice_id.company_id.account_purchase_tax_id # Keep only taxes of the company company_id = self.company_id or self.env.user.company_id - taxes = taxes.filtered(lambda r: r.company_id == company_id) + + if self.invoice_id.type in ('out_invoice', 'out_refund'): + taxes = self.product_id.taxes_id.filtered(lambda r: r.company_id == company_id) or self.account_id.tax_ids or self.invoice_id.company_id.account_sale_tax_id + else: + taxes = self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == company_id) or self.account_id.tax_ids or self.invoice_id.company_id.account_purchase_tax_id self.invoice_line_tax_ids = fp_taxes = self.invoice_id.fiscal_position_id.map_tax(taxes, self.product_id, self.invoice_id.partner_id) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a32bdb0b818..40526a35e70 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1060,7 +1060,7 @@ class AccountMoveLine(models.Model): partner_id = self.env['res.partner']._find_accounting_partner(self[0].partner_id).id company_currency = self[0].account_id.company_id.currency_id - writeoff_currency = self[0].currency_id or company_currency + writeoff_currency = self[0].account_id.currency_id or company_currency line_to_reconcile = self.env['account.move.line'] # Iterate and create one writeoff by journal writeoff_moves = self.env['account.move'] diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index baca985def8..405abd5c3ce 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -108,6 +108,16 @@ class account_payment(models.Model): # Check all invoices are open if any(invoice.state != 'open' for invoice in invoices): raise UserError(_("You can only register payments for open invoices")) + # Check if, in batch payments, there are not negative invoices and positive invoices + dtype = invoices[0].type + for inv in invoices[1:]: + if inv.type != dtype: + if ((dtype == 'in_refund' and inv.type == 'in_invoice') or + (dtype == 'in_invoice' and inv.type == 'in_refund')): + raise UserError(_("You cannot register payments for vendor bills and supplier refunds at the same time.")) + if ((dtype == 'out_refund' and inv.type == 'out_invoice') or + (dtype == 'out_invoice' and inv.type == 'out_refund')): + raise UserError(_("You cannot register payments for customer invoices and credit notes at the same time.")) amount = self._compute_payment_amount(invoices, invoices[0].currency_id) rec.update({ @@ -660,6 +670,20 @@ class account_payment(models.Model): return vals + def _get_invoice_payment_amount(self, inv): + """ + Computes the amount covered by the current payment in the given invoice. + + :param inv: an invoice object + :returns: the amount covered by the payment in the invoice + """ + self.ensure_one() + return sum([ + data['amount'] + for data in inv._get_payments_vals() + if data['account_payment_id'] == self.id + ]) + class payment_register(models.TransientModel): _name = 'account.payment.register' diff --git a/addons/account/models/res_users.py b/addons/account/models/res_users.py index ab85e359d22..8030423e74d 100644 --- a/addons/account/models/res_users.py +++ b/addons/account/models/res_users.py @@ -14,8 +14,8 @@ class Users(models.Model): super(Users, self)._check_one_user_type() users_with_both_groups = self.filtered(lambda user: - user.user_has_groups('account.group_show_line_subtotals_tax_included') and - user.user_has_groups('account.group_show_line_subtotals_tax_excluded') + 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')) diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index bad495b9290..10fdb27f620 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -64,6 +64,17 @@ + + account.analytic.line.billing.user + + [(1, '=', 1)] + + + + + + + diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 33b4430ab2f..b59b99bc881 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -277,6 +277,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { 'field_changed': '_onFieldChanged', }), _avoidFieldUpdate: {}, + MV_LINE_DEBOUNCE: 200, /** * create partner_id field in editable mode @@ -289,6 +290,11 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { this.model = model; this._initialState = state; + if (this.MV_LINE_DEBOUNCE) { + this._onSelectMoveLine = _.debounce(this._onSelectMoveLine, this.MV_LINE_DEBOUNCE, true); + } else { + this._onSelectMoveLine = this._onSelectMoveLine; + } }, /** * @override diff --git a/addons/account/static/src/scss/account_journal_dashboard.scss b/addons/account/static/src/scss/account_journal_dashboard.scss index 711021fbbad..b842a94f058 100644 --- a/addons/account/static/src/scss/account_journal_dashboard.scss +++ b/addons/account/static/src/scss/account_journal_dashboard.scss @@ -41,6 +41,15 @@ @include o-position-absolute($bottom: 25%, $right: 0); z-index: 1; } + .o_kanban_group { + &:not(.o_column_folded) { + width: 450px + 2*$o-kanban-group-padding; + + @include media-breakpoint-down(sm) { + width: 100%; + } + } + } } // Style for the widget "dashboard_graph" diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 5e8d71b466d..0248d0cec7e 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -585,6 +585,7 @@ odoo.define('account.reconciliation_tests', function (require) { "use strict"; var ReconciliationClientAction = require('account.ReconciliationClientAction'); +var ReconciliationRenderer = require('account.ReconciliationRenderer'); var demoData = require('account.reconciliation_tests.data'); var testUtils = require('web.test_utils'); @@ -592,7 +593,13 @@ var testUtils = require('web.test_utils'); QUnit.module('account', { beforeEach: function () { this.params = demoData.getParams(); - } + testUtils.patch(ReconciliationRenderer.LineRenderer, { + MV_LINE_DEBOUNCE: 0, + }); + }, + afterEach: function () { + testUtils.unpatch(ReconciliationRenderer.LineRenderer); + }, }, function () { QUnit.module('Reconciliation'); diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 1c67e6c4824..e81ca343db5 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -165,8 +165,8 @@ - - + + diff --git a/addons/account/views/account_portal_templates.xml b/addons/account/views/account_portal_templates.xml index 858a7cf58df..30303fdb6ee 100644 --- a/addons/account/views/account_portal_templates.xml +++ b/addons/account/views/account_portal_templates.xml @@ -83,7 +83,7 @@
- +

diff --git a/addons/account/views/report_payment_receipt_templates.xml b/addons/account/views/report_payment_receipt_templates.xml index 871ae39479f..c0e11a2ef7f 100644 --- a/addons/account/views/report_payment_receipt_templates.xml +++ b/addons/account/views/report_payment_receipt_templates.xml @@ -46,7 +46,7 @@ - + diff --git a/addons/account_cancel/views/account_views.xml b/addons/account_cancel/views/account_views.xml index e3dd3821294..18f308bd9c5 100644 --- a/addons/account_cancel/views/account_views.xml +++ b/addons/account_cancel/views/account_views.xml @@ -17,7 +17,7 @@ - +

diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 3ba8f85bf26..9d1eb319286 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -115,8 +115,11 @@ class ProductProduct(models.Model): def _get_fifo_candidates_in_move(self): """ Find IN moves that can be used to value OUT moves. """ + return self._get_fifo_candidates_in_move_with_company() + + def _get_fifo_candidates_in_move_with_company(self, move_company_id=False): self.ensure_one() - domain = [('product_id', '=', self.id), ('remaining_qty', '>', 0.0)] + self.env['stock.move']._get_in_base_domain() + domain = [('product_id', '=', self.id), ('remaining_qty', '>', 0.0)] + self.env['stock.move']._get_in_base_domain(move_company_id) candidates = self.env['stock.move'].search(domain, order='date, id') return candidates @@ -158,18 +161,30 @@ class ProductProduct(models.Model): for row in res: fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4])) - product_values = {product: 0 for product in self} - product_move_ids = {product: [] for product in self} + product_values = {product.id: 0 for product in self} + product_move_ids = {product.id: [] for product in self} + if to_date: domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain() - for move in StockMove.search(domain).with_context(prefetch_fields=False): - product_values[move.product_id] += move.value - product_move_ids[move.product_id].append(move.id) + value_field_name = 'value' else: domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain() - for move in StockMove.search(domain).with_context(prefetch_fields=False): - product_values[move.product_id] += move.remaining_value - product_move_ids[move.product_id].append(move.id) + value_field_name = 'remaining_value' + + StockMove.check_access_rights('read') + query = StockMove._where_calc(domain) + StockMove._apply_ir_rules(query, 'read') + from_clause, where_clause, params = query.get_sql() + query_str = """ + SELECT stock_move.product_id, SUM(COALESCE(stock_move.{}, 0.0)), ARRAY_AGG(stock_move.id) + FROM {} + WHERE {} + GROUP BY stock_move.product_id + """.format(value_field_name, from_clause, where_clause) + self.env.cr.execute(query_str, params) + for product_id, value, move_ids in self.env.cr.fetchall(): + product_values[product_id] = value + product_move_ids[product_id] = move_ids for product in self: if product.cost_method in ['standard', 'average']: @@ -185,9 +200,9 @@ class ProductProduct(models.Model): elif product.cost_method == 'fifo': if to_date: if product.product_tmpl_id.valuation == 'manual_periodic': - product.stock_value = product_values[product] + product.stock_value = product_values[product.id] product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available - product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product]) + product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product.id]) elif product.product_tmpl_id.valuation == 'real_time': valuation_account_id = product.categ_id.property_stock_valuation_account_id.id value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, []) @@ -195,10 +210,10 @@ class ProductProduct(models.Model): product.qty_at_date = quantity product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids) else: - product.stock_value, moves = product_values[product], StockMove.browse(product_move_ids[product]) + product.stock_value = product_values[product.id] product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available if product.product_tmpl_id.valuation == 'manual_periodic': - product.stock_fifo_manual_move_ids = moves + product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product.id]) elif product.product_tmpl_id.valuation == 'real_time': valuation_account_id = product.categ_id.property_stock_valuation_account_id.id value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, []) diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 82981e657cb..b816669e706 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -266,7 +266,7 @@ class StockMove(models.Model): # Find back incoming stock moves (called candidates here) to value this move. qty_to_take_on_candidates = quantity or valued_quantity - candidates = move.product_id._get_fifo_candidates_in_move() + candidates = move.product_id._get_fifo_candidates_in_move_with_company(move.company_id.id) new_standard_price = 0 tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: @@ -320,6 +320,7 @@ class StockMove(models.Model): def _run_valuation(self, quantity=None): self.ensure_one() + value_to_return = 0 if self._is_in(): valued_move_lines = self.move_line_ids.filtered(lambda ml: not ml.location_id._should_be_valued() and ml.location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 @@ -377,7 +378,7 @@ class StockMove(models.Model): 'value': value_to_return, 'price_unit': price_unit if self._is_dropshipped() else -price_unit, }) - return value_to_return + return value_to_return def _action_done(self, cancel_backorder=False): self.product_price_update_before_done() diff --git a/addons/stock_dropshipping/models/purchase.py b/addons/stock_dropshipping/models/purchase.py index 39196cd20bf..7fee03bc250 100644 --- a/addons/stock_dropshipping/models/purchase.py +++ b/addons/stock_dropshipping/models/purchase.py @@ -14,12 +14,12 @@ class PurchaseOrderLine(models.Model): re['sale_line_id'] = self.sale_line_id.id return res - def _merge_in_existing_line(self, product_id, product_qty, product_uom, location_id, name, origin, values): - if values.get('route_ids') and values['route_ids'] == self.env.ref('stock_dropshipping.route_drop_shipping'): - return False - return super(PurchaseOrderLine, self)._merge_in_existing_line( - product_id=product_id, product_qty=product_qty, product_uom=product_uom, - location_id=location_id, name=name, origin=origin, values=values) + def _find_candidate(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values): + # if this is defined, this is a dropshipping line, so no + # this is to correctly map delivered quantities to the so lines + lines = self.filtered(lambda po_line: po_line.sale_line_id.id == values['sale_line_id']) if values.get('sale_line_id') else self + return super(PurchaseOrderLine, lines)._find_candidate(product_id, product_qty, product_uom, location_id, name, origin, company_id, values) + class StockRule(models.Model): _inherit = 'stock.rule' diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py index 9837d93f0af..f70897f9eb8 100644 --- a/addons/stock_dropshipping/tests/test_dropship.py +++ b/addons/stock_dropshipping/tests/test_dropship.py @@ -48,8 +48,21 @@ class TestDropship(common.TransactionCase): so.order_line.product_uom_qty = 2.00 self.assertAlmostEqual(po_line.product_qty, 2.00) - -class TestDropship(common.TransactionCase): + # Create a new so line + sol2 = self.env['sale.order.line'].create({ + 'order_id': so.id, + 'name': prod.name, + 'product_id': prod.id, + 'product_uom_qty': 3.00, + 'product_uom': prod.uom_id.id, + 'price_unit': 12, + }) + # there is a new line + pol2 = po.order_line - po_line + # the first line is unchanged + self.assertAlmostEqual(po_line.product_qty, 2.00) + # the new line matches the new line on the so + self.assertAlmostEqual(pol2.product_qty, sol2.product_uom_qty) def test_00_dropship(self): diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 9b52a86f2ed..b7002cf052c 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -239,7 +239,8 @@ class Survey(models.Model): """ Ensure conditions to create new tokens are met. """ self.ensure_one() if test_entry: - if not user.has_group('survey.group_survey_manager') or not user.has_group('survey.group_survey_user'): + # the current user must have the access rights to survey + if not user.has_group('survey.group_survey_user'): raise UserError(_('Creating test token is not allowed for you.')) else: if not self.active: diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index daa8e446718..26ac5981768 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -120,7 +120,7 @@ diff --git a/addons/theme_bootswatch/static/lib/bootswatch/cyborg/_variables.scss b/addons/theme_bootswatch/static/lib/bootswatch/cyborg/_variables.scss index fda2a4bcc74..dc354a087ac 100644 --- a/addons/theme_bootswatch/static/lib/bootswatch/cyborg/_variables.scss +++ b/addons/theme_bootswatch/static/lib/bootswatch/cyborg/_variables.scss @@ -45,7 +45,7 @@ $body-color: $gray-500 !default; // Fonts -$font-family-sans-serif: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default; +$font-family-sans-serif: "Roboto", "Noto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !default; $font-size-base: .875rem !default; diff --git a/addons/web/doc/module.rst b/addons/web/doc/module.rst index ffbe90d0933..ca808254567 100644 --- a/addons/web/doc/module.rst +++ b/addons/web/doc/module.rst @@ -412,7 +412,7 @@ responses": .. note:: - By defaut, tests cases don't load templates either. We had not + By default, tests cases don't load templates either. We had not needed to perform any template rendering before here, so we must now enable templates loading via :js:attr:`the corresponding option `. diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 5919f100bcc..e546eb02be0 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -802,7 +802,7 @@ var FieldBoolean = AbstractField.extend({ * @returns {jQuery} the focusable checkbox input */ getFocusableElement: function () { - return this.$input || $(); + return this.mode === 'readonly' ? $() : this.$input; }, /** * A boolean field is always set since false is a valid value. @@ -1032,7 +1032,13 @@ var FieldFloatToggle = AbstractField.extend({ * @returns {float} The current formatted value */ _getDisplayedValue: function () { - return parseFloat(this._formatValue(this.value)); + // this.value is a plain float + // Matches what is in Database + var usrFormatValue = this._formatValue(this.value); + // usrFormatValue is string + // contains a float represented in a user specific format + // the float is the fraction by [this.factor] of this.value + return field_utils.parse['float'](usrFormatValue); }, /** * Formats the HTML input tag for edit mode and stores selection status. @@ -1059,7 +1065,7 @@ var FieldFloatToggle = AbstractField.extend({ * one is not in the range, the next value of the closest one will be chosen. * * @private - * @returns {number} The next formatted value in the range + * @returns {number} The next value in the range */ _nextValue: function () { var range = this.nodeOptions.range; @@ -1088,7 +1094,8 @@ var FieldFloatToggle = AbstractField.extend({ if (this.mode === 'edit') { ev.stopPropagation(); // only stop propagation in edit mode var next_val = this._nextValue(); - this._setValue(next_val.toString()); // will be parsed in _setValue + next_val = field_utils.format['float'](next_val); + this._setValue(next_val); // will be parsed in _setValue } }, diff --git a/addons/web/static/src/js/public/public_root.js b/addons/web/static/src/js/public/public_root.js index 5d0483ad886..31b39e97abb 100644 --- a/addons/web/static/src/js/public/public_root.js +++ b/addons/web/static/src/js/public/public_root.js @@ -294,8 +294,8 @@ var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, { var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit'); _.each($buttons, function (btn) { var $btn = $(btn); - $btn.attr('data-loading-text', ' ' + $(btn).text()); - $btn.button('loading'); + $btn.html(' ' + $btn.text()); + $btn.prop('disabled', true); }); }, /** diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js index 54e98bbb87e..e5eff8d7fc1 100644 --- a/addons/web/static/src/js/tools/debug_manager.js +++ b/addons/web/static/src/js/tools/debug_manager.js @@ -436,6 +436,7 @@ DebugManager.include({ var fields = state.fields; var fieldsInfo = state.fieldsInfo.form; var fieldNamesInView = state.getFieldNames(); + var fieldNamesOnlyOnView = ['message_attachment_count']; var fieldsValues = state.data; var modifierDatas = {}; _.each(fieldNamesInView, function (fieldName) { @@ -444,6 +445,7 @@ DebugManager.include({ }); }); this.fields = _.chain(fieldNamesInView) + .difference(fieldNamesOnlyOnView) .map(function (fieldName) { var modifierData = modifierDatas[fieldName]; var invisibleOrReadOnly; diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 804e6d6d1c9..2a22828b8d8 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -564,9 +564,16 @@ var BasicModel = AbstractModel.extend({ } } else if (field.type === 'one2many' || field.type === 'many2many') { if (options.raw) { - relDataPoint = this.localData[data[fieldName]]; - relDataPoint = this._applyX2ManyOperations(relDataPoint); - data[fieldName] = relDataPoint.res_ids; + if (typeof data[fieldName] === 'string') { + relDataPoint = this.localData[data[fieldName]]; + relDataPoint = this._applyX2ManyOperations(relDataPoint); + data[fieldName] = relDataPoint.res_ids; + } else { + // no datapoint has been created yet (because the loading of relational + // data has been batched, and hasn't started yet), so the value is still + // the list of ids in the relation + data[fieldName] = data[fieldName] || []; + } } else { data[fieldName] = this.get(data[fieldName]) || []; } diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 1bcf4fbb298..ba338a96e4d 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -60,10 +60,11 @@ return AbstractModel.extend({ end.add(-1, 'days'); } + var isDateEvent = this.fields[this.mapping.date_start].type === 'date'; // An "allDay" event without the "all_day" option is not considered // as a 24h day. It's just a part of the day (by default: 7h-19h). if (event.allDay) { - if (!this.mapping.all_day) { + if (!this.mapping.all_day && !isDateEvent) { if (event.r_start) { start.hours(event.r_start.hours()) .minutes(event.r_start.minutes()) diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 75c52ce81a7..3aeb1d2f6fb 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -252,6 +252,17 @@ var DateWidget = Widget.extend({ this.$input.select(); } }, + /** + * @private + * @param {KeyEvent} ev + */ + _onKeydown: function (ev) { + if (ev.which === $.ui.keyCode.ESCAPE) { + this.__libInput++; + this.$el.datetimepicker('hide'); + this.__libInput--; + } + }, /** * @private * @param {KeyEvent} ev diff --git a/addons/web/static/src/scss/bootstrap_overridden.scss b/addons/web/static/src/scss/bootstrap_overridden.scss index c1f8cbb44d3..50fde1fa6c2 100644 --- a/addons/web/static/src/scss/bootstrap_overridden.scss +++ b/addons/web/static/src/scss/bootstrap_overridden.scss @@ -61,7 +61,7 @@ $component-active-bg: $gray-200 !default; // // Font, line-height, and color for body text, headings, and more. -$font-family-sans-serif: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif !default; +$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif !default; $font-size-base: $o-font-size-base !default; $line-height-base: $o-line-height-base !default; diff --git a/addons/web/static/src/scss/fonts.scss b/addons/web/static/src/scss/fonts.scss index bfabf4fe6fc..0a8793c9a18 100644 --- a/addons/web/static/src/scss/fonts.scss +++ b/addons/web/static/src/scss/fonts.scss @@ -4,6 +4,36 @@ $lato-font-path: '../fonts/lato'; @mixin lato-font($type, $weight, $style) { + // Cyrillic: U+0400-04FF, U+0500-052F + @font-face { + font-family: 'Noto'; + src: url('https://fonts.odoocdn.com/fonts/noto/NotoSans-#{$type}.woff2') format('woff2'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSans-#{$type}.woff') format('woff'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSans-#{$type}.ttf') format('truetype'); + font-weight: $weight; + font-style: $style; + unicode-range: U+0400-04FF, U+0500-052F; + } + // Hebrew: U+0590-05FF, U+FB1D-FB4F + @font-face { + font-family: 'Noto'; + src: url('https://fonts.odoocdn.com/fonts/noto/NotoSansHebrew-#{$type}.woff2') format('woff2'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansHebrew-#{$type}.woff') format('woff'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansHebrew-#{$type}.ttf') format('truetype'); + font-weight: $weight; + font-style: $style; + unicode-range: U+0590-05FF, U+FB1D-FB4F; + } + // Arabic: U+0600-06FF, U+0750-077F + @font-face { + font-family: 'Noto'; + src: url('https://fonts.odoocdn.com/fonts/noto/NotoSansArabic-#{$type}.woff2') format('woff2'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansArabic-#{$type}.woff') format('woff'), + url('https://fonts.odoocdn.com/fonts/noto/NotoSansArabic-#{$type}.ttf') format('truetype'); + font-weight: $weight; + font-style: $style; + unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF; + } @font-face { font-family: 'Lato'; src: url('#{$lato-font-path}/Lato-#{$type}-webfont.eot'); diff --git a/addons/web/static/src/scss/layout_clean.scss b/addons/web/static/src/scss/layout_clean.scss index 910abc763a4..0a8f4b065af 100644 --- a/addons/web/static/src/scss/layout_clean.scss +++ b/addons/web/static/src/scss/layout_clean.scss @@ -1,5 +1,5 @@ .o_clean_footer, .o_clean_header, .o_report_layout_clean { - font-family: 'Lato'; + font-family: 'Lato', 'Noto'; color: #000; } .o_clean_header { diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 01f431589fc..94200b95f5e 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -2322,6 +2322,69 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('timzeone does not affect calendar with date field', async function (assert) { + assert.expect(8); + + var calendar = await createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + ''+ + '', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + mockRPC: function (route, args) { + if (args.method === "create") { + assert.strictEqual(args.args[0].start_date, "2016-12-20 00:00:00"); + } + if (args.method === "write") { + assert.step(args.args[1].start_date); + } + return this._super(route, args); + }, + session: { + getTZOffset: function () { + return 120; // 2 hours timezone + }, + }, + }); + + // Create event (on 20 december) + var $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)'); + testUtils.triggerMouseEvent($cell, "mousedown"); + testUtils.triggerMouseEvent($cell, "mouseup"); + await testUtils.nextTick(); + var $input = $('.modal-body input:first'); + await testUtils.fields.editInput($input, "An event"); + await testUtils.dom.click($('.modal button.btn:contains(Create)')); + await testUtils.nextTick(); + + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "12/20/2016") + + // Move event to another day (on 27 november) + testUtils.dragAndDrop( + calendar.$('.fc-event').first(), + calendar.$('.fc-day-top').first() + ); + assert.verifySteps(["2016-11-27 00:00:00"]); + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "11/27/2016") + + // Move event to last day (on 7 january) + await testUtils.dragAndDrop( + calendar.$('.fc-event').first(), + calendar.$('.fc-day-top').last() + ); + assert.verifySteps(["2016-11-27 00:00:00", "2017-01-07 00:00:00"]); + assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017") + + calendar.destroy(); + }); + QUnit.test('form_view_id attribute works (for creating events)', async function (assert) { assert.expect(1); diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js index e69b08aa3a8..af27cadad88 100644 --- a/addons/web/static/tests/views/kanban_model_tests.js +++ b/addons/web/static/tests/views/kanban_model_tests.js @@ -302,6 +302,70 @@ QUnit.module('Views', { }) }); + + QUnit.test('call get (raw: true) before loading x2many data', function (assert) { + // Sometimes, get can be called on a datapoint that is currently being + // reloaded, and thus in a partially updated state (e.g. in a kanban + // view, the user interacts with the searchview, and before the view is + // fully reloaded, it clicks on CREATE). Ideally, this shouldn't happen, + // but with the sync API of get, we can't change that easily. So at most, + // we can ensure that it doesn't crash. Moreover, sensitive functions + // requesting the state for more precise information that, e.g., the + // count, can do that in the mutex to ensure that the state isn't + // currently being reloaded. + // In this test, we have a grouped kanban view with a one2many, whose + // relational data is loaded in batch, once for all groups. We call get + // when the search_read for the first group has returned, but not the + // second (and thus, the read of the one2many hasn't started yet). + // Note: this test can be removed as soon as search_reads are performed + // alongside read_group. + assert.expect(2); + + this.data.partner.records[1].product_ids = [37, 41]; + this.params.fieldsInfo = { + kanban: { + product_ids: { + fieldsInfo: { + default: { display_name: {}, color: {} }, + }, + relatedFields: this.data.product.fields, + viewType: 'default', + }, + }, + }; + this.params.viewType = 'kanban'; + this.params.groupedBy = ['foo']; + + var block; + var def = testUtils.makeTestPromise(); + var model = createModel({ + Model: KanbanModel, + data: this.data, + mockRPC: function (route) { + var result = this._super.apply(this, arguments); + if (route === '/web/dataset/search_read' && block) { + block = false; + return Promise.all([def]).then(_.constant(result)); + } + return result; + }, + }); + + model.load(this.params).then(function (handle) { + block = true; + model.reload(handle, {}); + + var state = model.get(handle, {raw: true}); + assert.strictEqual(state.count, 2); + + def.resolve(); + + state = model.get(handle, {raw: true}); + assert.strictEqual(state.count, 2); + }); + + model.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index f10771a8e66..80e293a5126 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -4683,6 +4683,50 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('readonly boolean in editable list is readonly', function (assert) { + assert.expect(6); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '' + + '', + }); + + // clicking on disabled checkbox with active row does not work + var $disabledCell = list.$('.o_data_row:eq(1) .o_data_cell:last-child'); + testUtils.dom.click($disabledCell.prev()); + assert.containsOnce($disabledCell, ':disabled:checked'); + var $disabledLabel = $disabledCell.find('.custom-control-label'); + testUtils.dom.click($disabledLabel); + assert.containsOnce($disabledCell, ':checked', + "clicking disabled checkbox did not work" + ); + assert.ok( + $(document.activeElement).is('input[type="text"]'), + "disabled checkbox is not focused after click" + ); + + // clicking on enabled checkbox with active row toggles check mark + var $enabledCell = list.$('.o_data_row:eq(0) .o_data_cell:last-child'); + testUtils.dom.click($enabledCell.prev()); + assert.containsOnce($enabledCell, ':checked:not(:disabled)'); + var $enabledLabel = $enabledCell.find('.custom-control-label'); + testUtils.dom.click($enabledLabel); + assert.containsNone($enabledCell, ':checked', + "clicking enabled checkbox worked and unchecked it" + ); + assert.ok( + $(document.activeElement).is('input[type="checkbox"]'), + "enabled checkbox is focused after click" + ); + + list.destroy(); + }); + QUnit.test('grouped list with async widget', async function (assert) { assert.expect(4); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index 5980ab9211b..64c7f351e6a 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -732,6 +732,37 @@ QUnit.module('Search View', { actionManager.destroy(); }); + QUnit.test('Filter with JSON-parsable domain works', function (assert) { + assert.expect(1); + + var domain = [['foo' ,'=', 'Gently Weeps']]; + var xml_domain = JSON.stringify(domain); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (route === '/web/dataset/search_read') { + assert.deepEqual(args.domain, domain, + 'A JSON parsable xml domain should be handled just like any other'); + } + return this._super.apply(this, arguments); + } + }); + + this.archs['partner,5,search'] = + ''+ + '' + + ''; + this.actions[0].search_view_id = [5, 'search']; + this.actions[0].context = {search_default_gently_weeps: true}; + + actionManager.doAction(1); + + actionManager.destroy(); + }); + QUnit.module('Favorites Menu'); QUnit.test('dynamic filters are saved dynamic', async function (assert) { diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js index 506646d4697..dc2a444384b 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js @@ -173,6 +173,7 @@ var BulletPlugin = AbstractPlugin.extend({ } else { ul = this.document.createElement(sorted === "ol" ? "ol" : "ul"); ul.className = ol.className; + $(ul).removeClass('o_checklist'); $(ul).insertBefore(ol).append(lis); parent.removeChild(ol); } @@ -312,6 +313,10 @@ var BulletPlugin = AbstractPlugin.extend({ if (!range) { return; } + // list groups shouldn't be outdented like regular lists + if ($(dom.ancestor(range.sc, dom.isList)).hasClass('list-group')) { + return; + } var self = this; var nodes = []; diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js b/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js index 4ba44fe3c9b..521afe6137a 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js @@ -109,12 +109,14 @@ var HelperPlugin = AbstractPlugin.extend({ this.orderStyle(node); this.orderClass(otherNode); this.orderStyle(otherNode); - if (node.attributes.length !== otherNode.attributes.length) { + var nodeAttributes = this._getAttrsNoID(node); + var otherNodeAttributes = this._getAttrsNoID(otherNode); + if (nodeAttributes.length !== otherNodeAttributes.length) { return false; } - for (var i = 0; i < node.attributes.length; i++) { - var attr = node.attributes[i]; - var otherAttr = otherNode.attributes[i]; + for (var i = 0; i < nodeAttributes.length; i++) { + var attr = nodeAttributes[i]; + var otherAttr = otherNodeAttributes[i]; if (attr.name !== otherAttr.name || attr.value !== otherAttr.value) { return false; } @@ -306,6 +308,7 @@ var HelperPlugin = AbstractPlugin.extend({ var range = this.context.invoke('editor.createRange'); var spaceToRemove = []; + var offset; while ((node = nodes.pop())) { next = node[prevOrNext]; while (next && !next.tagName) { @@ -316,6 +319,31 @@ var HelperPlugin = AbstractPlugin.extend({ } break; } + + // handle merge of text with p (useful in case of
  • text
  • with
  • text

  • ) + if (next && dom.isText(node) && this.isNodeBlockType(next)) { + node = [next, next = node][0]; // swap node and next + direction = direction === 'next' ? 'prev' : 'next'; // switch direction + } + if (next && dom.isText(next) && this.isNodeBlockType(node)) { + if (this.isBlankNode(node, true)) { + $(node).empty(); + } + $(node)[direction === 'next' ? 'append' : 'prepend'](next); + offset = direction === 'next' ? 0 : dom.nodeLength(next); + if (dom.isText(next[prevOrNext])) { + // merge text if needed + var sib = next[prevOrNext]; + var firstText = (direction === 'next' ? sib : next).textContent; + var secondText = (direction === 'next' ? next : sib).textContent; + next.textContent = firstText + secondText; + $(sib).remove(); + offset = firstText.length; + } + result = this.makePoint(next, offset); + break; + } + if ( !next || !(node.tagName || next.tagName === 'BR') || @@ -328,7 +356,7 @@ var HelperPlugin = AbstractPlugin.extend({ var newNext = next[prevOrNext]; $(next).remove(); next = newNext; - var offset = (next ? direction === 'prev' : direction === 'next') ? dom.nodeLength(next) : 0; + offset = (next ? direction === 'prev' : direction === 'next') ? dom.nodeLength(next) : 0; result = this.makePoint(next || node, offset); if (!ifBrRemovedAndMerge) { continue; @@ -455,6 +483,11 @@ var HelperPlugin = AbstractPlugin.extend({ return this.makePoint(this.firstLeaf(blockToMergeFrom), 0); } + // UL/OL can only have LI, UL or OR as direct children + if (dom.isList(blockToMergeInto) && !dom.isLi(blockToMergeFrom) && !dom.isList(blockToMergeFrom)) { + return; + } + return this.mergeNonSimilarBlocks(blockToMergeFrom, blockToMergeInto); }, /** @@ -601,22 +634,24 @@ var HelperPlugin = AbstractPlugin.extend({ var li = dom.ancestor(node, function (n) { return n !== node && self.isNodeBlockType(n) || dom.isLi(n); }); + var nextElementSibling = direction === 'next' ? 'nextElementSibling' : 'previousElementSibling'; li = li && dom.isLi(li) ? li : undefined; - if (li && direction === 'next') { - if (li.nextElementSibling) { + if (li) { + if (li[nextElementSibling]) { node = li; } else { node = dom.ancestor(node, function (n) { - return ((n.tagName === 'UL' || n.tagName === 'OL') && n.nextElementSibling); + return ((n.tagName === 'UL' || n.tagName === 'OL') && n[nextElementSibling] && + (direction === 'next' || dom.isList(n[nextElementSibling]))); }); } } - if (!node || !node[direction === 'next' ? 'nextElementSibling' : 'previousElementSibling']) { + if (!node || !node[nextElementSibling]) { return false; } - node = node[direction === 'next' ? 'nextElementSibling' : 'previousElementSibling']; + node = node[nextElementSibling]; var ulFoldedSnippetNode = dom.ancestor(node, function (n) { return $(n).hasClass('o_ul_folded'); @@ -633,7 +668,7 @@ var HelperPlugin = AbstractPlugin.extend({ node = this.firstBlockAncestor(node); - li = dom.ancestor(node, function (n) { + li = dom.isList(node) ? undefined : dom.ancestor(node, function (n) { return n !== node && self.isNodeBlockType(n) || dom.isLi(n); }); li = li && dom.isLi(li) ? li : undefined; @@ -1051,9 +1086,15 @@ var HelperPlugin = AbstractPlugin.extend({ * but without any. * * @param {Node} node + * @param {Boolean} brIsBlank true to consider nodes with BR element + * as ultimate only child to be blank * @returns {Boolean} */ - isBlankNode: function (node) { + isBlankNode: function (node, brIsBlank) { + var self = this; + if (brIsBlank && dom.isBR(node)) { + return true; + } if (dom.isVoid(node) || dom.isIcon(node)) { return false; } @@ -1062,7 +1103,10 @@ var HelperPlugin = AbstractPlugin.extend({ }).test(node[dom.isText(node) ? 'textContent' : 'innerHTML'])) { return true; } - if (node.childNodes.length && _.all(node.childNodes, this.isBlankNode.bind(this))) { + var areAllChildrenBlank = _.all(node.childNodes, function (node) { + return self.isBlankNode(node, brIsBlank); + }); + if (node.childNodes.length && areAllChildrenBlank) { return true; } return false; @@ -1306,7 +1350,7 @@ var HelperPlugin = AbstractPlugin.extend({ $lastContents.after($contents); } } - while (mergeFromBlock.parentNode && this.isBlankNode(mergeFromBlock.parentNode)) { + while (mergeFromBlock.parentNode && this.isBlankNode(mergeFromBlock.parentNode, true)) { mergeFromBlock = mergeFromBlock.parentNode; } $(mergeFromBlock).remove(); @@ -1478,7 +1522,7 @@ var HelperPlugin = AbstractPlugin.extend({ if (isAfterBlank) { $(node.previousSibling).remove(); } - var isBeforeBlank = node.nextSibling && this.isBlankNode(node.nextSibling) + var isBeforeBlank = node.nextSibling && this.isBlankNode(node.nextSibling); if (isBeforeBlank) { $(node.nextSibling).remove(); } @@ -1733,6 +1777,15 @@ var HelperPlugin = AbstractPlugin.extend({ // Private //-------------------------------------------------------------------------- + _getAttrsNoID: function (node) { + var nodeAttributes = []; + for (var i = 0; i < node.attributes.length; i++) { + if (node.attributes[i].name !== 'id') { + nodeAttributes.push(node.attributes[i]); + } + } + return nodeAttributes; + }, _insertTextNodeInEditableArea: function (range, text) { // try to insert the text node in editable area var textNode = this.document.createTextNode(text); diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js index 3ab6a5f0317..94ec9306f7d 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js @@ -296,6 +296,7 @@ var KeyboardPlugin = AbstractPlugin.extend({ var next = this.context.invoke('HelperPlugin.splitTree', ancestor, point, { isSkipPaddingBlankHTML: !this.context.invoke('HelperPlugin.isNodeBlockType', point.node.parentNode) && !!point.node.parentNode.nextSibling }); + $(next).removeClass('o_checked'); while (next.firstChild) { next = next.firstChild; } diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js index d3ac908c507..16e22939cd4 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js @@ -85,7 +85,7 @@ var TextPlugin = AbstractPlugin.extend({ return; } nodes = textOnly ? this.document.createTextNode($(nodes).text()) : nodes; - nodes = this._mergeAdjacentULs(nodes); + nodes = textOnly ? nodes : this._mergeAdjacentULs(nodes); var point = this._getPastePoint(); // Prevent pasting HTML within a link: diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js index 63692848eb1..9bc9124688b 100644 --- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js +++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js @@ -918,7 +918,7 @@ var SnippetsMenu = Widget.extend({ && (float_next === 'left' || float_next === 'right')) { zone.remove(); } else if (disp_prev !== null && disp_next !== null - && disp_prev !== 'block' && disp_next !== 'block') { + && disp_prev.indexOf('inline') >= 0 && disp_next.indexOf('inline') >= 0) { zone.remove(); } }); diff --git a/addons/web_editor/static/src/scss/bootstrap_overridden.scss b/addons/web_editor/static/src/scss/bootstrap_overridden.scss index d1644a1174e..4517852076f 100644 --- a/addons/web_editor/static/src/scss/bootstrap_overridden.scss +++ b/addons/web_editor/static/src/scss/bootstrap_overridden.scss @@ -55,3 +55,5 @@ $gray-700: gray('700') !default; $gray-800: gray('800') !default; $gray-900: gray('900') !default; $black: gray('black') !default; + +$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; diff --git a/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js index 6a4e456cd00..6c08fec0d98 100644 --- a/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js +++ b/addons/web_editor/static/tests/wysiwyg_keyboard_tests.js @@ -1185,6 +1185,30 @@ var keyboardTestsEnter = [{ start: "br->0", }, }, + { + name: "in ul.o_checklist > li.o_checked > p: ENTER at end", + content: '
    • test

    ', + steps: [{ + start: "p:contents()[0]->4", + key: 'ENTER', + }], + test: { + content: '
    • test


    ', + start: "br->0", + }, + }, + { + name: "in ul.o_checklist > li.o_checked > p > b: ENTER within text", + content: '
    • test

    ', + steps: [{ + start: "b:contents()[0]->2", + key: 'ENTER', + }], + test: { + content: '
    • te

    • st

    ', + start: "b:eq(1):contents()[0]->0", + }, + }, // end list UL / OL @@ -2088,6 +2112,142 @@ var keyboardTestsComplex = [{ start: "h1:contents()[0]->15", }, }, + { + name: "in ul > li > ul > 1st li > empty p (other ul li ul before): BACKSPACE at start", + content: '
    • 1

      • 2


    • 3

    ', + steps: [{ + start: "p:eq(2)->0", + key: 'BACKSPACE', + }], + test: { + content: '
    • 1

      • 2

    • 3

    ', + start: "p:eq(1):contents()[0]->1", + }, + }, + { + name: "in ul > li > p > text (li > text before): BACKSPACE at start", + content: '
    • 1
    • 2

    • 3
    ', + steps: [{ + start: "p:contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: '
    • 1
    • 2

    • 3
    ', + start: "p:contents()[0]->1", + }, + }, + { + name: "in ul > li > p > text (li > text before): 3x BACKSPACE after first character", + content: '
    • 1
    • 2
    • 34

    • 5
    ', + steps: [{ + start: "p:contents()[0]->1", + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: '
    • 1
    • 4

    • 5
    ', + start: "p:contents()[0]->0", + }, + }, + { + name: "in ul > li > text (li > p > text before): BACKSPACE at start", + content: '
    • 1
    • 2

    • 3
    • 4
    ', + steps: [{ + start: "li:eq(2):contents()[0]->0", + key: 'BACKSPACE', + }], + test: { + content: '
    • 1
    • 23

    • 4
    ', + start: "p:contents()[0]->1", + }, + }, + { + name: "in ul > li > text (li > p > text before): 3x BACKSPACE after first character", + content: '
    • 1
    • 2

    • 34
    • 5
    ', + steps: [{ + start: "li:eq(2):contents()[0]->1", + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }, { + key: 'BACKSPACE', + }], + test: { + content: '
    • 1
    • 4

    • 5
    ', + start: "p:contents()[0]->0", + }, + }, + { + name: "in ul > li > p > text (li > text after): DELETE at end", + content: '
    • 1

    • 2
    • 3
    ', + steps: [{ + start: "p->0", + key: 'DELETE', + }], + test: { + content: '
    • 1
    • 2

    • 3
    ', + start: "p:contents()[0]->0", + }, + }, + { + name: "in ul > li > p > text (li > text after): 3x DELETE before last character", + content: '
    • 1
    • 23

    • 4
    • 5
    ', + steps: [{ + start: "p:contents()[0]->1", + key: 'DELETE', + }, { + key: 'DELETE', + }, { + key: 'DELETE', + }], + test: { + content: '
    • 1
    • 2

    • 5
    ', + start: "p:contents()[0]->1", + }, + }, + { + name: "in ul > li > text (li > p > text after): DELETE at end", + content: '
    • 1
    • 2
    • 3

    • 4
    ', + steps: [{ + start: "li:eq(1):contents()[0]->1", + key: 'DELETE', + }], + test: { + content: '
    • 1
    • 23

    • 4
    ', + start: "p:contents()[0]->1", + }, + }, + { + name: "in ul > li > text (li > p > text after): 3x DELETE before last character", + content: '
    • 1
    • 23
    • 4

    • 5
    ', + steps: [{ + start: "li:eq(1):contents()[0]->1", + key: 'DELETE', + }, { + key: 'DELETE', + }, { + key: 'DELETE', + }], + test: { + content: '
    • 1
    • 2

    • 5
    ', + start: "p:contents()[0]->1", + }, + }, + { + name: "in ul.list-group (div > p > text before): BACKSPACE at start", + content: '

    1


    ', + steps: [{ + start: "p:eq(1)->0", + key: 'BACKSPACE', + }], + test: { + content: '

    1


    ', + start: "p:eq(1)->0", + }, + }, ]; QUnit.test('Complex', function (assert) { diff --git a/addons/web_editor/static/tests/wysiwyg_tests.js b/addons/web_editor/static/tests/wysiwyg_tests.js index bb95a6776d4..7462f5a330b 100644 --- a/addons/web_editor/static/tests/wysiwyg_tests.js +++ b/addons/web_editor/static/tests/wysiwyg_tests.js @@ -1141,7 +1141,7 @@ QUnit.test('Text bgcolor', function (assert) { }); QUnit.test('Unordered list', function (assert) { - assert.expect(10); + assert.expect(34); return weTestUtils.createWysiwyg({ debug: false, @@ -1223,6 +1223,176 @@ QUnit.test('Unordered list', function (assert) { end: 'p:eq(2):contents()[0]->5', }, }, + // Conversion from OL + { + name: "Click UL: ol -> ul", + content: '

    dom not to edit

    1. dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + }, + }, + { + name: "Click UL: ol -> ul (across li's)", + content: '

    dom not to edit

    1. dom to edit

    2. dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • dom to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ol -> ul (from second li)", + content: '

    dom not to edit

    1. xxx

    2. dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. xxx

    • dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ul ol -> ul ul (from indented li)", + content: '

    dom not to edit

    • xxx

      1. dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      • dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ul ol -> ul ul (across several indented li)", + content: '

    dom not to edit

    • xxx

      1. dom to edit 1

      2. dom to edit 2

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      • dom to edit 1

      • dom to edit 2

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, + { + name: "Click UL: ul ol -> ul ul (from second indented li)", + content: '

    dom not to edit

    • xxx

      1. dom not to edit

      2. dom to edit

      3. dom not to edit

    ', + start: 'li:eq(2) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      1. dom not to edit

      • dom to edit

      1. dom not to edit

    ', + start: 'p:eq(3):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, + // Conversion from Checklist + { + name: "Click UL: ul.o_checklist -> ul", + content: '

    dom not to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + }, + }, + { + name: "Click UL: ul.o_checklist -> ul (across li's)", + content: '

    dom not to edit

    • dom to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • dom to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ul.o_checklist -> ul (from second li)", + content: '

    dom not to edit

    • xxx

    • dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

    • dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ul ul.o_checklist -> ul ul (from indented li)", + content: '

    dom not to edit

    • xxx

      • dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      • dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click UL: ul ul.o_checklist -> ul ul (across several indented li)", + content: '

    dom not to edit

    • xxx

      • dom to edit 1

      • dom to edit 2

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      • dom to edit 1

      • dom to edit 2

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, + { + name: "Click UL: ul ul.o_checklist -> ul ul (from second indented li)", + content: '

    dom not to edit

    • xxx

      • dom not to edit

      • dom to edit

      • dom not to edit

    ', + start: 'li:eq(2) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnUL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      • dom not to edit

      • dom to edit

      • dom not to edit

    ', + start: 'p:eq(3):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, ]; var def = Promise.resolve(); @@ -1244,7 +1414,7 @@ QUnit.test('Unordered list', function (assert) { }); QUnit.test('Ordered list', function (assert) { - assert.expect(32); + assert.expect(56); return weTestUtils.createWysiwyg({ debug: false, @@ -1675,6 +1845,176 @@ QUnit.test('Ordered list', function (assert) { start: 'font:contents(0)->11', }, }, + // Conversion from UL + { + name: "Click OL: ul -> ol", + content: '

    dom not to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + }, + }, + { + name: "Click OL: ul -> ol (across li's)", + content: '

    dom not to edit

    • dom to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. dom to edit

    2. dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ul -> ol (from second li)", + content: '

    dom not to edit

    • xxx

    • dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

    1. dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ol ul -> ol ol (from indented li)", + content: '

    dom not to edit

    1. xxx

      • dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. xxx

      1. dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ol ul -> ol ol (across several indented li)", + content: '

    dom not to edit

    1. xxx

      • dom to edit 1

      • dom to edit 2

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. xxx

      1. dom to edit 1

      2. dom to edit 2

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, + { + name: "Click OL: ol ul -> ol ol (from second indented li)", + content: '

    dom not to edit

    1. xxx

      • dom not to edit

      • dom to edit

      • dom not to edit

    ', + start: 'li:eq(2) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. xxx

      • dom not to edit

      1. dom to edit

      • dom not to edit

    ', + start: 'p:eq(3):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, + // Conversion from Checklist + { + name: "Click OL: ul.o_checklist -> ol", + content: '

    dom not to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(1):contents()[0]->5', + }, + }, + { + name: "Click OL: ul.o_checklist -> ol (across li's)", + content: '

    dom not to edit

    • dom to edit

    • dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. dom to edit

    2. dom to edit

    ', + start: 'p:eq(1):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ul.o_checklist -> ol (from second li)", + content: '

    dom not to edit

    • xxx

    • dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

    1. dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ol ul.o_checklist -> ol ol (from indented li)", + content: '

    dom not to edit

    1. xxx

      • dom to edit

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(1) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    1. xxx

      1. dom to edit

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(2):contents()[0]->5', + }, + }, + { + name: "Click OL: ul ul.o_checklist -> ul ol (across several indented li)", + content: '

    dom not to edit

    • xxx

      • dom to edit 1

      • dom to edit 2

    ', + start: 'li:eq(1) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      1. dom to edit 1

      2. dom to edit 2

    ', + start: 'p:eq(2):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, + { + name: "Click OL: ul ul.o_checklist -> ul ol (from second indented li)", + content: '

    dom not to edit

    • xxx

      • dom not to edit

      • dom to edit

      • dom not to edit

    ', + start: 'li:eq(2) p:contents()[0]->1', + end: 'li:eq(2) p:contents()[0]->5', + do: function () { + $btnOL.mousedown().click(); + }, + test: { + content: '

    dom not to edit

    • xxx

      • dom not to edit

      1. dom to edit

      • dom not to edit

    ', + start: 'p:eq(3):contents()[0]->1', + end: 'p:eq(3):contents()[0]->5', + }, + }, ]; @@ -2116,16 +2456,20 @@ QUnit.test('Indent/outdent', function (assert) { }); QUnit.test('checklist', function (assert) { - assert.expect(10); + assert.expect(11); return weTestUtils.createWysiwyg({ debug: false, wysiwygOptions: { - + generateOptions: function (options) { + options.toolbar[4][1] = ['ul', 'ol', 'checklist', 'paragraph']; + }, }, }).then(function (wysiwyg) { var $editable = wysiwyg.$('.note-editable'); + var $btnChecklist = wysiwyg.$('.note-para .fa-check-square'); + var checklistTests = [ { name: "check checkbox in checklist with children", @@ -2477,6 +2821,60 @@ QUnit.test('checklist', function (assert) { '

    y

    ', }, }, + { + name: "convert 2 ul li ul li into two ul li ul.o_checklist li", + content: '
      ' + + '
    • ' + + '

      1

      ' + + '
    • ' + + '
    • ' + + '
        ' + + '
      • ' + + '

        2

        ' + + '
      • ' + + '
      • ' + + '

        3

        ' + + '
      • ' + + '
      • ' + + '

        4

        ' + + '
      • ' + + '
      ' + + '
    • ' + + '
    • ' + + '

      5

      ' + + '
    • ' + + '
    ', + start: 'p:eq(2):contents()[0]->0', + end: 'p:eq(3):contents()[0]->1', + do: function () { + $btnChecklist.mousedown().click(); + }, + test: { + content: '
      ' + + '
    • ' + + '

      1

      ' + + '
    • ' + + '
    • ' + + '
        ' + + '
      • ' + + '

        2

        ' + + '
      • ' + + '
      ' + + '
        ' + + '
      • ' + + '

        3

        ' + + '
      • ' + + '
      • ' + + '

        4

        ' + + '
      • ' + + '
      ' + + '
    • ' + + '
    • ' + + '

      5

      ' + + '
    • ' + + '
    ', + }, + }, ]; var def = Promise.resolve(); @@ -2485,6 +2883,11 @@ QUnit.test('checklist', function (assert) { def = def.then(async function(){ testName = test.name; wysiwyg.setValue(test.content); + if (test.start) { + var range = weTestUtils.select(test.start, test.end || test.start, $editable); + $(range.sc).mousedown(); + Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo); + } await test.do(); assert.deepEqual(wysiwyg.getValue(), test.test.content, testName); }); diff --git a/addons/web_unsplash/static/src/js/unsplash_image_widget.js b/addons/web_unsplash/static/src/js/unsplash_image_widget.js index 01785fc12d9..3c0d1964513 100644 --- a/addons/web_unsplash/static/src/js/unsplash_image_widget.js +++ b/addons/web_unsplash/static/src/js/unsplash_image_widget.js @@ -229,7 +229,8 @@ widgetsMedia.ImageWidget.include({ params: params, }).then(function () { self.unsplashAPI.clientId = key; - self._renderImages(); + self._unsplash.error = false; + self.search(self._unsplash.query); }); } } diff --git a/addons/website/models/res_users.py b/addons/website/models/res_users.py index 6338a656d2c..a84eaaeb3c6 100644 --- a/addons/website/models/res_users.py +++ b/addons/website/models/res_users.py @@ -35,10 +35,12 @@ class ResUsers(models.Model): @api.model def _signup_create_user(self, values): - new_user = super(ResUsers, self)._signup_create_user(values) current_website = self.env['website'].get_current_website() if request and current_website.specific_user_account: - new_user.website_id = current_website + values['company_id'] = current_website.company_id.id + values['company_ids'] = [(4, current_website.company_id.id)] + values['website_id'] = current_website.id + new_user = super(ResUsers, self)._signup_create_user(values) return new_user @api.model diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 8f0c42d2873..7f57eb81f2c 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -965,7 +965,7 @@ registry.facebookPage = publicWidget.Widget.extend({ if (!params.href) { return def; } - params.width = utils.confine(this.$el.width(), 180, 500); + params.width = utils.confine(Math.floor(this.$el.width()), 180, 500); var src = $.param.querystring('https://www.facebook.com/plugins/page.php', params); this.$iframe = $('