diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 0c2aafe0f71..bf2daeaae28 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -610,6 +610,9 @@ class AccountInvoice(models.Model):
for field in changed_fields:
if field not in vals and invoice[field]:
vals[field] = invoice._fields[field].convert_to_write(invoice[field], invoice)
+ bank_account = self._get_default_bank_id(vals.get('type'), vals.get('company_id'))
+ if bank_account and not vals.get('partner_bank_id'):
+ vals['partner_bank_id'] = bank_account.id
invoice = super(AccountInvoice, self).create(vals)
@@ -645,14 +648,20 @@ class AccountInvoice(models.Model):
"""
res = super(AccountInvoice, self).default_get(default_fields)
- if res.get('type', False) not in ('out_invoice', 'in_refund') or not 'company_id' in res:
- return res
-
- partner_bank_result = self._get_partner_bank_id(res['company_id'])
+ partner_bank_result = self._get_default_bank_id(res.get('type'), res.get('company_id'))
if partner_bank_result:
res['partner_bank_id'] = partner_bank_result.id
return res
+ def _get_default_bank_id(self, type, company_id):
+ """When setting the default bank account, we have two cases:
+ in the case of 'out_invoice', 'in_refund', we want the bank account of the company.
+ otherwise, we want the bank account of the partner.
+ """
+ if type not in ('out_invoice', 'in_refund') or not company_id:
+ return False
+ return self._get_partner_bank_id(company_id)
+
def _get_partner_bank_id(self, company_id):
company = self.env['res.company'].browse(company_id)
if company.partner_id:
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index 85ec5f9bb66..3911a07ee6b 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -240,9 +240,9 @@ class ResPartner(models.Model):
LEFT JOIN account_move_line aml ON aml.partner_id = partner.id
RIGHT JOIN account_account acc ON aml.account_id = acc.id
WHERE acc.internal_type = %s
- AND NOT acc.deprecated
+ AND NOT acc.deprecated AND acc.company_id = %s
GROUP BY partner.id
- HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand))
+ HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, sign, operand))
res = self._cr.fetchall()
if not res:
return [('id', '=', '0')]
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 08d0b976263..cb9873950e1 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -215,7 +215,8 @@ class ReportAgedPartnerBalance(models.AbstractModel):
total[(i + 1)] += values['total']
values['partner_id'] = partner['partner_id']
if partner['partner_id']:
- browsed_partner = self.env['res.partner'].browse(partner['partner_id'])
+ #browse the partner name and trust field in sudo, as we may not have full access to the record (but we still have to see it in the report)
+ browsed_partner = self.env['res.partner'].sudo().browse(partner['partner_id'])
values['name'] = browsed_partner.name and len(browsed_partner.name) >= 45 and browsed_partner.name[0:40] + '...' or browsed_partner.name
values['trust'] = browsed_partner.trust
else:
diff --git a/addons/account/wizard/account_invoice_send_views.xml b/addons/account/wizard/account_invoice_send_views.xml
index 49c78cb32d9..cae3b296b63 100644
--- a/addons/account/wizard/account_invoice_send_views.xml
+++ b/addons/account/wizard/account_invoice_send_views.xml
@@ -13,6 +13,7 @@
+
diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py
index 44b31a8a129..c707330b730 100644
--- a/addons/account_voucher/models/account_voucher.py
+++ b/addons/account_voucher/models/account_voucher.py
@@ -304,7 +304,7 @@ class AccountVoucher(models.Model):
'payment_id': self._context.get('payment_id'),
}
# Create one line per tax and fix debit-credit for the move line if there are tax included
- if (line.tax_ids):
+ if (line.tax_ids and tax_calculation_rounding_method == 'round_per_line'):
tax_group = line.tax_ids.compute_all(self._convert(line.price_unit), self.company_id.currency_id, line.quantity, line.product_id, self.partner_id)
if move_line['debit']: move_line['debit'] = tax_group['total_excluded']
if move_line['credit']: move_line['credit'] = tax_group['total_excluded']
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 6c7cbad5aac..fdf988e7db2 100644
--- a/addons/base_geolocalize/models/res_partner.py
+++ b/addons/base_geolocalize/models/res_partner.py
@@ -12,10 +12,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/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!
-
Reach the next rank to show the rest of the world you exist!
-
+
Earn your first points and join the adventure !
+
1
@@ -27,8 +27,8 @@
You're a young padawan now. May the force be with you!
-
Reach the next rank to improve your status.
-
+
Reach the next rank to show the rest of the world you exist.
+
100
@@ -40,8 +40,8 @@
You love learning things. Curiosity is a good way to progress.
-
Reach the next rank and become a Master!
-
+
Reach the next rank to improve your status!
+
500
@@ -53,8 +53,8 @@
You know what you are talking about. People learn from you.
Reach the next rank and gain a very nice unicorn !
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/res_users.py b/addons/hr/models/res_users.py
index 499586befcf..ba13e835312 100644
--- a/addons/hr/models/res_users.py
+++ b/addons/hr/models/res_users.py
@@ -116,7 +116,8 @@ class User(models.Model):
# However, in this case, we want the user to be able to read/write its own data,
# even if they are protected by groups.
# We make the front-end aware of those fields by sending all field definitions.
- if not self.env.user.share:
+ profile_view = self.env.ref("hr.res_users_view_form_profile")
+ if profile_view and view_id == profile_view.id:
self = self.sudo()
return super(User, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index 8e8d5327749..0c97ca5a3c1 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -246,6 +246,11 @@
+
+
+
+
+
diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py
index 29e9896ac7e..e92e3d9d69b 100644
--- a/addons/hr_contract/models/hr_contract.py
+++ b/addons/hr_contract/models/hr_contract.py
@@ -52,7 +52,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/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 @@
= 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 98e1fac532d..ebbfeb1a44f 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_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_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index 259035e4bd6..e5c018ded62 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 43d325513d9..380dd2c1df0 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 date_po
- # To allow testing validation of Delivery
+ return today
def _now(*args, **kwargs):
- return date_delivery + ' 01:00:00'
+ return today + ' 01:00:00'
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()
+ # 1 Unit received at rate 0.7 = 42.86
+ self.assertAlmostEqual(product_avg.standard_price, 42.86)
+ today = date_invoice
inv = self.env['account.invoice'].create({
'type': 'in_invoice',
'date_invoice': date_invoice,
@@ -945,7 +949,7 @@ class TestStockValuationWithCOA(AccountingTestCase):
product_avg = self.product1.product_tmpl_id.copy({
'purchase_method': 'purchase',
'name': 'AVG',
- 'standard_price': 60,
+ 'standard_price': 0,
'property_account_creditor_price_difference': self.price_diff_account.id
}).product_variant_id
product_avg.invoice_policy = 'order'
@@ -992,13 +996,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 date_po
- # To allow testing validation of Delivery
- delivery_now = date_delivery
+ return today
def _now(*args, **kwargs):
- return delivery_now + ' 01:00:00'
+ return today + ' 01:00:00'
patchers = [
patch('odoo.fields.Date.context_today', _today),
@@ -1028,6 +1031,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)
@@ -1035,7 +1039,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.invoice'].create({
'type': 'in_invoice',
'date_invoice': date_invoice,
@@ -1058,13 +1065,16 @@ class TestStockValuationWithCOA(AccountingTestCase):
inv.action_invoice_open()
+ 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.invoice'].create({
'type': 'in_invoice',
'date_invoice': date_invoice1,
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index 8529c4135f7..0dfc0f4ba5e 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;
@@ -473,44 +478,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,
@@ -520,7 +560,7 @@ var ajax = {
loadLibs: loadLibs,
get_file: get_file,
post: post,
-};
+});
return ajax;
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 7a461b9126c..94fb69bb5c5 100644
--- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
+++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js
@@ -935,7 +935,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);
@@ -964,7 +964,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 cd20ee3f4ef..b45525e9deb 100644
--- a/addons/web_editor/static/src/xml/wysiwyg.xml
+++ b/addons/web_editor/static/src/xml/wysiwyg.xml
@@ -252,7 +252,11 @@
-
+
diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py
index 20dd0ce6f9d..f68895b64d9 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,42 +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, 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):
@@ -130,7 +93,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/'})
@@ -139,7 +102,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'})
@@ -147,19 +110,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'})
@@ -169,13 +132,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
new file mode 100644
index 00000000000..e634db9d603
--- /dev/null
+++ b/addons/website/tools.py
@@ -0,0 +1,63 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo
+import werkzeug
+
+from odoo.tools import DotDict
+
+
+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,
+ 'debug': False,
+ 'env': env,
+ 'httprequest': {
+ 'path': '/hello/',
+ 'app': app,
+ },
+ 'redirect': werkzeug.utils.redirect,
+ 'session': {
+ 'geoip': {
+ 'country_code': kw.get('country_code'),
+ },
+ '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_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py
index 429e8f9d822..ef9ccdf6406 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 fdae02d6d51..985bfdb0879 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 f8a75af3130..2d95523c082 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -129,6 +129,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 bdd1de724a4..88dd7e222a7 100644
--- a/addons/website_forum/static/src/js/website_forum.js
+++ b/addons/website_forum/static/src/js/website_forum.js
@@ -17,9 +17,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',
@@ -210,43 +207,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');
- var reader = new window.FileReader();
- reader.onload = function (ev) {
- $form.find('.o_forum_avatar_img').attr('src', ev.target.result);
- };
- reader.readAsDataURL(ev.currentTarget.files[0]);
- $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 @@