diff --git a/addons/account/models/product.py b/addons/account/models/product.py index 5d9d80c1e1..bb0c230a29 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -99,20 +99,20 @@ class ProductProduct(models.Model): # Apply fiscal position. if product_taxes and fiscal_position: product_taxes_after_fp = fiscal_position.map_tax(product_taxes) + flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy() + flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy() + taxes_before_included = any(tax.price_include for tax in flattened_taxes_before_fp) - if set(product_taxes.ids) != set(product_taxes_after_fp.ids): - flattened_taxes_before_fp = product_taxes._origin.flatten_taxes_hierarchy() - if any(tax.price_include for tax in flattened_taxes_before_fp): - taxes_res = flattened_taxes_before_fp.compute_all( - product_price_unit, - quantity=1.0, - currency=currency, - product=product, - is_refund=is_refund_document, - ) - product_price_unit = taxes_res['total_excluded'] + if set(product_taxes.ids) != set(product_taxes_after_fp.ids) and taxes_before_included: + taxes_res = flattened_taxes_before_fp.compute_all( + product_price_unit, + quantity=1.0, + currency=currency, + product=product, + is_refund=is_refund_document, + ) + product_price_unit = taxes_res['total_excluded'] - flattened_taxes_after_fp = product_taxes_after_fp._origin.flatten_taxes_hierarchy() if any(tax.price_include for tax in flattened_taxes_after_fp): taxes_res = flattened_taxes_after_fp.compute_all( product_price_unit, diff --git a/addons/calendar/models/calendar_event.py b/addons/calendar/models/calendar_event.py index 000d477360..8b61aec221 100644 --- a/addons/calendar/models/calendar_event.py +++ b/addons/calendar/models/calendar_event.py @@ -3,6 +3,8 @@ from datetime import timedelta import math +from itertools import repeat + import babel.dates import logging import pytz @@ -116,7 +118,7 @@ class Meeting(models.Model): return self._get_recurrent_fields() | self._get_time_fields() | self._get_custom_fields() | { 'id', 'active', 'allday', 'duration', 'user_id', 'interval', - 'count', 'rrule', 'recurrence_id', 'show_as'} + 'count', 'rrule', 'recurrence_id', 'show_as', 'privacy'} @api.model def _get_display_time(self, start, stop, zduration, zallday): @@ -742,51 +744,33 @@ class Meeting(models.Model): self.env['calendar.alarm_manager']._notify_next_alarm(event.partner_ids.ids) return events - def read(self, fields=None, load='_classic_read'): - def hide(field, value): - """ - :param field: field name - :param value: field value - :return: obfuscated field value - """ - if field in {'name', 'display_name'}: - return _('Busy') - return [] if isinstance(value, list) else False + def _read(self, fields): + if self.env.is_system(): + super()._read(fields) + return - def split_privacy(events): - """ - :param events: list of event values (dict) - :return: tuple(private events, public events) - """ - private = [event for event in events if event.get('privacy') == 'private'] - public = [event for event in events if event.get('privacy') != 'private'] - return private, public + fields = set(fields) + private_fields = fields - self._get_public_fields() + if not private_fields: + super()._read(fields) + return - def my_events(events): - """ - :param events: list of event values (dict) - :return: tuple(my events, other events) - """ - my = [event for event in events if event.get('user_id') and event.get('user_id')[0] == self.env.uid] - others = [event for event in events if not event.get('user_id') or event.get('user_id')[0] != self.env.uid] - return my, others + super()._read(fields | {'privacy', 'user_id', 'partner_ids'}) + current_partner_id = self.env.user.partner_id + others_private_events = self.filtered( + lambda e: e.privacy == 'private' \ + and e.user_id != self.env.user \ + and current_partner_id not in e.partner_ids + ) + if not others_private_events: + return - def obfuscated(events): - """ - :param events: list of event values (dict) - :return: events with private field values obfuscated - """ - public_fields = self._get_public_fields() - return [{ - field: hide(field, value) if field not in public_fields else value - for field, value in event.items() - } for event in events] - - events = super().read(fields=fields + ['privacy', 'user_id'], load=load) - private_events, public_events = split_privacy(events) - my_private_events, others_private_events = my_events(private_events) - - return public_events + my_private_events + obfuscated(others_private_events) + for field_name in private_fields: + field = self._fields[field_name] + replacement = field.convert_to_cache( + _('Busy') if field_name == 'name' else False, + others_private_events) + self.env.cache.update(others_private_events, field, repeat(replacement)) def name_get(self): """ Hide private events' name for events which don't belong to the current user diff --git a/addons/calendar/tests/test_access_rights.py b/addons/calendar/tests/test_access_rights.py index a4e5c95a05..b5a0ed8072 100644 --- a/addons/calendar/tests/test_access_rights.py +++ b/addons/calendar/tests/test_access_rights.py @@ -5,54 +5,82 @@ from datetime import datetime from flectra.tests.common import SavepointCase, new_test_user from flectra.exceptions import AccessError +from flectra.tools import mute_logger class TestAccessRights(SavepointCase): @classmethod + @mute_logger('flectra.tests', 'flectra.addons.auth_signup.models.res_users') def setUpClass(cls): super().setUpClass() cls.john = new_test_user(cls.env, login='john', groups='base.group_user') cls.raoul = new_test_user(cls.env, login='raoul', groups='base.group_user') + cls.george = new_test_user(cls.env, login='george', groups='base.group_user') cls.portal = new_test_user(cls.env, login='pot', groups='base.group_portal') def create_event(self, user, **values): - return self.env['calendar.event'].with_user(user).create(dict({ + e = self.env['calendar.event'].with_user(user).create(dict({ 'name': 'Event', 'start': datetime(2020, 2, 2, 8, 0), 'stop': datetime(2020, 2, 2, 18, 0), 'user_id': user.id, }, **values)) + e.partner_ids += self.george.partner_id + return e def read_event(self, user, events, field): data = events.with_user(user).read([field]) if len(events) == 1: return data[0][field] - mapped_data = {record['id']: record for record in data} - # Keep the same order - return [mapped_data[eid][field] for eid in events.ids] + return [r[field] for r in data] - def test_private_read_name(self): + # don't spam logs with ACL failures from portal + @mute_logger('flectra.addons.base.models.ir_rule') + def test_privacy(self): event = self.create_event( self.john, privacy='private', name='my private event', + location='in the Sky' ) - self.assertEqual(self.read_event(self.john, event, 'name'), 'my private event', "Owner should be able to read the event") - self.assertEqual(self.read_event(self.raoul, event, 'name'), 'Busy', "Private value should be obfuscated") - with self.assertRaises(AccessError): - self.read_event(self.portal, event, 'name') - - def test_private_other_field(self): - event = self.create_event( - self.john, - privacy='private', - location='in the Sky', - ) - self.assertEqual(self.read_event(self.john, event, 'location'), 'in the Sky', "Owner should be able to read the event") - self.assertEqual(self.read_event(self.raoul, event, 'location'), False, "Private value should be obfuscated") - with self.assertRaises(AccessError): - self.read_event(self.portal, event, 'location') + for user, field, expect, error in [ + # public field, any employee can read + (self.john, 'privacy', 'private', None), + (self.george, 'privacy', 'private', None), + (self.raoul, 'privacy', 'private', None), + (self.portal, 'privacy', None, AccessError), + # substituted private field, only owner and invitees can read, other + # employees get substitution + (self.john, 'name', 'my private event', None), + (self.george, 'name', 'my private event', None), + (self.raoul, 'name', 'Busy', None), + (self.portal, 'name', None, AccessError), + # computed from private field + (self.john, 'display_name', 'my private event', None), + (self.george, 'display_name', 'my private event', None), + (self.raoul, 'display_name', 'Busy', None), + (self.portal, 'display_name', None, AccessError), + # non-substituted private field, only owner and invitees can read, + # other employees get an empty field + (self.john, 'location', 'in the Sky', None), + (self.george, 'location', 'in the Sky', None), + (self.raoul, 'location', False, None), + (self.portal, 'location', None, AccessError), + # non-substituted sequence field + (self.john, 'partner_ids', self.john.partner_id | self.george.partner_id, None), + (self.george, 'partner_ids', self.john.partner_id | self.george.partner_id, None), + (self.raoul, 'partner_ids', self.env['res.partner'], None), + (self.portal, 'partner_ids', None, AccessError), + ]: + event.invalidate_cache() + with self.subTest("private read", user=user.display_name, field=field, error=error): + e = event.with_user(user) + if error: + with self.assertRaises(error): + _ = e[field] + else: + self.assertEqual(e[field], expect) def test_private_and_public(self): private = self.create_event( diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index 5c745b30d8..231a61dfef 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -103,7 +103,9 @@ class EventEvent(models.Model): return event_stages[0] if event_stages else False def _default_description(self): - return self.env['ir.ui.view']._render_template('event.event_default_descripton') + # avoid template branding with rendering_bundle=True + return self.env['ir.ui.view'].with_context(rendering_bundle=True) \ + ._render_template('event.event_default_descripton') name = fields.Char(string='Event', translate=True, required=True) note = fields.Text(string='Note') diff --git a/addons/gamification/models/gamification_karma_rank.py b/addons/gamification/models/gamification_karma_rank.py index fe67c3dd3f..90f2949276 100644 --- a/addons/gamification/models/gamification_karma_rank.py +++ b/addons/gamification/models/gamification_karma_rank.py @@ -35,23 +35,25 @@ class KarmaRank(models.Model): @api.model_create_multi def create(self, values_list): res = super(KarmaRank, self).create(values_list) - users = self.env['res.users'].sudo().search([('karma', '>', 0)]) - users._recompute_rank() + if any(res.mapped('karma_min')) > 0: + users = self.env['res.users'].sudo().search([('karma', '>=', max(min(res.mapped('karma_min')), 1))]) + if users: + users._recompute_rank() return res def write(self, vals): if 'karma_min' in vals: previous_ranks = self.env['gamification.karma.rank'].search([], order="karma_min DESC").ids - low = min(vals['karma_min'], self.karma_min) - high = max(vals['karma_min'], self.karma_min) + low = min(vals['karma_min'], min(self.mapped('karma_min'))) + high = max(vals['karma_min'], max(self.mapped('karma_min'))) res = super(KarmaRank, self).write(vals) if 'karma_min' in vals: after_ranks = self.env['gamification.karma.rank'].search([], order="karma_min DESC").ids if previous_ranks != after_ranks: - users = self.env['res.users'].sudo().search([('karma', '>', 0)]) + users = self.env['res.users'].sudo().search([('karma', '>=', max(low, 1))]) else: - users = self.env['res.users'].sudo().search([('karma', '>=', low), ('karma', '<=', high)]) + users = self.env['res.users'].sudo().search([('karma', '>=', max(low, 1)), ('karma', '<=', high)]) users._recompute_rank() return res diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index 7c9c32a785..64dca5b4db 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -180,6 +180,10 @@ WHERE sub.user_id IN %%s""" % { """ Method that can be called on a batch of users with the same new rank """ + if self.env.context.get('install_mode', False): + # avoid sending emails in install mode (prevents spamming users when creating data ranks) + return + template = self.env.ref('gamification.mail_template_data_new_rank_reached', raise_if_not_found=False) if template: for u in self: diff --git a/addons/l10n_ch/report/swissqr_report.xml b/addons/l10n_ch/report/swissqr_report.xml index 1ceb71d2e1..15022fe9fb 100644 --- a/addons/l10n_ch/report/swissqr_report.xml +++ b/addons/l10n_ch/report/swissqr_report.xml @@ -22,8 +22,8 @@ + diff --git a/addons/l10n_ch/static/src/scss/report_swissqr.scss b/addons/l10n_ch/static/src/scss/report_swissqr.scss index 38592ed356..1b3f8d3ce8 100644 --- a/addons/l10n_ch/static/src/scss/report_swissqr.scss +++ b/addons/l10n_ch/static/src/scss/report_swissqr.scss @@ -8,8 +8,7 @@ src: url('../font/LiberationSans-Regular.woff') format('woff'); } - -body.l10n_ch_qr { +body.l10n_ch_qr, body[data-oe-report="l10n_ch.l10n_ch_qr_report"] { padding: 0; /* Disable custom bakground */ diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml index 82ea7880da..874e4a574d 100644 --- a/addons/l10n_it_edi/data/invoice_it_template.xml +++ b/addons/l10n_it_edi/data/invoice_it_template.xml @@ -8,18 +8,18 @@ EAN - + INTERNAL - + - + - + @@ -36,9 +36,9 @@