diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-06-27 19:10:09 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-06-27 19:10:09 +0200 |
| commit | 6c5783bd5a8b20dce99b26a81e734a50c168bae0 (patch) | |
| tree | 28f6179342d5799f1cee8bd5da21dc638c70b49a /addons | |
| parent | fdcabf19b27cb5427b8f55f922479cb740d590f3 (diff) | |
| parent | 465909f0fde4d02b101d35c61bab8f0dea20f003 (diff) | |
[MERGE] forward port branch saas-12.2 up to 465909f0fde
Diffstat (limited to 'addons')
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', '<', 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 && widget.publicUser && 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 && 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}&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 && !widget.publicUser && 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 && widget.publicUser && 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 && 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 && 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 && 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="{"widget": "integer"}"/> 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"> |
