summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-06-27 19:10:09 +0200
committerChristophe Simonis <chs@odoo.com>2019-06-27 19:10:09 +0200
commit6c5783bd5a8b20dce99b26a81e734a50c168bae0 (patch)
tree28f6179342d5799f1cee8bd5da21dc638c70b49a
parentfdcabf19b27cb5427b8f55f922479cb740d590f3 (diff)
parent465909f0fde4d02b101d35c61bab8f0dea20f003 (diff)
[MERGE] forward port branch saas-12.2 up to 465909f0fde
-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/base_geolocalize/models/base_geocoder.py2
-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_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.js88
-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_profile/static/src/js/website_profile.js42
-rw-r--r--addons/website_profile/views/website_profile.xml6
-rw-r--r--addons/website_sale_slides/__manifest__.py1
-rw-r--r--addons/website_sale_slides/static/src/js/slides_course_quiz.js25
-rw-r--r--addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml43
-rw-r--r--addons/website_sale_slides/views/assets.xml10
-rw-r--r--addons/website_sale_slides/views/website_slides_templates.xml22
-rw-r--r--addons/website_sale_stock/models/product_template.py2
-rw-r--r--addons/website_slides/controllers/main.py6
-rw-r--r--addons/website_slides/static/src/js/slides_course_fullscreen_player.js22
-rw-r--r--addons/website_slides/static/src/js/slides_course_join.js9
-rw-r--r--addons/website_slides/static/src/js/slides_course_quiz.js20
-rw-r--r--addons/website_slides/static/src/xml/channel_management.xml12
-rw-r--r--addons/website_slides/static/src/xml/slide_quiz.xml54
-rw-r--r--addons/website_slides/views/website_slides_templates_homepage.xml2
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson.xml5
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml5
-rw-r--r--odoo/addons/base/models/ir_http.py7
-rw-r--r--odoo/addons/base/models/ir_model.py2
-rw-r--r--odoo/addons/base/views/ir_qweb_widget_templates.xml2
-rw-r--r--odoo/modules/migration.py18
55 files changed, 478 insertions, 80 deletions
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 @@
<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 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 @@
<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 cb4820d4a0f..60ef897439b 100644
--- a/addons/account/wizard/setup_wizards.py
+++ b/addons/account/wizard/setup_wizards.py
@@ -41,6 +41,23 @@ class FinancialYearOpeningWizard(models.TransientModel):
)
@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"/>
<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_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: "<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]);
+ 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
@@ -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 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 @@
<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 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('<p><br/></p>');
}
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('<p><br/></p>');
+ }
+ 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 @@
</div>
<div class="col-9 mb-3">
<div class="card">
- <div class="card-body">
+ <div class="card-body o_wprofile_editor_form">
<div class="row">
<input name="user_id" t-att-value="user.id" type="hidden"/>
<div class="form-group col-12">
@@ -174,7 +174,7 @@
class="form-control load_editor"><t t-esc="user.partner_id.website_description"/></textarea>
</div>
<div class="col">
- <button type="submit" class="btn btn-primary btn">Update</button>
+ <button type="submit" class="btn btn-primary o_wprofile_submit_btn">Update</button>
</div>
</div>
</div>
@@ -403,7 +403,7 @@
<t t-foreach="ranks" t-as="rank">
<li t-attf-class="list-group-item">
<div class="media align-items-center">
- <img height="40" t-attf-src="/web/image/gamification.karma.rank/#{rank.id}/image" class="mr-2" alt="Rank badge"/>
+ <img width="40" t-attf-src="/web/image/gamification.karma.rank/#{rank.id}/image" class="mr-2" alt="Rank badge"/>
<div class="media-body">
<h5 class="mt-0 mb-0" t-field="rank.name"/>
<span class="badge badge-success"><span t-field="rank.karma_min"/></span> point<span t-if="rank.karma_min">s</span>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-extend="slide.slide.quiz.validation">
+ <t t-jquery="#validation" t-operation="append">
+ <div t-if="widget.readonly &amp;&amp; widget.publicUser &amp;&amp; widget.channel.channelEnroll == 'payment'" class="alert alert-info d-flex align-items-center justify-content-between">
+ <div>
+ <b class="h5 mb-0">Sign in and buy the course to join it and take the quiz!</b>
+ <span class="my-0 h4" style="line-height: 1">
+ <span title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2 py-1">
+ + <t t-esc="widget.quiz.quizKarmaGain"/> XP
+ </span>
+ </span>
+ </div>
+ <div>
+ <a t-att-href="'/web/login?redirect=' + widget.redirectURL" class="btn btn-primary font-weight-bold text-uppercase">Sign in</a>
+ <span t-if="widget.channel.signupAllowed" class="d-block mt-2">Don't have an account ? <a class="font-weight-bold" t-att-href="'/web/signup?redirect=' + widget.url">Sign Up !</a></span>
+ </div>
+ </div>
+ <div t-if="!widget.publicUser &amp;&amp; widget.channel.channelEnroll == 'payment'" class="card col-md-3">
+ <div class="card-body d-flex flex-column align-items-center">
+ <a role="button"
+ class="btn btn-primary btn-block o_wslides_js_course_join_link d-block mb-2"
+ title="Start Course" aria-label="Start Course Channel"
+ t-attf-href="/shop/cart/update?product_id=#{widget.channel.productId}&amp;express=1"
+ t-att-data-channel-id="widget.slide.channelId">
+ <span class="text-white text-uppercase font-weight-bold">
+ Buy the course
+ </span>
+ </a>
+ <div class="text-center">
+ <del t-if="widget.channel.hasDiscountedPrice"
+ class="text-600 text-nowrap oe_default_price d-inline"
+ t-esc="widget.channel.listPrice"/>
+ <span class="oe_price font-weight-bold text-nowrap h3 my-2"
+ t-esc="widget.channel.price"/>
+ <span t-if="widget.channel.currencySymbol" class="text-nowrap font-weight-bold h3 my-2" itemprop="priceCurrency" t-esc="widget.channel.currencySymbol"/>
+ <span t-else="" class="text-nowrap ml-1" itemprop="priceCurrency" t-esc="widget.channel.currencyName"/>
+ </div>
+ </div>
+ </div>
+ </t>
+ </t>
+</templates>
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 @@
+<?xml version="1.0" ?>
+<odoo>
+ <data>
+ <template id="assets_frontend" inherit_id="website.assets_frontend" name="Buy course on quiz">
+ <xpath expr="//script[last()]" position="after">
+ <script type="text/javascript" src="/website_sale_slides/static/src/js/slides_course_quiz.js"/>
+ </xpath>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/website_sale_slides/views/website_slides_templates.xml b/addons/website_sale_slides/views/website_slides_templates.xml
index 39c6747ed26..a4f885182e7 100644
--- a/addons/website_sale_slides/views/website_slides_templates.xml
+++ b/addons/website_sale_slides/views/website_slides_templates.xml
@@ -31,4 +31,26 @@
</xpath>
</template>
+<template id="lesson_content_quiz" inherit_id="website_slides.lesson_content_quiz">
+ <xpath expr="//div[hasclass('o_wslides_js_lesson_quiz')]" position="attributes">
+ <attribute name="t-att-data-price">product_info['price'] if product_info else None</attribute>
+ <attribute name="t-att-data-currency-name">product_info['currency_id'].name if product_info else None</attribute>
+ <attribute name="t-att-data-currency-symbol">product_info['currency_id'].symbol if product_info else None</attribute>
+ <attribute name="t-att-data-has-discounted-price">product_info['has_discounted_price'] if product_info else None</attribute>
+ <attribute name="t-att-data-product-id">slide.channel_id.product_id.id if slide.channel_id.product_id else None</attribute>
+ <attribute name="t-att-data-list-price">product_info['list_price'] if product_info else None</attribute>
+ </xpath>
+</template>
+
+<template id="slide_fullscreen" inherit_id="website_slides.slide_fullscreen">
+ <xpath expr="//div[hasclass('o_wslides_fs_main')]" position="attributes">
+ <attribute name="t-att-data-price">product_info['price'] if product_info else None</attribute>
+ <attribute name="t-att-data-currency-name">product_info['currency_id'].name if product_info else None</attribute>
+ <attribute name="t-att-data-currency-symbol">product_info['currency_id'].symbol if product_info else None</attribute>
+ <attribute name="t-att-data-has-discounted-price">product_info['has_discounted_price'] if product_info else None</attribute>
+ <attribute name="t-att-data-product-id">slide.channel_id.product_id.id if product_info else None</attribute>
+ <attribute name="t-att-data-list-price">product_info['list_price'] if product_info else None</attribute>
+ </xpath>
+</template>
+
</data></odoo>
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/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index dfcc4e4e7de..5b690682d35 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -537,6 +537,12 @@ class WebsiteSlides(WebsiteProfile):
'search_uncategorized': kwargs.get('search_uncategorized')
})
+ values['channel'] = slide.channel_id
+ values = self._prepare_additional_channel_values(values, **kwargs)
+ values.pop('channel', None)
+
+ values['signup_allowed'] = request.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'
+
if kwargs.get('fullscreen') == '1':
return request.render("website_slides.slide_fullscreen", values)
return request.render("website_slides.slide_main", values)
diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js
index 8a648a3cb3b..42fce775674 100644
--- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js
+++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js
@@ -299,14 +299,15 @@ odoo.define('website_slides.fullscreen', function (require) {
* @param {Object} slides Contains the list of all slides of the course
* @param {integer} defaultSlideId Contains the ID of the slide requested by the user
*/
- init: function (el, slides, defaultSlideId){
+ init: function (parent, slides, defaultSlideId, channelData){
var result = this._super.apply(this,arguments);
this.initialSlideID = defaultSlideId;
this.slides = this._preprocessSlideData(slides);
-
+ this.channel = channelData;
var slide;
+ var urlParams = $.deparam.querystring();
if (defaultSlideId) {
- slide = findSlide(this.slides, {id: defaultSlideId});
+ slide = findSlide(this.slides, {id: defaultSlideId, isQuiz: urlParams.quiz === "1" });
} else {
slide = this.slides[0];
}
@@ -417,7 +418,11 @@ odoo.define('website_slides.fullscreen', function (require) {
urlParts[urlParts.length-1] = this.get('slide').slug;
var url = urlParts.join('/');
this.$('.o_wslides_fs_exit_fullscreen').attr('href', url);
- var fullscreenUrl = _.str.sprintf('%s?fullscreen=1', url);
+ var params = {'fullscreen': 1 };
+ if (this.get('slide').isQuiz){
+ params.quiz = 1;
+ }
+ var fullscreenUrl = _.str.sprintf('%s?%s', url, $.param(params));
history.pushState(null, '', fullscreenUrl);
},
/**
@@ -437,7 +442,7 @@ odoo.define('website_slides.fullscreen', function (require) {
// display quiz slide, or quiz attached to a slide
if (slide.type === 'quiz' || slide.isQuiz) {
$content.addClass('bg-white');
- var QuizWidget = new Quiz(this, slide);
+ var QuizWidget = new Quiz(this, slide, this.channel);
return QuizWidget.appendTo($content);
}
@@ -490,13 +495,13 @@ odoo.define('website_slides.fullscreen', function (require) {
_onChangeSlide: function () {
var self = this;
var slide = this.get('slide');
+ self._pushUrlState();
return this._fetchSlideContent().then(function() { // render content
return self._renderSlide();
}).then(function() {
if (slide._autoSetDone && !session.is_website_user) { // no useless RPC call
self._setCompleted(slide.id);
}
- self._pushUrlState();
});
},
/**
@@ -568,10 +573,13 @@ odoo.define('website_slides.fullscreen', function (require) {
xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'],
start: function (){
var proms = [this._super.apply(this, arguments)];
- var fullscreen = new Fullscreen(this, this._getSlides(), this._getCurrentSlideID());
+ var fullscreen = new Fullscreen(this, this._getSlides(), this._getCurrentSlideID(), this._extractChannelData());
proms.push(fullscreen.attachTo(".o_wslides_fs_main"));
return Promise.all(proms);
},
+ _extractChannelData: function (){
+ return this.$el.data();
+ },
_getCurrentSlideID: function (){
return parseInt(this.$('.o_wslides_fs_sidebar_list_item.active').data('id'));
},
diff --git a/addons/website_slides/static/src/js/slides_course_join.js b/addons/website_slides/static/src/js/slides_course_join.js
index 002ea049e5f..1e753b967f6 100644
--- a/addons/website_slides/static/src/js/slides_course_join.js
+++ b/addons/website_slides/static/src/js/slides_course_join.js
@@ -8,10 +8,17 @@ require('website_slides.slides');
var _t = core._t;
var CourseJoinWidget = publicWidget.Widget.extend({
+ template: 'slide.course.join',
+ xmlDependencies: ['/website_slides/static/src/xml/channel_management.xml'],
events: {
'click .o_wslides_js_course_join_link': '_onClickJoin',
},
+ init: function (parent, channelId){
+ this.channelId = channelId;
+ return this._super.apply(this, arguments);
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -41,7 +48,7 @@ var CourseJoinWidget = publicWidget.Widget.extend({
* @private
*/
_onClickJoin: function (event) {
- var channelId = $(event.currentTarget).data('channel-id');
+ var channelId = this.channelId || $(event.currentTarget).data('channel-id');
var self = this;
this._rpc({
route: '/slides/channel/join',
diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js
index 8b02ee4beae..8ca2108c368 100644
--- a/addons/website_slides/static/src/js/slides_course_quiz.js
+++ b/addons/website_slides/static/src/js/slides_course_quiz.js
@@ -3,6 +3,9 @@ odoo.define('website_slides.quiz', function (require) {
var publicWidget = require('web.public.widget');
var core = require('web.core');
+ var session = require('web.session');
+
+ var CourseJoinWidget = require('website_slides.course.join.widget').courseJoinWidget;
var QWeb = core.qweb;
var _t = core._t;
@@ -34,7 +37,7 @@ odoo.define('website_slides.quiz', function (require) {
* @param {Object} slide_data holding all the classic slide informations
* @param {Object} quiz_data : optional quiz data to display. If not given, will be fetched. (questions and answers).
*/
- init: function (parent, slide_data, quiz_data) {
+ init: function (parent, slide_data, channel_data, quiz_data) {
this.slide = _.defaults(slide_data, {
id: 0,
name: '',
@@ -44,6 +47,9 @@ odoo.define('website_slides.quiz', function (require) {
});
this.quiz = quiz_data || false;
this.readonly = slide_data.readonly || false;
+ this.publicUser = session.is_website_user;
+ this.redirectURL = encodeURIComponent(document.URL);
+ this.channel = channel_data;
return this._super.apply(this, arguments);
},
@@ -69,6 +75,7 @@ odoo.define('website_slides.quiz', function (require) {
self._renderAnswers();
self._renderAnswersHighlighting();
self._renderValidationInfo();
+ new CourseJoinWidget(self, self.channel.channelId).appendTo(self.$('.o_wslides_course_join_widget'));
});
},
@@ -285,6 +292,7 @@ odoo.define('website_slides.quiz', function (require) {
var defs = [this._super.apply(this, arguments)];
this.$('.o_wslides_js_lesson_quiz').each(function () {
var slideData = $(this).data();
+ var channelData = self._extractChannelData(slideData);
slideData.quizData = {
questions: self._extractQuestionsAndAnswers(),
quizKarmaMax: slideData.quizKarmaMax,
@@ -292,7 +300,7 @@ odoo.define('website_slides.quiz', function (require) {
quizKarmaGain: slideData.quizKarmaGain,
quizAttemptsCount: slideData.quizAttemptsCount,
};
- defs.push(new Quiz(self, slideData, slideData.quizData).attachTo($(this)));
+ defs.push(new Quiz(self, slideData, channelData, slideData.quizData).attachTo($(this)));
});
return Promise.all(defs);
},
@@ -318,6 +326,14 @@ odoo.define('website_slides.quiz', function (require) {
// Private
//---------------------------------------------------------------------
+ _extractChannelData: function (slideData){
+ return {
+ id: slideData.channelId,
+ channelEnroll: slideData.enroll,
+ signupAllowed: slideData.signupAllowed
+ };
+ },
+
/**
* Extract data from exiting DOM rendered server-side, to have the list of questions with their
* relative answers.
diff --git a/addons/website_slides/static/src/xml/channel_management.xml b/addons/website_slides/static/src/xml/channel_management.xml
new file mode 100644
index 00000000000..a616cc1baa1
--- /dev/null
+++ b/addons/website_slides/static/src/xml/channel_management.xml
@@ -0,0 +1,12 @@
+<templates>
+ <t t-name="slide.course.join">
+ <div class="p-0 col-md-2">
+ <a role="button"
+ class="btn btn-primary btn-block o_wslides_js_course_join_link text-uppercase font-weight-bold"
+ title="Start Course" aria-label="Start Course Channel"
+ href="#">
+ <span class="cta-title text_small_caps">Join Course</span>
+ </a>
+ </div>
+ </t>
+</templates>
diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml
index 645ecae94cd..f8f84d20d8e 100644
--- a/addons/website_slides/static/src/xml/slide_quiz.xml
+++ b/addons/website_slides/static/src/xml/slide_quiz.xml
@@ -45,28 +45,42 @@
</t>
<t t-name="slide.slide.quiz.validation">
- <div t-if="widget.readonly" class="alert alert-info">
- <b>Join course to take quiz</b>
- <span class="my-0 h4" style="line-height: 1">
- <span title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2 py-1">
- + <t t-esc="widget.quiz.quizKarmaGain"/> XP
- </span>
- </span>
- </div>
- <div t-else="" class="d-flex align-items-center justify-content-between">
- <div t-att-class="'d-flex align-items-center' + (widget.slide.completed ? ' alert alert-success my-0 py-1 px-3' : '')">
- <button t-if="! widget.slide.completed" role="button" title="Check answers" aria-label="Check answers"
- class="btn btn-primary text-uppercase font-weight-bold o_wslides_js_lesson_quiz_submit">Check your answers</button>
- <b t-else="" class="my-0 h5">Done !</b>
- <span class="my-0 h5" style="line-height: 1">
- <span role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2">
- + <t t-if="! widget.slide.completed" t-esc="widget.quiz.quizKarmaGain"/><t t-else="" t-esc="widget.quiz.quizKarmaWon"/> XP
+ <div id="validation">
+ <div t-if="widget.readonly &amp;&amp; !widget.publicUser &amp;&amp; widget.channel.channelEnroll == 'public'" class="o_wslides_course_join_widget">
+ <!-- Here comes the Join button rendered by the widget -->
+ </div>
+ <div t-if="widget.readonly &amp;&amp; widget.publicUser &amp;&amp; widget.channel.channelEnroll == 'public'" class="alert alert-info d-flex align-items-center justify-content-between">
+ <div>
+ <b class="h5 mb-0">Sign in and join the course to take the quiz!</b>
+ <span class="my-0 h4" style="line-height: 1">
+ <span title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2 py-1">
+ + <t t-esc="widget.quiz.quizKarmaGain"/> XP
+ </span>
+ </span>
+ </div>
+ <div>
+ <a t-att-href="'/web/login?redirect=' + widget.redirectURL" class="btn btn-primary font-weight-bold text-uppercase">Sign in</a>
+ <span t-if="widget.channel.signupAllowed" class="d-block mt-2">Don't have an account ? <a class="font-weight-bold" t-att-href="'/web/signup?redirect=' + widget.url">Sign Up !</a></span>
+ </div>
+ </div>
+ <div t-if="widget.readonly &amp;&amp; widget.channel.channelEnroll == 'invite'" class="alert alert-info">
+ <b>This course is private. <a href="/contactus" class="font-weight-bold">Contact the website administrator</a> to enroll.</b>
+ </div>
+ <div t-if="!widget.readonly" class="d-flex align-items-center justify-content-between">
+ <div t-att-class="'d-flex align-items-center' + (widget.slide.completed ? ' alert alert-success my-0 py-1 px-3' : '')">
+ <button t-if="! widget.slide.completed" role="button" title="Check answers" aria-label="Check answers"
+ class="btn btn-primary text-uppercase font-weight-bold o_wslides_js_lesson_quiz_submit">Check your answers</button>
+ <b t-else="" class="my-0 h5">Done !</b>
+ <span class="my-0 h5" style="line-height: 1">
+ <span role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2">
+ + <t t-if="!widget.slide.completed" t-esc="widget.quiz.quizKarmaGain"/><t t-else="" t-esc="widget.quiz.quizKarmaWon"/> XP
+ </span>
</span>
- </span>
+ </div>
+ <button t-if="widget.slide.completed &amp;&amp; widget.slide.hasNext" class="btn btn-primary o_wslides_quiz_continue">
+ Continue <i class="fa fa-chevron-right ml-1"/>
+ </button>
</div>
- <button t-if="widget.slide.completed &amp;&amp; widget.slide.hasNext" class="btn btn-primary o_wslides_quiz_continue">
- Continue <i class="fa fa-chevron-right ml-1"/>
- </button>
</div>
</t>
diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml
index 7b161f446a2..d163f3fbeb6 100644
--- a/addons/website_slides/views/website_slides_templates_homepage.xml
+++ b/addons/website_slides/views/website_slides_templates_homepage.xml
@@ -303,7 +303,7 @@
</div>
<div class="card-footer bg-white text-600 px-3">
<div class="d-flex justify-content-between align-items-center">
- <small class="font-weight-bold" t-field="channel.total_time" t-options='{"widget": "float_time"}'/>
+ <small t-if="channel.total_time" class="font-weight-bold"><t t-esc="channel.total_time * 60" t-options="{&quot;widget&quot;: &quot;integer&quot;}"/> min</small>
<div class="d-flex flex-grow-1 justify-content-end">
<t t-if="channel.is_member and channel.completed">
<span class="badge badge-pill badge-success pull-right py-1 px-2"><i class="fa fa-check"/> Completed</span>
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index 77e154b1ccc..6bf4256ea29 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -404,7 +404,10 @@
t-att-data-quiz-karma-gain="quiz_karma_gain"
t-att-data-quiz-karma-won="quiz_karma_won"
t-att-data-has-next="1 if next_slide else 0"
- t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
+ t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None"
+ t-att-data-channel-id="slide.channel_id.id"
+ t-att-data-channel-enroll="slide.channel_id.enroll"
+ t-att-data-signup-allowed="signup_allowed">
<div t-foreach="slide_questions" t-as="question"
t-att-class="'o_wslides_js_lesson_quiz_question my-5 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
<div class="h4">
diff --git a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
index 7022de0df99..43b5f348375 100644
--- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
@@ -7,7 +7,10 @@
<link rel="canonical" t-att-href="slide.website_url" />
</t>
<t t-call="website.layout">
- <div class="o_wslides_fs_main d-flex flex-column font-weight-light">
+ <div class="o_wslides_fs_main d-flex flex-column font-weight-light"
+ t-att-data-channel-id="slide.channel_id.id"
+ t-att-data-channel-enroll="slide.channel_id.enroll"
+ t-att-data-signup-allowed="signup_allowed">
<div class="o_wslides_slide_fs_header d-flex flex-shrink-0 text-white">
<div class="d-flex">
diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py
index ed66354a5eb..74da0f9826c 100644
--- a/odoo/addons/base/models/ir_http.py
+++ b/odoo/addons/base/models/ir_http.py
@@ -337,12 +337,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/addons/base/views/ir_qweb_widget_templates.xml b/odoo/addons/base/views/ir_qweb_widget_templates.xml
index d0cd46bf631..6da8ec0efd6 100644
--- a/odoo/addons/base/views/ir_qweb_widget_templates.xml
+++ b/odoo/addons/base/views/ir_qweb_widget_templates.xml
@@ -13,7 +13,7 @@
</div>
</template>
<template id="contact">
- <address t-ignore="true" class="mb0" itemscope="itemscope" itemtype="http://schema.org/Organization">
+ <address t-ignore="true" class="d-inline-block mb-0" itemscope="itemscope" itemtype="http://schema.org/Organization">
<div t-if="not (('name' in fields) or (address and 'address' in fields) or (city and 'city' in fields) or (mobile and 'mobile' in fields) or (website and 'website' in fields) or (email and 'email' in fields))" class="css_non_editable_mode_hidden">
--<span class="text-muted" t-esc="name"/>--
</div>
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:
<moduledir>
`-- 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,