diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 65a2ddf53cc..0c2aafe0f71 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -922,6 +922,8 @@ class AccountInvoice(models.Model): bank_id = bank_ids[0].id if bank_ids else False self.partner_bank_id = bank_id domain = {'partner_bank_id': [('id', 'in', bank_ids.ids)]} + elif type == 'out_invoice': + domain = {'partner_bank_id': [('partner_id.ref_company_ids', 'in', [self.company_id.id])]} res = {} if warning: diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8aba8cc0a85..0a64d37bb1e 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1675,7 +1675,8 @@ class AccountPartialReconcile(models.Model): move_date = self.debit_move_id.date newly_created_move = self.env['account.move'] with self.env.norecompute(): - for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): + # We use a set here in case the reconciled lines belong to the same move (it happens with POS) + for move in {self.debit_move_id.move_id, self.credit_move_id.move_id}: #move_date is the max of the 2 reconciled items if move_date < move.date: move_date = move.date diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 6dd9552541b..225ac6ac381 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -616,7 +616,10 @@ - + + diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 107623839de..2988b04843a 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -67,6 +67,7 @@
Rounding Method +
How total tax amount is computed in orders and invoices
diff --git a/addons/account/wizard/setup_wizards.py b/addons/account/wizard/setup_wizards.py index cb4820d4a0f..60ef897439b 100644 --- a/addons/account/wizard/setup_wizards.py +++ b/addons/account/wizard/setup_wizards.py @@ -40,6 +40,23 @@ class FinancialYearOpeningWizard(models.TransientModel): (wiz.fiscalyear_last_month, wiz.fiscalyear_last_day) ) + @api.multi + def write(self, vals): + # Amazing workaround: non-stored related fields on company are a BAD idea since the 3 fields + # must follow the constraint '_check_fiscalyear_last_day'. The thing is, in case of related + # fields, the inverse write is done one value at a time, and thus the constraint is verified + # one value at a time... so it is likely to fail. + for wiz in self: + wiz.company_id.write({ + 'account_opening_date': vals.get('opening_date') or wiz.company_id.account_opening_date, + 'fiscalyear_last_day': vals.get('fiscalyear_last_day') or wiz.company_id.fiscalyear_last_day, + 'fiscalyear_last_month': vals.get('fiscalyear_last_month') or wiz.company_id.fiscalyear_last_month, + }) + vals.pop('opening_date', None) + vals.pop('fiscalyear_last_day', None) + vals.pop('fiscalyear_last_month', None) + return super().write(vals) + @api.multi def action_save_onboarding_fiscal_year(self): self.env.user.company_id.set_onboarding_step_done('account_setup_fy_data_state') diff --git a/addons/base_geolocalize/models/base_geocoder.py b/addons/base_geolocalize/models/base_geocoder.py index 6da9c2f7f03..17a10f7e0e5 100644 --- a/addons/base_geolocalize/models/base_geocoder.py +++ b/addons/base_geolocalize/models/base_geocoder.py @@ -112,6 +112,8 @@ class GeoCoder(models.AbstractModel): self._raise_query_error(e) try: + if result['status'] == 'ZERO_RESULTS': + return None if result['status'] != 'OK': _logger.debug('Invalid Gmaps call: %s - %s', result['status'], result.get('error_message', '')) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 78bc490c674..e6857e7f4ad 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -734,6 +734,9 @@ help="Opportunities that are assigned to me"/> + diff --git a/addons/crm/views/crm_team_views.xml b/addons/crm/views/crm_team_views.xml index e16453588f4..73b191009cd 100644 --- a/addons/crm/views/crm_team_views.xml +++ b/addons/crm/views/crm_team_views.xml @@ -206,7 +206,7 @@
- + Open Opportunity Open Opportunities diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py index fb9213f96b2..aa3040d7947 100644 --- a/addons/google_account/models/google_service.py +++ b/addons/google_account/models/google_service.py @@ -189,12 +189,24 @@ class GoogleService(models.TransientModel): status = error.response.status_code response = "" else: - req = json.loads(error.request.body or 'null') - res = error.response.json() - _logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res)) + try: + exception_req = pformat(json.loads(error.request.body)) + except: + exception_req = error.request.body + + try: + exception_res = pformat(error.response.json()) + error_msg = _("Error while requesting Google Services: %s") % res['error']['message'] + warning_msg = _("Something went wrong with your request to google: %s") % res['error']['message'] + except: + exception_res = error.response.text + error_msg = _("Error while requesting Google Services") + warning_msg = _("Something went wrong with your request to google") + + _logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", exception_req, exception_res) if error.response.status_code in (400, 401, 410): - raise UserError(_("Error while requesting Google Services: %s") % res['error']['message']) - raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google: %s") % res['error']['message']) + raise UserError(error_msg) + raise self.env['res.config.settings'].get_config_warning(warning_msg) return (status, response, ask_time) # TODO : remove me, it is only used in google calendar. Make google_calendar use the constants diff --git a/addons/hr_timesheet/static/src/js/timesheet_uom.js b/addons/hr_timesheet/static/src/js/timesheet_uom.js index 503b923488c..345012d80e5 100644 --- a/addons/hr_timesheet/static/src/js/timesheet_uom.js +++ b/addons/hr_timesheet/static/src/js/timesheet_uom.js @@ -6,6 +6,7 @@ var basicFields = require('web.basic_fields'); var fieldUtils = require('web.field_utils'); var fieldRegistry = require('web.field_registry'); +var _registry = require('web._field_registry'); // wait registry to be filled... var session = require('web.session'); /** @@ -75,7 +76,6 @@ var FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({ * implementation (float_time, float_toggle, ...). The default * value will be 'float_factor'. **/ -var FieldTimesheetUom = FieldTimesheetFactor; var widgetName = 'timesheet_uom' in session ? session.timesheet_uom.timesheet_widget : 'float_factor'; var FieldTimesheetUom = widgetName === 'float_toggle' ? @@ -90,7 +90,7 @@ var _tweak_options = function(options) { options.factor = session.timesheet_uom_factor; } return options; -} +}; fieldUtils.format.timesheet_uom = function(value, field, options) { options = _tweak_options(options || {}); @@ -106,4 +106,3 @@ fieldUtils.parse.timesheet_uom = function(value, field, options) { return FieldTimesheetUom; }); - diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index fd4eb978262..057f3a49633 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -305,7 +305,9 @@ class AccountFrFec(models.TransientModel): END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, - CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib, + CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' + WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/' + ELSE replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index 083b46c9ff2..6214fc8b5b7 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -135,10 +135,6 @@ MailManager.include({ thread.close(); thread.isCreatingWindow = false; }); - } else if (!options.passively) { - if (threadWindow.isHidden()) { - this._makeThreadWindowVisible(threadWindow); - } } return prom.then(function () { threadWindow.updateVisualFoldState(); diff --git a/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js b/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js index 691e37df362..078230e276a 100644 --- a/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js +++ b/addons/mail/static/tests/thread_window/hidden_thread_window_tests.js @@ -229,6 +229,94 @@ QUnit.test('hidden thread windows dropdown when not enough horizontal space (ver testUtils.mock.unpatch(this.services.mail_service); }); +QUnit.test('receive message from hidden thread window', async function (assert) { + // This is almost the same test as the one before, except there are + // at most 3 thread windows visible, and 2 thread windows are visible + // when it shows the 'hidden thread window' button. + // This case occurs when the amount of available slots depends on + // whether the hidden button should be displayed or not + // Example: + // - global width of 400px + // - button width of 100px + // - thread width of 250px + // + // Without button: 1 thread window (250px < 800px) + // With button: 1 thread window (250px + 100px = 350px < 400px) + assert.expect(7); + + testUtils.patch(this.services.mail_service, { + HIDDEN_THREAD_WINDOW_DROPDOWN_BUTTON_WIDTH: 100, + THREAD_WINDOW_WIDTH: 250, + _getGlobalWidth: function () { return 400; }, + }); + + var channels = [{ + id: 1, + channel_type: 'channel', + name: "channel" + 1, + }, { + id: 2, + channel_type: 'channel', + name: "channel" + 2, + }]; + + this.data.initMessaging = { + channel_slots: { + channel_channel: channels, + }, + }; + + var parent = this.createParent({ + data: this.data, + services: this.services, + }); + await testUtils.nextTick(); + + // detach channel 2 first, so that chanel 1 is open and channel 2 is hidden + parent.call('mail_service', 'getChannel', 2).detach(); + parent.call('mail_service', 'getChannel', 1).detach(); + await testUtils.nextTick(); + + var $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)'); + + assert.strictEqual($visibleThreadWindows.length, 1, + "should have 1 thread windows visible (as many as available slots)"); + assert.strictEqual($visibleThreadWindows.filter('[data-thread-id="1"]').length, 1, + "the thread window with ID 1 should be visible"); + assert.strictEqual($('.o_thread_window.o_hidden[data-thread-id="2"]').length, 1, + "the thread window with ID 2 should be hidden"); + assert.strictEqual( + $('.o_thread_window_dropdown .o_total_unread_counter').text().trim(), + "", + "should have no unread counter on hidden dropup menu"); + + // simulate receiving a message in channel ID 2 + var messageData = { + author_id: [5, "Someone else"], + body: "

Test message

", + id: 1, + model: 'mail.channel', + res_id: 2, + channel_ids: [2], + }; + var notification = [[false, 'mail.channel', 2], messageData]; + parent.call('bus_service', 'trigger', 'notification', [notification]); + await testUtils.nextTick(); + + $visibleThreadWindows = $('.o_thread_window:not(.o_thread_window_dropdown, .o_hidden)'); + assert.strictEqual($visibleThreadWindows.filter('[data-thread-id="1"]').length, 1, + "the thread window with ID 1 should stay visible"); + assert.strictEqual($('.o_thread_window.o_hidden[data-thread-id="2"]').length, 1, + "the thread window with ID 2 should stay hidden"); + assert.strictEqual( + $('.o_thread_window_dropdown .o_total_unread_counter').text().trim(), + "1", + "should have unread counter of 1 on hidden dropup menu"); + + parent.destroy(); + testUtils.unpatch(this.services.mail_service); +}); + }); }); }); diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 8eab577cb36..eb046244c34 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -9,7 +9,7 @@ import math from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import AccessError, UserError -from odoo.tools import float_compare, float_round, DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools import float_compare, float_round, float_is_zero, DEFAULT_SERVER_DATETIME_FORMAT class MrpProduction(models.Model): """ Manufacturing Orders """ @@ -817,6 +817,8 @@ class MrpProduction(models.Model): moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) for move in moves_to_do.filtered(lambda m: m.product_qty == 0.0 and m.quantity_done > 0): move.product_uom_qty = move.quantity_done + for move in moves_to_do.filtered(lambda m: float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding)): + move._action_cancel() # MRP do not merge move, catch the result of _action_done in order # to get extra moves. moves_to_do = moves_to_do._action_done() diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 8e709a7df67..1c93de90212 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -14,8 +14,7 @@ class ReportBomStructure(models.AbstractModel): docs = [] for bom_id in docids: bom = self.env['mrp.bom'].browse(bom_id) - variant = data and data.get('variant') - candidates = variant and self.env['product.product'].browse(variant) or bom.product_tmpl_id.product_variant_ids + candidates = bom.product_id or bom.product_tmpl_id.product_variant_ids for product_variant_id in candidates: if data and data.get('childs'): doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=float(data.get('quantity')), child_bom_ids=json.loads(data.get('childs'))) diff --git a/addons/payment_stripe/i18n/payment_stripe.pot b/addons/payment_stripe/i18n/payment_stripe.pot index 22a18d4cc0d..007ca7be5c3 100644 --- a/addons/payment_stripe/i18n/payment_stripe.pot +++ b/addons/payment_stripe/i18n/payment_stripe.pot @@ -184,6 +184,12 @@ msgstr "" msgid "Stripe: no order found for reference %s" msgstr "" +#. module: payment_stripe +#: code:addons/payment_stripe/models/payment.py:260 +#, python-format +msgid "Stripe: no payment token was provided or the token creation failed." +msgstr "" + #. module: payment_stripe #: code:addons/payment_stripe/models/payment.py:170 #, python-format diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py index 78920e93580..f35a44165e7 100644 --- a/addons/payment_stripe/models/payment.py +++ b/addons/payment_stripe/models/payment.py @@ -257,7 +257,7 @@ class PaymentTokenStripe(models.Model): description = 'Partner: %s (id: %s)' % (partner_id.name, partner_id.id) if not token: - raise Exception('stripe_create: No token provided!') + raise UserError(_("Stripe: no payment token was provided or the token creation failed.")) res = self._stripe_create_customer(token, description, payment_acquirer.id) diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js index db3e48a9867..53c0b79e7d7 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -107,9 +107,17 @@ odoo.define('payment_stripe.stripe', function(require) { var invoice_num = get_input_value("invoice_num"); var merchant = get_input_value("merchant"); + // Search if the user wants to save the credit card information + var form_save_token = false; + var acquirer_form = $('#o_payment_form_acq_' + acquirer_id); + if (acquirer_form.length) { + form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked'); + } + ajax.jsonRpc(payment_tx_url, 'call', { acquirer_id: acquirer_id, access_token: access_token, + save_token: form_save_token, }).then(function(data) { var $pay_stripe = $('#pay_stripe').detach(); try { provider_form[0].innerHTML = data; } catch (e) {} diff --git a/addons/pos_discount/static/src/js/discount.js b/addons/pos_discount/static/src/js/discount.js index 87b97f88e16..fc41e55bc05 100644 --- a/addons/pos_discount/static/src/js/discount.js +++ b/addons/pos_discount/static/src/js/discount.js @@ -42,10 +42,16 @@ var DiscountButton = screens.ActionButtonWidget.extend({ } // Add discount + // We add the price as manually set to avoid recomputation when changing customer. var discount = - pc / 100.0 * order.get_total_with_tax(); if( discount < 0 ){ - order.add_product(product, { price: discount }); + order.add_product(product, { + price: discount, + extras: { + price_manually_set: true, + }, + }); } }, }); diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 8684c59a59b..4236a313c69 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -557,6 +557,16 @@ class ProductTemplate(models.Model): if variants_to_create: Product.create(variants_to_create) + # Avoid access errors in case the products is shared amongst companies but the underlying + # objects are not. If unlink fails because of an AccessError (e.g. while recomputing + # fields), the 'write' call will fail as well for the same reason since the field has + # been set to recompute. + if variants_to_unlink: + variants_to_unlink.check_access_rights('unlink') + variants_to_unlink.check_access_rule('unlink') + variants_to_unlink.check_access_rights('write') + variants_to_unlink.check_access_rule('write') + variants_to_unlink = variants_to_unlink.sudo() # unlink or inactive product # try in batch first because it is much faster try: diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 1c1bfc1db54..8347d283cfb 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1441,7 +1441,7 @@ class SaleOrderLine(models.Model): if currency != self.order_id.pricelist_id.currency_id: base_price = currency._convert( base_price, self.order_id.pricelist_id.currency_id, - self.order_id.company_id, self.order_id.date_order or fields.Date.today()) + self.order_id.company_id or self.env.user.company_id, self.order_id.date_order or fields.Date.today()) # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) @@ -1622,7 +1622,7 @@ class SaleOrderLine(models.Model): # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency new_list_price = currency._convert( new_list_price, self.order_id.pricelist_id.currency_id, - self.order_id.company_id, self.order_id.date_order or fields.Date.today()) + self.order_id.company_id or self.env.user.company_id, self.order_id.date_order or fields.Date.today()) discount = (new_list_price - price) / new_list_price * 100 if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0): self.discount = discount diff --git a/addons/sale/models/sales_team.py b/addons/sale/models/sales_team.py index aae3b6c6e31..6d297bb0ee5 100644 --- a/addons/sale/models/sales_team.py +++ b/addons/sale/models/sales_team.py @@ -57,7 +57,7 @@ class CrmTeam(models.Model): def _compute_sales_to_invoice(self): sale_order_data = self.env['sale.order'].read_group([ ('team_id', 'in', self.ids), - ('order_line.qty_to_invoice', '>', 0), + ('invoice_status','=','to invoice'), ], ['team_id'], ['team_id']) data_map = {datum['team_id'][0]: datum['team_id_count'] for datum in sale_order_data } for team in self: diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 574acba5afb..f140d78a2bb 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -1158,7 +1158,7 @@ form tree,form,calendar,graph,kanban,pivot - [('state', '=', 'sale'),('invoice_status','=','to invoice')] + [('invoice_status','=','to invoice')] { 'search_default_team_id': [active_id], 'default_team_id': active_id, diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py index 85ada359385..ce5e886ac9f 100644 --- a/addons/sale_margin/models/sale_order.py +++ b/addons/sale_margin/models/sale_order.py @@ -18,7 +18,8 @@ class SaleOrderLine(models.Model): if product_uom_id != product_id.uom_id: purchase_price = product_id.uom_id._compute_price(purchase_price, product_uom_id) price = frm_cur._convert( - purchase_price, to_cur, order_id.company_id, order_id.date_order or fields.Date.today(), round=False) + purchase_price, to_cur, order_id.company_id or self.env.user.company_id, + order_id.date_order or fields.Date.today(), round=False) return price @api.model diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py index 19e39d753ca..b9b7dc79490 100644 --- a/addons/sale_timesheet/models/project_overview.py +++ b/addons/sale_timesheet/models/project_overview.py @@ -324,7 +324,7 @@ class Project(models.Model): def _table_get_empty_so_lines(self): """ get the Sale Order Lines having no timesheet but having generated a task or a project """ - so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense) + so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense and not sol.is_downpayment) # include the service SO line of SO sharing the same project sale_order = self.env['sale.order'].search([('project_id', 'in', self.ids)]) return set(so_lines.ids) | set(sale_order.mapped('order_line').filtered(lambda sol: sol.is_service and not sol.is_expense).ids), set(so_lines.mapped('order_id').ids) | set(sale_order.ids) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 4f47f83cf56..faad5b30c43 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -396,10 +396,12 @@ class StockMove(models.Model): receipt_moves_to_reassign = self.env['stock.move'] if 'product_uom_qty' in vals: for move in self.filtered(lambda m: m.state not in ('done', 'draft') and m.picking_id): - if vals['product_uom_qty'] != move.product_uom_qty: + if float_compare(vals['product_uom_qty'], move.product_uom_qty, precision_rounding=move.product_uom.rounding): self.env['stock.move.line']._log_message(move.picking_id, move, 'stock.track_move_template', vals) if self.env.context.get('do_not_unreserve') is None: - move_to_unreserve = self.filtered(lambda m: m.state not in ['draft', 'done', 'cancel'] and m.reserved_availability > vals.get('product_uom_qty')) + move_to_unreserve = self.filtered( + lambda m: m.state not in ['draft', 'done', 'cancel'] and float_compare(m.reserved_availability, vals.get('product_uom_qty'), precision_rounding=m.product_uom.rounding) == 1 + ) move_to_unreserve._do_unreserve() (self - move_to_unreserve).filtered(lambda m: m.state == 'assigned').write({'state': 'partially_available'}) # When editing the initial demand, directly run again action assign on receipt moves. diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 631bf4379cf..315113f08f4 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -278,7 +278,10 @@ class StockMoveLine(models.Model): # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves. next_moves = self.env['stock.move'] if updates or 'qty_done' in vals: - for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'): + mls = self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product') + if not updates: # we can skip those where qty_done is already good up to UoM rounding + mls = mls.filtered(lambda ml: not float_is_zero(ml.qty_done - vals['qty_done'], precision_rounding=ml.product_uom_id.rounding)) + for ml in mls: # undo the original move line qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id, diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index b816669e706..404282ff39e 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -88,7 +88,10 @@ class StockMoveLine(models.Model): if 'qty_done' in vals: moves_to_update = {} for move_line in self.filtered(lambda ml: ml.state == 'done' and (ml.move_id._is_in() or ml.move_id._is_out())): - moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done + rounding = move_line.product_uom_id.rounding + qty_difference = float_round(vals['qty_done'] - move_line.qty_done, precision_rounding=rounding) + if not float_is_zero(qty_difference, precision_rounding=rounding): + moves_to_update[move_line.move_id] = qty_difference for move_id, qty_difference in moves_to_update.items(): move_vals = {} diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 546831a11ce..bf1b1139998 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1664,7 +1664,7 @@ var FieldBinaryFile = AbstractFieldBinary.extend({ 'id': this.res_id, 'field': this.name, 'filename_field': filename_fieldname, - 'filename': this.recordData[filename_fieldname] || null, + 'filename': this.recordData[filename_fieldname] || "", 'download': true, 'data': utils.is_bin_size(this.value) ? null : this.value, }, 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 c3934d930d7..57e7a36c403 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -274,7 +274,6 @@ return AbstractModel.extend({ this.data.highlight_date = this.data.target_date = start.clone(); // set dates in UTC with timezone applied manually this.data.start_date = this.data.end_date = start; - this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); switch (this.data.scale) { case 'month': @@ -289,6 +288,9 @@ return AbstractModel.extend({ this.data.start_date = this.data.start_date.clone().startOf('day'); this.data.end_date = this.data.end_date.clone().endOf('day'); } + + this.data.start_date.utc(); + this.data.end_date.utc(); }, /** * @param {string} scale the scale to set diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index e28a5baed61..04f818d825d 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -957,8 +957,8 @@ var PivotModel = AbstractModel.extend({ } if (measureComparisonValue instanceof Array) { dataPoints[groupIdentifier][measureName + 'Aggregate'].comparisonData = 1; - dataPoints[groupIdentifier][measureName].variation = computeVariation( - dataPoints[groupIdentifier][measureName].data, + dataPoints[groupIdentifier][measureName + 'Aggregate'].variation = computeVariation( + dataPoints[groupIdentifier][measureName + 'Aggregate'].data, 1 ); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 0907e471b39..fa171f21015 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -1654,7 +1654,7 @@ QUnit.module('Views', { results = [ "2", "0", "100%", "0", "1", "-100%", "2", "1", "100%" , "1", "0", "100%", "1", "0" , "100%" , - "1" , "0", "100%", "0", "1", "-100%", "1" , "1" , "100%" + "1" , "0", "100%", "0", "1", "-100%", "1" , "1" , "0%" ]; checkCellValues(results); diff --git a/addons/web_tour/static/src/js/tip.js b/addons/web_tour/static/src/js/tip.js index 38e67a8b48c..45e4ea2c90a 100644 --- a/addons/web_tour/static/src/js/tip.js +++ b/addons/web_tour/static/src/js/tip.js @@ -146,6 +146,7 @@ var Tip = Widget.extend({ }, _reposition: function () { if (this.tip_opened) return; + if (!this.$el) return; this.$el.removeClass("o_animated"); // Reverse left/right position if direction is right to left diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 42e3340e0ac..bdd1de724a4 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -114,7 +114,7 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ _.each($('textarea.load_editor'), function (textarea) { var $textarea = $(textarea); - var editorKarma = $textarea.data('karma') || 30; // default value for backward compatibility + var editorKarma = $textarea.data('karma') || 0; // default value for backward compatibility if (!$textarea.val().match(/\S/)) { $textarea.val('


'); } diff --git a/addons/website_profile/static/src/js/website_profile.js b/addons/website_profile/static/src/js/website_profile.js index b1638f4b30e..caccf454f1a 100644 --- a/addons/website_profile/static/src/js/website_profile.js +++ b/addons/website_profile/static/src/js/website_profile.js @@ -2,6 +2,7 @@ odoo.define('website_profile.website_profile', function (require) { 'use strict'; var publicWidget = require('web.public.widget'); +var Wysiwyg = require('web_editor.wysiwyg.root'); publicWidget.registry.websiteProfile = publicWidget.Widget.extend({ selector: '.o_wprofile_email_validation_container', read_events: { @@ -40,6 +41,47 @@ publicWidget.registry.websiteProfile = publicWidget.Widget.extend({ }, }); +publicWidget.registry.websiteProfileEditor = publicWidget.Widget.extend({ + selector: '.o_wprofile_editor_form', + read_events: { + 'click .o_wprofile_submit_btn': '_onSubmitClick', + }, + + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + if (this.editableMode) { + return def; + } + var $textarea = this.$('textarea.load_editor'); + var wysiwyg = new Wysiwyg(this, { + recordInfo: { + context: this._getContext(), + res_model: 'res.users', + res_id: parseInt(this.$('input[name=user_id]').val()), + }, + }); + + if (!$textarea.val().match(/\S/)) { + $textarea.val('


'); + } + return Promise.all([def, wysiwyg.attachTo($textarea)]); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onSubmitClick: function () { + this.$('textarea.load_editor').data('wysiwyg').save(); + }, +}); + return publicWidget.registry.websiteProfile; }); diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml index 8ea869d5455..3060431e87f 100644 --- a/addons/website_profile/views/website_profile.xml +++ b/addons/website_profile/views/website_profile.xml @@ -125,7 +125,7 @@
-
+
@@ -174,7 +174,7 @@ class="form-control load_editor">
- +
@@ -403,7 +403,7 @@
  • - Rank badge + Rank badge
    points diff --git a/addons/website_sale_slides/__manifest__.py b/addons/website_sale_slides/__manifest__.py index 951611eba62..d37fb3dbcda 100644 --- a/addons/website_sale_slides/__manifest__.py +++ b/addons/website_sale_slides/__manifest__.py @@ -11,6 +11,7 @@ 'installable': True, 'auto_install': True, 'data': [ + 'views/assets.xml', 'views/slide_channel_views.xml', 'views/website_slides_templates.xml', ], diff --git a/addons/website_sale_slides/static/src/js/slides_course_quiz.js b/addons/website_sale_slides/static/src/js/slides_course_quiz.js new file mode 100644 index 00000000000..9dc0154596a --- /dev/null +++ b/addons/website_sale_slides/static/src/js/slides_course_quiz.js @@ -0,0 +1,25 @@ +odoo.define('website_sale_slides.quiz', function (require) { +"use strict"; + +var sAnimations = require('website.content.snippets.animation'); +var Quiz = require('website_slides.quiz'); + +sAnimations.registry.websiteSlidesQuizNoFullscreen.include({ + _extractChannelData: function (slideData){ + return _.extend({}, this._super.apply(this, arguments), { + productId: slideData.productId, + enroll: slideData.enroll, + currencyName: slideData.currencyName, + currencySymbol: slideData.currencySymbol, + price: slideData.price, + hasDiscountedPrice: slideData.hasDiscountedPrice + }); + } +}); + +Quiz.include({ + xmlDependencies: (Quiz.prototype.xmlDependencies || []).concat( + ["/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml"] + ) +}); +}); diff --git a/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml b/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml new file mode 100644 index 00000000000..bcf0dc5a032 --- /dev/null +++ b/addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml @@ -0,0 +1,43 @@ + + + + + +
    +
    + + + Buy the course + + +
    + + + + +
    +
    +
    + + + diff --git a/addons/website_sale_slides/views/assets.xml b/addons/website_sale_slides/views/assets.xml new file mode 100644 index 00000000000..ce0ecddc580 --- /dev/null +++ b/addons/website_sale_slides/views/assets.xml @@ -0,0 +1,10 @@ + + + +