summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-06-27 20:45:52 +0200
committerChristophe Simonis <chs@odoo.com>2019-06-27 20:45:52 +0200
commit5e81b18e24a358b3f4145168f88614a9be37ac14 (patch)
tree198950ff1118ddd91ad296a8d27b861bf6c21d76
parentc3a609eae4ff5396c1814228d3fc75623ab21b49 (diff)
parent0247d2f35fdeebdb48e8eafc2ae69d1675158303 (diff)
[MERGE] forward port branch saas-12.3 up to 0247d2f35fd
-rw-r--r--addons/account/models/account.py12
-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.js4
-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.py6
-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.py6
-rw-r--r--addons/sale/models/sales_team.py2
-rw-r--r--addons/sale/views/sale_views.xml2
-rw-r--r--addons/sale_coupon/models/sale_order.py8
-rw-r--r--addons/sale_coupon/tests/common.py19
-rw-r--r--addons/sale_coupon/tests/test_program_with_code_operations.py39
-rw-r--r--addons/sale_coupon_delivery/models/sale_order.py2
-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/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_delivery/static/src/js/website_free_delivery_tour.js14
-rw-r--r--addons/website_sale_delivery/tests/test_ui.py1
-rw-r--r--addons/website_sale_slides/__manifest__.py1
-rw-r--r--addons/website_sale_slides/static/src/js/slides_course_quiz.js25
-rw-r--r--addons/website_sale_slides/static/src/xml/website_sale_slides_quiz.xml43
-rw-r--r--addons/website_sale_slides/views/assets.xml10
-rw-r--r--addons/website_sale_slides/views/website_slides_templates.xml22
-rw-r--r--addons/website_sale_stock/models/product_template.py2
-rw-r--r--addons/website_slides/controllers/main.py6
-rw-r--r--addons/website_slides/static/src/js/slides_course_fullscreen_player.js22
-rw-r--r--addons/website_slides/static/src/js/slides_course_join.js9
-rw-r--r--addons/website_slides/static/src/js/slides_course_quiz.js20
-rw-r--r--addons/website_slides/static/src/xml/channel_management.xml12
-rw-r--r--addons/website_slides/static/src/xml/slide_quiz.xml54
-rw-r--r--addons/website_slides/views/website_slides_templates_homepage.xml2
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson.xml5
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml5
-rw-r--r--odoo/addons/base/models/ir_http.py7
-rw-r--r--odoo/addons/base/models/ir_model.py2
-rw-r--r--odoo/addons/base/views/ir_qweb_widget_templates.xml2
-rw-r--r--odoo/modules/migration.py18
61 files changed, 552 insertions, 99 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 326e1574014..da381195dd2 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -1071,16 +1071,20 @@ class AccountTax(models.Model):
rslt = super(AccountTax, self).default_get(vals + ['company_id'])
company_id = rslt.get('company_id')
+ company = self.env['res.company'].browse(company_id)
+
if 'refund_repartition_line_ids' in vals:
+ # We write on the related country_id field so that the field is recomputed. Without that, it will stay empty until we save the record.
rslt['refund_repartition_line_ids'] = [
- (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}),
- (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}),
+ (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id, 'country_id': company.country_id.id}),
+ (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id, 'country_id': company.country_id.id}),
]
if 'invoice_repartition_line_ids' in vals:
+ # We write on the related country_id field so that the field is recomputed. Without that, it will stay empty until we save the record.
rslt['invoice_repartition_line_ids'] = [
- (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}),
- (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}),
+ (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id, 'country_id': company.country_id.id}),
+ (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id, 'country_id': company.country_id.id}),
]
return rslt
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index dbdb3b075a2..8e0744403e4 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -926,6 +926,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 24a3709a3a6..3c181f3357b 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1663,7 +1663,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 5898f41489a..83f4f188163 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -636,7 +636,10 @@
<field name="name"/>
<field name="origin" groups="base.group_user" attrs="{'invisible': [('origin', '=', False)]}"/>
<field name="reference" attrs="{'readonly':[('state','!=','draft')]}"/>
- <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 e9154f04d31..f557e54b64d 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 c9d52ee6e02..13a70058741 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.company.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 0384fe3b40e..d98717b7bb8 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -772,6 +772,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 6fc8b9c22b5..9699dc6feaf 100644
--- a/addons/hr_timesheet/static/src/js/timesheet_uom.js
+++ b/addons/hr_timesheet/static/src/js/timesheet_uom.js
@@ -80,7 +80,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' ?
@@ -96,7 +95,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 || {});
@@ -112,4 +111,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 34ca956a2c4..14b89d8c7fa 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 cf65644076b..1f01b90b9da 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -7,7 +7,7 @@ from itertools import groupby
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import AccessError, UserError
-from odoo.tools import date_utils, float_round
+from odoo.tools import date_utils, float_round, float_is_zero
class MrpProduction(models.Model):
@@ -776,7 +776,7 @@ class MrpProduction(models.Model):
)
move_lines = self.move_raw_ids.mapped('move_line_ids').filtered(lambda ml: lots_short & ml.lot_produced_ids)
for ml in move_lines:
- error_msg += ml.product_id.display_name + ' (' + (lots_short & ml.lot_produced_ids).mapped('name') + ')\n'
+ error_msg += ml.product_id.display_name + ' (' + ', '.join((lots_short & ml.lot_produced_ids).mapped('name')) + ')\n'
raise UserError(error_msg)
@api.multi
@@ -829,6 +829,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 b67583361ca..c7a56b8bf7b 100644
--- a/addons/mrp/report/mrp_report_bom_structure.py
+++ b/addons/mrp/report/mrp_report_bom_structure.py
@@ -14,8 +14,7 @@ class ReportBomStructure(models.AbstractModel):
docs = []
for bom_id in docids:
bom = self.env['mrp.bom'].browse(bom_id)
- variant = data and data.get('variant')
- candidates = variant and self.env['product.product'].browse(variant) or bom.product_tmpl_id.product_variant_ids
+ candidates = bom.product_id or bom.product_tmpl_id.product_variant_ids
for product_variant_id in candidates:
if data and data.get('childs'):
doc = self._get_pdf_line(bom_id, product_id=product_variant_id, qty=float(data.get('quantity')), child_bom_ids=json.loads(data.get('childs')))
diff --git a/addons/payment_stripe/i18n/payment_stripe.pot b/addons/payment_stripe/i18n/payment_stripe.pot
index 16b83b04500..6f2e50dc7ec 100644
--- a/addons/payment_stripe/i18n/payment_stripe.pot
+++ b/addons/payment_stripe/i18n/payment_stripe.pot
@@ -185,6 +185,12 @@ msgid "Stripe: no order found for reference %s"
msgstr ""
#. module: payment_stripe
+#: code:addons/payment_stripe/models/payment.py:260
+#, python-format
+msgid "Stripe: no payment token was provided or the token creation failed."
+msgstr ""
+
+#. module: payment_stripe
#: code:addons/payment_stripe/models/payment.py:170
#, python-format
msgid "We're sorry to report that the transaction has failed."
diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py
index 78920e93580..f35a44165e7 100644
--- a/addons/payment_stripe/models/payment.py
+++ b/addons/payment_stripe/models/payment.py
@@ -257,7 +257,7 @@ class PaymentTokenStripe(models.Model):
description = 'Partner: %s (id: %s)' % (partner_id.name, partner_id.id)
if not token:
- raise Exception('stripe_create: No token provided!')
+ raise UserError(_("Stripe: no payment token was provided or the token creation failed."))
res = self._stripe_create_customer(token, description, payment_acquirer.id)
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 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 4d52ca6a5e2..e35665d38ce 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 be124faa658..41331af134e 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1446,7 +1446,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.company, self.order_id.date_order or fields.Date.today())
# negative discounts (= surcharge) are included in the display price
return max(base_price, final_price)
@@ -1581,7 +1581,7 @@ class SaleOrderLine(models.Model):
if currency_id.id == product_currency.id:
cur_factor = 1.0
else:
- cur_factor = currency_id._get_conversion_rate(product_currency, currency_id, self.company_id or self.env.user.company_id, self.order_id.date_order or fields.Date.today())
+ cur_factor = currency_id._get_conversion_rate(product_currency, currency_id, self.company_id or self.env.company, self.order_id.date_order or fields.Date.today())
product_uom = self.env.context.get('uom') or product.uom_id.id
if uom and uom.id != product_uom:
@@ -1627,7 +1627,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.company, 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 69359a982cc..8a38dfa6cae 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -1214,7 +1214,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_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py
index 3487317330e..6c0fa712229 100644
--- a/addons/sale_coupon/models/sale_order.py
+++ b/addons/sale_coupon/models/sale_order.py
@@ -79,7 +79,7 @@ class SaleOrder(models.Model):
return applied_programs.filtered(lambda program: program._is_global_discount_program())
def _get_reward_values_product(self, program):
- price_unit = self.order_line.filtered(lambda line: program.reward_product_id == line.product_id)[0].price_unit
+ price_unit = self.order_line.filtered(lambda line: program.reward_product_id == line.product_id)[0].price_reduce
order_lines = (self.order_line - self._get_reward_lines()).filtered(lambda x: program._is_valid_product(x.product_id))
max_product_qty = sum(order_lines.mapped('product_uom_qty')) or 1
@@ -120,10 +120,10 @@ class SaleOrder(models.Model):
def _get_cheapest_line(self):
# Unit prices tax included
- return min(self.order_line.filtered(lambda x: not x.is_reward_line and x.price_unit > 0), key=lambda x: x['price_unit'])
+ return min(self.order_line.filtered(lambda x: not x.is_reward_line and x.price_reduce > 0), key=lambda x: x['price_reduce'])
def _get_reward_values_discount_percentage_per_line(self, program, line):
- discount_amount = line.product_uom_qty * line.price_unit * (program.discount_percentage / 100)
+ discount_amount = line.product_uom_qty * line.price_reduce * (program.discount_percentage / 100)
return discount_amount
def _get_reward_values_discount(self, program):
@@ -142,7 +142,7 @@ class SaleOrder(models.Model):
if program.discount_apply_on == 'cheapest_product':
line = self._get_cheapest_line()
if line:
- discount_line_amount = line.price_unit * (program.discount_percentage / 100)
+ discount_line_amount = line.price_reduce * (program.discount_percentage / 100)
if discount_line_amount:
taxes = line.tax_id
if self.fiscal_position_id:
diff --git a/addons/sale_coupon/tests/common.py b/addons/sale_coupon/tests/common.py
index fec5d669aba..e6b8bd1b359 100644
--- a/addons/sale_coupon/tests/common.py
+++ b/addons/sale_coupon/tests/common.py
@@ -60,6 +60,14 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueSetup):
'taxes_id': [(6, 0, [self.tax_15pc_excl.id])],
})
+ self.product_C = self.env['product.product'].create({
+ 'name': 'Product C',
+ 'list_price': 100,
+ 'sale_ok': True,
+ 'taxes_id': [(6, 0, [])],
+
+ })
+
# Immediate Program By A + B: get B free
# No Conditions
self.immediate_promotion_program = self.env['sale.coupon.program'].create({
@@ -79,3 +87,14 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueSetup):
'rule_products_domain': "[('id', 'in', [%s])]" % (self.product_A.id),
'active': True,
})
+
+ self.code_promotion_program_with_discount = self.env['sale.coupon.program'].create({
+ 'name': 'Buy 1 C + Enter code, 10 percent discount on C',
+ 'promo_code_usage': 'code_needed',
+ 'reward_type': 'discount',
+ 'discount_type': 'percentage',
+ 'discount_percentage': 10,
+ 'rule_products_domain': "[('id', 'in', [%s])]" % (self.product_C.id),
+ 'active': True,
+ 'discount_apply_on': 'on_order',
+ })
diff --git a/addons/sale_coupon/tests/test_program_with_code_operations.py b/addons/sale_coupon/tests/test_program_with_code_operations.py
index 276b20c2221..2a19854f25e 100644
--- a/addons/sale_coupon/tests/test_program_with_code_operations.py
+++ b/addons/sale_coupon/tests/test_program_with_code_operations.py
@@ -66,6 +66,45 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
self.assertEqual(len(order.order_line.ids), 0)
self.assertEqual(coupon.state, 'new')
+ def test_coupon_code_with_pricelist(self):
+ # Test case: Generate a coupon (10% discount) and apply it on an order with a specific pricelist (10% discount)
+
+ self.env['sale.coupon.generate'].with_context(active_id=self.code_promotion_program_with_discount.id).create({
+ 'generation_type': 'nbr_coupon',
+ 'nbr_coupons': 1,
+ }).generate_coupon()
+ coupon = self.code_promotion_program_with_discount.coupon_ids
+
+ first_pricelist = self.env['product.pricelist'].create({
+ 'name': 'First pricelist',
+ 'discount_policy': 'with_discount',
+ 'item_ids': [(0, 0, {
+ 'compute_price': 'percentage',
+ 'base': 'list_price',
+ 'percent_price': 10,
+ 'applied_on': '3_global',
+ 'name': 'First discount'
+ })]
+ })
+
+ order = self.empty_order
+ order.pricelist_id = first_pricelist
+ order.write({'order_line': [
+ (0, False, {
+ 'product_id': self.product_C.id,
+ 'name': '1 Product C',
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ ]})
+ self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
+ 'coupon_code': coupon.code
+ }).process_coupon()
+ order.recompute_coupon_lines()
+ self.assertEqual(len(order.order_line.ids), 2)
+ self.assertEqual(coupon.state, 'used')
+ self.assertEqual(order.amount_total, 81, "SO total should be 81: (10% of 100 with pricelist) + 10% of 90 with coupon code")
+
def test_on_next_order_reward_promotion_program(self):
# The flow:
# 1. Create a program `A` that gives a free `Product B` on next order if you buy a an `product A`
diff --git a/addons/sale_coupon_delivery/models/sale_order.py b/addons/sale_coupon_delivery/models/sale_order.py
index e4002fa9abf..dab962d9e78 100644
--- a/addons/sale_coupon_delivery/models/sale_order.py
+++ b/addons/sale_coupon_delivery/models/sale_order.py
@@ -42,7 +42,7 @@ class SaleOrder(models.Model):
def _get_cheapest_line(self):
# Unit prices tax included
- return min(self.order_line.filtered(lambda x: not x.is_reward_line and not x.is_delivery and x.price_unit > 0), key=lambda x: x['price_unit'])
+ return min(self.order_line.filtered(lambda x: not x.is_reward_line and not x.is_delivery and x.price_reduce > 0), key=lambda x: x['price_reduce'])
class SalesOrderLine(models.Model):
_inherit = "sale.order.line"
diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py
index ae0515d7bf2..a500d65c48e 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.company,
+ 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 d1a78b9318c..e495efe906a 100644
--- a/addons/sale_timesheet/models/project_overview.py
+++ b/addons/sale_timesheet/models/project_overview.py
@@ -340,7 +340,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 254d34f0c6d..7df6c05510b 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -393,10 +393,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 a75c793b161..ec2a917a4ed 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -279,7 +279,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/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 14f03a83222..171a6da7735 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1682,7 +1682,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 25aa4be7e26..bf7c70e2859 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 ba69596c916..333f51f75ba 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -1693,7 +1693,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 021098f7ede..5bd64ce1c49 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 5ef38ab9a49..1557d6fd50f 100644
--- a/addons/website_forum/static/src/js/website_forum.js
+++ b/addons/website_forum/static/src/js/website_forum.js
@@ -115,7 +115,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_delivery/static/src/js/website_free_delivery_tour.js b/addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js
index a1093ca98ee..4ac148854f9 100644
--- a/addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js
+++ b/addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js
@@ -5,21 +5,16 @@ var tour = require("web_tour.tour");
tour.register('check_free_delivery', {
test: true,
- url: '/shop?search=conference chair',
+ url: '/shop?search=office chair black',
},
[
{
- content: "select conference chair",
- trigger: '.oe_product_cart:first a:contains("Conference Chair")',
+ content: "select office chair black",
+ trigger: '.oe_product_cart:first a:contains("Office Chair Black")',
},
{
content: "click on add to cart",
- extra_trigger: 'label:contains(Steel) input:propChecked',
- trigger: '#product_detail form[action^="/shop/cart/update"] .btn-primary',
- },
- {
- content: "click in modal on 'Proceed to checkout' button",
- trigger: 'button:contains("Proceed to Checkout")',
+ trigger: '#product_details #add_to_cart',
},
{
content: "go to checkout",
@@ -28,6 +23,7 @@ tour.register('check_free_delivery', {
},
{
content: "Check Free Delivery value to be zero",
+ extra_trigger: '#delivery_carrier label:containsExact("Delivery Now Free Over 10")',
trigger: "#delivery_carrier span:contains('0.0')"
},
]);
diff --git a/addons/website_sale_delivery/tests/test_ui.py b/addons/website_sale_delivery/tests/test_ui.py
index 1c61c00b9fb..1bf4ce3d3bf 100644
--- a/addons/website_sale_delivery/tests/test_ui.py
+++ b/addons/website_sale_delivery/tests/test_ui.py
@@ -7,6 +7,7 @@ class TestUi(odoo.tests.HttpCase):
def test_01_free_delivery_when_exceed_threshold(self):
self.env.ref("delivery.free_delivery_carrier").write({
+ 'name': 'Delivery Now Free Over 10',
'fixed_price': 2,
'free_over': True,
'amount': 10,
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 ed39cdd0659..6d056d1ed2e 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 a2e56835b2f..7bb67444493 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 7141ea25299..d65299942b8 100644
--- a/addons/website_slides/views/website_slides_templates_homepage.xml
+++ b/addons/website_slides/views/website_slides_templates_homepage.xml
@@ -303,7 +303,7 @@
</div>
<div class="card-footer bg-white text-600 px-3">
<div class="d-flex justify-content-between align-items-center">
- <small class="font-weight-bold" t-field="channel.total_time" t-options='{"widget": "float_time"}'/>
+ <small t-if="channel.total_time" class="font-weight-bold"><t t-esc="channel.total_time * 60" t-options="{&quot;widget&quot;: &quot;integer&quot;}"/> min</small>
<div class="d-flex flex-grow-1 justify-content-end">
<t t-if="channel.is_member and channel.completed">
<span class="badge badge-pill badge-success pull-right py-1 px-2"><i class="fa fa-check"/> Completed</span>
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index 77e154b1ccc..6bf4256ea29 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -404,7 +404,10 @@
t-att-data-quiz-karma-gain="quiz_karma_gain"
t-att-data-quiz-karma-won="quiz_karma_won"
t-att-data-has-next="1 if next_slide else 0"
- t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
+ t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None"
+ t-att-data-channel-id="slide.channel_id.id"
+ t-att-data-channel-enroll="slide.channel_id.enroll"
+ t-att-data-signup-allowed="signup_allowed">
<div t-foreach="slide_questions" t-as="question"
t-att-class="'o_wslides_js_lesson_quiz_question my-5 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
<div class="h4">
diff --git a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
index 7022de0df99..43b5f348375 100644
--- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
@@ -7,7 +7,10 @@
<link rel="canonical" t-att-href="slide.website_url" />
</t>
<t t-call="website.layout">
- <div class="o_wslides_fs_main d-flex flex-column font-weight-light">
+ <div class="o_wslides_fs_main d-flex flex-column font-weight-light"
+ t-att-data-channel-id="slide.channel_id.id"
+ t-att-data-channel-enroll="slide.channel_id.enroll"
+ t-att-data-signup-allowed="signup_allowed">
<div class="o_wslides_slide_fs_header d-flex flex-shrink-0 text-white">
<div class="d-flex">
diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py
index 25ea6fd0835..897ca1de9ba 100644
--- a/odoo/addons/base/models/ir_http.py
+++ b/odoo/addons/base/models/ir_http.py
@@ -360,12 +360,17 @@ class IrHttp(models.AbstractModel):
if not filename:
if filename_field in record:
filename = record[filename_field]
- else:
+ if not filename:
filename = "%s-%s-%s" % (record._name, record.id, field)
if not mimetype:
mimetype = guess_mimetype(base64.b64decode(content), default=default_mimetype)
+ # extension
+ extension = mimetypes.guess_extension(mimetype)
+ if extension and not filename.endswith(extension):
+ filename = "%s%s" % (filename, extension)
+
if not filehash:
filehash = '"%s"' % hashlib.md5(pycompat.to_text(content).encode('utf-8')).hexdigest()
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index e25cb176b60..e9cee3207c1 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -855,7 +855,7 @@ class IrModelFields(models.Model):
keys = [key for key in new_vals if old_vals[key] != new_vals[key]]
self.pool.post_init(record.modified, keys)
old_vals.update(new_vals)
- if module and (module == model._original_module or module in field._modules):
+ if module and not field.manual and (module == model._original_module or module in field._modules):
to_xmlids.append(name)
if to_insert:
diff --git a/odoo/addons/base/views/ir_qweb_widget_templates.xml b/odoo/addons/base/views/ir_qweb_widget_templates.xml
index d0cd46bf631..6da8ec0efd6 100644
--- a/odoo/addons/base/views/ir_qweb_widget_templates.xml
+++ b/odoo/addons/base/views/ir_qweb_widget_templates.xml
@@ -13,7 +13,7 @@
</div>
</template>
<template id="contact">
- <address t-ignore="true" class="mb0" itemscope="itemscope" itemtype="http://schema.org/Organization">
+ <address t-ignore="true" class="d-inline-block mb-0" itemscope="itemscope" itemtype="http://schema.org/Organization">
<div t-if="not (('name' in fields) or (address and 'address' in fields) or (city and 'city' in fields) or (mobile and 'mobile' in fields) or (website and 'website' in fields) or (email and 'email' in fields))" class="css_non_editable_mode_hidden">
--<span class="text-muted" t-esc="name"/>--
</div>
diff --git a/odoo/modules/migration.py b/odoo/modules/migration.py
index 2dec12674de..6331af43660 100644
--- a/odoo/modules/migration.py
+++ b/odoo/modules/migration.py
@@ -36,6 +36,8 @@ class MigrationManager(object):
Python file names must start by `pre-` or `post-` and will be executed, respectively,
before and after the module initialisation. `end-` scripts are run after all modules have
been updated.
+ A special folder named `0.0.0` can contain scripts that will be run on any version change.
+ In `pre` stage, `0.0.0` scripts are run first, while in `post` and `end`, they are run last.
Example:
<moduledir>
`-- migrations
@@ -48,6 +50,8 @@ class MigrationManager(object):
|-- 9.0.1.1 # processed only on a 9.0 server
| |-- pre-delete_table_z.py
| `-- post-clean-data.py
+ |-- 0.0.0
+ | `-- end-invariants.py # processed on all version update
`-- foo.py # not processed
"""
@@ -94,13 +98,20 @@ class MigrationManager(object):
return version # the version number already containt the server version
return "%s.%s" % (release.major_version, version)
- def _get_migration_versions(pkg):
+ def _get_migration_versions(pkg, stage):
versions = sorted({
ver
for lv in self.migrations[pkg.name].values()
for ver, lf in lv.items()
if lf
}, key=lambda k: parse_version(convert_version(k)))
+ if "0.0.0" in versions:
+ # reorder versions
+ versions.remove("0.0.0")
+ if stage == "pre":
+ versions.insert(0, "0.0.0")
+ else:
+ versions.append("0.0.0")
return versions
def _get_migration_files(pkg, version, stage):
@@ -127,10 +138,11 @@ class MigrationManager(object):
parsed_installed_version = parse_version(installed_version)
current_version = parse_version(convert_version(pkg.data['version']))
- versions = _get_migration_versions(pkg)
+ versions = _get_migration_versions(pkg, stage)
for version in versions:
- if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
+ if ((version == "0.0.0" and parsed_installed_version < current_version)
+ or parsed_installed_version < parse_version(convert_version(version)) <= current_version):
strfmt = {'addon': pkg.name,
'stage': stage,