summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-06-09 17:11:47 +0200
committerChristophe Simonis <chs@odoo.com>2017-06-09 17:11:47 +0200
commitab02bf34b0c1d3fa3752ca1f50b098d04dc5b137 (patch)
treea29591cae8777390cbeb35d65e23c9f8ae143135
parentb350c29e44b4ed1df0c428308c396771b22d73e8 (diff)
parentcc38b93ab2a6daf9c5badbc387cff20d1a01ddee (diff)
[MERGE] forward port branch saas-16 up to cc38b93ab2a
-rw-r--r--addons/account/models/account.py5
-rw-r--r--addons/account/models/account_bank_statement.py2
-rw-r--r--addons/account/tests/account_test_classes.py27
-rw-r--r--addons/base_geolocalize/models/res_partner.py3
-rw-r--r--addons/base_import_module/models/ir_module.py29
-rw-r--r--addons/calendar/models/calendar.py13
-rw-r--r--addons/crm/models/crm_lead.py1
-rw-r--r--addons/delivery/models/delivery_carrier.py40
-rw-r--r--addons/hr_holidays/models/hr_holidays.py7
-rw-r--r--addons/hr_timesheet/models/hr_timesheet.py7
-rw-r--r--addons/hr_timesheet/views/project_views.xml2
-rw-r--r--addons/mrp/models/mrp_production.py7
-rw-r--r--addons/project/static/src/js/project.js23
-rw-r--r--addons/project/static/src/xml/project.xml2
-rw-r--r--addons/project/static/tests/project_tests.js133
-rw-r--r--addons/project/views/project_templates.xml5
-rw-r--r--addons/project_issue_sheet/views/project_issue_view.xml2
-rw-r--r--addons/sale/tests/test_sale_order.py3
-rw-r--r--addons/sale_expense/tests/test_sale_expense.py3
-rw-r--r--addons/sale_timesheet/models/account_invoice.py9
-rw-r--r--addons/stock/i18n/stock.pot6
-rw-r--r--addons/stock/models/product.py3
-rw-r--r--addons/stock/models/stock_production_lot.py10
-rw-r--r--addons/stock/models/stock_scrap.py6
-rw-r--r--addons/stock/security/ir.model.access.csv6
-rw-r--r--addons/stock/views/stock_pack_operation_views.xml2
-rw-r--r--addons/stock_account/models/product.py3
-rw-r--r--addons/stock_landed_costs/tests/common.py3
-rw-r--r--addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js6
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js17
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js24
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js22
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js30
-rw-r--r--addons/web/static/src/js/services/data_manager.js1
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js11
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js6
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js4
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js20
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js40
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js2
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js13
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js1
-rw-r--r--addons/web/static/src/js/widgets/model_field_selector.js5
-rw-r--r--addons/web/static/src/less/model_field_selector.less4
-rw-r--r--addons/web/static/src/less/webclient.less1
-rw-r--r--addons/web/static/src/xml/base.xml7
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js286
-rw-r--r--addons/web/static/tests/helpers/test_utils.js73
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js49
-rw-r--r--addons/web/static/tests/views/form_tests.js97
-rw-r--r--addons/web/static/tests/views/list_tests.js23
-rw-r--r--addons/website/models/website.py5
-rw-r--r--addons/website/static/src/js/website.ace.js7
-rw-r--r--addons/website/static/src/js/website.seo.js42
-rw-r--r--addons/website_crm_partner_assign/models/crm_lead.py2
-rw-r--r--addons/website_event_sale/static/src/js/website.tour.event_sale.js2
-rw-r--r--addons/website_forum/i18n/website_forum.pot36
-rw-r--r--addons/website_forum/static/src/xml/website_forum_share_templates.xml2
-rw-r--r--addons/website_forum/views/website_forum.xml38
-rw-r--r--addons/website_sale/models/website.py10
60 files changed, 1051 insertions, 197 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 96ade1cb3d9..4a2846255d2 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -523,7 +523,10 @@ class AccountJournal(models.Model):
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
args = args or []
- recs = self.search(['|', ('code', operator, name), ('name', operator, name)] + args, limit=limit)
+ connector = '|'
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ connector = '&'
+ recs = self.search([connector, ('code', operator, name), ('name', operator, name)] + args, limit=limit)
return recs.name_get()
@api.multi
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 6c728362378..90223d85923 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -248,7 +248,7 @@ class AccountBankStatement(models.Model):
for aml in st_line.journal_entry_ids:
moves |= aml.move_id
if moves:
- moves.post()
+ moves.filtered(lambda m: m.state != 'posted').post()
statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,))
statements.link_bank_to_partner()
statements.write({'state': 'confirm', 'date_done': time.strftime("%Y-%m-%d %H:%M:%S")})
diff --git a/addons/account/tests/account_test_classes.py b/addons/account/tests/account_test_classes.py
index cf37e2493ef..8477f4109d9 100644
--- a/addons/account/tests/account_test_classes.py
+++ b/addons/account/tests/account_test_classes.py
@@ -28,3 +28,30 @@ class AccountingTestCase(HttpCase):
if theorical_lines:
raise ValidationError('Remaining theorical line (not found). %s)' % ([(aml[0], aml[1], aml[2]) for aml in theorical_lines]))
return True
+
+ def ensure_account_property(self, property_name):
+ '''Ensure the ir.property targetting an account.account passed as parameter exists.
+ In case it's not: create it with a random account. This is useful when testing with
+ partially defined localization (missing stock properties for example)
+
+ :param property_name: The name of the property.
+ '''
+ company_id = self.env.user.company_id
+ field_id = self.env['ir.model.fields'].search(
+ [('model', '=', 'product.template'), ('name', '=', property_name)], limit=1)
+ property_id = self.env['ir.property'].search([
+ ('company_id', '=', company_id.id),
+ ('name', '=', property_name),
+ ('res_id', '=', None),
+ ('fields_id', '=', field_id.id)], limit=1)
+ account_id = self.env['account.account'].search([('company_id', '=', company_id.id)], limit=1)
+ value_reference = 'account.account,%d' % account_id.id
+ if property_id and not property_id.value_reference:
+ property_id.value_reference = value_reference
+ else:
+ self.env['ir.property'].create({
+ 'name': property_name,
+ 'company_id': company_id.id,
+ 'fields_id': field_id.id,
+ 'value_reference': value_reference,
+ })
diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py
index 0331d2c9734..855b4d8bbd1 100644
--- a/addons/base_geolocalize/models/res_partner.py
+++ b/addons/base_geolocalize/models/res_partner.py
@@ -9,8 +9,9 @@ from odoo.exceptions import UserError
def geo_find(addr):
+ if not addr:
+ return None
url = 'https://maps.googleapis.com/maps/api/geocode/json'
-
try:
result = requests.get(url, params={'sensor': 'false', 'address': addr}).json()
except Exception as e:
diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index 0ebd1144ada..ec94bc22d27 100644
--- a/addons/base_import_module/models/ir_module.py
+++ b/addons/base_import_module/models/ir_module.py
@@ -100,18 +100,23 @@ class IrModule(models.Model):
raise UserError(_("File '%s' exceed maximum allowed file size") % zf.filename)
with tempdir() as module_dir:
- z.extractall(module_dir)
- dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
- for mod_name in dirs:
- module_names.append(mod_name)
- try:
- # assert mod_name.startswith('theme_')
- path = opj(module_dir, mod_name)
- self._import_module(mod_name, path, force=force)
- success.append(mod_name)
- except Exception as e:
- _logger.exception('Error while importing module')
- errors[mod_name] = exception_to_unicode(e)
+ import odoo.modules as addons
+ try:
+ addons.module.ad_paths.append(module_dir)
+ z.extractall(module_dir)
+ dirs = [d for d in os.listdir(module_dir) if os.path.isdir(opj(module_dir, d))]
+ for mod_name in dirs:
+ module_names.append(mod_name)
+ try:
+ # assert mod_name.startswith('theme_')
+ path = opj(module_dir, mod_name)
+ self._import_module(mod_name, path, force=force)
+ success.append(mod_name)
+ except Exception as e:
+ _logger.exception('Error while importing module')
+ errors[mod_name] = exception_to_unicode(e)
+ finally:
+ addons.module.ad_paths.remove(module_dir)
r = ["Successfully imported module '%s'" % mod for mod in success]
for mod, error in pycompat.items(errors):
r.append("Error while importing module '%s': %r" % (mod, error))
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 740f6281e9a..9c8b088aeb1 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -567,15 +567,18 @@ class Meeting(models.Model):
if not event_date:
event_date = datetime.now()
- # Convert the event date to saved timezone (or context tz) as it'll
- # define the correct hour/day asked by the user to repeat for recurrence.
- event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
- rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True)
+ if self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule:
+ rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True)
+ else:
+ # Convert the event date to saved timezone (or context tz) as it'll
+ # define the correct hour/day asked by the user to repeat for recurrence.
+ event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
+ rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)])
for meeting in recurring_meetings:
rset1._exdate.append(todate(meeting.recurrent_id_date))
- return [d.astimezone(pytz.UTC) for d in rset1]
+ return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1]
@api.multi
def _get_recurrency_end_date(self):
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 3f5d4511836..ea09ec486b4 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -247,6 +247,7 @@ class Lead(models.Model):
team_id = self.env['crm.team']._get_default_team_id(user_id=user_id)
return {'team_id': team_id}
+ @api.constrains('user_id')
@api.onchange('user_id')
def _onchange_user_id(self):
""" When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """
diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py
index f48eb8d9835..4a2756d2563 100644
--- a/addons/delivery/models/delivery_carrier.py
+++ b/addons/delivery/models/delivery_carrier.py
@@ -226,26 +226,26 @@ class DeliveryCarrier(models.Model):
if record.delivery_type == 'fixed':
PriceRule.search([('carrier_id', '=', record.id)]).unlink()
- line_data = {
- 'carrier_id': record.id,
- 'variable': 'price',
- 'operator': '>=',
- }
- # Create the delivery price rules
- if record.free_if_more_than:
- line_data.update({
- 'max_value': record.amount,
- 'standard_price': 0.0,
- 'list_base_price': 0.0,
- })
- PriceRule.create(line_data)
- if record.fixed_price is not False:
- line_data.update({
- 'max_value': 0.0,
- 'standard_price': record.fixed_price,
- 'list_base_price': record.fixed_price,
- })
- PriceRule.create(line_data)
+ line_data = {
+ 'carrier_id': record.id,
+ 'variable': 'price',
+ 'operator': '>=',
+ }
+ # Create the delivery price rules
+ if record.free_if_more_than:
+ line_data.update({
+ 'max_value': record.amount,
+ 'standard_price': 0.0,
+ 'list_base_price': 0.0,
+ })
+ PriceRule.create(line_data)
+ if record.fixed_price is not False:
+ line_data.update({
+ 'max_value': 0.0,
+ 'standard_price': record.fixed_price,
+ 'list_base_price': record.fixed_price,
+ })
+ PriceRule.create(line_data)
return True
@api.model
diff --git a/addons/hr_holidays/models/hr_holidays.py b/addons/hr_holidays/models/hr_holidays.py
index ce3b7d1a827..2061e6be1a0 100644
--- a/addons/hr_holidays/models/hr_holidays.py
+++ b/addons/hr_holidays/models/hr_holidays.py
@@ -329,9 +329,12 @@ class Holidays(models.Model):
res = []
for leave in self:
if leave.type == 'remove':
- res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp)))
+ if self.env.context.get('short_name'):
+ res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days_temp)))
+ else:
+ res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp)))
else:
- res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp,leave.employee_id.name)))
+ res.append((leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name)))
return res
def _check_state_access_right(self, vals):
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index 74704a459f2..d8d07c42fd8 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -7,6 +7,13 @@ from odoo import api, fields, models
class AccountAnalyticLine(models.Model):
_inherit = 'account.analytic.line'
+ @api.model
+ def default_get(self, field_list):
+ result = super(AccountAnalyticLine, self).default_get(field_list)
+ if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'):
+ result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
+ return result
+
task_id = fields.Many2one('project.task', 'Task')
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml
index 7886e39b870..c84663c2985 100644
--- a/addons/hr_timesheet/views/project_views.xml
+++ b/addons/hr_timesheet/views/project_views.xml
@@ -67,7 +67,7 @@
<field name="planned_hours" widget="float_time"/>
</field>
<xpath expr="//notebook/page[@name='description_page']" position="after">
- <page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}">
+ <page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user">
<field name="timesheet_ids" context="{'default_project_id': project_id}">
<tree editable="bottom" string="Timesheet Activities" default_order="date">
<field name="date"/>
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index a797892a4c2..4f728d48698 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -208,8 +208,11 @@ class MrpProduction(models.Model):
@api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done')
def _compute_post_visible(self):
for order in self:
- order.post_visible = any(order.move_raw_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel']))) or \
- any(order.move_finished_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel'])))
+ if order.product_tmpl_id._is_cost_method_standard():
+ order.post_visible = any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids) or \
+ any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids)
+ else:
+ order.post_visible = any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids)
@api.multi
@api.depends('workorder_ids.state', 'move_finished_ids')
diff --git a/addons/project/static/src/js/project.js b/addons/project/static/src/js/project.js
index 5da3151aa68..a1ca1a60302 100644
--- a/addons/project/static/src/js/project.js
+++ b/addons/project/static/src/js/project.js
@@ -51,19 +51,23 @@ KanbanRecord.include({
}
function open_cover_images_dialog(attachment_ids) {
- var cover_id = self.record.displayed_image_id.raw_value[0];
+ var coverID = self.record.displayed_image_id && self.record.displayed_image_id.raw_value[0];
var $content = $(QWeb.render("project.SetCoverModal", {
- cover_id: cover_id,
+ cover_id: coverID,
attachment_ids: attachment_ids,
}));
var $imgs = $content.find('img');
-
var dialog = new Dialog(self, {
title: _t("Set a Cover Image"),
- buttons: [{text: _t("Select"), classes: 'btn-primary', close: true, disabled: !cover_id, click: function () {
- self._updateRecord({displayed_image_id: $imgs.filter('.o_selected').data('id')});
+ buttons: [{text: _t("Select"), classes: 'btn-primary', close: true, disabled: !coverID, click: function () {
+ var $img = $imgs.filter('.o_selected');
+ var data = {
+ id: $img.data('id'),
+ display_name: $img.data('name')
+ };
+ self._updateRecord({displayed_image_id: data});
}}, {text: _t("Remove Cover Image"), close: true, click: function () {
- self._updateRecord({displayed_image_id: 0});
+ self._updateRecord({displayed_image_id: false});
}}, {text: _t("Discard"), close: true}],
$content: $content,
}).open();
@@ -75,7 +79,12 @@ KanbanRecord.include({
});
$content.on('dblclick', 'img', function (ev) {
- self._updateRecord({displayed_image_id: $(ev.currentTarget).data('id')});
+ var $img = $(ev.currentTarget);
+ var data = {
+ id: $img.data('id'),
+ display_name: $img.data('name')
+ };
+ self._updateRecord({displayed_image_id: data});
dialog.close();
});
}
diff --git a/addons/project/static/src/xml/project.xml b/addons/project/static/src/xml/project.xml
index d9c4f3fa675..a2d222784b9 100644
--- a/addons/project/static/src/xml/project.xml
+++ b/addons/project/static/src/xml/project.xml
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="project.SetCoverModal">
<div t-if="attachment_ids.length > 0" class="o_kanban_task_cover_image">
- <img t-foreach="attachment_ids" t-as="attachment" t-attf-class="img-responsive#{cover_id == attachment['id'] ? ' o_selected' : ''}" t-attf-src="/web/image/#{attachment['id']}?unique=1" t-att-data-id="attachment['id']"/>
+ <img t-foreach="attachment_ids" t-as="attachment" t-attf-class="img-responsive#{cover_id == attachment['id'] ? ' o_selected' : ''}" t-attf-src="/web/image/#{attachment['id']}?unique=1" t-att-data-id="attachment['id']" t-att-data-name="attachment['name']"/>
</div>
<div t-if="attachment_ids.length == 0">There is no available image to be set as cover. Send a message on the task with an attached image.</div>
</t>
diff --git a/addons/project/static/tests/project_tests.js b/addons/project/static/tests/project_tests.js
new file mode 100644
index 00000000000..fa92415684e
--- /dev/null
+++ b/addons/project/static/tests/project_tests.js
@@ -0,0 +1,133 @@
+odoo.define('project.project_kanban_tests', function (require) {
+"use strict";
+
+var KanbanView = require('web.KanbanView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('project', {
+ beforeEach: function () {
+ this.data = {
+ 'ir.attachment': {
+ fields: {
+ name: {
+ string: "Name",
+ type: "char"
+ },
+ },
+ records: [{
+ id: 1,
+ name: "1.png"
+ },
+ {
+ id: 2,
+ name: "2.png"
+ },
+ ]
+ },
+ 'project.task': {
+ fields: {
+ name: {
+ string: "Task Title",
+ type: "char"
+ },
+ sequence: {
+ string: "sequence",
+ type: "integer"
+ },
+ displayed_image_id: {
+ string: "cover",
+ type: "many2one",
+ relation: "ir.attachment"
+ },
+ kanban_state: {
+ string: "State",
+ type: "selection",
+ selection: [
+ ["abc", "ABC"],
+ ["def", "DEF"],
+ ["ghi", "GHI"]
+ ]
+ },
+ },
+ records: [{
+ id: 1,
+ name: "task1",
+ sequence: 1,
+ kanban_state: "abc"
+ },
+ {
+ id: 2,
+ name: "task2",
+ sequence: 2,
+ kanban_state: "abc"
+ },
+ ]
+ },
+ };
+ }
+}, function () {
+ QUnit.module('image test');
+
+ QUnit.test('cover_image_test', function (assert) {
+ assert.expect(6);
+ var kanban = createView({
+ View: KanbanView,
+ model: 'project.task',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="name"/>' +
+ '<div class="o_dropdown_kanban dropdown">' +
+ '<a class="dropdown-toggle btn" data-toggle="dropdown" href="#">' +
+ '<span class="fa fa-bars fa-lg"/>' +
+ '</a>' +
+ '<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">' +
+ '<li>' +
+ '<a type="set_cover">Set Cover Image</a>'+
+ '</li>' +
+ '</ul>' +
+ '</div>' +
+ '<div>'+
+ '<field name="displayed_image_id" widget="attachment_image"/>'+
+ '</div>'+
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ mockRPC: function(route, args) {
+ if (args.model === 'ir.attachment' && args.method === 'search_read') {
+ return $.when([{
+ id: 1,
+ name: "1.png"
+ },{
+ id: 2,
+ name: "2.png"
+ }]);
+ }
+ if (args.model === 'project.task' && args.method === 'write') {
+ assert.step(args.args[0][0]);
+ return this._super(route, args);
+ }
+ return this._super(route, args);
+ },
+ });
+ assert.strictEqual(kanban.$('img').length, 0, "Initially there is no image.");
+ kanban.$('.o_dropdown_kanban [data-type=set_cover]').eq(0).click();
+ // single click on image
+ $('.modal').find("img[data-id='1']").click();
+ $('.modal-footer .btn-primary').click();
+ assert.strictEqual(kanban.$('img[src*="/web/image/1"]').length, 1, "Image inserted in record");
+ $('.o_dropdown_kanban [data-type=set_cover]').eq(1).click();
+ // double click on image
+ $('.modal').find("img[data-id='2']").dblclick();
+ assert.strictEqual(kanban.$('img[src*="/web/image/2"]').length, 1, "Image inserted after double click");
+ // varify write on both kanban record
+ assert.verifySteps([1,2]);
+ kanban.destroy();
+ });
+});
+});
diff --git a/addons/project/views/project_templates.xml b/addons/project/views/project_templates.xml
index 3806674c4b9..8baab3b7657 100644
--- a/addons/project/views/project_templates.xml
+++ b/addons/project/views/project_templates.xml
@@ -10,4 +10,9 @@
<script type="text/javascript" src="/project/static/src/js/web_planner_project.js"></script>
</xpath>
</template>
+ <template id="qunit_suite" name="project_test" inherit_id="web.qunit_suite">
+ <xpath expr="//t[@t-set='head']" position="inside">
+ <script type="text/javascript" src="/project/static/tests/project_tests.js"></script>
+ </xpath>
+ </template>
</odoo>
diff --git a/addons/project_issue_sheet/views/project_issue_view.xml b/addons/project_issue_sheet/views/project_issue_view.xml
index 7bd27cf2c7f..99928d28bdb 100644
--- a/addons/project_issue_sheet/views/project_issue_view.xml
+++ b/addons/project_issue_sheet/views/project_issue_view.xml
@@ -10,7 +10,7 @@
<field name="progress" widget="progressbar" attrs="{'invisible':[('task_id','=',False)]}" class="oe_inline"/>
</field>
<xpath expr="//notebook/page[@name='extra_info']" position="before">
- <page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}">
+ <page string="Timesheets" attrs="{'invisible': [('project_id', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user">
<field name="timesheet_ids" colspan="4" nolabel="1" context="{'default_user_id' : uid, 'default_project_id': project_id}"
groups="base.group_user">
<tree editable="top" string="Timesheet Activities">
diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py
index 9ec3ce6b9d5..84dbee6a3fd 100644
--- a/addons/sale/tests/test_sale_order.py
+++ b/addons/sale/tests/test_sale_order.py
@@ -105,6 +105,9 @@ class TestSaleOrder(TestSale):
def test_cost_invoicing(self):
""" Test confirming a vendor invoice to reinvoice cost on the so """
+ # force the pricelist to have the same currency as the company
+ self.env.ref('product.list0').currency_id = self.env.ref('base.main_company').currency_id
+
serv_cost = self.env.ref('product.service_cost_01')
prod_gap = self.env.ref('product.product_product_1')
so = self.env['sale.order'].create({
diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py
index bc8d314206e..91a094bf901 100644
--- a/addons/sale_expense/tests/test_sale_expense.py
+++ b/addons/sale_expense/tests/test_sale_expense.py
@@ -7,6 +7,9 @@ from odoo.addons.sale.tests.test_sale_common import TestSale
class TestSaleExpense(TestSale):
def test_sale_expense(self):
""" Test the behaviour of sales orders when managing expenses """
+ # force the pricelist to have the same currency as the company
+ self.env.ref('product.list0').currency_id = self.env.ref('base.main_company').currency_id
+
# create a so with a product invoiced on delivery
prod = self.env.ref('product.product_product_1')
so = self.env['sale.order'].create({
diff --git a/addons/sale_timesheet/models/account_invoice.py b/addons/sale_timesheet/models/account_invoice.py
index 64ac5d7a8aa..17ba8ad96e2 100644
--- a/addons/sale_timesheet/models/account_invoice.py
+++ b/addons/sale_timesheet/models/account_invoice.py
@@ -53,7 +53,7 @@ class AccountInvoice(models.Model):
def _compute_timesheet_revenue(self):
for invoice in self:
- for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service'):
+ for invoice_line in invoice.invoice_line_ids.filtered(lambda line: line.product_id.type == 'service').sorted(key=lambda inv_line: (inv_line.invoice_id, inv_line.id)):
uninvoiced_timesheet_lines = self.env['account.analytic.line'].sudo().search([
('so_line', 'in', invoice_line.sale_line_ids.ids),
('project_id', '!=', False),
@@ -63,13 +63,12 @@ class AccountInvoice(models.Model):
# NOTE JEM : changing quantity (or unit price) of invoice line does not impact the revenue calculation. (FP specs)
if uninvoiced_timesheet_lines:
- precision = invoice_line.currency_id.decimal_places
# delivered : update revenue with the prorata of number of hours on the timesheet line
if invoice_line.product_id.invoice_policy == 'delivery':
- invoiced_price_per_hour = float_round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount'))), precision)
+ invoiced_price_per_hour = invoice_line.currency_id.round(invoice_line.price_subtotal / float(sum(uninvoiced_timesheet_lines.mapped('unit_amount'))))
# invoicing analytic lines of different currency
total_revenue_per_currency = dict.fromkeys(uninvoiced_timesheet_lines.mapped('company_currency_id').ids, 0.0)
- for index, timesheet_line in enumerate(uninvoiced_timesheet_lines):
+ for index, timesheet_line in enumerate(uninvoiced_timesheet_lines.sorted(key=lambda ts: ts.date)):
if index+1 != len(uninvoiced_timesheet_lines):
line_revenue = invoice_line.currency_id.compute(invoiced_price_per_hour, timesheet_line.company_currency_id) * timesheet_line.unit_amount
total_revenue_per_currency[timesheet_line.company_currency_id.id] += line_revenue
@@ -92,7 +91,7 @@ class AccountInvoice(models.Model):
# invoicing analytic lines of different currency
total_revenue_per_currency = dict.fromkeys(no_zero_timesheet_revenue.mapped('company_currency_id').ids, 0.0)
- for index, timesheet_line in enumerate(no_zero_timesheet_revenue):
+ for index, timesheet_line in enumerate(no_zero_timesheet_revenue.sorted(key=lambda ts: ts.date)):
if index+1 != len(no_zero_timesheet_revenue):
price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.company_currency_id)
price_subtotal_sol = timesheet_line.so_line.currency_id.compute(timesheet_line.so_line.price_subtotal, timesheet_line.company_currency_id)
diff --git a/addons/stock/i18n/stock.pot b/addons/stock/i18n/stock.pot
index 1296f083384..8e86883b4aa 100644
--- a/addons/stock/i18n/stock.pot
+++ b/addons/stock/i18n/stock.pot
@@ -6384,6 +6384,12 @@ msgid "You cannot set a negative product quantity in an inventory line:\n"
msgstr ""
#. module: stock
+#: code:addons/stock/models/stock_production_lot.py:37
+#, python-format
+msgid "You are not allowed to create a lot for this picking type"
+msgstr ""
+
+#. module: stock
#: code:addons/stock/models/stock_move.py:927
#, python-format
msgid "You cannot split a draft move. It needs to be confirmed first."
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 54b4a877fd7..6e26cf6dc77 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -419,6 +419,9 @@ class ProductTemplate(models.Model):
relation="stock.location.route", string="Category Routes",
related='categ_id.total_route_ids')
+ def _is_cost_method_standard(self):
+ return True
+
def _compute_quantities(self):
res = self._compute_quantities_dict()
for template in self:
diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py
index 60f370fff51..a568cd80fe3 100644
--- a/addons/stock/models/stock_production_lot.py
+++ b/addons/stock/models/stock_production_lot.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
+from odoo.exceptions import UserError
class ProductionLot(models.Model):
@@ -27,6 +28,15 @@ class ProductionLot(models.Model):
('name_ref_uniq', 'unique (name, product_id)', 'The combination of serial number and product must be unique !'),
]
+ @api.model
+ def create(self, vals):
+ pack_id = self.env.context.get('active_pack_operation', False)
+ if pack_id:
+ pack = self.env['stock.pack.operation'].browse(pack_id)
+ if pack.picking_id and not pack.picking_id.picking_type_id.use_create_lots:
+ raise UserError(_("You are not allowed to create a lot for this picking type"))
+ return super(ProductionLot, self).create(vals)
+
@api.one
@api.depends('quant_ids.qty')
def _product_qty(self):
diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py
index 20de2d25775..8453a49b4e3 100644
--- a/addons/stock/models/stock_scrap.py
+++ b/addons/stock/models/stock_scrap.py
@@ -66,6 +66,12 @@ class StockScrap(models.Model):
scrap.do_scrap()
return scrap
+ @api.multi
+ def unlink(self):
+ if 'done' in self.mapped('state'):
+ raise UserError(_('You cannot delete a scrap which is done.'))
+ return super(StockScrap, self).unlink()
+
def _get_origin_moves(self):
return self.picking_id and self.picking_id.move_lines.filtered(lambda x: x.product_id == self.product_id)
diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv
index dc8f1756134..6b13597199d 100644
--- a/addons/stock/security/ir.model.access.csv
+++ b/addons/stock/security/ir.model.access.csv
@@ -78,4 +78,8 @@ access_barcode_rule_stock_manager,barcode.rule.stock.manager,barcodes.model_barc
access_stock_forecast_user,report.stock.forecast.user,model_report_stock_forecast,stock.group_stock_user,1,0,0,0
access_stock_forecast_manager,report.stock.forecast.manager,model_report_stock_forecast,stock.group_stock_manager,1,1,1,1
access_stock_scrap_user,stock.scrap.user,model_stock_scrap,stock.group_stock_user,1,1,1,0
-access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0 \ No newline at end of file
+access_procurement_order,procurement.order,stock.model_procurement_order,base.group_portal,1,0,0,0
+access_product_attribute_manager,product.attribute manager,product.model_product_attribute,stock.group_stock_manager,1,1,1,1
+access_product_attribute_value_manager,product.attribute manager value,product.model_product_attribute_value,stock.group_stock_manager,1,1,1,1
+access_product_attribute_price_manager,product.attribute manager price,product.model_product_attribute_price,stock.group_stock_manager,1,1,1,1
+access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1 \ No newline at end of file
diff --git a/addons/stock/views/stock_pack_operation_views.xml b/addons/stock/views/stock_pack_operation_views.xml
index 5b6919dab83..cd8469d4672 100644
--- a/addons/stock/views/stock_pack_operation_views.xml
+++ b/addons/stock/views/stock_pack_operation_views.xml
@@ -85,7 +85,7 @@
<field name="lot_name" invisible="not context.get('only_create', False)"/>
<field name="lot_id" invisible="context.get('only_create', False)"
domain="[('product_id','=', parent.product_id)]"
- context="{'default_product_id': parent.product_id}"/>
+ context="{'default_product_id': parent.product_id, 'active_pack_operation': parent.id}"/>
<field name="qty_todo" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
<field name="qty" invisible="context.get('serial')"/>
<button name="do_minus" type="object" icon="fa-minus-square" attrs="{'invisible': [('qty', '&lt;=', 0.99)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index 1e4ac7ffbe1..1b15d2aa066 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -51,6 +51,9 @@ class ProductTemplate(models.Model):
def _compute_cost_method(self):
self.cost_method = self.property_cost_method or self.categ_id.property_cost_method
+ def _is_cost_method_standard(self):
+ return self.property_cost_method == 'standard'
+
@api.one
def _set_cost_method(self):
return self.write({'property_cost_method': self.cost_method})
diff --git a/addons/stock_landed_costs/tests/common.py b/addons/stock_landed_costs/tests/common.py
index cdd99228dab..205d2171853 100644
--- a/addons/stock_landed_costs/tests/common.py
+++ b/addons/stock_landed_costs/tests/common.py
@@ -59,6 +59,9 @@ class TestStockLandedCostsCommon(AccountingTestCase):
self.brokerage_quantity = self._create_services('Brokerage Cost')
self.transportation_weight = self._create_services('Transportation Cost')
self.packaging_volume = self._create_services('Packaging Cost')
+ # Ensure the account properties exists.
+ self.ensure_account_property('property_stock_account_input')
+ self.ensure_account_property('property_stock_account_output')
def _create_services(self, name):
return self.Product.create({
diff --git a/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js b/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
index 807bdff70dd..8f0724bbb00 100644
--- a/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
+++ b/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
@@ -421,6 +421,12 @@
element.children().first().after(widget);
}
+ // !! ODOO FIX START !!
+ var parentOffset = parent.offset();
+ position.top = offset.top - parentOffset.top;
+ position.left = offset.left - parentOffset.left;
+ // !! ODOO FIX END !!
+
// Top and bottom logic
if (vertical === 'auto') {
if (offset.top + widget.height() * 1.5 >= $(window).height() + $(window).scrollTop() &&
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 9f56cd01ef4..481e9a7fc54 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -82,6 +82,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
}
});
},
+ show_wow: '_onShowWow',
},
init: function (parent) {
this.client_options = {};
@@ -319,6 +320,22 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky);
}
},
+ /**
+ * Displays a thumb up, heart or peace image (randomly) for a moment (e.g.
+ * used when an opportunity is won)
+ *
+ * @private
+ */
+ _onShowWow: function () {
+ var className = 'o_wow_thumbs';
+ if (Math.random() > 0.9) {
+ var otherClasses = ['o_wow_peace', 'o_wow_heart'];
+ className = otherClasses[Math.floor(Math.random()*otherClasses.length)];
+ }
+ var $body = $('body');
+ $body.addClass(className);
+ setTimeout($body.removeClass.bind($body, className), 1000);
+ },
});
return AbstractWebClient;
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 5908f7f5a8b..492d9f26552 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -176,8 +176,8 @@ var AbstractField = Widget.extend({
* focusable element was not found or invisible
*/
activate: function (options) {
- var $focusable = this.getFocusableElement();
- if ($focusable.length && $focusable.is(':visible')) {
+ if (this.isFocusable()) {
+ var $focusable = this.getFocusableElement();
$focusable.focus();
if ($focusable.is('input[type="text"], textarea')) {
$focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length;
@@ -212,6 +212,15 @@ var AbstractField = Widget.extend({
return $();
},
/**
+ * Returns true iff the widget has a visible element that can take the focus
+ *
+ * @returns {boolean}
+ */
+ isFocusable: function () {
+ var $focusable = this.getFocusableElement();
+ return $focusable.length && $focusable.is(':visible');
+ },
+ /**
* this method is used to determine if the field value is set to a meaningful
* value. This is useful to determine if a field should be displayed as empty
*
@@ -351,12 +360,13 @@ var AbstractField = Widget.extend({
* @param {Object} [options]
* @param {boolean} [options.forceChange=false] if true, the change event will be
* triggered even if the new value is the same as the old one
+ * @returns {Deferred}
*/
_setValue: function (value, options) {
// we try to avoid doing useless work, if the value given has not
// changed. Note that we compare the unparsed values.
if (this.lastSetValue === value || (this.value === false && value === '')) {
- return;
+ return $.when();
}
this.lastSetValue = value;
try {
@@ -364,18 +374,22 @@ var AbstractField = Widget.extend({
this._isValid = true;
} catch (e) {
this._isValid = false;
- return;
+ return $.Deferred().reject();
}
if (!(options && options.forceChange) && this._isSameValue(value)) {
- return;
+ return $.when();
}
+ var def = $.Deferred();
var changes = {};
changes[this.name] = value;
this.trigger_up('field_changed', {
dataPointID: this.dataPointID,
changes: changes,
viewType: this.viewType,
+ onSuccess: def.resolve.bind(def),
+ onFailure: def.reject.bind(def),
});
+ return def;
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index a5ceba50d3f..d4013a8bcdb 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1281,8 +1281,26 @@ var PriorityWidget = AbstractField.extend({
},
});
-var AttachmentImage = AbstractField.extend({
- template: 'AttachmentImage',
+var AttachmentImage = AbstractField.extend({
+ className: 'o_attachment_image',
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Reset cover image when widget value change
+ *
+ * @private
+ */
+ _render: function () {
+ if (this.value) {
+ this.$el.empty().append($('<img>/', {
+ src: "/web/image/" + this.value.data.id + "?unique=1",
+ title: this.value.data.display_name
+ }));
+ }
+ }
});
var StateSelectionWidget = AbstractField.extend({
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index e13c3d29004..ae3b06d3a02 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -213,17 +213,20 @@ var FieldMany2One = AbstractField.extend({
var self = this;
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
if (this.nodeOptions.quick_create) {
- return this._rpc({
- model: this.field.relation,
- method: 'name_create',
- args: [name],
- context: this.record.getContext(this.recordParams),
- })
- .then(function (result) {
- if (self.mode === "edit") {
- self.reinitialize({id: result[0], display_name: result[1]});
- }
- }, slowCreate);
+ this.trigger_up('mutexify', {
+ action: function () {
+ return self._rpc({
+ model: self.field.relation,
+ method: 'name_create',
+ args: [name],
+ context: self.record.getContext(self.recordParams),
+ }).then(function (result) {
+ if (self.mode === "edit") {
+ self.reinitialize({id: result[0], display_name: result[1]});
+ }
+ }, slowCreate);
+ },
+ });
} else {
slowCreate();
}
@@ -964,7 +967,6 @@ var FieldOne2Many = FieldX2Many.extend({
var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
var newID = self.value.data[index].id;
self.renderer.editRecord(newID);
- self.creatingRecord = false;
}
}
});
@@ -1014,6 +1016,7 @@ var FieldOne2Many = FieldX2Many.extend({
* in the kanban view
*/
_onAddRecord: function (ev) {
+ var self = this;
// we don't want interference with the components upstream.
ev.stopPropagation();
@@ -1023,10 +1026,11 @@ var FieldOne2Many = FieldX2Many.extend({
this._setValue({
operation: 'CREATE',
position: this.editable,
+ }).always(function () {
+ self.creatingRecord = false;
});
}
} else {
- var self = this;
this._openFormDialog({
on_saved: function (record) {
self._setValue({ operation: 'ADD', id: record.id });
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 5131db19028..41fc293e383 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -11,6 +11,7 @@ var utils = require('web.utils');
return core.Class.extend({
init: function () {
this._init_cache();
+ core.bus.on('clear_cache', this, this.invalidate.bind(this));
},
_init_cache: function () {
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 53e5be8ff6e..16a066db495 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -46,6 +46,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @returns {Deferred}
*/
start: function () {
+ // add classname to reflect the (absence of) access rights (used to
+ // correctly display the nocontent helper)
+ this.$el.toggleClass('o_cannot_create', !this.activeActions.create);
return this._super.apply(this, arguments)
.then(this._updateEnv.bind(this));
},
@@ -186,8 +189,12 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// user if some discarding has to be made). This operation must also be
// mutex-protected as commitChanges function of x2m has to be aware of
// all final changes made to a row.
- this.mutex.exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle)); // TODO write a test for this
- return this.mutex.exec(this._saveRecord.bind(this, recordID, options));
+ var self = this;
+ return this.mutex
+ .exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle))
+ .then(function () {
+ return self.mutex.exec(self._saveRecord.bind(self, recordID, options));
+ });
},
/**
* @override
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 60abfde4344..61c285d6581 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1841,7 +1841,6 @@ var BasicModel = AbstractModel.extend({
return d.res_id === res_id;
});
m2mList.data.push(dataPoint.id);
- m2mList.count++;
});
});
});
@@ -2476,7 +2475,10 @@ var BasicModel = AbstractModel.extend({
// REPLACE_WITH
x2manyList.res_ids = value[2];
x2manyList.count = x2manyList.res_ids.length;
- defs.push(self._readUngroupedList(x2manyList));
+ var def = self._readUngroupedList(x2manyList).then(function () {
+ return self._fetchX2ManysBatched(x2manyList);
+ });
+ defs.push(def);
}
});
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index 9578e568558..422af121d96 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -99,7 +99,9 @@ var FieldManagerMixin = {
// subrecord's form view), otherwise it bubbles up to the main form view
// but its model doesn't have any data related to the given dataPointID
event.stopPropagation();
- this._applyChanges(event.data.dataPointID, event.data.changes, event);
+ this._applyChanges(event.data.dataPointID, event.data.changes, event)
+ .done(event.data.onSuccess || function () {})
+ .fail(event.data.onFailure || function () {});
},
/**
* Some widgets need to trigger a reload of their data. For example, a
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index ce449f2dcc9..8eef70a916c 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -39,12 +39,10 @@ var FormController = BasicController.extend({
//--------------------------------------------------------------------------
/**
- * This method is supposed to focus the first active control, I think. It
- * is currently only called by the FormViewDialog.
- *
- * @todo To be implemented
+ * Calls autofocus on the renderer
*/
autofocus: function () {
+ this.renderer.autofocus();
},
/**
* This method switches the form view in edit mode, with a new record.
@@ -93,6 +91,14 @@ var FormController = BasicController.extend({
return dataPoint.data.display_name || _t('New');
},
/**
+ * Called each time the form view is attached into the DOM
+ *
+ * @todo convert to new style
+ */
+ on_attach_callback: function () {
+ this.autofocus();
+ },
+ /**
* Render buttons for the control panel. The form view can be rendered in
* a dialog, and in that case, if we have buttons defined in the footer, we
* have to use them instead of the standard buttons.
@@ -374,10 +380,8 @@ var FormController = BasicController.extend({
});
}
- if (event.data.show_wow) {
- def.then(function () {
- self.show_wow();
- });
+ if (event.data.showWow) {
+ def.then(this.trigger_up.bind(this, 'show_wow'));
}
def.always(this._enableButtons.bind(this));
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index ddd77ee5dc6..08f28438368 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -26,6 +26,29 @@ var FormRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
/**
+ * Focuses the field having attribute 'default_focus' set, if any, or the
+ * first focusable field otherwise.
+ */
+ autofocus: function () {
+ if (this.mode === 'readonly') {
+ return;
+ }
+ var focusWidget = this.defaultFocusField;
+ if (!focusWidget || !focusWidget.isFocusable()) {
+ var widgets = this.allFieldWidgets[this.state.id];
+ for (var i = 0; i < (widgets ? widgets.length : 0); i++) {
+ var widget = widgets[i];
+ if (widget.isFocusable()) {
+ focusWidget = widget;
+ break;
+ }
+ }
+ }
+ if (focusWidget) {
+ focusWidget.activate({noselect: true});
+ }
+ },
+ /**
* Extend the method so that labels also receive the 'o_field_invalid' class
* if necessary.
*
@@ -183,7 +206,7 @@ var FormRenderer = BasicRenderer.extend({
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: self.state,
- show_wow: self.$el.hasClass('o_wow'), // TODO: implement this (in view)
+ showWow: $el.hasClass('o_wow'),
});
});
},
@@ -709,7 +732,10 @@ var FormRenderer = BasicRenderer.extend({
* @returns {jQueryElement}
*/
_renderTagSeparator: function (node) {
- return $('<div/>').addClass('o_horizontal_separator').text(node.attrs.string);
+ var $separator = $('<div/>').addClass('o_horizontal_separator').text(node.attrs.string);
+ this._handleAttributes($separator, node);
+ this._registerModifiers(node, this.state, $separator);
+ return $separator;
},
/**
* @private
@@ -769,20 +795,16 @@ var FormRenderer = BasicRenderer.extend({
core.bus.trigger('DOM_updated');
// Attach the tooltips on the fields' label
- var focusWidget = this.defaultFocusField;
_.each(this.allFieldWidgets[this.state.id], function (widget) {
- if (!focusWidget) {
- focusWidget = widget;
- }
if (core.debug || widget.attrs.help || widget.field.help) {
var idForLabel = self.idsForLabels[widget.name];
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
self._addFieldTooltip(widget, $label);
}
});
- if (focusWidget) {
- focusWidget.activate({noselect: true});
- }
+
+ // Focus the default field (in edit mode)
+ self.autofocus();
},
/**
* Sets id attribute of given widget to idForLabel
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 9364d1ba9fe..08b9e7f1602 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -155,7 +155,7 @@ ListRenderer.include({
var self = this;
var rowIndex = _.findIndex(this.state.data, {id: recordID});
if (rowIndex < 0) {
- return;
+ return $.when();
}
var editMode = (mode === 'edit');
var record = this.state.data[rowIndex];
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index b84d60d5b7c..a19e440913e 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -4,6 +4,7 @@ odoo.define('web.view_dialogs', function (require) {
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
+var dom = require('web.dom');
var ListController = require('web.ListController');
var ListView = require('web.ListView');
var pyeval = require('web.pyeval');
@@ -194,8 +195,10 @@ var FormViewDialog = ViewDialog.extend({
if ($buttons.children().length) {
self.$footer.empty().append($buttons.contents());
}
- _super().$el.append(fragment);
- self.form_view.autofocus();
+ dom.append(_super().$el, fragment, {
+ callbacks: [{widget: self.form_view}],
+ in_DOM: true,
+ });
});
});
@@ -281,6 +284,7 @@ var SelectCreateDialog = ViewDialog.extend({
if(this.options.initial_view !== "search") {
return this.create_edit_record();
}
+ var self = this;
var user_context = this.getSession().user_context;
var _super = this._super.bind(this);
@@ -298,7 +302,10 @@ var SelectCreateDialog = ViewDialog.extend({
this.loadViews(this.dataset.model, this.dataset.get_context(), [[false, 'list'], [false, 'search']], {})
.then(this.setup.bind(this, search_defaults))
.then(function (fragment) {
- _super().$el.append(fragment);
+ dom.append(_super().$el, fragment, {
+ callbacks: [{widget: self.list_controller}],
+ in_DOM: true,
+ });
});
return this;
},
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 353a5dc78f2..76281d9366a 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -42,6 +42,7 @@ var DateWidget = Widget.extend({
locale : moment.locale(),
allowInputToggle: true,
keyBinds: null,
+ widgetParent: 'body',
});
},
/**
diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js
index 279428e8939..6607561a2bb 100644
--- a/addons/web/static/src/js/widgets/model_field_selector.js
+++ b/addons/web/static/src/js/widgets/model_field_selector.js
@@ -16,6 +16,11 @@ var modelFieldsCache = {
cacheDefs: {},
};
+core.bus.on('clear_cache', null, function () {
+ modelFieldsCache.cache = {};
+ modelFieldsCache.cacheDefs = {};
+});
+
/**
* The ModelFieldSelector widget can be used to display/select a particular
* field chain from a given model.
diff --git a/addons/web/static/src/less/model_field_selector.less b/addons/web/static/src/less/model_field_selector.less
index 124c15cb8e5..aee5bd17867 100644
--- a/addons/web/static/src/less/model_field_selector.less
+++ b/addons/web/static/src/less/model_field_selector.less
@@ -7,6 +7,10 @@
.o-flex-flow(row, wrap);
.o-align-items(center);
height: 100%;
+ min-height: 20px; // needed when there is no value in it and used standalone
+ &:active, &:focus, &:active:focus {
+ outline: none;
+ }
> .o_field_selector_chain_part {
padding: 0px 1px;
diff --git a/addons/web/static/src/less/webclient.less b/addons/web/static/src/less/webclient.less
index 2f9878fbdc3..5e84d74b599 100644
--- a/addons/web/static/src/less/webclient.less
+++ b/addons/web/static/src/less/webclient.less
@@ -12,6 +12,7 @@ a {
// General
// ------------------------------------------------------------------
.o_web_client {
+ position: relative; // normally useless but required by bootstrap-datepicker
background-color: @odoo-webclient-background-color;
}
.o_text_overflow {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 394abe18c7c..20e38e98912 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -24,7 +24,7 @@
<div t-name="CrashManager.warning" class="o_dialog_warning">
<t t-js="d">
- var message = d.message ? d.message : d.error.data.message;
+ var message = (d.message !== undefined) ? d.message : d.error.data.message;
d.html_error = context.engine.tools.html_escape(message).replace(/\n/g, '<br/>');
</t>
<t t-raw="html_error"/>
@@ -836,11 +836,6 @@
t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"/>
</t>
-<t t-name="AttachmentImage">
- <div class="o_attachment_image">
- <img t-att-src="'/web/image/' + widget.value + '?unique=1'"/>
- </div>
-</t>
<t t-name="FieldBinaryFile">
<a t-if="widget.mode === 'readonly'" href="javascript:void(0)" class="o_form_uri"/>
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 4f326e14f98..127d5761fd2 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -482,6 +482,183 @@ QUnit.module('relational_fields', {
// assert.strictEqual(form.$('input').eq(1).val(), 'xphone', "onchange should have been applied");
// });
+ QUnit.test('form: quick create then save directly', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var def = $.Deferred();
+ var newRecordID;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return def.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ form.$('.o_field_many2one input').val('b').trigger('keydown');
+ concurrency.delay(0).then(function () {
+ $dropdown.find('li:first()').click(); // quick create 'b'
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+
+ def.resolve();
+
+ assert.verifySteps(['name_create', 'create']);
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('list: quick create then save directly', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var def = $.Deferred();
+ var newRecordID;
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="trululu"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return def.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ var $dropdown = list.$('.o_field_many2one input').autocomplete('widget');
+ list.$('.o_field_many2one input').val('b').trigger('keydown');
+ concurrency.delay(0).then(function () {
+ $dropdown.find('li:first()').click(); // quick create 'b'
+ list.$buttons.find('.o_list_button_add').click();
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+ assert.strictEqual(list.$('.o_data_row').length, 4,
+ "should wait for the name_create before adding the new row");
+
+ def.resolve();
+
+ assert.verifySteps(['name_create', 'create']);
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), 'b',
+ "created row should have the correct m2o value");
+ assert.strictEqual(list.$('.o_data_row').length, 5,
+ "should have added the fifth row");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ list.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('list in form: quick create then save directly', function (assert) {
+ var done = assert.async();
+ assert.expect(6);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var def = $.Deferred();
+ var newRecordID;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return def.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].p[0][2].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ form.$('.o_field_many2one input').val('b').trigger('keydown');
+ concurrency.delay(0).then(function () {
+ $dropdown.find('li:first()').click(); // quick create 'b'
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+
+ def.resolve();
+
+ assert.verifySteps(['name_create', 'create']);
+ assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b',
+ "first row should have the correct m2o value");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ done();
+ });
+ });
+
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
@@ -3852,6 +4029,115 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editable list: value reset by an onchange', function (assert) {
+ // this test reproduces a subtle behavior that may occur in a form view:
+ // the user adds a record in a one2many field, and directly clicks on a
+ // datetime field of the form view which has an onchange, which totally
+ // overrides the value of the one2many (commands 5 and 0). The handler
+ // that switches the edited row to readonly is then called after the
+ // new value of the one2many field is applied (the one returned by the
+ // onchange), so the row that must go to readonly doesn't exist anymore.
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ datetime: function (obj) {
+ obj.turtles = [[5], [0, 0, {display_name: 'new'}]];
+ },
+ };
+
+ var def;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="datetime"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // trigger the two onchanges
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_data_row .o_field_widget').val('a name').trigger('input');
+ def = $.Deferred();
+ form.$('.o_datepicker_input').click(); // focusout o2m and set value to today
+
+ // resolve the onchange def
+ def.resolve();
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should have one record in the o2m");
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new',
+ "should be the record created by the onchange");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable list: onchange that returns a warning', function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.onchanges = {
+ display_name: function () {},
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step(args.method);
+ return $.when({
+ value: {},
+ warning: {
+ title: "Warning",
+ message: "You must first select a partner"
+ },
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ intercepts: {
+ warning: function () {
+ assert.step('warning');
+ },
+ },
+ });
+
+ // add a line (this should trigger an onchange and a warning)
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ // check if 'Add an item' still works (this should trigger an onchange
+ // and a warning again)
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.verifySteps(['onchange', 'warning', 'onchange', 'warning']);
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index eae31e68e80..bc2cf05f360 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -13,6 +13,7 @@ odoo.define('web.test_utils', function (require) {
var basic_fields = require('web.basic_fields');
var config = require('web.config');
var core = require('web.core');
+var dom = require('web.dom');
var session = require('web.session');
var MockServer = require('web.MockServer');
var Widget = require('web.Widget');
@@ -172,7 +173,14 @@ function createAsyncView(params) {
widget.destroy();
$('#qunit-fixture').off('DOMNodeInserted.removeSRC');
};
- return view.appendTo($view_manager).then(function () {
+ // render the view in a fragment as they must be able to render correctly
+ // without being in the DOM
+ var fragment = document.createDocumentFragment();
+ return view.appendTo(fragment).then(function () {
+ dom.append($view_manager, fragment, {
+ callbacks: [{widget: view}],
+ in_DOM: true,
+ });
view.$el.on('click', 'a', function (ev) {
ev.preventDefault();
});
@@ -244,52 +252,53 @@ function addMockEnvironment(widget, params) {
logLevel: params.logLevel,
currentDate: params.currentDate,
});
- var widgetDestroy;
-
// make sure the debounce value for input fields is set to 0
var initialDebounce = DebouncedField.prototype.DEBOUNCE;
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
-
+ var initialSession, initialConfig, initialParameters;
if ('session' in params) {
- var initialSession = _.extend({}, session);
+ initialSession = _.extend({}, session);
_.extend(session, params.session);
- widgetDestroy = widget.destroy;
- widget.destroy = function () {
- for (var key in session) {
- delete session[key];
- }
- _.extend(session, initialSession);
- widgetDestroy.call(this);
- };
}
-
if ('config' in params) {
- var initialConfig = _.extend({}, config);
+ initialConfig = _.extend({}, config);
_.extend(config, params.config);
- widgetDestroy = widget.destroy;
- widget.destroy = function () {
- for (var key in config) {
- delete config[key];
- }
- _.extend(config, initialConfig);
- DebouncedField.prototype.DEBOUNCE = initialDebounce;
- widgetDestroy.call(this);
- };
}
-
if ('translateParameters' in params) {
- var initialParameters = _.extend({}, core._t.database.parameters);
+ initialParameters = _.extend({}, core._t.database.parameters);
_.extend(core._t.database.parameters, params.translateParameters);
- var oldDestroy = widget.destroy;
- widget.destroy = function () {
- for (var key in core._t.database.parameters) {
+ }
+
+ var widgetDestroy = widget.destroy;
+ widget.destroy = function () {
+ // clear the caches (e.g. data_manager, ModelFieldSelector) when the
+ // widget is destroyed, at the end of each test to avoid collisions
+ core.bus.trigger('clear_cache');
+
+ DebouncedField.prototype.DEBOUNCE = initialDebounce;
+
+ var key;
+ if ('session' in params) {
+ for (key in session) {
+ delete session[key];
+ }
+ _.extend(session, initialSession);
+ }
+ if ('config' in params) {
+ for (key in config) {
+ delete config[key];
+ }
+ _.extend(config, initialConfig);
+ }
+ if ('translateParameters' in params) {
+ for (key in core._t.database.parameters) {
delete core._t.database.parameters[key];
}
_.extend(core._t.database.parameters, initialParameters);
- oldDestroy.call(this);
- };
- }
+ }
+ widgetDestroy.call(this);
+ };
intercept(widget, 'call_service', function (event) {
if (event.data.service === 'ajax') {
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 582e06ba62b..fefc7231e5b 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -1165,6 +1165,54 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('default_get: fetch x2manys inside x2manys', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.o2m = {
+ string: "O2M", type: 'one2many', relation: 'partner', default: [[6, 0, [1]]],
+ };
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['o2m'],
+ fields: this.data.partner.fields,
+ fieldsInfo: {
+ form: {
+ o2m: {
+ relatedFields: this.data.partner.fields,
+ fieldsInfo: {
+ list: {
+ category: {
+ relatedFields: { display_name: {} },
+ },
+ },
+ },
+ viewType: 'list',
+ },
+ },
+ },
+ modelName: 'partner',
+ type: 'record',
+ viewType: 'form',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.o2m.count, 1, "o2m field should contain 1 record");
+ var categoryList = record.data.o2m.data[0].data.category;
+ assert.strictEqual(categoryList.count, 1,
+ "category field should contain 1 record");
+ assert.strictEqual(categoryList.data[0].data.display_name,
+ 'gold', "category records should have been fetched");
+ });
+
+ model.destroy();
+ });
+
QUnit.test('contexts and domains can be properly fetched', function (assert) {
assert.expect(8);
@@ -1448,6 +1496,7 @@ QUnit.module('Views', {
fieldsInfo: {
form: {
product_ids: {
+ relatedFields: this.data.product.fields,
fieldsInfo: { list: { name: {}, date: {} } },
viewType: 'list',
}
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index a07f022eb74..3f829a654b7 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -837,6 +837,28 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('invisible attrs on separators', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<separator string="Geolocation" attrs=\'{"invisible": [["bar", "=", True]]}\'/>'+
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('div.o_horizontal_separator').hasClass('o_invisible_modifier'), true,
+ "separator div should be hidden");
+ form.destroy();
+ });
+
QUnit.test('buttons in form view', function (assert) {
assert.expect(7);
@@ -3770,7 +3792,7 @@ QUnit.module('Views', {
});
form.$buttons.find('.o_form_button_edit').click();
- assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
assert.strictEqual(form.$('input[name="foo"]')[0].selectionStart, 3,
"cursor should be at the end");
@@ -3792,7 +3814,25 @@ QUnit.module('Views', {
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
- assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
+ "foo field should have focus");
+
+ form.destroy();
+ });
+
+ QUnit.test('in create mode, autofocus fields are focused', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="foo" default_focus="1"/>' +
+ '</form>',
+ });
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
form.destroy();
@@ -3821,6 +3861,24 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('autofocus first visible field', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field" invisible="1"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ });
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
+ "foo field should have focus");
+
+ form.destroy();
+ });
+
QUnit.test('open one2many form containing many2many_tags', function (assert) {
assert.expect(4);
@@ -4893,5 +4951,40 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('wow effect', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.model_name = { string: "Model name", type: "char" };
+
+ var clickCount = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<button name="test" string="Mark Won" type="object" class="o_wow"/>' +
+ '</form>',
+ intercepts: {
+ execute_action: function (event) {
+ if (clickCount === 0) {
+ event.data.on_success();
+ } else {
+ event.data.on_fail();
+ }
+ clickCount++;
+ },
+ show_wow: function () {
+ assert.step('wow');
+ },
+ },
+ });
+
+ form.$('button.o_wow').click();
+ assert.verifySteps(['wow']);
+ form.$('button.o_wow').click();
+ assert.verifySteps(['wow']);
+
+ form.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index e1cfef2b23b..332ba6687f9 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -84,7 +84,7 @@ QUnit.module('Views', {
QUnit.module('ListView');
QUnit.test('simple readonly list', function (assert) {
- assert.expect(9);
+ assert.expect(10);
var list = createView({
View: ListView,
@@ -93,6 +93,9 @@ QUnit.module('Views', {
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
});
+ assert.notOk(list.$el.hasClass('o_cannot_create'),
+ "should not have className 'o_cannot_create'");
+
// 3 th (1 for checkbox, 2 for columns)
assert.strictEqual(list.$('th').length, 3, "should have 3 columns");
@@ -114,6 +117,24 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('list with create="0"', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree create="0"><field name="foo"/></tree>',
+ });
+
+ assert.ok(list.$el.hasClass('o_cannot_create'),
+ "should have className 'o_cannot_create'");
+ assert.strictEqual(list.$buttons.find('.o_list_button_add').length, 0,
+ "should not have the 'Create' button");
+
+ list.destroy();
+ });
+
QUnit.test('simple editable rendering', function (assert) {
assert.expect(9);
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index fe79a8ac1a3..0b407e4d753 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -397,9 +397,10 @@ class Website(models.Model):
@api.model
def get_current_website(self):
- domain_name = request.httprequest.environ.get('HTTP_HOST', '').split(':')[0]
+ domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None
website_id = self._get_current_website_id(domain_name)
- request.context = dict(request.context, website_id=website_id)
+ if request:
+ request.context = dict(request.context, website_id=website_id)
return self.browse(website_id)
@tools.cache('domain_name')
diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js
index 6e0e4437487..2ce486fc1c8 100644
--- a/addons/website/static/src/js/website.ace.js
+++ b/addons/website/static/src/js/website.ace.js
@@ -3,18 +3,23 @@ odoo.define('website.ace', function (require) {
var Widget = require('web.Widget');
var website = require('website.website');
+var mixins = require('web.mixins');
var ViewEditor = require('web_editor.ace');
var hash = "#advanced-view-editor";
-var Ace = Widget.extend({
+var Ace = Widget.extend(mixins.ServiceProvider, {
events: {
"click a[data-action=ace]": function (e) {
e.preventDefault();
this.launchAce();
},
},
+ init: function () {
+ mixins.ServiceProvider.init.apply(this, arguments);
+ this._super.apply(this, arguments);
+ },
start: function () {
if (window.location.hash.substr(0, hash.length) === hash) {
this.launchAce();
diff --git a/addons/website/static/src/js/website.seo.js b/addons/website/static/src/js/website.seo.js
index 219728aa552..a05fc4a5883 100644
--- a/addons/website/static/src/js/website.seo.js
+++ b/addons/website/static/src/js/website.seo.js
@@ -6,6 +6,7 @@ var ajax = require('web.ajax');
var Class = require('web.Class');
var Dialog = require('web.Dialog');
var mixins = require('web.mixins');
+var rpc = require("web.rpc");
var Widget = require('web.Widget');
var base = require('web_editor.base');
var website = require('website.website');
@@ -490,22 +491,21 @@ var Configurator = Dialog.extend({
def.resolve(null);
} else {
var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'];
- this._rpc({
- model: obj.model,
- method: 'read',
- args: [[obj.id], fields, base.get_context()],
- })
- .then(function (data) {
- if (data.length) {
- var meta = data[0];
- meta.model = obj.model;
- def.resolve(meta);
- } else {
- def.resolve(null);
- }
- }).fail(function () {
- def.reject();
- });
+ rpc.query({
+ model: obj.model,
+ method: 'read',
+ args: [[obj.id], fields, base.get_context()],
+ }).then(function (data) {
+ if (data.length) {
+ var meta = data[0];
+ meta.model = obj.model;
+ def.resolve(meta);
+ } else {
+ def.resolve(null);
+ }
+ }).fail(function () {
+ def.reject();
+ });
}
return def;
},
@@ -514,11 +514,11 @@ var Configurator = Dialog.extend({
if (!obj) {
return $.Deferred().reject();
} else {
- return this._rpc({
- model: obj.model,
- method: 'write',
- args: [[obj.id], data, base.get_context()],
- });
+ return rpc.query({
+ model: obj.model,
+ method: 'write',
+ args: [[obj.id], data, base.get_context()],
+ });
}
},
titleChanged: function () {
diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py
index f501a64eefa..f1f002d5dd0 100644
--- a/addons/website_crm_partner_assign/models/crm_lead.py
+++ b/addons/website_crm_partner_assign/models/crm_lead.py
@@ -48,8 +48,6 @@ class CrmLead(models.Model):
for salesman_id, leads_ids in pycompat.items(salesmans_leads):
leads = self.browse(leads_ids)
leads.write({'user_id': salesman_id})
- for lead in leads:
- lead._onchange_user_id()
@api.multi
def action_assign_partner(self):
diff --git a/addons/website_event_sale/static/src/js/website.tour.event_sale.js b/addons/website_event_sale/static/src/js/website.tour.event_sale.js
index 8a29796dab4..895e6191f86 100644
--- a/addons/website_event_sale/static/src/js/website.tour.event_sale.js
+++ b/addons/website_event_sale/static/src/js/website.tour.event_sale.js
@@ -75,7 +75,7 @@ tour.register('event_buy_tickets', {
trigger: 'a[href="/shop/confirm_order"]:contains("Confirm")',
},
{
- content: "Check that the subtotal is 5,500.00",
+ content: "Check that the subtotal is 5,500.00 USD", // this test will fail if the currency of the main company is not USD
trigger: '#order_total_untaxed .oe_currency_value:contains("5,500.00")',
run: function () {}, // it's a check
},
diff --git a/addons/website_forum/i18n/website_forum.pot b/addons/website_forum/i18n/website_forum.pot
index 5fddf1e74a5..2f70c77241b 100644
--- a/addons/website_forum/i18n/website_forum.pot
+++ b/addons/website_forum/i18n/website_forum.pot
@@ -604,6 +604,7 @@ msgid "Bad Request"
msgstr ""
#. module: website_forum
+#: model:ir.model.fields,field_description:website_forum.field_res_users_badge_ids
#: model:ir.ui.view,arch_db:website_forum.badge
#: model:ir.ui.view,arch_db:website_forum.header
#: model:ir.ui.view,arch_db:website_forum.user_detail_full
@@ -782,6 +783,7 @@ msgstr ""
#. openerp-web
#: code:addons/website_forum/static/src/xml/website_forum_share_templates.xml:46
#: selection:forum.post,state:0
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
#, python-format
msgid "Close"
msgstr ""
@@ -884,6 +886,16 @@ msgid "Convert all answers to comments and vice versa"
msgstr ""
#. module: website_forum
+#: model:ir.ui.view,arch_db:website_forum.post_answers
+msgid "Convert as a comment"
+msgstr ""
+
+#. module: website_forum
+#: model:ir.ui.view,arch_db:website_forum.post_comment
+msgid "Convert as an answer"
+msgstr ""
+
+#. module: website_forum
#: model:ir.model.fields,field_description:website_forum.field_forum_post_karma_accept
msgid "Convert comment to answer"
msgstr ""
@@ -969,6 +981,12 @@ msgid "Default Post"
msgstr ""
#. module: website_forum
+#: model:ir.ui.view,arch_db:website_forum.post_answers
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
+msgid "Delete"
+msgstr ""
+
+#. module: website_forum
#: model:ir.model.fields,field_description:website_forum.field_forum_forum_karma_unlink_all
msgid "Delete all posts"
msgstr ""
@@ -1038,6 +1056,8 @@ msgstr ""
#. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.edit_post
+#: model:ir.ui.view,arch_db:website_forum.post_answers
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
msgid "Edit"
msgstr ""
@@ -1185,6 +1205,11 @@ msgid "First upvote"
msgstr ""
#. module: website_forum
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
+msgid "Flag"
+msgstr ""
+
+#. module: website_forum
#: model:ir.model.fields,field_description:website_forum.field_forum_forum_karma_flag
#: model:ir.model.fields,field_description:website_forum.field_forum_post_karma_flag
msgid "Flag a post as offensive"
@@ -1193,6 +1218,7 @@ msgstr ""
#. module: website_forum
#: selection:forum.post,state:0
#: model:ir.ui.view,arch_db:website_forum.moderation_queue
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
msgid "Flagged"
msgstr ""
@@ -2016,6 +2042,11 @@ msgid "Re: %s"
msgstr ""
#. module: website_forum
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
+msgid "Reactivate"
+msgstr ""
+
+#. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.header
msgid "Read Guidelines"
msgstr ""
@@ -2068,6 +2099,11 @@ msgid "Relevance Computation"
msgstr ""
#. module: website_forum
+#: model:ir.ui.view,arch_db:website_forum.post_description_full
+msgid "Reopen"
+msgstr ""
+
+#. module: website_forum
#: model:ir.ui.view,arch_db:website_forum.post_answers
#: model:ir.ui.view,arch_db:website_forum.post_description_full
msgid "Reply"
diff --git a/addons/website_forum/static/src/xml/website_forum_share_templates.xml b/addons/website_forum/static/src/xml/website_forum_share_templates.xml
index ab44aa48fcb..670e143ced8 100644
--- a/addons/website_forum/static/src/xml/website_forum_share_templates.xml
+++ b/addons/website_forum/static/src/xml/website_forum_share_templates.xml
@@ -33,7 +33,7 @@
<t t-name="website_forum.social_message_answer">
<p>By sharing you answer, you will get additional <b>karma points</b> if your
answer is selected as the right one. See what you can do with karma
- <a href="https://www.odoo.com/forum/how-to/miscellaneous-14/how-does-karma-work-odoo-v8-forum-software-57845" target="_blank">here</a>.</p>
+ <a href="/forum/help-1/faq" target="_blank">here</a>.</p>
</t>
<t t-name="website_forum.social_message_default">
<p>Share this content to increase your chances to be featured on the front page and attract more visitors.</p>
diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml
index 74df06b10cb..3608b9b47c7 100644
--- a/addons/website_forum/views/website_forum.xml
+++ b/addons/website_forum/views/website_forum.xml
@@ -793,9 +793,10 @@
</div>
</div>
<div class="text-muted mb16">
- <i t-if="question.post_type == 'question'" t-attf-class="fa fa-question-circle" t-att-title="question.post_type.capitalize()" />
- <i t-if="question.post_type == 'discussion'" t-attf-class="fa fa-comments-o" t-att-title="question.post_type.capitalize()" />
- <i t-if="question.post_type == 'link'" t-attf-class="fa fa-link" t-att-title="question.post_type.capitalize()" />
+ <t t-set="type_labels" t-value="dict(question._fields['post_type']._description_selection(question.env))"/>
+ <i t-if="question.post_type == 'question'" t-attf-class="fa fa-question-circle" t-att-title="type_labels[question.post_type]" />
+ <i t-if="question.post_type == 'discussion'" t-attf-class="fa fa-comments-o" t-att-title="type_labels[question.post_type]" />
+ <i t-if="question.post_type == 'link'" t-attf-class="fa fa-link" t-att-title="type_labels[question.post_type]" />
<span>By </span>
<a t-att-href="(question.create_uid.id == request.session.uid or question.create_uid.sudo().website_published) and '/forum/%s/user/%s' % (slug(forum), question.create_uid.id) or None"
t-field="question.create_uid"
@@ -819,7 +820,7 @@
<div class="mt16 mb24 text-center">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) + '/question/' + slug(question) + '/reopen'"/>
- <t t-set="label" t-value="'Reopen'"/>
+ <t t-set="label">Reopen</t>
<t t-set="classes" t-value="'fa-arrow-right'"/>
<t t-set="karma" t-value="not question.can_close and question.karma_close or 0"/>
</t>
@@ -832,7 +833,7 @@
<li t-if="question.state == 'close'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/reopen'"/>
- <t t-set="label" t-value="'Reopen'"/>
+ <t t-set="label">Reopen</t>
<t t-set="classes" t-value="'fa-undo'"/>
<t t-set="karma" t-value="not question.can_close and question.karma_close or 0"/>
</t>
@@ -840,7 +841,7 @@
<li>
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/post/' + slug(question) + '/edit'"/>
- <t t-set="label" t-value="'Edit'"/>
+ <t t-set="label">Edit</t>
<t t-set="classes" t-value="'fa-pencil-square-o'"/>
<t t-set="karma" t-value="not question.can_edit and question.karma_edit or 0"/>
</t>
@@ -848,7 +849,7 @@
<li t-if="question.state != 'close'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/ask_for_close'"/>
- <t t-set="label" t-value="'Close'"/>
+ <t t-set="label">Close</t>
<t t-set="classes" t-value="'fa-times'"/>
<t t-set="karma" t-value="not question.can_close and question.karma_close or 0"/>
</t>
@@ -856,7 +857,7 @@
<li t-if="question.active">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/delete'"/>
- <t t-set="label" t-value="'Delete'"/>
+ <t t-set="label">Delete</t>
<t t-set="classes" t-value="'fa-trash-o'"/>
<t t-set="karma" t-value="not question.can_unlink and question.karma_unlink or 0"/>
</t>
@@ -864,7 +865,7 @@
<li t-if="not question.active and question.state != 'offensive'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/question/' + slug(question) + '/undelete'"/>
- <t t-set="label" t-value="'Reactivate'"/>
+ <t t-set="label">Reactivate</t>
<t t-set="classes" t-value="'fa-trash-o'"/>
<t t-set="karma" t-value="not question.can_unlink and question.karma_unlink or 0"/>
</t>
@@ -872,7 +873,7 @@
<li t-if="not question.active and question.state == 'offensive'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) +'/post/' + slug(question) + '/validate'"/>
- <t t-set="label" t-value="'Reactivate'"/>
+ <t t-set="label">Reactivate</t>
<t t-set="classes" t-value="'fa-trash-o'"/>
<t t-set="karma" t-value="not question.can_moderate and question.forum_id.karma_moderate or 0"/>
</t>
@@ -883,7 +884,12 @@
t-attf-data-karma="#{not question.can_flag and question.forum_id.karma_flag or 0}"
t-attf-data-href="/forum/#{slug(forum)}/post/#{slug(question)}/flag"
>
- <t class="flag_flagged_label" t-esc="question.state == 'flagged' and 'Flagged' or 'Flag'"/>
+ <t t-if="question.state == 'flagged'">
+ Flagged
+ </t>
+ <t t-if="question.state != 'flagged'">
+ Flag
+ </t>
</a>
</li>
</ul>
@@ -982,7 +988,7 @@
<li>
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/edit'"/>
- <t t-set="label" t-value="'Edit'"/>
+ <t t-set="label">Edit</t>
<t t-set="classes" t-value="'fa fa-pencil-square-o'"/>
<t t-set="karma" t-value="not answer.can_edit and answer.karma_edit or 0"/>
</t>
@@ -990,7 +996,7 @@
<li>
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/delete'"/>
- <t t-set="label" t-value="'Delete'"/>
+ <t t-set="label">Delete</t>
<t t-set="classes" t-value="'fa-trash-o'"/>
<t t-set="karma" t-value="not answer.can_unlink and answer.karma_unlink or 0"/>
</t>
@@ -1007,7 +1013,7 @@
<li t-if="question.post_type == 'question'">
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(answer) + '/convert_to_comment'"/>
- <t t-set="label" t-value="'Convert as a comment'"/>
+ <t t-set="label">Convert as a comment</t>
<t t-set="classes" t-value="'fa-magic'"/>
<t t-set="karma" t-value="not answer.can_comment_convert and answer.karma_comment_convert or 0"/>
</t>
@@ -1081,7 +1087,7 @@
<t t-set="required_karma" t-value="message.author_id.id == user.partner_id.id and object.forum_id.karma_comment_unlink_own or object.forum_id.karma_comment_unlink_all"/>
<t t-call="website_forum.link_button">
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/delete'"/>
- <t t-set="label" t-value="' '"/>
+ <t t-set="label"> </t>
<t t-set="karma" t-value="user.karma&lt;required_karma and required_karma or 0"/>
<t t-set="classes" t-value="'close comment_delete fa-times'"/>
</t>
@@ -1094,7 +1100,7 @@
<t t-if="allow_post_comment">
<t t-call="website_forum.link_button" >
<t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/convert_to_answer'"/>
- <t t-set="label" t-value="'Convert as an answer'"/>
+ <t t-set="label">Convert as an answer</t>
<t t-set="karma" t-value="user.karma&lt;required_karma and required_karma or 0"/>
<t t-set="classes" t-value="'fa-magic pull-right'"/>
</t>
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index 2878c0bfc20..e7415b137fa 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -77,19 +77,19 @@ class Website(models.Model):
:param bool show_visible: if True, we don't display pricelist where selectable is False (Eg: Code promo)
:returns: pricelist recordset
"""
- website = request.website
- if not request.website:
+ website = request and request.website or None
+ if not website:
if self.env.context.get('website_id'):
website = self.browse(self.env.context['website_id'])
else:
website = self.search([], limit=1)
- isocountry = request.session.geoip and request.session.geoip.get('country_code') or False
+ isocountry = request and request.session.geoip and request.session.geoip.get('country_code') or False
partner = self.env.user.partner_id
order_pl = partner.last_website_so_id and partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id
partner_pl = partner.property_product_pricelist
pricelists = website._get_pl_partner_order(isocountry, show_visible,
website.user_id.sudo().partner_id.property_product_pricelist.id,
- request.session.get('website_sale_current_pl'),
+ request and request.session.get('website_sale_current_pl') or None,
website.pricelist_ids,
partner_pl=partner_pl and partner_pl.id or None,
order_pl=order_pl and order_pl.id or None)
@@ -113,7 +113,7 @@ class Website(models.Model):
available_pricelists = self.get_pricelist_available()
pl = None
partner = self.env.user.partner_id
- if request.session.get('website_sale_current_pl'):
+ if request and request.session.get('website_sale_current_pl'):
# `website_sale_current_pl` is set only if the user specifically chose it:
# - Either, he chose it from the pricelist selection
# - Either, he entered a coupon code