diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 53ce4e57056..a782e8c33e8 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -879,6 +879,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 516675c8837..cda6b5e6873 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1597,7 +1597,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 433defaffdb..04b59f11c05 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -609,7 +609,10 @@ - + + diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 6b4a5df4983..9c0b1753f8d 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -82,6 +82,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 cde10af75f1..87ffd0f3da0 100644 --- a/addons/account/wizard/setup_wizards.py +++ b/addons/account/wizard/setup_wizards.py @@ -39,6 +39,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/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 4abe323d06e..4f2fdc9606c 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -733,6 +733,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/data/account_tax_data.xml b/addons/l10n_fr/data/account_tax_data.xml index b7bdcf79e32..72e16a92e26 100644 --- a/addons/l10n_fr/data/account_tax_data.xml +++ b/addons/l10n_fr/data/account_tax_data.xml @@ -864,7 +864,7 @@ purchase - + 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 4e481d6892a..3f7ce1aed3d 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -134,10 +134,6 @@ MailManager.include({ }).always(function () { thread.isCreatingWindow = false; }); - } else if (!options.passively) { - if (threadWindow.isHidden()) { - this._makeThreadWindowVisible(threadWindow); - } } def.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 b87dbe0f794..df6b3de472d 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 @@ -222,6 +222,91 @@ 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', 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, + }); + + // 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(); + + 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]); + + $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 739ce795fe7..ac0c9c13882 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 """ @@ -820,6 +820,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() moves_to_do._action_done() moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do order._cal_price(moves_to_do) 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 16b83b04500..6f2e50dc7ec 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 7d8d20a0590..ec60ec4fbc5 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -105,9 +105,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 4d20b5afbeb..164d499085d 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -552,6 +552,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 fb76644adbf..b28e8f26318 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) @@ -1624,7 +1624,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 d791f3cc768..6c2b18a1f41 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -1186,7 +1186,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 62ccbd510f3..e12933d33db 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) return set(so_lines.ids), set(so_lines.mapped('order_id').ids) # -------------------------------------------------- diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 4855357acfe..6346d18465d 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 9a12cc30b4a..8e795c80f7c 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 f8c2cf09b32..e8e155ce926 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -89,7 +89,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 f6975debc6a..e576e094e30 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1655,7 +1655,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 5d613df8ed8..be7239cf9b3 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -271,7 +271,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': @@ -286,6 +285,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 5758b906421..b60cf81ac86 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -950,8 +950,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 d2d3d487885..2fca169e591 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 cfca0be8454..9acf9965982 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 @@ sAnimations.registry.websiteForum = sAnimations.Class.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_sale_stock/models/product_template.py b/addons/website_sale_stock/models/product_template.py index ee8ea1e3ac3..b0706bfee30 100644 --- a/addons/website_sale_stock/models/product_template.py +++ b/addons/website_sale_stock/models/product_template.py @@ -14,7 +14,7 @@ class ProductTemplate(models.Model): ('custom', 'Show product-specific notifications'), ], string='Inventory Availability', help='Adds an inventory availability status on the web product page.', default='never') available_threshold = fields.Float(string='Availability Threshold', default=5.0) - custom_message = fields.Text(string='Custom Message', default='') + custom_message = fields.Text(string='Custom Message', default='', translate=True) @api.multi def _get_combination_info(self, combination=False, product_id=False, add_qty=1, pricelist=False, reference_product=False, only_template=False): diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 8920c8e9a6b..d26f6b700a1 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -329,12 +329,17 @@ class IrHttp(models.AbstractModel): if not filename: if filename_field in record: filename = record[filename_field] - else: + if not filename: filename = "%s-%s-%s" % (record._name, record.id, field) if not mimetype: mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype) + # extension + extension = mimetypes.guess_extension(mimetype) + if extension and not filename.endswith(extension): + filename = "%s%s" % (filename, extension) + if not filehash: filehash = '"%s"' % hashlib.md5(pycompat.to_text(content).encode('utf-8')).hexdigest() diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 84680f04f7c..e2d0d87f637 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -855,7 +855,7 @@ class IrModelFields(models.Model): keys = [key for key in new_vals if old_vals[key] != new_vals[key]] self.pool.post_init(record.modified, keys) old_vals.update(new_vals) - if module and (module == model._original_module or module in field._modules): + if module and not field.manual and (module == model._original_module or module in field._modules): to_xmlids.append(name) if to_insert: diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py index 2dec12674de..6331af43660 100644 --- a/odoo/modules/migration.py +++ b/odoo/modules/migration.py @@ -36,6 +36,8 @@ class MigrationManager(object): Python file names must start by `pre-` or `post-` and will be executed, respectively, before and after the module initialisation. `end-` scripts are run after all modules have been updated. + A special folder named `0.0.0` can contain scripts that will be run on any version change. + In `pre` stage, `0.0.0` scripts are run first, while in `post` and `end`, they are run last. Example: `-- migrations @@ -48,6 +50,8 @@ class MigrationManager(object): |-- 9.0.1.1 # processed only on a 9.0 server | |-- pre-delete_table_z.py | `-- post-clean-data.py + |-- 0.0.0 + | `-- end-invariants.py # processed on all version update `-- foo.py # not processed """ @@ -94,13 +98,20 @@ class MigrationManager(object): return version # the version number already containt the server version return "%s.%s" % (release.major_version, version) - def _get_migration_versions(pkg): + def _get_migration_versions(pkg, stage): versions = sorted({ ver for lv in self.migrations[pkg.name].values() for ver, lf in lv.items() if lf }, key=lambda k: parse_version(convert_version(k))) + if "0.0.0" in versions: + # reorder versions + versions.remove("0.0.0") + if stage == "pre": + versions.insert(0, "0.0.0") + else: + versions.append("0.0.0") return versions def _get_migration_files(pkg, version, stage): @@ -127,10 +138,11 @@ class MigrationManager(object): parsed_installed_version = parse_version(installed_version) current_version = parse_version(convert_version(pkg.data['version'])) - versions = _get_migration_versions(pkg) + versions = _get_migration_versions(pkg, stage) for version in versions: - if parsed_installed_version < parse_version(convert_version(version)) <= current_version: + if ((version == "0.0.0" and parsed_installed_version < current_version) + or parsed_installed_version < parse_version(convert_version(version)) <= current_version): strfmt = {'addon': pkg.name, 'stage': stage,