summaryrefslogtreecommitdiff
path: root/addons
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 /addons
parentfdcabf19b27cb5427b8f55f922479cb740d590f3 (diff)
parent465909f0fde4d02b101d35c61bab8f0dea20f003 (diff)
[MERGE] forward port branch saas-12.2 up to 465909f0fde
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/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
51 files changed, 455 insertions, 74 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">