From b31f3136c0987800f7affeb8cbfcf15c89768730 Mon Sep 17 00:00:00 2001 From: Henrik Norlin Date: Mon, 28 Aug 2023 18:11:50 +0200 Subject: [PATCH] [MIG] resource_booking: Migration to 16.0 Changes done: - [FIX] _get_available_slots - [IMP] new field slot_duration - [IMP] Button (partner -> booking) - [IMP] combination -> bookings -> create: default combination - [FIX] _get_intervals() when type_id is missing - [IMP] booking: search on combination - [FIX] _availability_is_fitting() - [IMP] booking list view with hidden partner_ids - [FIX] attendance hour_to 23:59 -> 24:00 - [FIX] pre-commit - [FIX] calendar_slot_duration format - [FIX] _get_calendar_context() with correct start / timezone Co-authored-by: Henrik Norlin --- resource_booking/README.rst | 20 ++-- resource_booking/__manifest__.py | 5 +- .../migrations/16.0.1.0.0/post-migration.py | 18 +++ resource_booking/models/__init__.py | 1 + resource_booking/models/res_partner.py | 26 ++++ resource_booking/models/resource_booking.py | 111 +++++++++++++----- .../models/resource_booking_combination.py | 1 + .../models/resource_booking_type.py | 60 ++-------- resource_booking/readme/CONTRIBUTORS.rst | 1 + .../static/description/index.html | 14 ++- resource_booking/templates/portal.xml | 18 +-- resource_booking/tests/common.py | 6 +- resource_booking/tests/test_backend.py | 14 ++- .../views/calendar_event_views.xml | 1 - resource_booking/views/res_partner_views.xml | 25 ++++ .../resource_booking_combination_views.xml | 2 +- .../views/resource_booking_type_views.xml | 3 +- .../views/resource_booking_views.xml | 11 +- 18 files changed, 219 insertions(+), 118 deletions(-) create mode 100644 resource_booking/migrations/16.0.1.0.0/post-migration.py create mode 100644 resource_booking/models/res_partner.py create mode 100644 resource_booking/views/res_partner_views.xml diff --git a/resource_booking/README.rst b/resource_booking/README.rst index 9daa31aa..5af42b13 100644 --- a/resource_booking/README.rst +++ b/resource_booking/README.rst @@ -7,7 +7,7 @@ Resource booking !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:2a68fc2648f97ac64afcd5e5c6fa3ab28bfe94008b501d0ebad818d9e9bd3140 + !! source digest: sha256:69da76a920e2d581cfc27db05479e5705f196298cbbe67e881c92bf1ea6363af !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -17,13 +17,13 @@ Resource booking :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcalendar-lightgray.png?logo=github - :target: https://github.com/OCA/calendar/tree/15.0/resource_booking + :target: https://github.com/OCA/calendar/tree/16.0/resource_booking :alt: OCA/calendar .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/calendar-15-0/calendar-15-0-resource_booking + :target: https://translation.odoo-community.org/projects/calendar-16-0/calendar-16-0-resource_booking :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/calendar&target_branch=15.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/calendar&target_branch=16.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -170,7 +170,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -186,6 +186,7 @@ Contributors ~~~~~~~~~~~~ * Jairo Llopis (https://www.tecnativa.com/) +* Henrik Norlin (https://ows.cloud) Maintainers ~~~~~~~~~~~ @@ -203,11 +204,14 @@ promote its widespread use. .. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px :target: https://github.com/pedrobaeza :alt: pedrobaeza +.. |maintainer-ows-cloud| image:: https://github.com/ows-cloud.png?size=40px + :target: https://github.com/ows-cloud + :alt: ows-cloud -Current `maintainer `__: +Current `maintainers `__: -|maintainer-pedrobaeza| +|maintainer-pedrobaeza| |maintainer-ows-cloud| -This module is part of the `OCA/calendar `_ project on GitHub. +This module is part of the `OCA/calendar `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/resource_booking/__manifest__.py b/resource_booking/__manifest__.py index 432c3d19..4b71ba26 100644 --- a/resource_booking/__manifest__.py +++ b/resource_booking/__manifest__.py @@ -6,12 +6,12 @@ { "name": "Resource booking", "summary": "Manage appointments and resource booking", - "version": "15.0.2.0.0", + "version": "16.0.1.0.0", "development_status": "Production/Stable", "category": "Appointments", "website": "https://github.com/OCA/calendar", "author": "Tecnativa, Odoo Community Association (OCA)", - "maintainers": ["pedrobaeza"], + "maintainers": ["pedrobaeza", "ows-cloud"], "license": "AGPL-3", "application": True, "installable": True, @@ -37,6 +37,7 @@ "templates/portal.xml", "views/calendar_event_views.xml", "views/mail_activity_views.xml", + "views/res_partner_views.xml", "views/resource_booking_combination_views.xml", "views/resource_booking_type_views.xml", "views/resource_booking_views.xml", diff --git a/resource_booking/migrations/16.0.1.0.0/post-migration.py b/resource_booking/migrations/16.0.1.0.0/post-migration.py new file mode 100644 index 00000000..63b565e6 --- /dev/null +++ b/resource_booking/migrations/16.0.1.0.0/post-migration.py @@ -0,0 +1,18 @@ +from openupgradelib import openupgrade + + +def _update_attendance_hour_to(env): + """According to the refactoring of _merge_intervals() a booking from 23:00 to 01:00 + of the next day must be completely available, having defined hour_to=23.59 is not, + so it is changed to 24.0. + This behavior is confirmed to be correct because in the tests calendars are set + with full days (hour_from=0, hour_to=24).""" + lines = env["resource.calendar.attendance"].search( + [("hour_to", ">=", 23 + 59 / 60)] + ) + lines.hour_to = 24.0 + + +@openupgrade.migrate() +def migrate(env, version): + _update_attendance_hour_to(env) diff --git a/resource_booking/models/__init__.py b/resource_booking/models/__init__.py index 158a4b48..b57e4650 100644 --- a/resource_booking/models/__init__.py +++ b/resource_booking/models/__init__.py @@ -1,4 +1,5 @@ from . import calendar_event +from . import res_partner from . import resource_booking from . import resource_booking_combination from . import resource_booking_type diff --git a/resource_booking/models/res_partner.py b/resource_booking/models/res_partner.py new file mode 100644 index 00000000..180ea2aa --- /dev/null +++ b/resource_booking/models/res_partner.py @@ -0,0 +1,26 @@ +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + resource_booking_count = fields.Integer( + compute="_compute_resource_booking_count", string="Resource booking count" + ) + resource_booking_ids = fields.One2many( + "resource.booking", "partner_id", string="Bookings" + ) + + def _compute_resource_booking_count(self): + for p in self: + p.resource_booking_count = len(p.resource_booking_ids) + + def action_view_resource_booking(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "resource_booking.resource_booking_action" + ) + action["context"] = { + "default_partner_id": self.id, + } + return action diff --git a/resource_booking/models/resource_booking.py b/resource_booking/models/resource_booking.py index 85087983..1416386e 100644 --- a/resource_booking/models/resource_booking.py +++ b/resource_booking/models/resource_booking.py @@ -14,7 +14,42 @@ from odoo.addons.resource.models.resource import Intervals -def _availability_is_fitting(available_intervals, start_dt, end_dt): +def _merge_intervals(intervals): + # Merge intervals where start of current interval == stop of previous interval, + # assuming that the intervals are ordererd. + intervals = [list(tup) for tup in intervals._items] + # Handle 23:59:59:99999 + for i in range(len(intervals)): + stop = intervals[i][1] + if ( + stop.hour == 23 + and stop.minute == 59 + and stop.second == 59 + and stop.microsecond == 999999 + ): + intervals[i][1] += timedelta(microseconds=1) + # Begin with the last interval, to safely delete it if needed. + for i in range(len(intervals) - 1, 0, -1): + current_start = intervals[i][0] + current_stop = intervals[i][1] + previous_stop = intervals[i - 1][1] + if current_start == previous_stop: + intervals[i - 1][1] = current_stop + del intervals[i] + return Intervals([tuple(interval) for interval in intervals]) + + +def _availability_is_fitting(available_intervals, start_dt, stop_dt): + available_intervals = _merge_intervals(available_intervals) + for item in available_intervals._items: + available_start, available_stop = item[0], item[1] + if start_dt >= available_start and stop_dt <= available_stop: + return True + return False + + +def _availability_is_fitting_legacy(available_intervals, start_dt, end_dt): + """I keep the old method, since part of it may be needed in the new method.""" # Test whether the stretch between start_dt and end_dt is an uninterrupted # stretch of time as determined by `available_intervals`. # @@ -480,17 +515,22 @@ def _get_calendar_context(self, year=None, month=None, now=None): :param datetime now: Represents the current datetime. """ month1 = relativedelta(months=1) - now = now or fields.Datetime.now() + now = fields.Datetime.context_timestamp(self, now or fields.Datetime.now()) year = year or now.year month = month or now.month - start = datetime(year, month, 1) - start, now = ( - fields.Datetime.context_timestamp(self, dt) for dt in (start, now) + start = now.replace( + year=year, + month=month, + day=1, + hour=0, + minute=0, + second=0, + microsecond=0, ) - start = start.replace(hour=0, minute=0, second=0, microsecond=0) lang = self.env["res.lang"]._lang_get(self.env.lang or self.env.user.lang) weekday_names = dict(lang.fields_get(["week_start"])["week_start"]["selection"]) - slots = self._get_available_slots(start, start + month1) + booking_duration = timedelta(hours=self.duration) + slots = self._get_available_slots(start, start + month1 + booking_duration) return { "booking": self, "calendar": calendar.Calendar(int(lang.week_start) - 1), @@ -539,30 +579,39 @@ def _get_best_combination(self): def _get_available_slots(self, start_dt, end_dt): """Return available slots for scheduling current booking.""" result = {} - now = fields.Datetime.context_timestamp(self, fields.Datetime.now()) - slot_duration = timedelta(hours=self.type_id.duration) + slot_duration = timedelta(hours=self.type_id.slot_duration) booking_duration = timedelta(hours=self.duration) - current = max( + now = fields.Datetime.context_timestamp(self, fields.Datetime.now()) + start_dt = max( start_dt, now + timedelta(hours=self.type_id.modifications_deadline) ) - available_intervals = self._get_intervals(current, end_dt) - while current and current < end_dt: - slot_start = self.type_id._get_next_slot_start(current) - if current != slot_start: - current = slot_start - continue - current_interval = Intervals([(current, current + booking_duration, self)]) - for start, end, _meta in available_intervals & current_interval: - if end - start == booking_duration: - result.setdefault(current.date(), []) - result[current.date()].append(current) - # I actually only care about the 1st interval, if any - break - current += slot_duration + # available_intervals should start with the beginning of the work day, + # to compute each slot based on the beginning of the work day. + workday_min = start_dt.replace(hour=0, minute=0, second=0, microsecond=0) + available_intervals = self._get_intervals(workday_min, end_dt) + available_intervals = _merge_intervals(available_intervals) + # Loop through available times and append tested start/stop to the result. + test_start = False + for item in available_intervals._items: + available_start, available_stop = item[0], item[1] + test_start = available_start + while test_start and test_start < available_stop: + test_stop = test_start + booking_duration + if ( + test_start >= start_dt + and test_start >= available_start + and test_stop <= available_stop + ): + if not result.get(test_start.date()): + result.setdefault(test_start.date(), []) + result[test_start.date()].append(test_start) + test_start += slot_duration return result def _get_intervals(self, start_dt, end_dt, combination=None): - """Get available intervals for this booking.""" + """Get available intervals for this booking, + based on the calendar of the booking type + and the calendar(s) of the relevant resource combination(s).""" # Get all intervals except those from current booking try: booking_id = self.id or self._origin.id or -1 @@ -573,10 +622,12 @@ def _get_intervals(self, start_dt, end_dt, combination=None): analyzing_booking=booking_id, exclude_public_holidays=True ) # RBT calendar uses no resources to restrict bookings - resource = self.env["resource.resource"] - result = booking.type_id.resource_calendar_id._work_intervals_batch( - start_dt, end_dt - )[resource.id] + if booking.type_id: + result = booking.type_id.resource_calendar_id._work_intervals_batch( + start_dt, end_dt + )[False] + else: + result = Intervals([]) # Restrict with the chosen combination, or to at least one of the # available ones combinations = ( @@ -716,7 +767,7 @@ def action_confirm(self): # attendee.state='accepted' attendees_to_confirm |= attendee attendees_to_confirm.write({"state": "accepted"}) - self.recompute() + self.env.flush_all() # booking.meeting_id.partner_ids and attendees_to_confirm def action_unschedule(self): """Remove associated meetings.""" diff --git a/resource_booking/models/resource_booking_combination.py b/resource_booking/models/resource_booking_combination.py index a7cf2b1b..f8b3d390 100644 --- a/resource_booking/models/resource_booking_combination.py +++ b/resource_booking/models/resource_booking_combination.py @@ -101,6 +101,7 @@ def action_open_bookings(self): "res_model": "resource.booking", "type": "ir.actions.act_window", "view_mode": "calendar,tree,form", + "context": {"default_combination_id": self.id}, } def action_open_resource_booking_types(self): diff --git a/resource_booking/models/resource_booking_type.py b/resource_booking/models/resource_booking_type.py index d43f25a9..7d1597d0 100644 --- a/resource_booking/models/resource_booking_type.py +++ b/resource_booking/models/resource_booking_type.py @@ -1,8 +1,6 @@ # Copyright 2021 Tecnativa - Jairo Llopis # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from datetime import timedelta -from math import ceil from random import random from odoo import _, api, fields, models @@ -59,10 +57,12 @@ class ResourceBookingType(models.Model): duration = fields.Float( required=True, default=0.5, # 30 minutes - help=( - "Interval offered to start each resource booking. " - "Also used as booking default duration." - ), + help=("Booking default duration."), + ) + slot_duration = fields.Float( + required=True, + default=0.5, # 30 minutes + help=("Interval offered to start each resource booking."), ) location = fields.Char() videocall_location = fields.Char(string="Meeting URL") @@ -128,46 +128,8 @@ def _get_combinations_priorized(self): combinations = rels.mapped("combination_id") return combinations - def _get_next_slot_start(self, start_dt): - """Slot start as it would come from the beginning of work hours. - - Returns a `datetime` object indicating the next slot start (which could - be the same as `start_dt` if it matches), or `False` if no slot is - found in the next 2 weeks. - - If the RBT doesn't have a calendar, it returns `start_dt`, unaltered, - because there's no way to know when a slot would start. - """ - duration_delta = timedelta(hours=self.duration) - end_dt = start_dt + duration_delta - workday_min = start_dt.replace(hour=0, minute=0, second=0, microsecond=0) - # Detached compatibility with hr_holidays_public - res_calendar = self.resource_calendar_id.with_context( - exclude_public_holidays=True - ) - resource = self.env["resource.resource"] - attendance_intervals = res_calendar._attendance_intervals_batch( - workday_min, end_dt - )[resource.id] - try: - workday_start, valid_end, _meta = attendance_intervals._items[-1] - if valid_end != end_dt: - # Interval found, but without enough time; same as no interval - raise IndexError - except IndexError: - try: - # Returns `False` if no slot is found in the next 2 weeks - return ( - res_calendar.plan_hours(self.duration, end_dt, compute_leaves=True) - - duration_delta - ) - except TypeError: - return False - time_passed = valid_end - duration_delta - workday_start - return workday_start + duration_delta * ceil(time_passed / duration_delta) - def action_open_bookings(self): - FloatTimeParser = self.env["ir.qweb.field.float_time"] + DurationParser = self.env["ir.qweb.field.duration"] return { "context": dict( self.env.context, @@ -176,8 +138,12 @@ def action_open_bookings(self): default_duration=self.duration, default_type_id=self.id, # Context used by web_calendar_slot_duration module - calendar_slot_duration=FloatTimeParser.value_to_html( - self.duration, False + calendar_slot_duration=DurationParser.value_to_html( + self.slot_duration, + { + "unit": "hour", + "digital": True, + }, ), ), "domain": [("type_id", "=", self.id)], diff --git a/resource_booking/readme/CONTRIBUTORS.rst b/resource_booking/readme/CONTRIBUTORS.rst index 7ee45dc9..f7145432 100644 --- a/resource_booking/readme/CONTRIBUTORS.rst +++ b/resource_booking/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Jairo Llopis (https://www.tecnativa.com/) +* Henrik Norlin (https://ows.cloud) diff --git a/resource_booking/static/description/index.html b/resource_booking/static/description/index.html index 5bc10c36..676ce85f 100644 --- a/resource_booking/static/description/index.html +++ b/resource_booking/static/description/index.html @@ -1,3 +1,4 @@ + @@ -366,9 +367,9 @@

Resource booking

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:2a68fc2648f97ac64afcd5e5c6fa3ab28bfe94008b501d0ebad818d9e9bd3140 +!! source digest: sha256:69da76a920e2d581cfc27db05479e5705f196298cbbe67e881c92bf1ea6363af !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Production/Stable License: AGPL-3 OCA/calendar Translate me on Weblate Try me on Runboat

+

Production/Stable License: AGPL-3 OCA/calendar Translate me on Weblate Try me on Runboat

This module adds a new app to allow you to book resource combinations in given schedules.

Example use cases:

@@ -520,7 +521,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -535,6 +536,7 @@

Authors

Contributors

@@ -544,9 +546,9 @@

Maintainers

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

Current maintainer:

-

pedrobaeza

-

This module is part of the OCA/calendar project on GitHub.

+

Current maintainers:

+

pedrobaeza ows-cloud

+

This module is part of the OCA/calendar project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/resource_booking/templates/portal.xml b/resource_booking/templates/portal.xml index fc5165f5..4458958a 100644 --- a/resource_booking/templates/portal.xml +++ b/resource_booking/templates/portal.xml @@ -97,7 +97,7 @@ @@ -543,7 +543,7 @@ diff --git a/resource_booking/tests/common.py b/resource_booking/tests/common.py index 1bf5e4bb..3a57b86c 100644 --- a/resource_booking/tests/common.py +++ b/resource_booking/tests/common.py @@ -42,7 +42,7 @@ def create_test_data(obj): "name": "Fridays", "dayofweek": "4", "hour_from": 0, - "hour_to": 23.99, + "hour_to": 24, "day_period": "morning", }, ), @@ -53,7 +53,7 @@ def create_test_data(obj): "name": "Saturdays", "dayofweek": "5", "hour_from": 0, - "hour_to": 23.99, + "hour_to": 24, "day_period": "morning", }, ), @@ -64,7 +64,7 @@ def create_test_data(obj): "name": "Sunday", "dayofweek": "6", "hour_from": 0, - "hour_to": 23.99, + "hour_to": 24, "day_period": "morning", }, ), diff --git a/resource_booking/tests/test_backend.py b/resource_booking/tests/test_backend.py index f91e451e..9755eb96 100644 --- a/resource_booking/tests/test_backend.py +++ b/resource_booking/tests/test_backend.py @@ -203,8 +203,16 @@ def test_availability_is_fitting_malformed_date_skip(self): """ recset = self.env["resource.booking"] tuples = [ - (datetime(2021, 3, 1, 18, 0), datetime(2021, 3, 1, 23, 59), recset), - (datetime(2021, 3, 2, 0, 0), datetime(2021, 3, 2, 23, 59), recset), + ( + datetime(2021, 3, 1, 18, 0), + datetime(2021, 3, 1, 23, 59, 59, 999999), + recset, + ), + ( + datetime(2021, 3, 2, 0, 0), + datetime(2021, 3, 2, 23, 59, 59, 999999), + recset, + ), (datetime(2021, 3, 3, 0, 0), datetime(2021, 3, 3, 18, 0), recset), ] available_intervals = Intervals(tuples) @@ -769,7 +777,7 @@ def test_suggested_and_subscribed_recipients(self): # Requester and combination must be suggested self.assertEqual( rb._message_get_suggested_recipients(), - {rb.id: [(rb.partner_ids.id, "some customer", "Attendees")]}, + {rb.id: [(rb.partner_ids.id, "some customer", None, "Attendees")]}, ) def test_creating_rbt_has_tags(self): diff --git a/resource_booking/views/calendar_event_views.xml b/resource_booking/views/calendar_event_views.xml index 58671981..d3a9021a 100644 --- a/resource_booking/views/calendar_event_views.xml +++ b/resource_booking/views/calendar_event_views.xml @@ -6,7 +6,6 @@ calendar.event.view.form.inherit calendar.event -
diff --git a/resource_booking/views/res_partner_views.xml b/resource_booking/views/res_partner_views.xml new file mode 100644 index 00000000..22a70daf --- /dev/null +++ b/resource_booking/views/res_partner_views.xml @@ -0,0 +1,25 @@ + + + + res.partner + form + + +
+ +
+
+
+
diff --git a/resource_booking/views/resource_booking_combination_views.xml b/resource_booking/views/resource_booking_combination_views.xml index b20af153..ca778086 100644 --- a/resource_booking/views/resource_booking_combination_views.xml +++ b/resource_booking/views/resource_booking_combination_views.xml @@ -3,7 +3,7 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - + Resource booking combination form resource.booking.combination diff --git a/resource_booking/views/resource_booking_type_views.xml b/resource_booking/views/resource_booking_type_views.xml index bc00b278..7b9d505d 100644 --- a/resource_booking/views/resource_booking_type_views.xml +++ b/resource_booking/views/resource_booking_type_views.xml @@ -3,7 +3,7 @@ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - + Resource booking type form resource.booking.type @@ -40,6 +40,7 @@ groups="base.group_multi_company" /> + diff --git a/resource_booking/views/resource_booking_views.xml b/resource_booking/views/resource_booking_views.xml index f017dfca..f9e5d4a8 100644 --- a/resource_booking/views/resource_booking_views.xml +++ b/resource_booking/views/resource_booking_views.xml @@ -29,7 +29,7 @@ - + @@ -38,7 +38,7 @@ - + Resource booking form resource.booking @@ -159,11 +159,7 @@ - + @@ -207,6 +203,7 @@ +