summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-06-27 18:01:07 +0200
committerChristophe Simonis <chs@odoo.com>2019-06-27 18:01:07 +0200
commit465909f0fde4d02b101d35c61bab8f0dea20f003 (patch)
tree99774aad119bf339ab9e548577bff7b3bc5c186e /addons
parentcd8afe8546378130a36cca6f5e0f7cd3198ecad9 (diff)
parentcd21c016b0652f2fc36a33690f676ba6e2f113f1 (diff)
[MERGE] forward port branch 12.0 up to cd21c016b06
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_invoice.py2
-rw-r--r--addons/account/models/account_move.py3
-rw-r--r--addons/account/views/account_invoice_view.xml5
-rw-r--r--addons/account/views/res_config_settings_views.xml1
-rw-r--r--addons/account/wizard/setup_wizards.py17
-rw-r--r--addons/crm/views/crm_lead_views.xml3
-rw-r--r--addons/crm/views/crm_team_views.xml2
-rw-r--r--addons/google_account/models/google_service.py22
-rw-r--r--addons/hr_timesheet/static/src/js/timesheet_uom.js5
-rw-r--r--addons/l10n_fr/data/account_tax_data.xml2
-rw-r--r--addons/l10n_fr_fec/wizard/account_fr_fec.py4
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js4
-rw-r--r--addons/mail/static/tests/thread_window/hidden_thread_window_tests.js85
-rw-r--r--addons/mrp/models/mrp_production.py4
-rw-r--r--addons/mrp/report/mrp_report_bom_structure.py3
-rw-r--r--addons/payment_stripe/i18n/payment_stripe.pot6
-rw-r--r--addons/payment_stripe/models/payment.py2
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js8
-rw-r--r--addons/pos_discount/static/src/js/discount.js8
-rw-r--r--addons/product/models/product_template.py10
-rw-r--r--addons/sale/models/sale.py4
-rw-r--r--addons/sale/models/sales_team.py2
-rw-r--r--addons/sale/views/sale_views.xml2
-rw-r--r--addons/sale_margin/models/sale_order.py3
-rw-r--r--addons/sale_timesheet/models/project_overview.py2
-rw-r--r--addons/stock/models/stock_move.py6
-rw-r--r--addons/stock/models/stock_move_line.py5
-rw-r--r--addons/stock_account/models/stock.py5
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js2
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js4
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js4
-rw-r--r--addons/web/static/tests/views/pivot_tests.js2
-rw-r--r--addons/web_tour/static/src/js/tip.js1
-rw-r--r--addons/website_forum/static/src/js/website_forum.js2
-rw-r--r--addons/website_sale_stock/models/product_template.py2
35 files changed, 203 insertions, 39 deletions
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 @@
<field name="fiscal_position_id" options="{'no_create': True}" />
<field name="move_id" groups="account.group_account_user" attrs="{'invisible': [('move_id', '=', False)]}"/>
<field name="origin" groups="base.group_user" attrs="{'invisible': [('origin', '=', False)]}"/>
- <field name="partner_bank_id" domain="[('partner_id.ref_company_ids', 'in', [company_id])]" context="{'default_partner_id':company_id}"/>
+ <!-- Since the same form view is used for customer invoices and credit notes,
+ we can't set a domain or a default value context on partner_bank_id. We must
+ rely on the onchange -->
+ <field name="partner_bank_id"/>
</group>
</group>
<field name="tax_line_ids">
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 @@
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<span class="o_form_label">Rounding Method</span>
+ <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
How total tax amount is computed in orders and invoices
</div>
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"/>
<filter string="Unassigned" name="unassigned"
domain="[('user_id','=', False)]" help="No salesperson"/>
+ <filter string="Open Opportunities" name="open_opportunities"
+ domain="[('probability', '&lt;', 100), ('type', '=', 'opportunity')]"
+ help="Open Opportunities"/>
<separator/>
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/>
<separator/>
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 @@
</div>
<div class="row" t-if="record.opportunities_count.raw_value">
<div class="col-8">
- <a name="%(crm_case_form_view_salesteams_opportunity)d" type="action"> <!-- context="{'search_default_probability': NOT or < 100}" -->
+ <a name="%(crm_case_form_view_salesteams_opportunity)d" type="action" context="{'search_default_open_opportunities': True}"> <!-- context="{'search_default_probability': NOT or < 100}" -->
<field name="opportunities_count"/>
<t t-if="record.opportunities_count.raw_value == 1">Open Opportunity</t>
<t t-else="">Open Opportunities</t>
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 @@
<field name="account_id" ref="pcg_44566"/>
<field name="sequence" eval="10"/>
<field name="type_tax_use">purchase</field>
- <field name="tag_ids" eval="[(6,0,[ref('l10n_fr.tag_fr_08'),ref('l10n_fr.tag_fr_17')])]"/>
+ <field name="tag_ids" eval="[(6,0,[ref('l10n_fr.tag_fr_27'),ref('l10n_fr.tag_fr_36')])]"/>
<field name="tax_group_id" ref="tax_group_tva_55"/>
</record>
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: "<p>Test message</p>",
+ 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
@@ -185,6 +185,12 @@ 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
msgid "We're sorry to report that the transaction has failed."
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 @@
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,graph,kanban,pivot</field>
<field name="search_view_id" ref="sale.sale_order_view_search_inherit_sale"/>
- <field name="domain">[('state', '=', 'sale'),('invoice_status','=','to invoice')]</field>
+ <field name="domain">[('invoice_status','=','to invoice')]</field>
<field name="context">{
'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('<p><br/></p>');
}
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):