diff --git a/addons/account_payment/models/payment.py b/addons/account_payment/models/payment.py
index 02d52d25eb3..8dd98f98285 100644
--- a/addons/account_payment/models/payment.py
+++ b/addons/account_payment/models/payment.py
@@ -20,7 +20,7 @@ class PaymentTransaction(models.Model):
values.update(render_values)
return self.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=submit_txt or _('Pay Now')).sudo().render(
self.reference,
- invoice.residual_signed,
+ invoice.amount_residual_signed,
invoice.currency_id.id,
values=values,
)
diff --git a/addons/account_payment/views/account_portal_templates.xml b/addons/account_payment/views/account_portal_templates.xml
index bdc9c9da488..4f8c00964d6 100644
--- a/addons/account_payment/views/account_portal_templates.xml
+++ b/addons/account_payment/views/account_portal_templates.xml
@@ -7,7 +7,7 @@
-
Pay Now
@@ -16,16 +16,16 @@
-
+
Waiting for Payment
-
+
Authorized
-
+
Pending
-
+
Paid
@@ -36,7 +36,7 @@
-
+
@@ -71,14 +71,14 @@
-
Pay Now
-
+
Pending
-
@@ -87,11 +87,11 @@
-
+
-
+
@@ -133,11 +133,11 @@
-
- Communication:
+
+ Communication:
-
+
Done, your online payment has been successfully processed. Thank you for your order.
diff --git a/addons/base_geolocalize/models/base_geocoder.py b/addons/base_geolocalize/models/base_geocoder.py
index 17a10f7e0e5..5c2cee79cca 100644
--- a/addons/base_geolocalize/models/base_geocoder.py
+++ b/addons/base_geolocalize/models/base_geocoder.py
@@ -55,7 +55,7 @@ class GeoCoder(models.AbstractModel):
return self._geo_query_address_default(street=street, zip=zip, city=city, state=state, country=country)
@api.model
- def geo_find(self, addr):
+ def geo_find(self, addr, **kw):
"""Use a location provider API to convert an address string into a latitude, longitude tuple.
Here we use Openstreetmap Nominatim by default.
:param addr: Address string passed to API
@@ -64,7 +64,7 @@ class GeoCoder(models.AbstractModel):
provider = self._get_provider().tech_name
try:
service = getattr(self, '_call_' + provider)
- result = service(addr)
+ result = service(addr, **kw)
except AttributeError:
raise UserError(_(
'Provider %s is not implemented for geolocation service.'
@@ -77,7 +77,7 @@ class GeoCoder(models.AbstractModel):
return result
@api.model
- def _call_openstreetmap(self, addr):
+ def _call_openstreetmap(self, addr, **kw):
"""
Use Openstreemap Nominatim service to retrieve location
:return: (latitude, longitude) or None if not found
@@ -95,7 +95,7 @@ class GeoCoder(models.AbstractModel):
return float(geo['lat']), float(geo['lon'])
@api.model
- def _call_googlemap(self, addr):
+ def _call_googlemap(self, addr, **kw):
""" Use google maps API. It won't work without a valid API key.
:return: (latitude, longitude) or None if not found
"""
@@ -106,8 +106,11 @@ class GeoCoder(models.AbstractModel):
"Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information."
))
url = "https://maps.googleapis.com/maps/api/geocode/json"
+ params = {'sensor': 'false', 'address': addr, 'key': apikey}
+ if kw.get('force_country'):
+ params['components'] = 'country:%s' % kw['force_country']
try:
- result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json()
+ result = requests.get(url, params).json()
except Exception as e:
self._raise_query_error(e)
@@ -151,4 +154,3 @@ class GeoCoder(models.AbstractModel):
def _raise_query_error(self, error):
raise UserError(_('Error with geolocation server:') + ' %s' % error)
-
diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py
index a92c7af7df4..a0c887a1adb 100644
--- a/addons/base_geolocalize/models/res_partner.py
+++ b/addons/base_geolocalize/models/res_partner.py
@@ -10,10 +10,10 @@ class ResPartner(models.Model):
def _geo_localize(self, street='', zip='', city='', state='', country=''):
geo_obj = self.env['base.geocoder']
search = geo_obj.geo_query_address(street=street, zip=zip, city=city, state=state, country=country)
- result = geo_obj.geo_find(search)
+ result = geo_obj.geo_find(search, force_country=country)
if result is None:
search = geo_obj.geo_query_address(city=city, state=state, country=country)
- result = geo_obj.geo_find(search)
+ result = geo_obj.geo_find(search, force_country=country)
return result
@api.multi
diff --git a/addons/crm_livechat/__manifest__.py b/addons/crm_livechat/__manifest__.py
index bbded070830..1ff8aef366e 100644
--- a/addons/crm_livechat/__manifest__.py
+++ b/addons/crm_livechat/__manifest__.py
@@ -5,6 +5,9 @@
'name': 'CRM Livechat',
'category': 'crm',
'summary': 'Create lead from livechat conversation',
+ 'data': [
+ 'data/utm_data.xml',
+ ],
'depends': [
'crm',
'im_livechat'
diff --git a/addons/crm_livechat/data/utm_data.xml b/addons/crm_livechat/data/utm_data.xml
new file mode 100644
index 00000000000..0474945948b
--- /dev/null
+++ b/addons/crm_livechat/data/utm_data.xml
@@ -0,0 +1,6 @@
+
+
+
+ Livechat
+
+
diff --git a/addons/crm_livechat/models/mail_channel.py b/addons/crm_livechat/models/mail_channel.py
index 9db2cfd2ea4..9b3765942c8 100644
--- a/addons/crm_livechat/models/mail_channel.py
+++ b/addons/crm_livechat/models/mail_channel.py
@@ -4,6 +4,7 @@
from odoo import models, _
from odoo.tools import html2plaintext
+
class MailChannel(models.Model):
_inherit = 'mail.channel'
@@ -24,13 +25,15 @@ class MailChannel(models.Model):
'%s: %s\n' % (message.author_id.name or self.anonymous_name, message.body)
for message in self.channel_message_ids.sorted('id')
)
+ utm_source = self.env.ref('crm_livechat.utm_source_livechat', raise_if_not_found=False)
lead = self.env['crm.lead'].create({
'name': html2plaintext(key[5:]),
'partner_id': channel_partners.partner_id.id,
'user_id': None,
'team_id': None,
'description': html2plaintext(description),
- 'referred': partner.name
+ 'referred': partner.name,
+ 'source_id': utm_source and utm_source.id,
})
lead._onchange_partner_id()
msg = _('Created a new lead: %s') % (lead.id, lead.name)
diff --git a/addons/gamification/data/gamification_karma_rank_data.xml b/addons/gamification/data/gamification_karma_rank_data.xml
index f86b827cceb..5eba07b1a3d 100644
--- a/addons/gamification/data/gamification_karma_rank_data.xml
+++ b/addons/gamification/data/gamification_karma_rank_data.xml
@@ -14,8 +14,8 @@
You just began the adventure! Welcome!
1
@@ -27,8 +27,8 @@
You're a young padawan now. May the force be with you!
100
@@ -40,8 +40,8 @@
You love learning things. Curiosity is a good way to progress.
500
@@ -53,8 +53,8 @@
You know what you are talking about. People learn from you.
2000
@@ -66,7 +66,8 @@
You have reached the last rank. Congratulations!
10000
diff --git a/addons/gamification/data/gamification_karma_rank_demo.xml b/addons/gamification/data/gamification_karma_rank_demo.xml
index 6250ee227f6..839e8750047 100644
--- a/addons/gamification/data/gamification_karma_rank_demo.xml
+++ b/addons/gamification/data/gamification_karma_rank_demo.xml
@@ -1,5 +1,5 @@
-
+
20
@@ -39,7 +39,7 @@
diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py
index daa9d4bc1e1..a0ca65ced45 100644
--- a/addons/gamification/models/res_users.py
+++ b/addons/gamification/models/res_users.py
@@ -96,6 +96,14 @@ class Users(models.Model):
if old_rank != user.rank_id:
user._rank_changed()
+ def _get_next_rank(self):
+ """ For fresh users with 0 karma that don't have a rank_id and next_rank_id yet
+ this method returns the first karma rank (by karma ascending). This acts as a
+ default value in related views.
+
+ TDE FIXME in post-12.4: make next_rank_id a non-stored computed field correctly computed """
+ return self.next_rank_id or (not self.rank_id and self.env['gamification.karma.rank'].search([], order="karma_min ASC", limit=1))
+
def get_gamification_redirection_data(self):
"""
Hook for other modules to add redirect button(s) in new rank reached mail
diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py
index aa3040d7947..1797dd66bea 100644
--- a/addons/google_account/models/google_service.py
+++ b/addons/google_account/models/google_service.py
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
-from pprint import pformat
import json
import logging
@@ -184,29 +183,14 @@ class GoogleService(models.TransientModel):
except:
pass
except requests.HTTPError as error:
- # https://developers.google.com/calendar/v3/errors
if error.response.status_code in (204, 404):
status = error.response.status_code
response = ""
else:
- 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)
+ _logger.exception("Bad google request : %s !", error.response.content)
if error.response.status_code in (400, 401, 410):
- raise UserError(error_msg)
- raise self.env['res.config.settings'].get_config_warning(warning_msg)
+ raise error
+ raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
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/models/hr_employee.py b/addons/hr/models/hr_employee.py
index c8d566aa3a8..887d23bd904 100644
--- a/addons/hr/models/hr_employee.py
+++ b/addons/hr/models/hr_employee.py
@@ -143,6 +143,12 @@ class HrEmployeePrivate(models.Model):
raise AccessError(_('The fields "%s" you try to read is not available on the public employee profile.') % (','.join(private_fields)))
return self.env['hr.employee.public'].browse(self.ids).read(fields, load=load)
+ @api.model
+ def load_views(self, views, options=None):
+ if self.check_access_rights('read', raise_exception=False):
+ return super(HrEmployeePrivate, self).load_views(views, options=options)
+ return self.env['hr.employee.public'].load_views(views, options=options)
+
@api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
"""
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml
index f353c4d72bf..d7e52389246 100644
--- a/addons/hr/views/hr_employee_views.xml
+++ b/addons/hr/views/hr_employee_views.xml
@@ -146,6 +146,11 @@
+
+
+
+
+
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index 3a5c490ec70..439fa07148e 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -100,5 +100,6 @@
parent="menu_config_plan"
groups="group_hr_manager"
sequence="100"/>
+
diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py
index 343479f3bdb..9195bf138d6 100644
--- a/addons/hr_contract/models/hr_contract.py
+++ b/addons/hr_contract/models/hr_contract.py
@@ -16,7 +16,7 @@ class Contract(models.Model):
name = fields.Char('Contract Reference', required=True)
active = fields.Boolean(default=True)
- employee_id = fields.Many2one('hr.employee', string='Employee')
+ employee_id = fields.Many2one('hr.employee', string='Employee', tracking=True)
department_id = fields.Many2one('hr.department', string="Department")
job_id = fields.Many2one('hr.job', string='Job Position')
date_start = fields.Date('Start Date', required=True, default=fields.Date.today,
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 12c44f26cb4..ec194dcb1b3 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -600,7 +600,7 @@ class HolidaysRequest(models.Model):
return
current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
for record in self:
- emp_id = record._cache.get('employee_id', [False])[0]
+ emp_id = record._cache.get('employee_id', False) and record._cache.get('employee_id')[0]
if emp_id != current_employee.id:
try:
record._cache['name']
diff --git a/addons/hr_holidays/report/hr_holidays_reports.xml b/addons/hr_holidays/report/hr_holidays_reports.xml
index 59030b52b99..26386e7933b 100644
--- a/addons/hr_holidays/report/hr_holidays_reports.xml
+++ b/addons/hr_holidays/report/hr_holidays_reports.xml
@@ -4,7 +4,7 @@
+
+ hr.applicant.resumes.server
+
+ code
+
+act = env.ref('hr_recruitment.hr_applicant_resumes').read()[0]
+act['domain'] = [('res_model', '=', 'hr.applicant'), '|', ('company_id', '=', False), ('company_id', '=', env.user.company_id.id)]
+action = act
+
+
+
+
hr.recruitment.stage.tree
diff --git a/addons/l10n_br/data/account_tax_template_data.xml b/addons/l10n_br/data/account_tax_template_data.xml
index 72f61cd9716..5515bb686ff 100644
--- a/addons/l10n_br/data/account_tax_template_data.xml
+++ b/addons/l10n_br/data/account_tax_template_data.xml
@@ -2296,7 +2296,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_201010302'),
+ 'account_id': ref('account_template_101050505'),
'minus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2309,7 +2309,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_101050505'),
+ 'account_id': ref('account_template_201010302'),
'plus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2333,7 +2333,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_201010302'),
+ 'account_id': ref('account_template_101050505'),
'minus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2346,7 +2346,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_101050505'),
+ 'account_id': ref('account_template_201010302'),
'plus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2405,7 +2405,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_201010302'),
+ 'account_id': ref('account_template_101050505'),
'minus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2418,7 +2418,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_101050505'),
+ 'account_id': ref('account_template_201010302'),
'plus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2442,7 +2442,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_201010302'),
+ 'account_id': ref('account_template_101050505'),
'minus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2455,7 +2455,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_101050505'),
+ 'account_id': ref('account_template_201010302'),
'plus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2479,7 +2479,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_201010302'),
+ 'account_id': ref('account_template_101050505'),
'minus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2492,7 +2492,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_101050505'),
+ 'account_id': ref('account_template_201010302'),
'plus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2516,7 +2516,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_201010302'),
+ 'account_id': ref('account_template_101050505'),
'minus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
@@ -2529,7 +2529,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'tax',
- 'account_id': ref('account_template_101050505'),
+ 'account_id': ref('account_template_201010302'),
'plus_report_line_ids': [ref('tax_report_icms_1')],
}),
]"/>
diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py
index 14b89d8c7fa..09665c73540 100644
--- a/addons/l10n_fr_fec/wizard/account_fr_fec.py
+++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py
@@ -307,7 +307,7 @@ class AccountFrFec(models.TransientModel):
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
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,
+ ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') 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/models/mail_template.py b/addons/mail/models/mail_template.py
index d92a8a5ecb0..e657e146837 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -502,6 +502,7 @@ class MailTemplate(models.Model):
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
'model_description': self.env['ir.model']._get(record._name).display_name,
'company': 'company_id' in record and record['company_id'] or self.env.company,
+ 'record': record,
}
body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
values['body_html'] = self.env['mail.thread']._replace_local_links(body)
diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js
index e84d7588b1b..2a496faf86d 100644
--- a/addons/mail/static/src/js/composers/basic_composer.js
+++ b/addons/mail/static/src/js/composers/basic_composer.js
@@ -438,10 +438,27 @@ var BasicComposer = Widget.extend({
* @param {boolean} params.submitForm [optional]
*/
_processAttachmentChange: function (params) {
- var self = this,
- attachments = this.get('attachment_ids'),
- files = params.files,
- submitForm = params.submitForm;
+ var self = this;
+ var attachments = this.get('attachment_ids');
+ var files = params.files;
+ var submitForm = params.submitForm;
+ var $form = this.$('form.o_form_binary_form');
+
+ /**
+ * makes a new formData as formData.delete() is not supported by IE or Safari Mobile.
+ *
+ * @return {FormData}
+ */
+ function makeFormDataWithoutUfile() {
+ var newFormData = new window.FormData();
+ $form.find('input').each(function (index, input) {
+ if (input.name !== 'ufile') {
+ newFormData.append(input.name, input.value);
+ }
+ });
+ return newFormData;
+ }
+
_.each(files, function (file) {
var attachment = _.findWhere(attachments, {
name: file.name,
@@ -453,24 +470,20 @@ var BasicComposer = Widget.extend({
attachments = _.without(attachments, attachment);
}
});
- var $form = this.$('form.o_form_binary_form');
if (submitForm) {
$form.submit();
this._$attachmentButton.prop('disabled', true);
} else {
- var data = new FormData($form[0]);
_.each(files, function (file) {
- // removing existing key with blank data and appending again with file info
- // In safari, existing key will not be updated when append with new file.
- data.delete("ufile");
- data.append("ufile", file, file.name);
+ var formData = makeFormDataWithoutUfile();
+ formData.append("ufile", file, file.name);
$.ajax({
url: $form.attr("action"),
type: "POST",
enctype: 'multipart/form-data',
processData: false,
contentType: false,
- data: data,
+ data: formData,
success: function (result) {
var $el = $(result);
$.globalEval($el.contents().text());
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index 5d0647d9762..6eeeaecf7c3 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -167,9 +167,9 @@ var RenameConversationDialog = Dialog.extend({
kwargs: {
name: name,
}
- }).then(function (updatedName) {
+ }).then(function () {
var channel = self.call('mail_service', 'getThread', self._channelID);
- channel.setName(updatedName);
+ channel.setName(name);
self._callback();
});
},
diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js
index b5b2e068cbf..f93209a913f 100644
--- a/addons/mail/static/src/js/models/messages/message.js
+++ b/addons/mail/static/src/js/models/messages/message.js
@@ -801,6 +801,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
_warnMessageModerated: function () {
var mailBus = this.call('mail_service', 'getMailBus');
if (this.needsModerationByUser()) {
+ this._setModeratedByUser(false);
var moderationBox = this.call('mail_service', 'getMailbox', 'moderation');
moderationBox.decrementMailboxCounter();
moderationBox.removeMessage(this.getID());
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 01397f6ab27..6c7cefe42e3 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -94,9 +94,7 @@ var MailManager = AbstractService.extend({
additionalThreadIDs: data.channel_ids
});
}
- if (options.domain && options.domain !== []) {
- this._addMessageToThreads(message, options);
- }
+ this._addMessageToThreads(message, options);
}
return prom;
},
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index daf34b4a5be..8c65266a392 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -1425,7 +1425,7 @@ QUnit.test('rename DM conversation', async function (assert) {
assert.step(args.method);
assert.strictEqual(args.args[0], 1);
assert.strictEqual(args.kwargs.name, "Demo");
- return Promise.resolve("Demo");
+ return Promise.resolve();
}
return this._super.apply(this, arguments);
},
diff --git a/addons/mass_mailing/i18n/mass_mailing.pot b/addons/mass_mailing/i18n/mass_mailing.pot
index 5e2624c624d..9d3a4b33523 100644
--- a/addons/mass_mailing/i18n/mass_mailing.pot
+++ b/addons/mass_mailing/i18n/mass_mailing.pot
@@ -2320,9 +2320,9 @@ msgstr ""
#. module: mass_mailing
#. openerp-web
-#: code:addons/mass_mailing/static/src/js/unsubscribe.js:49
+#: code:addons/mass_mailing/static/src/js/unsubscribe.js:50
#, python-format
-msgid "You have been successfully unsubscribed from ."
+msgid "You have been successfully unsubscribed from %s."
msgstr ""
#. module: mass_mailing
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index 6d718a9ffbf..34ef0472c7e 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -95,7 +95,7 @@ class MailMail(models.Model):
return res
@api.multi
- def _postprocess_sent_message(self, failure_type=None, **kwargs):
+ def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type=None):
mail_sent = not failure_type # we consider that a recipient error is a failure with mass mailling and show them as failed
for mail in self:
if mail.mailing_id:
@@ -103,5 +103,4 @@ class MailMail(models.Model):
mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False})
elif mail_sent is False and mail.statistics_ids:
mail.statistics_ids.write({'exception': fields.Datetime.now()})
- return super(MailMail, self)._postprocess_sent_message(failure_type=failure_type, **kwargs)
-
\ No newline at end of file
+ return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
diff --git a/addons/mass_mailing/static/src/js/unsubscribe.js b/addons/mass_mailing/static/src/js/unsubscribe.js
index ae1c34f864b..f43aefcd976 100644
--- a/addons/mass_mailing/static/src/js/unsubscribe.js
+++ b/addons/mass_mailing/static/src/js/unsubscribe.js
@@ -46,7 +46,10 @@ odoo.define('mass_mailing.unsubscribe', function (require) {
var unsubscribed_list = $("input[name='unsubscribed_list']").val();
if (unsubscribed_list){
- $('#subscription_info').html(_t('You have been successfully unsubscribed from ' + unsubscribed_list + "."));
+ $('#subscription_info').html(_.str.sprintf(
+ _t("You have been successfully unsubscribed from %s."),
+ unsubscribed_list
+ ));
}
else{
$('#subscription_info').html(_t('You have been successfully unsubscribed.'));
diff --git a/addons/mass_mailing/views/unsubscribe_templates.xml b/addons/mass_mailing/views/unsubscribe_templates.xml
index 793bb61034d..575824aedcc 100644
--- a/addons/mass_mailing/views/unsubscribe_templates.xml
+++ b/addons/mass_mailing/views/unsubscribe_templates.xml
@@ -121,7 +121,7 @@
Odoo
- ![]()
+
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 90f21112012..cb6575673ff 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -828,8 +828,6 @@ 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()
@@ -860,6 +858,11 @@ class MrpProduction(models.Model):
if wo.time_ids.filtered(lambda x: (not x.date_end) and (x.loss_type in ('productive', 'performance'))):
raise UserError(_('Work order %s is still running') % wo.name)
self._check_lots()
+
+ # Cancel unfinished move
+ move_to_cancel = self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel') and float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding))
+ move_to_cancel._action_cancel()
+
self.post_inventory()
# Moves without quantity done are not posted => set them as done instead of canceling. In
# case the user edits the MO later on and sets some consumed quantity on those, we do not
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index a6adb02778d..7b747de0ce6 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -825,8 +825,8 @@ class PaymentTransaction(models.Model):
@api.model
def _compute_reference_prefix(self, values):
if values and values.get('invoice_ids'):
- many_list = self.resolve_2many_commands('invoice_ids', values['invoice_ids'], fields=['number'])
- return ','.join(dic['number'] for dic in many_list)
+ many_list = self.resolve_2many_commands('invoice_ids', values['invoice_ids'], fields=['name'])
+ return ','.join(dic['name'] for dic in many_list)
return None
@api.model
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index 946f031484d..6361a211f16 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -62,7 +62,8 @@ class PaymentAcquirerAuthorize(models.Model):
# The signature key is now '128-character hexadecimal format', while the
# transaction key was only 16-character.
if len(values['x_trans_key']) == 128:
- return hmac.new(values['x_trans_key'].decode("hex").encode('utf-8'), data, hashlib.sha512).hexdigest().upper()
+ key = bytes.fromhex(values['x_trans_key'])
+ return hmac.new(key, data, hashlib.sha512).hexdigest().upper()
else:
return hmac.new(values['x_trans_key'].encode('utf-8'), data, hashlib.md5).hexdigest()
@@ -315,13 +316,14 @@ class TxAuthorize(models.Model):
'acquirer_reference': tree.get('x_trans_id'),
'date': fields.Datetime.now(),
})
- if init_state != 'authorized':
- self.execute_callback()
if self.payment_token_id:
self.payment_token_id.verified = True
self._set_transaction_done()
+
+ if init_state != 'authorized':
+ self.execute_callback()
if tree.get('x_type').lower() == 'auth_only':
self.write({'acquirer_reference': tree.get('x_trans_id')})
self._set_transaction_authorized()
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 9a173995152..f4d56fad0cb 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1738,8 +1738,11 @@ exports.Orderline = Backbone.Model.extend({
else
var price_include = !price_exclude;
if (tax.amount_type === 'fixed') {
- var sign_base_amount = base_amount >= 0 ? 1 : -1;
- return (Math.abs(tax.amount) * sign_base_amount) * quantity;
+ var sign_base_amount = Math.sign(base_amount) || 1;
+ // Since base amount has been computed with quantity
+ // we take the abs of quantity
+ // Same logic as bb72dea98de4dae8f59e397f232a0636411d37ce
+ return tax.amount * sign_base_amount * Math.abs(quantity);
}
if (tax.amount_type === 'percent' && !price_include){
return base_amount * tax.amount / 100;
diff --git a/addons/point_of_sale/wizard/pos_payment.py b/addons/point_of_sale/wizard/pos_payment.py
index 4c46927d1af..ca366987f4b 100644
--- a/addons/point_of_sale/wizard/pos_payment.py
+++ b/addons/point_of_sale/wizard/pos_payment.py
@@ -33,7 +33,7 @@ class PosMakePayment(models.TransientModel):
journal_id = fields.Many2one('account.journal', string='Payment Mode', required=True, default=_default_journal)
amount = fields.Float(digits=0, required=True, default=_default_amount)
payment_name = fields.Char(string='Payment Reference')
- payment_date = fields.Date(string='Payment Date', required=True, default=lambda *a: fields.Date.today())
+ payment_date = fields.Date(string='Payment Date', required=True, default=lambda self: fields.Date.context_today(self))
@api.onchange('session_id')
def _on_change_session(self):
diff --git a/addons/pos_cache/static/src/js/pos_cache.js b/addons/pos_cache/static/src/js/pos_cache.js
index 6a26143cc6e..1b59761bc26 100644
--- a/addons/pos_cache/static/src/js/pos_cache.js
+++ b/addons/pos_cache/static/src/js/pos_cache.js
@@ -15,22 +15,21 @@ models.PosModel = models.PosModel.extend({
return model.model === "product.product";
});
- // Give both the fields and domain to pos_cache in the
- // backend. This way we don't have to hardcode these
- // values in the backend and they automatically stay in
- // sync with whatever is defined (and maybe extended by
- // other modules) in js.
- var product_model = this.models[product_index];
- var product_fields = product_model.fields;
- var product_domain = product_model.domain;
+ var product_model = self.models[product_index];
// We don't want to load product.product the normal
// uncached way, so get rid of it.
if (product_index !== -1) {
this.models.splice(product_index, 1);
}
-
return posmodel_super.load_server_data.apply(this, arguments).then(function () {
+ // Give both the fields and domain to pos_cache in the
+ // backend. This way we don't have to hardcode these
+ // values in the backend and they automatically stay in
+ // sync with whatever is defined (and maybe extended by
+ // other modules) in js.
+ var product_fields = typeof product_model.fields === 'function' ? product_model.fields(self) : product_model.fields;
+ var product_domain = typeof product_model.domain === 'function' ? product_model.domain(self) : product_model.domain;
var records = rpc.query({
model: 'pos.config',
method: 'get_products_from_cache',
diff --git a/addons/pos_hr/static/src/js/screens.js b/addons/pos_hr/static/src/js/screens.js
index bef83516f0f..84551507148 100644
--- a/addons/pos_hr/static/src/js/screens.js
+++ b/addons/pos_hr/static/src/js/screens.js
@@ -16,10 +16,11 @@ ScreenWidget.include({
barcode_cashier_action: function(code){
var self = this;
var employees = this.pos.employees;
+ var prom;
for(var i = 0, len = employees.length; i < len; i++){
if(employees[i].barcode === Sha1.hash(code.code)){
if (employees[i].id !== this.pos.get_cashier().id && employees[i].pin) {
- return this.gui.ask_password(employees[i].pin).then(function(){
+ prom = this.gui.ask_password(employees[i].pin).then(function(){
self.pos.set_cashier(employees[i]);
self.chrome.widget.username.renderElement();
return true;
@@ -27,12 +28,18 @@ ScreenWidget.include({
} else {
this.pos.set_cashier(employees[i]);
this.chrome.widget.username.renderElement();
- return true;
+ prom = Promise.resolve(true);
}
+ break;
}
}
- this.barcode_error_action(code);
- return false;
+ if (!prom){
+ this.barcode_error_action(code);
+ return Promise.resolve(false);
+ }
+ else {
+ return prom
+ }
},
show: function() {
this._super();
@@ -73,8 +80,10 @@ var LoginScreenWidget = ScreenWidget.extend({
* @override
*/
barcode_cashier_action: function(code) {
- this._super(code);
- this.unlock_screen();
+ var self = this;
+ return this._super(code).then(function () {
+ self.unlock_screen();
+ });
},
unlock_screen: function() {
diff --git a/addons/pos_mercury/__manifest__.py b/addons/pos_mercury/__manifest__.py
index 4727dbafdea..50778cac342 100644
--- a/addons/pos_mercury/__manifest__.py
+++ b/addons/pos_mercury/__manifest__.py
@@ -29,6 +29,7 @@ following:
'views/pos_mercury_views.xml',
'views/pos_mercury_transaction_templates.xml',
'views/pos_config_setting_views.xml',
+ 'views/pos_config_views.xml',
],
'demo': [
'data/pos_mercury_demo.xml',
diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js
index 818a17db300..9dbe66a5a8d 100644
--- a/addons/pos_mercury/static/src/js/pos_mercury.js
+++ b/addons/pos_mercury/static/src/js/pos_mercury.js
@@ -509,7 +509,7 @@ PaymentScreenWidget.include({
self.credit_code_transaction(parsed_result);
} else { // this is for supporting another payment system like mercury
this.gui.show_popup('selection',{
- title: 'Pay ' + this.pos.get_order().get_due().toFixed(2) + ' with : ',
+ title: _t('Pay with: '),
list: online_payment_journals,
confirm: function (item) {
parsed_result.journal_id = item;
diff --git a/addons/pos_mercury/views/pos_config_views.xml b/addons/pos_mercury/views/pos_config_views.xml
new file mode 100644
index 00000000000..8da1d16bd8a
--- /dev/null
+++ b/addons/pos_mercury/views/pos_config_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ pos.config.form.view.inherit.mercury
+ pos.config
+
+
+
+
+
+
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index d3ecb8a9d71..3ab10e2d3b2 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -606,12 +606,7 @@ class PurchaseOrderLine(models.Model):
if product_lang.description_purchase:
self.name += '\n' + product_lang.description_purchase
- fpos = self.order_id.fiscal_position_id
- if self.env.uid == SUPERUSER_ID:
- company_id = self.env.company.id
- self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id.id == company_id))
- else:
- self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
+ self._compute_tax_id()
self._suggest_quantity()
self._onchange_quantity()
@@ -681,9 +676,8 @@ class PurchaseOrderLine(models.Model):
'''
if not self.product_id:
return
-
seller_min_qty = self.product_id.seller_ids\
- .filtered(lambda r: r.name == self.order_id.partner_id)\
+ .filtered(lambda r: r.name == self.order_id.partner_id and (not r.product_id or r.product_id == self.product_id))\
.sorted(key=lambda r: r.min_qty)
if seller_min_qty:
self.product_qty = seller_min_qty[0].min_qty or 1.0
diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index fdb03cf3289..9a1e06fe644 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -45,8 +45,12 @@ class StockMove(models.Model):
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
+ # The date must be today, and not the date of the move since the move move is still
+ # in assigned state. However, the move date is the scheduled date until move is
+ # done, then date of actual move processing. See:
+ # https://github.com/odoo/odoo/blob/2f789b6863407e63f90b3a2d4cc3be09815f7002/addons/stock/models/stock_move.py#L36
price_unit = order.currency_id._convert(
- price_unit, order.company_id.currency_id, order.company_id, self.date, round=False)
+ price_unit, order.company_id.currency_id, order.company_id, fields.Date.context_today(self), round=False)
return price_unit
return super(StockMove, self)._get_price_unit()
diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py
index 07cf6272ca9..9490d218f17 100644
--- a/addons/purchase_stock/tests/test_stockvaluation.py
+++ b/addons/purchase_stock/tests/test_stockvaluation.py
@@ -822,12 +822,12 @@ class TestStockValuationWithCOA(AccountingTestCase):
'company_id': company.id,
})
- # To allow testing validation of PO
+ # To allow testing validation of PO and Delivery
+ today = date_po
def _today(*args, **kwargs):
- return datetime.strptime(date_po, "%Y-%m-%d").date()
- # To allow testing validation of Delivery
+ return datetime.strptime(today, "%Y-%m-%d").date()
def _now(*args, **kwargs):
- return datetime.strptime(date_delivery + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
+ return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
patchers = [
patch('odoo.fields.Date.context_today', _today),
@@ -856,13 +856,17 @@ class TestStockValuationWithCOA(AccountingTestCase):
line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
+ today = date_delivery
picking = po.picking_ids
(picking.move_lines
.filtered(lambda l: l.purchase_line_id == line_product_avg)
.write({'quantity_done': 1.0}))
picking.button_validate()
+ # 5 Units received at rate 0.7 = 42.86
+ self.assertAlmostEqual(product_avg.standard_price, 42.86)
+ today = date_invoice
inv = self.env['account.move'].with_context(default_type='in_invoice').create({
'type': 'in_invoice',
'invoice_date': date_invoice,
@@ -944,7 +948,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
product_avg = self.product1.copy({
'purchase_method': 'purchase',
'name': 'AVG',
- 'standard_price': 60,
+ 'standard_price': 0,
'property_account_creditor_price_difference': self.price_diff_account.id
})
product_avg.invoice_policy = 'order'
@@ -991,13 +995,12 @@ class TestStockValuationWithCOA(AccountingTestCase):
'company_id': company.id,
})
- # To allow testing validation of PO
+ # To allow testing validation of PO and Delivery
+ today = date_po
def _today(*args, **kwargs):
- return datetime.strptime(date_po, "%Y-%m-%d").date()
- # To allow testing validation of Delivery
- delivery_now = date_delivery
+ return datetime.strptime(today, "%Y-%m-%d").date()
def _now(*args, **kwargs):
- return datetime.strptime(delivery_now + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
+ return datetime.strptime(today + ' 01:00:00', "%Y-%m-%d %H:%M:%S")
patchers = [
patch('odoo.fields.Date.context_today', _today),
@@ -1027,6 +1030,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
line_product_avg = po.order_line.filtered(lambda l: l.product_id == product_avg)
+ today = date_delivery
picking = po.picking_ids
(picking.move_lines
.filtered(lambda l: l.purchase_line_id == line_product_avg)
@@ -1034,7 +1038,10 @@ class TestStockValuationWithCOA(AccountingTestCase):
picking.button_validate()
picking.action_done() # Create Backorder
+ # 5 Units received at rate 0.7 = 42.86
+ self.assertAlmostEqual(product_avg.standard_price, 42.86)
+ today = date_invoice
inv = self.env['account.move'].with_context(default_type='in_invoice').create({
'type': 'in_invoice',
'invoice_date': date_invoice,
@@ -1055,13 +1062,16 @@ class TestStockValuationWithCOA(AccountingTestCase):
inv.post()
+ today = date_delivery1
backorder_picking = self.env['stock.picking'].search([('backorder_id', '=', picking.id)])
- delivery_now = date_delivery1
(backorder_picking.move_lines
.filtered(lambda l: l.purchase_line_id == line_product_avg)
.write({'quantity_done': 5.0}))
backorder_picking.button_validate()
+ # 5 Units received at rate 0.7 (42.86) + 5 Units received at rate 0.8 (37.50) = 40.18
+ self.assertAlmostEqual(product_avg.standard_price, 40.18)
+ today = date_invoice1
inv1 = self.env['account.move'].with_context(default_type='in_invoice').create({
'type': 'in_invoice',
'invoice_date': date_invoice1,
diff --git a/addons/sale_expense/security/ir.model.access.csv b/addons/sale_expense/security/ir.model.access.csv
index f9e8f32a752..228b1262f58 100644
--- a/addons/sale_expense/security/ir.model.access.csv
+++ b/addons/sale_expense/security/ir.model.access.csv
@@ -1,2 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_sale_order_employee,sale.order.employee.expense,sale.model_sale_order,base.group_user,1,0,0,0
\ No newline at end of file
+access_sale_order_employee,sale.order.employee.expense,sale.model_sale_order,base.group_user,0,0,0,0
\ No newline at end of file
diff --git a/addons/sale_expense/security/sale_expense_security.xml b/addons/sale_expense/security/sale_expense_security.xml
index 4b40a737f7a..c8087457e9d 100644
--- a/addons/sale_expense/security/sale_expense_security.xml
+++ b/addons/sale_expense/security/sale_expense_security.xml
@@ -13,6 +13,7 @@
+
diff --git a/addons/sale_expense/views/hr_expense_views.xml b/addons/sale_expense/views/hr_expense_views.xml
index c86de76cad0..ca46f6a58f7 100644
--- a/addons/sale_expense/views/hr_expense_views.xml
+++ b/addons/sale_expense/views/hr_expense_views.xml
@@ -6,7 +6,7 @@
-
+
@@ -17,7 +17,7 @@
-
+
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py
index 35a106fa9ea..edeff617fd9 100644
--- a/addons/sale_timesheet/models/project.py
+++ b/addons/sale_timesheet/models/project.py
@@ -183,7 +183,7 @@ class ProjectTask(models.Model):
@api.depends('project_id.sale_line_employee_ids')
def _compute_is_project_map_empty(self):
for task in self:
- task.is_project_map_empty = not bool(task.project_id.sale_line_employee_ids)
+ task.is_project_map_empty = not bool(task.sudo().project_id.sale_line_employee_ids)
@api.onchange('project_id')
def _onchange_project(self):
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index 17b0bc2799f..2fbe2d2dc35 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -354,7 +354,7 @@
diff --git a/addons/sale_timesheet_purchase/models/project_overview.py b/addons/sale_timesheet_purchase/models/project_overview.py
index 600bf28094d..6af3cbef830 100644
--- a/addons/sale_timesheet_purchase/models/project_overview.py
+++ b/addons/sale_timesheet_purchase/models/project_overview.py
@@ -22,8 +22,8 @@ class Project(models.Model):
context={'create': False, 'edit': False, 'delete': False}
)
})
- account_invoice_lines = self.env['account.invoice.line'].search([('account_analytic_id', 'in', accounts)])
- account_invoices = account_invoice_lines.mapped('invoice_id')
+ account_invoice_lines = self.env['account.move.line'].search([('analytic_account_id', 'in', accounts)])
+ account_invoices = account_invoice_lines.mapped('move_id')
if account_invoices:
stat_buttons.append({
'name': _('Vendor Bills'),
diff --git a/addons/web/static/lib/jSignature/jSignatureCustom.js b/addons/web/static/lib/jSignature/jSignatureCustom.js
index ae51103da3d..6ccd2af79a3 100644
--- a/addons/web/static/lib/jSignature/jSignatureCustom.js
+++ b/addons/web/static/lib/jSignature/jSignatureCustom.js
@@ -760,7 +760,7 @@ function jSignatureClass(parent, options, instanceExtensions) {
// these, when enabled, will hover above the sig area. Hence we append them to DOM before canvas.
this.$controlbarUpper = (function(){
var controlbarstyle = 'padding:0 !important; margin:0 !important;'+
- 'width: 100% !important; height: 0 !important; -ms-touch-action: none;'+
+ 'width: 100% !important; height: 0 !important; -ms-touch-action: none; touch-action: none;'+
'margin-top:-1em !important; margin-bottom:1em !important;';
return $('').appendTo($parent);
})();
@@ -771,7 +771,7 @@ function jSignatureClass(parent, options, instanceExtensions) {
this.$controlbarLower = (function(){
var controlbarstyle = 'padding:0 !important; margin:0 !important;'+
- 'width: 100% !important; height: 0 !important; -ms-touch-action: none;'+
+ 'width: 100% !important; height: 0 !important; -ms-touch-action: none; touch-action: none;'+
'margin-top:-1.5em !important; margin-bottom:1.5em !important; position: relative;';
return $('').appendTo($parent);
})();
@@ -876,7 +876,7 @@ function jSignatureClass(parent, options, instanceExtensions) {
$canvas.bind('mouseup.'+apinamespace, drawEndHandler);
$canvas.bind('mousedown.'+apinamespace, drawStartHandler);
} else {
- canvas.ontouchstart = function(e) {
+ canvas.addEventListener('touchstart', function(e) {
canvas.onmousedown = canvas.onmouseup = canvas.onmousemove = undef;
this.fatFingerCompensation = (
@@ -886,19 +886,19 @@ function jSignatureClass(parent, options, instanceExtensions) {
drawStartHandler(e);
- canvas.ontouchend = drawEndHandler;
- canvas.ontouchstart = drawStartHandler;
- canvas.ontouchmove = drawMoveHandler;
- };
- canvas.onmousedown = function(e) {
+ canvas.addEventListener('touchend', drawEndHandler);
+ canvas.addEventListener('touchstart', drawStartHandler);
+ canvas.addEventListener('touchmove', drawMoveHandler);
+ });
+ canvas.addEventListener('mousedown', function(e) {
canvas.ontouchstart = canvas.ontouchend = canvas.ontouchmove = undef;
drawStartHandler(e);
- canvas.onmousedown = drawStartHandler;
- canvas.onmouseup = drawEndHandler;
- canvas.onmousemove = drawMoveHandler;
- }
+ canvas.addEventListener('mousedown', drawStartHandler);
+ canvas.addEventListener('mouseup', drawEndHandler);
+ canvas.addEventListener('mousemove', drawMoveHandler);
+ });
if (window.navigator.msPointerEnabled) {
canvas.onmspointerdown = drawStartHandler;
canvas.onmspointerup = drawEndHandler;
@@ -1084,28 +1084,16 @@ jSignatureClass.prototype.initializeCanvas = function(settings) {
settings.width = '100%';
}
- $canvas.css(
- 'margin'
- , 0
- ).css(
- 'padding'
- , 0
- ).css(
- 'border'
- , 'none'
- ).css(
- 'height'
- , settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10)
- ).css(
- 'width'
- , settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10)
- ).css(
- '-ms-touch-action'
- , 'none'
- ).css(
- 'background-color',
- settings['background-color']
- );
+ $canvas.css({
+ 'margin': 0,
+ 'padding': 0,
+ 'border': 'none',
+ 'height': settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10),
+ 'width': settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10),
+ '-ms-touch-action': 'none',
+ 'touch-action': 'none',
+ 'background-color': settings['background-color'],
+ });
$canvas.appendTo(this.$parent);
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index b17a3f501e6..bc5250ab620 100644
--- a/addons/web/static/src/js/core/ajax.js
+++ b/addons/web/static/src/js/core/ajax.js
@@ -2,6 +2,7 @@ odoo.define('web.ajax', function (require) {
"use strict";
var config = require('web.config');
+var concurrency = require('web.concurrency');
var core = require('web.core');
var time = require('web.time');
var download = require('web.download');
@@ -9,6 +10,10 @@ var contentdisposition = require('web.contentdisposition');
var _t = core._t;
+// Create the final object containing all the functions first to allow monkey
+// patching them correctly if ever needed.
+var ajax = {};
+
function _genericJsonRpc (fct_name, params, settings, fct) {
var shadow = settings.shadow || false;
delete settings.shadow;
@@ -469,44 +474,79 @@ var loadAsset = (function () {
})();
/**
- * Loads the given js and css libraries. Note that the ajax loadJS and loadCSS methods
- * don't do anything if the given file is already loaded.
+ * Loads the given js/css libraries and asset bundles. Note that no library or
+ * asset will be loaded if it was already done before.
*
* @param {Object} libs
- * @Param {Array | Array} [libs.jsLibs=[]] The list of JS files that we want to
- * load. The list may contain strings (the files to load), or lists of strings. The
- * first level is loaded sequentially, and files listed in inner lists are loaded in
- * parallel.
- * @param {Array} [libs.cssLibs=[]] A list of css files, to be loaded in
- * parallel
- * @param {Array} [libs.assetLibs=[]] A list of xmlId. The loaded template
- * contains the script and link to be loaded
+ * @param {Array} [libs.assetLibs=[]]
+ * The list of assets to load. Each list item may be a string (the xmlID
+ * of the asset to load) or a list of strings. The first level is loaded
+ * sequentially (so use this if the order matters) while the assets in
+ * inner lists are loaded in parallel (use this for efficiency but only
+ * if the order does not matter, should rarely be the case for assets).
+ * @param {string[]} [libs.cssLibs=[]]
+ * The list of CSS files to load. They will all be loaded in parallel but
+ * put in the DOM in the given order (only the order in the DOM is used
+ * to determine priority of CSS rules, not loaded time).
+ * @param {Array} [libs.jsLibs=[]]
+ * The list of JS files to load. Each list item may be a string (the URL
+ * of the file to load) or a list of strings. The first level is loaded
+ * sequentially (so use this if the order matters) while the files in inner
+ * lists are loaded in parallel (use this for efficiency but only
+ * if the order does not matter).
+ * @param {string[]} [libs.cssContents=[]]
+ * List of inline styles to add after loading the CSS files.
+ * @param {string[]} [libs.jsContents=[]]
+ * List of inline scripts to add after loading the JS files.
*
* @returns {Promise}
*/
-function loadLibs (libs) {
- var defs = [];
- (libs.jsLibs || []).forEach(function (urls) {
- defs.push(Promise.all(defs).then(function () {
- if (typeof(urls) === 'string') {
- return ajax.loadJS(urls);
- } else {
- return Promise.all(urls.map(ajax.loadJS));
+function loadLibs(libs) {
+ var mutex = new concurrency.Mutex();
+ mutex.exec(function () {
+ var defs = [];
+ var cssLibs = [libs.cssLibs || []]; // Force loading in parallel
+ defs.push(_loadArray(cssLibs, ajax.loadCSS).then(function () {
+ if (libs.cssContents && libs.cssContents.length) {
+ $('head').append($('', {
+ html: libs.cssContents.join('\n'),
+ }));
}
}));
- });
- (libs.cssLibs || []).forEach(function (url) {
- defs.push(ajax.loadCSS(url));
- });
- _.each(libs.assetLibs || [], function (xmlId) {
- defs.push(loadAsset(xmlId).then(function (asset) {
- return loadLibs(asset);
+ defs.push(_loadArray(libs.jsLibs || [], ajax.loadJS).then(function () {
+ if (libs.jsContents && libs.jsContents.length) {
+ $('head').append($('', {
+ html: libs.jsContents.join('\n'),
+ }));
+ }
}));
+ return Promise.all(defs);
});
- return Promise.all(defs);
+ mutex.exec(function () {
+ return _loadArray(libs.assetLibs || [], function (xmlID) {
+ return ajax.loadAsset(xmlID).then(function (asset) {
+ return ajax.loadLibs(asset);
+ });
+ });
+ });
+
+ function _loadArray(array, loadCallback) {
+ var _mutex = new concurrency.Mutex();
+ array.forEach(function (urlData) {
+ _mutex.exec(function () {
+ if (typeof urlData === 'string') {
+ return loadCallback(urlData);
+ }
+ return Promise.all(urlData.map(loadCallback));
+ });
+ });
+ return _mutex.getUnlockedDef();
+ }
+
+ return mutex.getUnlockedDef();
}
-var ajax = {
+_.extend(ajax, {
jsonRpc: jsonRpc,
rpc: rpc,
loadCSS: loadCSS,
@@ -516,7 +556,7 @@ var ajax = {
loadLibs: loadLibs,
get_file: get_file,
post: post,
-};
+});
return ajax;
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 ca0c7829a2c..b9a7d26e470 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -155,6 +155,10 @@ var BasicModel = AbstractModel.extend({
// save is performed.
this.mutex = new concurrency.Mutex();
+ // this array is used to accumulate RPC requests done in the same call
+ // stack, so that they can be batched in the minimum number of RPCs
+ this.batchedRPCsRequests = [];
+
this.localData = Object.create(null);
this._super.apply(this, arguments);
},
@@ -4070,6 +4074,86 @@ var BasicModel = AbstractModel.extend({
});
});
},
+ /**
+ * This function accumulates RPC requests done in the same call stack, and
+ * performs them in the next micro task tick so that similar requests can be
+ * batched in a single RPC.
+ *
+ * For now, only 'read' calls are supported.
+ *
+ * @private
+ * @param {Object} params
+ * @returns {Promise}
+ */
+ _performRPC: function (params) {
+ var self = this;
+
+ // save the RPC request
+ var request = _.extend({}, params);
+ var prom = new Promise(function (resolve, reject) {
+ request.resolve = resolve;
+ request.reject = reject;
+ });
+ this.batchedRPCsRequests.push(request);
+
+ // empty the pool of RPC requests in the next micro tick
+ Promise.resolve().then(function () {
+ if (!self.batchedRPCsRequests.length) {
+ // pool has already been processed
+ return;
+ }
+
+ // reset pool of RPC requests
+ var batchedRPCsRequests = self.batchedRPCsRequests;
+ self.batchedRPCsRequests = [];
+
+ // batch similar requests
+ var batches = {};
+ var key;
+ for (var i = 0; i < batchedRPCsRequests.length; i++) {
+ var request = batchedRPCsRequests[i];
+ key = request.model + ',' + JSON.stringify(request.context);
+ if (!batches[key]) {
+ batches[key] = _.extend({}, request, {requests: [request]});
+ } else {
+ batches[key].ids = _.uniq(batches[key].ids.concat(request.ids));
+ batches[key].fieldNames = _.uniq(batches[key].fieldNames.concat(request.fieldNames));
+ batches[key].requests.push(request);
+ }
+ }
+
+ // perform batched RPCs
+ function onSuccess(batch, results) {
+ for (var i = 0; i < batch.requests.length; i++) {
+ var request = batch.requests[i];
+ var fieldNames = request.fieldNames.concat(['id']);
+ var filteredResults = results.filter(function (record) {
+ return request.ids.indexOf(record.id) >= 0;
+ }).map(function (record) {
+ return _.pick(record, fieldNames);
+ });
+ request.resolve(filteredResults);
+ }
+ }
+ function onFailure(batch, error) {
+ for (var i = 0; i < batch.requests.length; i++) {
+ var request = batch.requests[i];
+ request.reject(error);
+ }
+ }
+ for (key in batches) {
+ var batch = batches[key];
+ self._rpc({
+ model: batch.model,
+ method: 'read',
+ args: [batch.ids, batch.fieldNames],
+ context: batch.context,
+ }).then(onSuccess.bind(null, batch)).guardedCatch(onFailure.bind(null, batch));
+ }
+ });
+
+ return prom;
+ },
/**
* Once a record is created and some data has been fetched, we need to do
* quite a lot of computations to determine what needs to be fetched. This
@@ -4277,11 +4361,12 @@ var BasicModel = AbstractModel.extend({
var def;
if (missingIDs.length && fieldNames.length) {
- def = self._rpc({
- model: list.model,
- method: 'read',
- args: [missingIDs, fieldNames],
+ def = self._performRPC({
context: list.getContext(),
+ fieldNames: fieldNames,
+ ids: missingIDs,
+ method: 'read',
+ model: list.model,
});
} else {
def = Promise.resolve(_.map(missingIDs, function (id) {
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 6ffe35574bd..f73209a0330 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -651,8 +651,8 @@ var BasicRenderer = AbstractRenderer.extend({
// this.renderFieldWidget(...).addClass(...), the class is added on
// the temporary div and not on the actual element that will be
// rendered. As we do not return a promise and some callers cannot
- // wait for this.defs, we copy those attributes to the final element.
- widget.$el.attr($el.getAttributes());
+ // wait for this.defs, we copy those classnames to the final element.
+ widget.$el.addClass($el.attr('class'));
$el.replaceWith(widget.$el);
self._registerModifiers(node, record, widget, {
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 23a26bb6d73..ad1a95cd14f 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -405,7 +405,8 @@ var ListRenderer = BasicRenderer.extend({
isPassword: 'password' in node.attrs,
});
this._handleAttributes($td, node);
- return $td.html(formattedValue).attr('title', formattedValue);
+ var title = field.type !== 'boolean' ? formattedValue : '';
+ return $td.html(formattedValue).attr('title', title);
},
/**
* Renders the button element associated to the given node and record.
diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss
index 55559546e74..120896dceee 100644
--- a/addons/web/static/src/scss/fields.scss
+++ b/addons/web/static/src/scss/fields.scss
@@ -88,6 +88,7 @@
&.o_field_many2manytags {
flex-flow: row wrap;
align-items: baseline;
+ max-width: 100%;
> .o_field_widget {
flex: 1 0 100px;
@@ -101,6 +102,8 @@
font-size: 12px;
cursor: pointer;
user-select: none;
+ display: flex;
+ max-width: 100%;
a {
color: inherit;
@@ -115,6 +118,7 @@
.o_delete {
color: inherit;
cursor: pointer;
+ padding-left: 4px;
}
}
diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
index 508ce38a961..4f5958f65cd 100644
--- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
@@ -8422,6 +8422,57 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+ QUnit.test('many2manys inside a one2many are fetched in batch after onchange', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 1, {
+ turtle_foo: "leonardo",
+ partner_ids: [[4, 2]],
+ }],
+ [1, 2, {
+ turtle_foo: "donatello",
+ partner_ids: [[4, 2], [4, 4]],
+ }],
+ ];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ enableBasicModelBachedRPCs: true,
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'read') {
+ assert.deepEqual(args.args[0], [2, 4],
+ 'should read the partner_ids once, batched');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2);
+ assert.strictEqual(form.$('.o_field_widget[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordsecondrecordaaa");
+
+ assert.verifySteps(['default_get', 'onchange', 'read']);
+
+ form.destroy();
+ });
+
QUnit.test('two one2many fields with same relation and onchanges', async function (assert) {
// this test simulates the presence of two one2many fields with onchanges, such that
// changes to the first o2m are repercuted on the second one
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 282e93bdc60..c1ffac7bd6f 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -414,6 +414,39 @@ QUnit.module('Views', {
form.destroy();
});
+
+ QUnit.test('group containing both a field and a group', async function (assert) {
+ // The purpose of this test is to check that classnames defined in a
+ // field widget and those added by the form renderer are correctly
+ // combined. For instance, the renderer adds className 'o_group_col_x'
+ // on outer group's children (an outer group being a group that contains
+ // at least a group).
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_group .o_field_widget[name=foo]');
+ assert.containsOnce(form, '.o_group .o_inner_group .o_field_widget[name=int_field]');
+
+ assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_field_char');
+ assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_group_col_6');
+
+ form.destroy();
+ });
+
QUnit.test('Form and subview with _view_ref contexts', async function (assert) {
assert.expect(2);
@@ -485,6 +518,7 @@ QUnit.module('Views', {
form.destroy();
actionManager.destroy();
});
+
QUnit.test('invisible fields are properly hidden', async function (assert) {
assert.expect(4);
@@ -6862,7 +6896,6 @@ QUnit.module('Views', {
delete widgetRegistry.map.test;
});
-
QUnit.test('bounce edit button in readonly mode', async function (assert) {
assert.expect(3);
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 2e6969bab48..0513253507a 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -271,6 +271,19 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('boolean field has no title', async function (assert) {
+ assert.expect(1);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ });
+ assert.equal(list.$('tbody tr:first td:eq(1)').attr('title'), "");
+ list.destroy();
+ });
+
QUnit.test('record-depending invisible lines are correctly aligned', async function (assert) {
assert.expect(4);
diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html
index d39a77ef4e7..4649366dd61 100644
--- a/addons/web/views/database_manager.html
+++ b/addons/web/views/database_manager.html
@@ -41,6 +41,10 @@
});
// close modal on submit
$('.modal').on('submit', 'form', function (ev) {
+ var form = $(this).closest('form')[0];
+ if (form && form.checkValidity && !form.checkValidity()) {
+ return;
+ }
var modal = $(this).parentsUntil('body', '.modal');
if (modal.hasClass('o_database_backup')) {
$(modal).modal('hide');
diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
index 3f808964beb..b196552709d 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
@@ -1108,7 +1108,7 @@ var VideoWidget = MediaWidget.extend({
return {errorCode: 0};
}
- var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0';
+ var autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';
if (ytMatch && ytMatch[2].length === 11) {
$video.attr('src', '//www.youtube' + (ytMatch[1] || '') + '.com/embed/' + ytMatch[2] + autoplay);
@@ -1137,7 +1137,8 @@ var VideoWidget = MediaWidget.extend({
$video.attr('src', $video.attr('src') + '&rel=0');
}
if (options.loop && (ytMatch || vimMatch)) {
- $video.attr('src', $video.attr('src') + '&loop=1');
+ var videoSrc = _.str.sprintf('%s&loop=1', $video.attr('src'));
+ $video.attr('src', ytMatch ? _.str.sprintf('%s&playlist=%s', videoSrc, ytMatch[2]) : videoSrc);
}
if (options.hide_controls && (ytMatch || dmMatch)) {
$video.attr('src', $video.attr('src') + '&controls=0');
diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml
index 9ed3d2010da..baff05e2356 100644
--- a/addons/web_editor/static/src/xml/wysiwyg.xml
+++ b/addons/web_editor/static/src/xml/wysiwyg.xml
@@ -318,7 +318,11 @@
-
+
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 96e7bc39b2b..3a5b773f81d 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -53,7 +53,7 @@ class Website(models.Model):
domain = fields.Char('Website Domain')
country_group_ids = fields.Many2many('res.country.group', 'website_country_group_rel', 'website_id', 'country_group_id',
string='Country Groups', help='Used when multiple websites have the same domain.')
- company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.ref('base.main_company').id, required=True)
+ company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company, required=True)
language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages)
default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True)
default_lang_code = fields.Char("Default language code", related='default_lang_id.code', store=True, readonly=False)
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index 33e1e01ebcc..ab7f4218a5b 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -76,7 +76,7 @@ publicWidget.Widget.include({
this.editableMode = this.options.editableMode || false;
var extraEvents = this.editableMode ? this.edit_events : this.read_events;
if (extraEvents) {
- this.events = _.extend(this.events || {}, extraEvents);
+ this.events = _.extend({}, this.events || {}, extraEvents);
}
},
});
diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js
index 716562389e7..7b2c8511fc3 100644
--- a/addons/website/static/tests/tours/rte.js
+++ b/addons/website/static/tests/tours/rte.js
@@ -176,7 +176,7 @@ tour.register('rte_translator', {
}, {
content: "check bis: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
- extra_trigger: 'body:not(:has(button[data-action=save]))',
+ extra_trigger: 'html[lang*="fr"] body:not(:has(button[data-action=save]))',
}, {
content: "check bis: placeholder translation",
trigger: 'input[placeholder="test french placeholder"]',
diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py
index 5ca9344aa32..8e2eb382a40 100644
--- a/addons/website/tests/test_qweb.py
+++ b/addons/website/tests/test_qweb.py
@@ -2,10 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
-import werkzeug
-import odoo
from odoo import tools
+from odoo.addons.website.tools import MockRequest
from odoo.modules.module import get_module_resource
from odoo.tests.common import TransactionCase
@@ -68,45 +67,6 @@ class TestQweb(TransactionCase):
"alt": "Marc Demo",
}).encode('utf8'))
-class MockObject(object):
- _log_call = []
- def __init__(self, *args, **kwargs):
- self.__dict__ = kwargs
- def __call__(self, *args, **kwargs):
- self._log_call.append((args, kwargs))
- return self
- def __getitem__(self, index):
- return self
-
-def werkzeugRaiseNotFound(*args, **kwargs):
- raise werkzeug.exceptions.NotFound()
-
-class MockRequest(object):
- """ Class with context manager mocking odoo.http.request for tests """
- def __init__(self, env, website=None, context=None, multilang=True, routing=True):
- app = MockObject(routing={
- 'type': 'http',
- 'website': True,
- 'multilang': multilang,
- })
- app.get_db_router = app.bind = app.match = app
- if not routing:
- app.match = werkzeugRaiseNotFound
- self.request = MockObject(
- env=env, context=context or {}, db=None,
- session=MockObject(
- debug=False
- ),
- website=website, httprequest=MockObject(
- path='/hello/',
- app=app
- )
- )
- odoo.http._request_stack.push(self.request)
- def __enter__(self):
- return self.request
- def __exit__(self, exc_type, exc_value, traceback):
- odoo.http._request_stack.pop()
class TestQwebProcessAtt(TransactionCase):
def setUp(self):
@@ -134,7 +94,7 @@ class TestQwebProcessAtt(TransactionCase):
self._test_att('/a', {'href': '/a'})
def test_process_att_no_website(self):
- with MockRequest(self.env) as request:
+ with MockRequest(self.env):
# no website so URL rewriting
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/en_US/'})
@@ -143,7 +103,7 @@ class TestQwebProcessAtt(TransactionCase):
self._test_att('/a', {'href': '/a'})
def test_process_att_monolang_route(self):
- with MockRequest(self.env, website=self.website, multilang=False) as request:
+ with MockRequest(self.env, website=self.website, multilang=False):
# lang not changed in URL but CDN enabled
self._test_att('/a', {'href': 'http://test.cdn/a'})
self._test_att('/en_US/a', {'href': 'http://test.cdn/en_US/a'})
@@ -151,19 +111,19 @@ class TestQwebProcessAtt(TransactionCase):
self._test_att('/en_US/b', {'href': '/en_US/b'})
def test_process_att_no_request_lang(self):
- with MockRequest(self.env, self.website) as request:
+ with MockRequest(self.env, website=self.website):
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
def test_process_att_with_request_lang(self):
- with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}) as request:
+ with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}):
self._test_att('/', {'href': '/fr_FR/'})
self._test_att('/en_US/', {'href': '/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
def test_process_att_matching_cdn_and_lang(self):
- with MockRequest(self.env, self.website) as request:
+ with MockRequest(self.env, website=self.website):
# lang prefix is added before CDN
self._test_att('/a', {'href': 'http://test.cdn/a'})
self._test_att('/en_US/a', {'href': 'http://test.cdn/a'})
@@ -173,13 +133,13 @@ class TestQwebProcessAtt(TransactionCase):
self._test_att('/fr_FR/b', {'href': '/fr_FR/b'})
def test_process_att_no_route(self):
- with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}, routing=False) as request:
+ with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}, routing=False):
# default on multilang=True if route is not /{module}/static/
self._test_att('/web/static/hi', {'href': '/web/static/hi'})
self._test_att('/my-page', {'href': '/fr_FR/my-page'})
def test_process_att_url_crap(self):
- with MockRequest(self.env, self.website) as request:
+ with MockRequest(self.env, website=self.website) as request:
# #{fragment} is stripped from URL when testing route
self._test_att('/x#y?z', {'href': '/x#y?z'})
self.assertEqual(
diff --git a/addons/website/tools.py b/addons/website/tools.py
index 2abe896afff..e3bb897ec52 100644
--- a/addons/website/tools.py
+++ b/addons/website/tools.py
@@ -1,7 +1,11 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import odoo
import re
+import werkzeug
+
+from odoo.tools import DotDict
def get_video_embed_code(video_url):
@@ -49,3 +53,59 @@ def get_video_embed_code(video_url):
# We directly use the provided URL as it is
embedUrl = video_url
return '' % embedUrl
+
+
+class MockObject(object):
+ _log_call = []
+
+ def __init__(self, *args, **kwargs):
+ self.__dict__ = kwargs
+
+ def __call__(self, *args, **kwargs):
+ self._log_call.append((args, kwargs))
+ return self
+
+ def __getitem__(self, index):
+ return self
+
+
+def werkzeugRaiseNotFound(*args, **kwargs):
+ raise werkzeug.exceptions.NotFound()
+
+
+class MockRequest(object):
+ """ Class with context manager mocking odoo.http.request for tests """
+ def __init__(self, env, **kw):
+ app = MockObject(routing={
+ 'type': 'http',
+ 'website': True,
+ 'multilang': kw.get('multilang', True),
+ })
+ app.get_db_router = app.bind = app.match = app
+ if not kw.get('routing', True):
+ app.match = werkzeugRaiseNotFound
+ self.request = DotDict({
+ 'context': kw.get('context', {}),
+ 'db': None,
+ 'env': env,
+ 'httprequest': {
+ 'path': '/hello/',
+ 'app': app,
+ },
+ 'redirect': werkzeug.utils.redirect,
+ 'session': {
+ 'geoip': {
+ 'country_code': kw.get('country_code'),
+ },
+ 'debug': False,
+ 'sale_order_id': kw.get('sale_order_id'),
+ },
+ 'website': kw.get('website'),
+ })
+ odoo.http._request_stack.push(self.request)
+
+ def __enter__(self):
+ return self.request
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ odoo.http._request_stack.pop()
diff --git a/addons/website/views/res_config_settings_views.xml b/addons/website/views/res_config_settings_views.xml
index f2a8d0b3e02..90fe854dbae 100644
--- a/addons/website/views/res_config_settings_views.xml
+++ b/addons/website/views/res_config_settings_views.xml
@@ -26,8 +26,19 @@
Website
-
-
+
+
+
+ Select a website to load its settings.
+
+
+ There is no website available for this company. You could create a new one.
+
+
+
+
+
+
@@ -41,7 +52,7 @@
-
+
diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py
index 3acffdacd50..99422b03160 100644
--- a/addons/website_crm_partner_assign/controllers/main.py
+++ b/addons/website_crm_partner_assign/controllers/main.py
@@ -192,7 +192,8 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage):
if not qs or qs.lower() in loc:
yield {'loc': loc}
- partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True), ('grade_id.website_published', '=', True)]
+ partners_dom = [('is_company', '=', True), ('grade_id', '!=', False), ('website_published', '=', True),
+ ('grade_id.website_published', '=', True), ('country_id', '!=', False)]
dom += sitemap_qs2dom(qs=qs, route='/partners/country/')
countries = env['res.partner'].sudo().read_group(partners_dom, fields=['id', 'country_id'], groupby='country_id')
for country in countries:
diff --git a/addons/website_crm_partner_assign/tests/test_partner_assign.py b/addons/website_crm_partner_assign/tests/test_partner_assign.py
index 773eb86beff..98ad2d451c6 100644
--- a/addons/website_crm_partner_assign/tests/test_partner_assign.py
+++ b/addons/website_crm_partner_assign/tests/test_partner_assign.py
@@ -29,7 +29,7 @@ class TestPartnerAssign(TransactionCase):
'partner_id': self.customer_uk.id
})
- def geo_find(addr):
+ def geo_find(addr, **kw):
return {
'Wavre, Belgium': (50.7158956, 4.6128075),
'Cannon Hill Park, B46 3AG Birmingham, United Kingdom': (52.45216, -1.898578),
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index 1785766def7..e80dd1ffe07 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -128,6 +128,8 @@ class WebsiteForm(http.Controller):
# If it's not, we'll use attachments instead
if field_name in authorized_fields and authorized_fields[field_name]['type'] == 'binary':
data['record'][field_name] = base64.b64encode(field_value.read())
+ if authorized_fields[field_name]['manual']:
+ data['record'][field_name + "_filename"] = field_value.filename
else:
field_value.field_name = field_name
data['attachments'].append(field_value)
diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js
index e25be65ba48..9528a9b5e73 100644
--- a/addons/website_forum/static/src/js/website_forum.js
+++ b/addons/website_forum/static/src/js/website_forum.js
@@ -18,9 +18,6 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
'click .karma_required': '_onKarmaRequiredClick',
'mouseenter .o_js_forum_tag_follow': '_onTagFollowBoxMouseEnter',
'mouseleave .o_js_forum_tag_follow': '_onTagFollowBoxMouseLeave',
- 'click .o_forum_profile_pic_edit': '_onEditProfilePicClick',
- 'change .o_forum_file_upload': '_onFileUploadChange',
- 'click .o_forum_profile_pic_clear': '_onProfilePicClearClick',
'mouseenter .o_forum_user_info': '_onUserInfoMouseEnter',
'mouseleave .o_forum_user_info': '_onUserInfoMouseLeave',
'mouseleave .o_forum_user_bio_expand': '_onUserBioExpandMouseLeave',
@@ -214,41 +211,6 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({
_onTagFollowBoxMouseLeave: function (ev) {
$(ev.currentTarget).find('.o_forum_tag_follow_box').stop().fadeOut().css('display', 'none');
},
- /**
- * @private
- * @param {Event} ev
- */
- _onEditProfilePicClick: function (ev) {
- ev.preventDefault();
- $(ev.currentTarget).closest('form').find('.o_forum_file_upload').trigger('click');
- },
- /**
- * @private
- * @param {Event} ev
- */
- _onFileUploadChange: function (ev) {
- if (!ev.currentTarget.files.length) {
- return;
- }
- var $form = $(ev.currentTarget).closest('form');
- utils.getDataURLFromFile(ev.currentTarget.files[0]).then(function (result) {
- $form.find('.o_forum_avatar_img').attr('src', result);
- });
- $form.find('#forum_clear_image').remove();
- },
- /**
- * @private
- * @param {Event} ev
- */
- _onProfilePicClearClick: function (ev) {
- var $form = $(ev.currentTarget).closest('form');
- $form.find('.o_forum_avatar_img').attr('src', '/web/static/src/img/placeholder.png');
- $form.append($(' ', {
- name: 'clear_image',
- id: 'forum_clear_image',
- type: 'hidden',
- }));
- },
/**
* @private
* @param {Event} ev
diff --git a/addons/website_profile/static/src/js/website_profile.js b/addons/website_profile/static/src/js/website_profile.js
index caccf454f1a..b78d2ed9f06 100644
--- a/addons/website_profile/static/src/js/website_profile.js
+++ b/addons/website_profile/static/src/js/website_profile.js
@@ -44,6 +44,9 @@ publicWidget.registry.websiteProfile = publicWidget.Widget.extend({
publicWidget.registry.websiteProfileEditor = publicWidget.Widget.extend({
selector: '.o_wprofile_editor_form',
read_events: {
+ 'click .o_forum_profile_pic_edit': '_onEditProfilePicClick',
+ 'change .o_forum_file_upload': '_onFileUploadChange',
+ 'click .o_forum_profile_pic_clear': '_onProfilePicClearClick',
'click .o_wprofile_submit_btn': '_onSubmitClick',
},
@@ -74,6 +77,43 @@ publicWidget.registry.websiteProfileEditor = publicWidget.Widget.extend({
// Handlers
//--------------------------------------------------------------------------
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onEditProfilePicClick: function (ev) {
+ ev.preventDefault();
+ $(ev.currentTarget).closest('form').find('.o_forum_file_upload').trigger('click');
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onFileUploadChange: function (ev) {
+ if (!ev.currentTarget.files.length) {
+ return;
+ }
+ var $form = $(ev.currentTarget).closest('form');
+ var reader = new window.FileReader();
+ reader.readAsDataURL(ev.currentTarget.files[0]);
+ reader.onload = function (ev) {
+ $form.find('.o_forum_avatar_img').attr('src', ev.target.result);
+ };
+ $form.find('#forum_clear_image').remove();
+ },
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onProfilePicClearClick: function (ev) {
+ var $form = $(ev.currentTarget).closest('form');
+ $form.find('.o_forum_avatar_img').attr('src', '/web/static/src/img/placeholder.png');
+ $form.append($(' ', {
+ name: 'clear_image',
+ id: 'forum_clear_image',
+ type: 'hidden',
+ }));
+ },
/**
* @private
*/
diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml
index 3060431e87f..c28712f06ac 100644
--- a/addons/website_profile/views/website_profile.xml
+++ b/addons/website_profile/views/website_profile.xml
@@ -97,20 +97,20 @@
| |