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 become a powerful user!
- +
Reach the next rank and become a Master!
+
2000 @@ -66,7 +66,8 @@

You have reached the last rank. Congratulations!

-
You have reached the last rank. Congratulations!
+
Reach the next rank and become a powerful user!
+
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 @@
-
You have reached the last rank. Congratulations!
+
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($('