From 1f27de79eb69c3cf75cee1bd543bda3b17ffef6a Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Mon, 24 Oct 2022 08:31:09 +0200 Subject: [PATCH] [MIG] resource_booking: Migration to 14.0 - Standard procedure - No more virtual ids - Adapt tests - FIX: Proper show_as meeting generation - IMP: Add name to tree view TT32344 --- requirements.txt | 1 - resource_booking/__manifest__.py | 9 +- resource_booking/controllers/portal.py | 5 +- .../13.0.1.0.0/noupdate_changes.xml | 17 ---- .../migrations/13.0.1.0.0/post-migrate.py | 11 --- .../migrations/13.0.2.0.0/pre-migrate.py | 55 ----------- resource_booking/models/calendar_event.py | 92 ++++++++++--------- resource_booking/models/resource_booking.py | 11 +-- resource_booking/models/resource_calendar.py | 9 +- resource_booking/tests/test_backend.py | 25 +++-- .../views/resource_booking_views.xml | 1 + 11 files changed, 74 insertions(+), 162 deletions(-) delete mode 100644 resource_booking/migrations/13.0.1.0.0/noupdate_changes.xml delete mode 100644 resource_booking/migrations/13.0.1.0.0/post-migrate.py delete mode 100644 resource_booking/migrations/13.0.2.0.0/pre-migrate.py diff --git a/requirements.txt b/requirements.txt index fcceab51..f61de956 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ # generated from manifests external_dependencies cssselect -freezegun diff --git a/resource_booking/__manifest__.py b/resource_booking/__manifest__.py index 2ba04fc2..fa5bee57 100644 --- a/resource_booking/__manifest__.py +++ b/resource_booking/__manifest__.py @@ -1,15 +1,16 @@ # Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2022 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Resource booking", "summary": "Manage appointments and resource booking", - "version": "13.0.2.6.5", - "development_status": "Beta", + "version": "14.0.1.0.0", + "development_status": "Production/Stable", "category": "Appointments", "website": "https://github.com/OCA/calendar", "author": "Tecnativa, Odoo Community Association (OCA)", - "maintainers": ["Yajo"], + "maintainers": ["pedrobaeza"], "license": "AGPL-3", "application": True, "installable": True, @@ -17,8 +18,6 @@ "python": [ # Used implicitly "cssselect", - # Only for tests - "freezegun", ], }, "depends": [ diff --git a/resource_booking/controllers/portal.py b/resource_booking/controllers/portal.py index 3f0f2e9b..6d529f8b 100644 --- a/resource_booking/controllers/portal.py +++ b/resource_booking/controllers/portal.py @@ -1,4 +1,5 @@ # Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2022 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import datetime @@ -125,9 +126,7 @@ def portal_booking_cancel(self, booking_id, access_token=None, **kwargs): ) def portal_booking_confirm(self, booking_id, access_token, when, **kwargs): """Confirm a booking in a given datetime.""" - booking_sudo = self._get_booking_sudo(booking_id, access_token).with_context( - autoconfirm_booking_requester=True - ) + booking_sudo = self._get_booking_sudo(booking_id, access_token) when_tz_aware = isoparse(when) when_naive = datetime.utcfromtimestamp(when_tz_aware.timestamp()) try: diff --git a/resource_booking/migrations/13.0.1.0.0/noupdate_changes.xml b/resource_booking/migrations/13.0.1.0.0/noupdate_changes.xml deleted file mode 100644 index ba69b8ec..00000000 --- a/resource_booking/migrations/13.0.1.0.0/noupdate_changes.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - ['|', ('company_id','=',False), ('company_id','in',company_ids)] - - - - ['|', ('type_id.company_id', '=', False), ('type_id.company_id', 'in', company_ids)] - - diff --git a/resource_booking/migrations/13.0.1.0.0/post-migrate.py b/resource_booking/migrations/13.0.1.0.0/post-migrate.py deleted file mode 100644 index 16b091ae..00000000 --- a/resource_booking/migrations/13.0.1.0.0/post-migrate.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright 2021 Tecnativa - Jairo Llopis -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - openupgrade.load_data( - env.cr, "resource_booking", "migrations/13.0.1.0.0/noupdate_changes.xml" - ) diff --git a/resource_booking/migrations/13.0.2.0.0/pre-migrate.py b/resource_booking/migrations/13.0.2.0.0/pre-migrate.py deleted file mode 100644 index 8e38ade4..00000000 --- a/resource_booking/migrations/13.0.2.0.0/pre-migrate.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2021 Tecnativa - Jairo Llopis -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from openupgradelib import openupgrade - - -def remove_start_stop_together_constraint(cr): - """Remove old unnecessary constraint. - - This one is no longer needed because `stop` is now computed and readonly, - so it will always have or not have value automatically. - """ - openupgrade.logged_query( - cr, - """ - ALTER TABLE resource_booking - DROP CONSTRAINT IF EXISTS resource_booking_start_stop_together - """, - ) - - -def fill_resource_booking_duration(env): - """Pre-create and pre-fill resource.booking duration.""" - openupgrade.add_fields( - env, - [ - ( - "duration", - "resource.booking", - "resource_booking", - "float", - None, - "resource_booking", - None, - ) - ], - ) - openupgrade.logged_query( - env.cr, - """ - UPDATE resource_booking rb - SET duration = COALESCE( - -- See https://stackoverflow.com/a/952600/1468388 - EXTRACT(EPOCH FROM rb.stop - rb.start) / 3600, - rbt.duration - ) - FROM resource_booking_type rbt - WHERE rb.type_id = rbt.id - """, - ) - - -@openupgrade.migrate() -def migrate(env, version): - remove_start_stop_together_constraint(env.cr) - fill_resource_booking_duration(env) diff --git a/resource_booking/models/calendar_event.py b/resource_booking/models/calendar_event.py index b7f8b02e..c3fd7851 100644 --- a/resource_booking/models/calendar_event.py +++ b/resource_booking/models/calendar_event.py @@ -1,4 +1,5 @@ # Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2022 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models @@ -6,7 +7,6 @@ class CalendarEvent(models.Model): - _name = "calendar.event" _inherit = "calendar.event" # One2one field, actually @@ -36,10 +36,10 @@ def _validate_booking_modifications(self): % "\n- ".join(frozen.mapped("display_name")) ) - def unlink(self, can_be_deleted=True): + def unlink(self): """Check you're allowed to unschedule it.""" self._validate_booking_modifications() - return super().unlink(can_be_deleted=can_be_deleted) + return super().unlink() def write(self, vals): """Check you're allowed to reschedule it.""" @@ -52,44 +52,26 @@ def write(self, vals): rescheduled._validate_booking_modifications() return result - def create_attendees(self): - """Autoconfirm resource attendees if preselected.""" - old_attendees = self.attendee_ids - result = super( - # This context avoids sending invitations to new attendees - CalendarEvent, - self.with_context(detaching=True), - ).create_attendees() - new_attendees = (self.attendee_ids - old_attendees).with_context( - self.env.context - ) - for attendee in new_attendees: - # No need to change state if it's already done - if attendee.state in {"accepted", "declined"}: - continue - rb = attendee.event_id.resource_booking_ids - # Confirm requester attendee always if requested - if ( - self.env.context.get("autoconfirm_booking_requester") - and attendee.partner_id == rb.partner_id - ): - attendee.state = "accepted" - continue - # Auto-confirm if attendee comes from a handpicked combination - if rb.combination_auto_assign: - continue - if attendee.partner_id in rb.combination_id.resource_ids.user_id.partner_id: - attendee.state = "accepted" - # Don't send notifications if you're a public user - if self.env.user._is_public(): - return result - # Send invitations like upstream would have done - to_notify = new_attendees.filtered(lambda a: a.email != self.env.user.email) - if to_notify and not self.env.context.get("detaching"): - to_notify._send_mail_to_attendees( - "calendar.calendar_template_meeting_invitation" - ) - return result + @api.model_create_multi + def create(self, vals_list): + """Transfer resource booking to _attendees_values by context. + + We need to serialize the creation in that case. + """ + vals_list2 = [] + records = self.env["calendar.event"] + for vals in vals_list: + if "resource_booking_ids" in vals: + records += super( + CalendarEvent, + self.with_context( + resource_booking_ids=vals["resource_booking_ids"] + ), + ).create(vals) + else: + vals_list2.append(vals) + records += super().create(vals_list2) + return records def get_interval(self, interval, tz=None): """Autofix tz from related resource booking. @@ -100,3 +82,31 @@ def get_interval(self, interval, tz=None): """ tz = self.resource_booking_ids.type_id.resource_calendar_id.tz or tz return super().get_interval(interval=interval, tz=tz) + + def _attendees_values(self, partner_commands): + """Autoconfirm resource attendees if preselected and hand-picked on RB. + + NOTE: There's no support for changing `resource_booking_ids` once the meeting + is created nor having more than one Rb attached to the same meeting, but that's + not a real case for now. + """ + attendee_commands = super()._attendees_values(partner_commands) + ctx_partner_id = False + for cmd in self.env.context.get("resource_booking_ids", []): + if cmd[0] == 0 and not cmd[2].get("combination_auto_assign", True): + ctx_partner_id = cmd[2]["partner_id"] + elif cmd[0] == 6: + rb = self.env["resource.booking"].browse(cmd[2]) + if rb.combination_auto_assign: + continue # only auto-confirm if handpicked combination + ctx_partner_id = rb.combination_id.resource_ids.user_id.partner_id.id + for command in attendee_commands: + if command[0] != 0: + continue + att_partner_id = ctx_partner_id + if not att_partner_id: + rb = self.resource_booking_ids + att_partner_id = rb.combination_id.resource_ids.user_id.partner_id.id + if command[2]["partner_id"] == att_partner_id: + command[2]["state"] = "accepted" + return attendee_commands diff --git a/resource_booking/models/resource_booking.py b/resource_booking/models/resource_booking.py index b9aa04ca..8ed156cb 100644 --- a/resource_booking/models/resource_booking.py +++ b/resource_booking/models/resource_booking.py @@ -1,4 +1,5 @@ # Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2022 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import calendar @@ -119,14 +120,12 @@ class ResourceBooking(models.Model): index=True, readonly=False, store=True, - track_sequence=200, tracking=True, ) duration = fields.Float( compute="_compute_duration", readonly=False, store=True, - track_sequence=220, tracking=True, help="Amount of time that the resources will be booked and unavailable for others.", ) @@ -135,7 +134,6 @@ class ResourceBooking(models.Model): copy=False, index=True, store=True, - track_sequence=210, tracking=True, ) type_id = fields.Many2one( @@ -304,12 +302,7 @@ def _sync_meeting(self): start=one.start, stop=one.stop, user_id=one.user_id.id, - # If you're not booked, you're free - show_as=( - "busy" - if self.env.user.partner_id in resource_partners - else "free" - ), + show_as="busy", # These 2 avoid creating event as activity res_model_id=False, res_id=False, diff --git a/resource_booking/models/resource_calendar.py b/resource_booking/models/resource_calendar.py index 5621e28c..7ab270f0 100644 --- a/resource_booking/models/resource_calendar.py +++ b/resource_booking/models/resource_calendar.py @@ -1,11 +1,11 @@ # Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2022 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from pytz import UTC from odoo import api, fields, models -from odoo.addons.calendar.models.calendar import calendar_id2real_id from odoo.addons.resource.models.resource import Intervals @@ -63,15 +63,12 @@ def _calendar_event_busy_intervals( self.env["calendar.event"].with_context(active_test=True).search(domain) ) for event in all_events: - real_event = self.env["calendar.event"].browse( - calendar_id2real_id(event.id) - ) # Is the event the same one we're currently checking? - if real_event.resource_booking_ids.id == analyzed_booking_id: + if event.resource_booking_ids.id == analyzed_booking_id: continue try: # Is the event not booking our resource? - if resource & real_event.mapped( + if resource & event.mapped( "resource_booking_ids.combination_id.resource_ids" ): raise Busy diff --git a/resource_booking/tests/test_backend.py b/resource_booking/tests/test_backend.py index 0c19fd0b..6a975ff8 100644 --- a/resource_booking/tests/test_backend.py +++ b/resource_booking/tests/test_backend.py @@ -1,4 +1,5 @@ # Copyright 2021 Tecnativa - Jairo Llopis +# Copyright 2022 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from datetime import date, datetime from unittest.mock import patch @@ -327,7 +328,7 @@ def test_recurring_event(self): ce_f.name = "recurring event past monday" for user in self.users: ce_f.partner_ids.add(user.partner_id) - ce_f.start_datetime = datetime(2021, 2, 22, 8) + ce_f.start = datetime(2021, 2, 22, 8) ce_f.duration = 1 ce_f.recurrency = True ce_f.interval = 1 @@ -602,6 +603,7 @@ def test_suggested_and_subscribed_recipients(self): rb = ( self.env["resource.booking"] .with_user(rb_user) + .sudo() .create( { "partner_id": self.partner.id, @@ -639,17 +641,13 @@ def test_event_show_as_free(self): owner of both automatically. However, there are 2 RBC available (one is me), so I still should be able to create 2 events. """ - env = self.env( - user=self.users[0], context=dict(self.env.context, tracking_disable=False) - ) - env.user.groups_id = self.env.ref("base.group_user") | self.env.ref( - "resource_booking.group_user" - ) + user = self.users[0] + rb_obj = self.env["resource.booking"].with_context(tracking_disable=True) # I'm the last option self.rbt.combination_assignment = "sorted" self.rbt.combination_rel_ids[0].sequence = 10 # Create one long event on Monday, where there are 2 RBC available (one is me) - rb_f = Form(env["resource.booking"]) + rb_f = Form(rb_obj) rb_f.type_id = self.rbt rb_f.start = "2021-03-01 09:00:00" rb_f.duration = 1 @@ -657,9 +655,9 @@ def test_event_show_as_free(self): rb1 = rb_f.save() # I'm not booked, so I'm free self.assertEqual(rb1.combination_id, self.rbcs[2]) - self.assertEqual(rb1.meeting_id.show_as, "free") + self.assertNotIn(user.partner_id, rb1.meeting_id.partner_ids) # Create another event within the previous one - rb_f = Form(env["resource.booking"]) + rb_f = Form(rb_obj) rb_f.type_id = self.rbt rb_f.start = "2021-03-01 09:00:00" rb_f.duration = 1.5 @@ -668,10 +666,9 @@ def test_event_show_as_free(self): rb2 = rb_f.save() # I'm booked this time, so I'm busy self.assertEqual(rb2.combination_id, self.rbcs[0]) - self.assertEqual(rb2.meeting_id.show_as, "busy") - # But if I'm not free for 1st RB, it will fail without available resources - rb1.meeting_id.show_as = "busy" - rb_f = Form(env["resource.booking"]) + self.assertIn(user.partner_id, rb2.meeting_id.partner_ids) + # Thus, it will fail without available resources on a next one + rb_f = Form(rb_obj) rb_f.type_id = self.rbt rb_f.start = "2021-03-01 09:30:00" rb_f.duration = 0.5 diff --git a/resource_booking/views/resource_booking_views.xml b/resource_booking/views/resource_booking_views.xml index 87226128..d41cc8a0 100644 --- a/resource_booking/views/resource_booking_views.xml +++ b/resource_booking/views/resource_booking_views.xml @@ -27,6 +27,7 @@ resource.booking +