diff --git a/addons/account/models/account.py b/addons/account/models/account.py index cb7ce2dd8ac..f204abb0091 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -1358,7 +1358,7 @@ class AccountTax(models.Model): base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount) incl_fixed_amount = incl_percent_amount = incl_division_amount = 0 store_included_tax_total = True - if tax.price_include: + if tax.price_include or self._context.get('force_price_include'): if tax.amount_type == 'percent': incl_percent_amount += tax.amount elif tax.amount_type == 'division': @@ -1387,7 +1387,7 @@ class AccountTax(models.Model): cumulated_tax_included_amount = 0 for tax in taxes: #compute the tax_amount - if tax.price_include and total_included_checkpoints.get(i): + if (self._context.get('force_price_include') or tax.price_include) and total_included_checkpoints.get(i): # We know the total to reach for that tax, so we make a substraction to avoid any rounding issues tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount) cumulated_tax_included_amount = 0 @@ -1426,7 +1426,7 @@ class AccountTax(models.Model): 'sequence': tax.sequence, 'account_id': tax.cash_basis_transition_account_id.id if tax.tax_exigibility == 'on_payment' else repartition_line.account_id.id, 'analytic': tax.analytic, - 'price_include': tax.price_include, + 'price_include': tax.price_include or self._context.get('force_price_include'), 'tax_exigibility': tax.tax_exigibility, 'tax_repartition_line_id': repartition_line.id, 'tag_ids': (repartition_line.tag_ids + subsequent_tags).ids, diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 87acda2151f..67a6c6f381a 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1023,7 +1023,7 @@ class AccountMove(models.Model): vendor_display_name = _('From: ') + move.invoice_source_email move.invoice_vendor_icon = '@' else: - vendor_display_name = ('Created by: ') + move.create_uid.name + vendor_display_name = ('Created by: ') + move.sudo().create_uid.name move.invoice_vendor_icon = '#' move.invoice_vendor_display_name = vendor_display_name diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 67523d4e562..2834791ec38 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -174,19 +174,23 @@ class AccountReconcileModel(models.Model): tax = self.env['account.tax'].browse(tax_res['id']) new_aml_dicts.append({ - 'account_id': tax.account_id and tax.account_id.id or base_line_dict['account_id'], - 'name': tax.name, + 'account_id': tax_res['account_id'] or base_line_dict['account_id'], + 'name': tax_res['name'], 'partner_id': base_line_dict.get('partner_id'), 'debit': tax_res['amount'] > 0 and tax_res['amount'] or 0, 'credit': tax_res['amount'] < 0 and -tax_res['amount'] or 0, 'analytic_account_id': tax.analytic and base_line_dict['analytic_account_id'], 'analytic_tag_ids': tax.analytic and base_line_dict['analytic_tag_ids'], - 'tax_exigible': tax.tax_exigibility == 'on_payment', + 'tax_exigible': tax_res['tax_exigibility'], + 'tax_repartition_line_id': tax_res['tax_repartition_line_id'], + 'tax_ids': tax_res['tax_ids'], + 'tag_ids': tax_res['tag_ids'] }) # Handle price included taxes. base_line_dict['debit'] = tax_res['base'] > 0 and tax_res['base'] or base_line_dict['debit'] base_line_dict['credit'] = tax_res['base'] < 0 and -tax_res['base'] or base_line_dict['credit'] + base_line_dict['tag_ids'] = [(6, 0, res['base_tags'])] return new_aml_dicts @api.multi diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 2161634f93e..b9a3bb0c7ad 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')] @@ -260,7 +260,6 @@ class ResPartner(models.Model): def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: - self.total_invoiced = 0.0 return True user_currency_id = self.env.company.currency_id.id diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index 85a6f4bfcfb..b58d0c6376f 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/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index b9e9e7d465d..f6b79a26280 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -94,6 +94,12 @@ class TestReconciliationMatchingRules(AccountingTestCase): 'sequence': 1, }) + self.tax21 = self.env['account.tax'].create({ + 'name': '21%', + 'type_tax_use': 'purchase', + 'amount': 21, + }) + def test_matching_fields(self): ''' Test all fields used to restrict the rules's applicability.''' @@ -300,6 +306,34 @@ class TestReconciliationMatchingRules(AccountingTestCase): {'partner_id': self.partner_2.id, 'debit': 0.0, 'credit': 1000.0}, ]) + def test_auto_reconcile_with_tax(self): + ''' Test auto reconciliation with a tax amount included in the bank statement line''' + + self.rule_1.write({ + 'auto_reconcile': True, + 'force_tax_included': True, + 'tax_ids': [(6, 0, self.tax21.ids)], + 'rule_type': 'writeoff_suggestion', + }) + + self.bank_line_2.unlink() + self.bank_line_1.amount = -121 + + self._check_statement_matching( + self.rule_1, + { + self.bank_line_1.id: {'aml_ids': [], 'model': self.rule_1, 'status': 'reconciled'}, + }, + self.bank_st + ) + + # Check first line has been well reconciled. + self.assertRecordValues(self.bank_line_1.journal_entry_ids, [ + {'partner_id': self.partner_1.id, 'debit': 100.0, 'credit': 0.0, 'tax_ids': [self.tax21.id]}, + {'partner_id': self.partner_1.id, 'debit': 21.0, 'credit': 0.0, 'tax_line_id': self.tax21.id}, + {'partner_id': self.partner_1.id, 'debit': 0.0, 'credit': 121.0}, + ]) + def test_reverted_move_matching(self): AccountMove = self.env['account.move'] move = AccountMove.create({ diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index da8c942b974..9c0c6a206df 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -760,6 +760,26 @@ + +
+ + + + + + + + + + + + + + + + + +
+
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/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/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml index 2c0b6dd511c..a7a1a16dd0a 100644 --- a/addons/hr/views/hr_employee_views.xml +++ b/addons/hr/views/hr_employee_views.xml @@ -147,6 +147,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 f8662c1f55f..66943b74b9f 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 4403e000a8e..824fc566fab 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -578,7 +578,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 + + + Resumes and Letters ir.attachment @@ -668,7 +680,7 @@

- + diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index ea8b0fe6b74..4ae5ad004f8 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -183,13 +183,14 @@ class EscposDriver(Thread): except NoStatusError as e: print("Impossible to get the status of the printer %s" % e) except Exception as e: - self.set_status('error', e) - _logger.exception() + self.set_status('error') + _logger.exception(e) finally: if error: self.queue.put((timestamp, task, data)) if printer: printer.close() + printer = None def push_task(self,task, data = None): self.lockedstart() 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 c2f601003d7..6d521cfccd1 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -540,6 +540,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 dedeacca013..4a20d24af8d 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 1f01b90b9da..9f2f0644e5f 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -829,8 +829,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() @@ -861,6 +859,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_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 be621264cfb..12fa88b6ddf 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1728,8 +1728,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 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_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 4fc70000e3c..336ab7b94f4 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -608,12 +608,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() @@ -683,9 +678,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 14383c79b57..0781c64bcc7 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.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 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), @@ -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 7ee2d8b9969..6735431cf3a 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -184,7 +184,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/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 af7628cc5ea..435b17679fd 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($('