From 354e2ed8f4fb89cff8659b835943ce9ff461f4d0 Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Wed, 25 Sep 2024 13:46:06 +0300 Subject: [PATCH] Migrate reservation models to the new app --- actions/application_round.py | 2 +- actions/recurring_reservation.py | 322 ------ actions/rejected_occurrence.py | 11 - actions/reservation.py | 128 --- actions/reservation_unit.py | 15 +- .../admin/application_section/filters.py | 2 +- applications/enums.py | 2 +- ...lter_applicationround_purposes_and_more.py | 47 + applications/models/application_round.py | 6 +- applications/models/application_section.py | 6 +- applications/tasks.py | 14 +- common/connectors.py | 25 - .../commands/data_creation/create_caisa.py | 2 +- .../commands/data_creation/create_misc.py | 6 +- .../data_creation/create_reservation_units.py | 11 +- .../data_creation/create_reservations.py | 6 +- .../data_creation/create_seasonal_booking.py | 3 +- .../management/commands/data_creation/main.py | 2 +- locale/fi/LC_MESSAGES/django.po | 1016 ++++++++-------- locale/sv/LC_MESSAGES/django.po | 1018 ++++++++--------- ...0111_alter_reservationunit_metadata_set.py | 30 + reservation_units/models/reservation_unit.py | 4 +- .../first_reservable_time_helper.py | 3 +- reservations/admin/__init__.py | 23 - reservations/admin/age_group.py | 12 - reservations/admin/recurring_reservation.py | 43 - reservations/admin/reservation/__init__.py | 5 - reservations/admin/reservation/admin.py | 322 ------ reservations/admin/reservation_purpose.py | 13 - reservations/admin/reservation_statistics.py | 22 - reservations/apps.py | 8 - reservations/enums.py | 179 --- ..._change_staff_event_to_reservation_type.py | 4 +- .../0051_refesh_reservation_statistics.py | 2 +- .../migrations/0071_rejectedoccurrence.py | 8 +- .../migrations/0076_uppercase_enums.py | 13 +- .../0079_fix_reservation_price_net.py | 9 +- ...rringreservation_ability_group_and_more.py | 128 +++ ...roup_delete_rejectedoccurrence_and_more.py | 56 + reservations/models/__init__.py | 27 - reservations/models/ability_group.py | 19 - reservations/models/affecting_time_span.py | 145 --- reservations/models/age_group.py | 22 - reservations/models/recurring_reservation.py | 112 -- reservations/models/rejected_occurrence.py | 45 - reservations/models/reservation.py | 323 ------ .../models/reservation_cancel_reason.py | 24 - .../models/reservation_deny_reason.py | 25 - reservations/models/reservation_metadata.py | 50 - reservations/models/reservation_purpose.py | 24 - reservations/models/reservation_statistic.py | 280 ----- reservations/querysets/__init__.py | 11 - reservations/querysets/affecting_time_span.py | 9 - .../querysets/recurring_reservation.py | 5 - reservations/querysets/rejected_occurrence.py | 39 - reservations/querysets/reservation.py | 161 --- reservations/signals.py | 63 - reservations/tasks.py | 143 --- reservations/translation.py | 23 - tests/factories/ability_group.py | 2 +- tests/factories/age_group.py | 2 +- tests/factories/recurring_reservation.py | 2 +- tests/factories/rejected_occurrence.py | 4 +- tests/factories/reservation.py | 5 +- tests/factories/reservation_cancel_reason.py | 2 +- tests/factories/reservation_deny_reason.py | 2 +- tests/factories/reservation_metadata.py | 2 +- tests/factories/reservation_purpose.py | 2 +- .../test_application_round_actions.py | 4 +- ..._unit_actions_check_reservation_overlap.py | 2 +- ...ation_unit_actions_get_next_reservation.py | 2 +- ...t_actions_unit_get_previous_reservation.py | 2 +- .../test_email_builder_render_reservation.py | 3 +- .../test_email_context_reservation.py | 5 +- .../test_email_sender_reservation.py | 2 +- .../test_create_order_params.py | 2 +- .../test_verkkokauppa/test_helpers.py | 2 +- .../test_verkkokauppa/test_pruning.py | 5 +- .../test_verkkokauppa/test_tasks.py | 2 +- .../test_order_payment_webhooks.py | 3 +- tests/test_gdpr_api/test_gdpr_api.py | 7 +- .../test_order/test_refresh.py | 3 +- .../test_recurring_reservation/helpers.py | 2 +- .../test_recurring_reservation/test_create.py | 2 +- .../test_create_permissions.py | 2 +- .../test_create_series.py | 14 +- .../test_recurring_reservation/test_query.py | 2 +- .../test_rejected_occurrence/test_query.py | 2 +- .../test_reservation/helpers.py | 4 +- .../test_reservation/test_adjust_time.py | 5 +- .../test_affecting_reservations.py | 2 +- .../test_reservation/test_approve.py | 2 +- .../test_approve_permissions.py | 3 +- .../test_reservation/test_cancel.py | 5 +- .../test_reservation/test_confirm.py | 3 +- .../test_reservation/test_create.py | 4 +- .../test_reservation/test_delete.py | 5 +- .../test_delete_permissions.py | 2 +- .../test_reservation/test_deny.py | 3 +- .../test_reservation/test_filtering.py | 3 +- .../test_reservation/test_ordering.py | 3 +- .../test_reservation/test_query.py | 4 +- .../test_query_permissions.py | 2 +- .../test_reservation/test_refund.py | 3 +- .../test_reservation/test_require_handling.py | 3 +- .../test_staff_adjust_time.py | 3 +- .../test_reservation/test_staff_create.py | 4 +- .../test_reservation/test_staff_modify.py | 2 +- .../test_reservation/test_staff_update.py | 4 +- .../test_reservation/test_update.py | 2 +- .../test_reservation_unit/test_query.py | 3 +- .../test_query_first_reservable_time.py | 4 +- .../test_reservation_reservee_name.py | 8 +- .../test_reservation_statistics.py | 4 +- ..._reservation_unit_reservation_scheduler.py | 2 +- .../test_reservation_querysets.py | 4 +- .../test_prune_inactive_reservations.py | 6 +- .../test_prune_recurring_reservations.py | 4 +- .../test_prune_reservation_statistics.py | 4 +- ...rune_reservation_with_inactive_payments.py | 7 +- tests/test_utils/test_create_test_data.py | 8 +- ..._generate_reservations_from_allocations.py | 14 +- tilavarauspalvelu/admin/__init__.py | 20 + .../admin/ability_group}/__init__.py | 0 .../admin/ability_group/admin.py | 6 +- tilavarauspalvelu/admin/age_group/admin.py | 8 + tilavarauspalvelu/admin/deny_reason/admin.py | 0 .../admin/metadata_field/admin.py | 0 tilavarauspalvelu/admin/metadata_set/admin.py | 0 .../admin/recurring_reservation/admin.py | 20 + tilavarauspalvelu/admin/reservation/admin.py | 337 ++++++ .../admin/reservation/filters.py | 2 +- .../admin/reservation/form.py | 2 +- .../reservation_cancel_reason}/__init__.py | 0 .../admin/reservation_cancel_reason/admin.py | 6 +- .../__init__.py | 0 .../admin/reservation_deny_reason/admin.py | 6 +- .../__init__.py | 0 .../admin/reservation_metadata_field/admin.py | 6 +- .../__init__.py | 0 .../admin/reservation_metadata_set/admin.py | 6 +- .../admin/reservation_purpose/admin.py | 9 + .../admin/reservation_statistic/admin.py | 18 + .../__init__.py | 0 .../admin.py | 0 tilavarauspalvelu/api/graphql/schema.py | 3 +- .../api/graphql/types/ability_group/types.py | 2 +- .../api/graphql/types/age_group/types.py | 2 +- .../graphql/types/helsinki_profile/types.py | 3 +- .../types/recurring_reservation/filtersets.py | 3 +- .../recurring_reservation/permissions.py | 2 +- .../recurring_reservation/serializers.py | 8 +- .../types/recurring_reservation/types.py | 2 +- .../types/rejected_occurrence/filtersets.py | 5 +- .../types/rejected_occurrence/types.py | 2 +- .../graphql/types/reservation/filtersets.py | 11 +- .../graphql/types/reservation/mutations.py | 5 +- .../graphql/types/reservation/permissions.py | 2 +- .../serializers/adjust_time_serializers.py | 4 +- .../serializers/approve_serializers.py | 4 +- .../serializers/cancellation_serializers.py | 7 +- .../serializers/confirm_serializers.py | 6 +- .../serializers/create_serializers.py | 14 +- .../serializers/deny_serializers.py | 4 +- .../handling_required_serializers.py | 4 +- .../serializers/memo_serializers.py | 2 +- .../types/reservation/serializers/mixins.py | 4 +- .../serializers/refund_serializers.py | 8 +- .../staff_adjust_time_serializers.py | 4 +- .../serializers/staff_create_serializers.py | 12 +- .../staff_reservation_modify_serializers.py | 12 +- .../serializers/update_serializers.py | 4 +- .../api/graphql/types/reservation/types.py | 12 +- .../reservation_cancel_reason/filersets.py | 2 +- .../types/reservation_cancel_reason/types.py | 2 +- .../reservation_deny_reason/filtersets.py | 2 +- .../types/reservation_deny_reason/types.py | 2 +- .../types/reservation_metadata/types.py | 2 +- .../types/reservation_purpose/filtersets.py | 2 +- .../types/reservation_purpose/types.py | 2 +- .../graphql/types/reservation_unit/types.py | 5 +- .../api/mock_verkkokauppa_api/views.py | 6 +- tilavarauspalvelu/api/rest/views.py | 3 +- tilavarauspalvelu/enums.py | 174 +++ .../commands/create_missing_statistics.py | 4 +- .../migrations/0010_migrate_reservations.py | 697 +++++++++++ tilavarauspalvelu/models/__init__.py | 26 + .../ability_group}/__init__.py | 0 .../models/ability_group/actions.py | 9 + .../models/ability_group/model.py | 38 + .../models/ability_group/queryset.py | 10 + .../models/affecting_time_span/actions.py | 9 + .../models/affecting_time_span/model.py | 154 +++ .../models/affecting_time_span/queryset.py | 4 + tilavarauspalvelu/models/age_group/actions.py | 9 + tilavarauspalvelu/models/age_group/model.py | 40 + .../models/age_group/queryset.py | 10 + .../models/cancel_reason/actions.py | 0 .../models/cancel_reason/model.py | 0 .../models/cancel_reason/queryset.py | 0 .../models/deny_reason/actions.py | 0 tilavarauspalvelu/models/deny_reason/model.py | 0 .../models/deny_reason/queryset.py | 0 .../models/metadata_field/actions.py | 0 .../models/metadata_field/model.py | 0 .../models/metadata_field/queryset.py | 0 .../models/metadata_set/actions.py | 0 .../models/metadata_set/model.py | 0 .../models/metadata_set/queryset.py | 0 .../models/payment_order/model.py | 12 +- .../models/recurring_reservation/actions.py | 328 ++++++ .../models/recurring_reservation/model.py | 123 ++ .../models/recurring_reservation/queryset.py | 4 + .../models/rejected_occurrence/actions.py | 15 + .../models/rejected_occurrence/model.py | 60 + .../models/rejected_occurrence/queryset.py | 39 + .../models/reservation/actions.py | 132 +++ tilavarauspalvelu/models/reservation/model.py | 324 ++++++ .../models/reservation/queryset.py | 160 +++ .../__init__.py | 0 .../reservation_cancel_reason/actions.py | 9 + .../models/reservation_cancel_reason/model.py | 44 + .../reservation_cancel_reason/queryset.py | 10 + .../__init__.py | 0 .../models/reservation_deny_reason/actions.py | 9 + .../models/reservation_deny_reason/model.py | 39 + .../reservation_deny_reason/queryset.py | 10 + .../__init__.py | 0 .../reservation_metadata_field/actions.py | 9 + .../reservation_metadata_field/model.py | 36 + .../reservation_metadata_field/queryset.py | 10 + .../__init__.py | 0 .../reservation_metadata_set/actions.py | 9 + .../models/reservation_metadata_set/model.py | 50 + .../reservation_metadata_set/queryset.py | 10 + .../models/reservation_purpose/actions.py | 9 + .../models/reservation_purpose/model.py | 38 + .../models/reservation_purpose/queryset.py | 10 + .../models/reservation_statistic/actions.py | 9 + .../models/reservation_statistic/model.py | 231 ++++ .../models/reservation_statistic/queryset.py | 10 + .../reservation_statistic_unit/__init__.py} | 0 .../reservation_statistic_unit/actions.py | 9 + .../reservation_statistic_unit/model.py | 77 ++ .../reservation_statistic_unit/queryset.py | 10 + tilavarauspalvelu/models/unit/queryset.py | 2 +- tilavarauspalvelu/signals.py | 59 +- tilavarauspalvelu/tasks.py | 142 ++- tilavarauspalvelu/translation.py | 31 +- tilavarauspalvelu/typing.py | 5 + tilavarauspalvelu/utils/anonymisation.py | 6 +- .../utils/email/email_builder_reservation.py | 6 +- tilavarauspalvelu/utils/email/email_sender.py | 2 +- .../reservation_email_notification_sender.py | 11 +- tilavarauspalvelu/utils/helauth/pipeline.py | 3 +- .../utils/opening_hours/time_span_element.py | 25 +- .../utils/permission_resolver.py | 3 +- .../utils}/pruning.py | 2 +- .../utils/verkkokauppa/helpers.py | 3 +- 259 files changed, 5232 insertions(+), 4030 deletions(-) delete mode 100644 actions/recurring_reservation.py delete mode 100644 actions/rejected_occurrence.py delete mode 100644 actions/reservation.py create mode 100644 applications/migrations/0095_alter_applicationround_purposes_and_more.py create mode 100644 reservation_units/migrations/0111_alter_reservationunit_metadata_set.py delete mode 100644 reservations/admin/__init__.py delete mode 100644 reservations/admin/age_group.py delete mode 100644 reservations/admin/recurring_reservation.py delete mode 100644 reservations/admin/reservation/__init__.py delete mode 100644 reservations/admin/reservation/admin.py delete mode 100644 reservations/admin/reservation_purpose.py delete mode 100644 reservations/admin/reservation_statistics.py delete mode 100644 reservations/apps.py delete mode 100644 reservations/enums.py create mode 100644 reservations/migrations/0083_remove_recurringreservation_ability_group_and_more.py create mode 100644 reservations/migrations/0084_delete_agegroup_delete_rejectedoccurrence_and_more.py delete mode 100644 reservations/models/__init__.py delete mode 100644 reservations/models/ability_group.py delete mode 100644 reservations/models/affecting_time_span.py delete mode 100644 reservations/models/age_group.py delete mode 100644 reservations/models/recurring_reservation.py delete mode 100644 reservations/models/rejected_occurrence.py delete mode 100644 reservations/models/reservation.py delete mode 100644 reservations/models/reservation_cancel_reason.py delete mode 100644 reservations/models/reservation_deny_reason.py delete mode 100644 reservations/models/reservation_metadata.py delete mode 100644 reservations/models/reservation_purpose.py delete mode 100644 reservations/models/reservation_statistic.py delete mode 100644 reservations/querysets/__init__.py delete mode 100644 reservations/querysets/affecting_time_span.py delete mode 100644 reservations/querysets/recurring_reservation.py delete mode 100644 reservations/querysets/rejected_occurrence.py delete mode 100644 reservations/querysets/reservation.py delete mode 100644 reservations/signals.py delete mode 100644 reservations/tasks.py delete mode 100644 reservations/translation.py rename {reservations/management => tilavarauspalvelu/admin/ability_group}/__init__.py (100%) rename reservations/admin/ability_group.py => tilavarauspalvelu/admin/ability_group/admin.py (66%) delete mode 100644 tilavarauspalvelu/admin/deny_reason/admin.py delete mode 100644 tilavarauspalvelu/admin/metadata_field/admin.py delete mode 100644 tilavarauspalvelu/admin/metadata_set/admin.py rename {reservations => tilavarauspalvelu}/admin/reservation/filters.py (96%) rename {reservations => tilavarauspalvelu}/admin/reservation/form.py (99%) rename {reservations/management/commands => tilavarauspalvelu/admin/reservation_cancel_reason}/__init__.py (100%) rename reservations/admin/reservation_cancel_reason.py => tilavarauspalvelu/admin/reservation_cancel_reason/admin.py (64%) rename tilavarauspalvelu/admin/{cancel_reason => reservation_deny_reason}/__init__.py (100%) rename reservations/admin/reservation_deny_reason.py => tilavarauspalvelu/admin/reservation_deny_reason/admin.py (71%) rename tilavarauspalvelu/admin/{cancellation_rule => reservation_metadata_field}/__init__.py (100%) rename reservations/admin/reservation_metadata_field.py => tilavarauspalvelu/admin/reservation_metadata_field/admin.py (86%) rename tilavarauspalvelu/admin/{deny_reason => reservation_metadata_set}/__init__.py (100%) rename reservations/admin/reservation_metadata_set.py => tilavarauspalvelu/admin/reservation_metadata_set/admin.py (89%) rename tilavarauspalvelu/admin/{metadata_field => reservation_unit_cancellation_rule}/__init__.py (100%) rename tilavarauspalvelu/admin/{cancel_reason => reservation_unit_cancellation_rule}/admin.py (100%) rename {reservations => tilavarauspalvelu}/management/commands/create_missing_statistics.py (87%) create mode 100644 tilavarauspalvelu/migrations/0010_migrate_reservations.py rename tilavarauspalvelu/{admin/metadata_set => models/ability_group}/__init__.py (100%) create mode 100644 tilavarauspalvelu/models/ability_group/actions.py create mode 100644 tilavarauspalvelu/models/ability_group/model.py create mode 100644 tilavarauspalvelu/models/ability_group/queryset.py delete mode 100644 tilavarauspalvelu/models/cancel_reason/actions.py delete mode 100644 tilavarauspalvelu/models/cancel_reason/model.py delete mode 100644 tilavarauspalvelu/models/cancel_reason/queryset.py delete mode 100644 tilavarauspalvelu/models/deny_reason/actions.py delete mode 100644 tilavarauspalvelu/models/deny_reason/model.py delete mode 100644 tilavarauspalvelu/models/deny_reason/queryset.py delete mode 100644 tilavarauspalvelu/models/metadata_field/actions.py delete mode 100644 tilavarauspalvelu/models/metadata_field/model.py delete mode 100644 tilavarauspalvelu/models/metadata_field/queryset.py delete mode 100644 tilavarauspalvelu/models/metadata_set/actions.py delete mode 100644 tilavarauspalvelu/models/metadata_set/model.py delete mode 100644 tilavarauspalvelu/models/metadata_set/queryset.py rename tilavarauspalvelu/models/{cancel_reason => reservation_cancel_reason}/__init__.py (100%) create mode 100644 tilavarauspalvelu/models/reservation_cancel_reason/actions.py create mode 100644 tilavarauspalvelu/models/reservation_cancel_reason/model.py create mode 100644 tilavarauspalvelu/models/reservation_cancel_reason/queryset.py rename tilavarauspalvelu/models/{deny_reason => reservation_deny_reason}/__init__.py (100%) create mode 100644 tilavarauspalvelu/models/reservation_deny_reason/actions.py create mode 100644 tilavarauspalvelu/models/reservation_deny_reason/model.py create mode 100644 tilavarauspalvelu/models/reservation_deny_reason/queryset.py rename tilavarauspalvelu/models/{metadata_field => reservation_metadata_field}/__init__.py (100%) create mode 100644 tilavarauspalvelu/models/reservation_metadata_field/actions.py create mode 100644 tilavarauspalvelu/models/reservation_metadata_field/model.py create mode 100644 tilavarauspalvelu/models/reservation_metadata_field/queryset.py rename tilavarauspalvelu/models/{metadata_set => reservation_metadata_set}/__init__.py (100%) create mode 100644 tilavarauspalvelu/models/reservation_metadata_set/actions.py create mode 100644 tilavarauspalvelu/models/reservation_metadata_set/model.py create mode 100644 tilavarauspalvelu/models/reservation_metadata_set/queryset.py rename tilavarauspalvelu/{admin/cancellation_rule/admin.py => models/reservation_statistic_unit/__init__.py} (100%) create mode 100644 tilavarauspalvelu/models/reservation_statistic_unit/actions.py create mode 100644 tilavarauspalvelu/models/reservation_statistic_unit/model.py create mode 100644 tilavarauspalvelu/models/reservation_statistic_unit/queryset.py rename {reservations => tilavarauspalvelu/utils}/pruning.py (95%) diff --git a/actions/application_round.py b/actions/application_round.py index d7e868dbb..c77b5b657 100644 --- a/actions/application_round.py +++ b/actions/application_round.py @@ -3,7 +3,7 @@ from django.db import models from applications.enums import ApplicationRoundStatusChoice -from reservations.models import RecurringReservation, Reservation +from tilavarauspalvelu.models import RecurringReservation, Reservation if TYPE_CHECKING: from applications.models import ApplicationRound diff --git a/actions/recurring_reservation.py b/actions/recurring_reservation.py deleted file mode 100644 index 85477efa2..000000000 --- a/actions/recurring_reservation.py +++ /dev/null @@ -1,322 +0,0 @@ -from __future__ import annotations - -import dataclasses -import datetime -from itertools import chain -from typing import TYPE_CHECKING, Any, TypedDict - -from common.date_utils import DEFAULT_TIMEZONE, combine, get_periods_between -from reservations.enums import RejectionReadinessChoice, ReservationTypeChoice, ReservationTypeStaffChoice -from reservations.models import ( - AffectingTimeSpan, - RecurringReservation, - RejectedOccurrence, - Reservation, - ReservationPurpose, -) -from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement - -if TYPE_CHECKING: - from collections.abc import Collection, Iterable - - from django.db import models - - from applications.models import City - from reservations.enums import CustomerTypeChoice, ReservationStateChoice - from tilavarauspalvelu.models import ReservableTimeSpan, User - - -class ReservationPeriod(TypedDict): - begin: datetime.datetime - end: datetime.datetime - - -@dataclasses.dataclass -class ReservationSeriesCalculationResults: - non_overlapping: list[ReservationPeriod] = dataclasses.field(default_factory=list) - overlapping: list[ReservationPeriod] = dataclasses.field(default_factory=list) - not_reservable: list[ReservationPeriod] = dataclasses.field(default_factory=list) - invalid_start_interval: list[ReservationPeriod] = dataclasses.field(default_factory=list) - - def as_json(self, periods: list[ReservationPeriod]) -> list[dict[str, Any]]: - return [ - { - "begin": period["begin"].isoformat(timespec="seconds"), - "end": period["end"].isoformat(timespec="seconds"), - } - for period in periods - ] - - @property - def overlapping_json(self) -> list[dict[str, Any]]: - return self.as_json(self.overlapping) - - @property - def not_reservable_json(self) -> list[dict[str, Any]]: - return self.as_json(self.not_reservable) - - @property - def invalid_start_interval_json(self) -> list[dict[str, Any]]: - return self.as_json(self.invalid_start_interval) - - @property - def possible(self) -> Iterable[ReservationPeriod]: - return self.non_overlapping - - @property - def not_possible(self) -> Iterable[ReservationPeriod]: - return chain(self.overlapping, self.not_reservable, self.invalid_start_interval) - - -class ReservationDetails(TypedDict, total=False): - name: str - description: str - num_persons: int - state: ReservationStateChoice - type: ReservationTypeChoice | ReservationTypeStaffChoice - working_memo: str - - buffer_time_before: datetime.timedelta - buffer_time_after: datetime.timedelta - handled_at: datetime.datetime - confirmed_at: datetime.datetime - - applying_for_free_of_charge: bool - free_of_charge_reason: bool - - reservee_id: str - reservee_first_name: str - reservee_last_name: str - reservee_email: str - reservee_phone: str - reservee_organisation_name: str - reservee_address_street: str - reservee_address_city: str - reservee_address_zip: str - reservee_is_unregistered_association: bool - reservee_language: str - reservee_type: CustomerTypeChoice - - billing_first_name: str - billing_last_name: str - billing_email: str - billing_phone: str - billing_address_street: str - billing_address_city: str - billing_address_zip: str - - user: int | User - purpose: int | ReservationPurpose - home_city: int | City - - -class RecurringReservationActions: - def __init__(self, recurring_reservation: RecurringReservation) -> None: - self.recurring_reservation = recurring_reservation - - def pre_calculate_slots( - self, - *, - check_opening_hours: bool = False, - check_buffers: bool = False, - check_start_interval: bool = False, - skip_dates: Collection[datetime.date] = (), - closed_hours: Collection[TimeSpanElement] = (), - buffer_time_before: datetime.timedelta | None = None, - buffer_time_after: datetime.timedelta | None = None, - ) -> ReservationSeriesCalculationResults: - """ - Pre-calculate slots for reservations for the recurring reservation. - - :param check_opening_hours: Whether to check if the reservation falls within reservable times. - :param check_buffers: Whether to check if the reservation overlaps with other reservations' buffers. - :param check_start_interval: Whether to check if the reservation starts at the correct interval. - :param skip_dates: Dates to skip when calculating slots. - :param closed_hours: Explicitly closed opening hours for the resource. - :param buffer_time_before: Used buffer time before the reservation. - :param buffer_time_after: Used buffer time after the reservation. - """ - pk = self.recurring_reservation.reservation_unit.pk - - timespans = [ - timespan.as_time_span_element() - for timespan in AffectingTimeSpan.objects.filter( - affected_reservation_unit_ids__contains=[pk], - buffered_start_datetime__date__lte=self.recurring_reservation.end_date, - buffered_end_datetime__date__gte=self.recurring_reservation.begin_date, - ) - ] - - reservable_timespans = self.get_reservable_timespans() if check_opening_hours else [] - - results = ReservationSeriesCalculationResults() - - begin_time: datetime.time = self.recurring_reservation.begin_time - end_time: datetime.time = self.recurring_reservation.end_time - reservation_unit = self.recurring_reservation.reservation_unit - - weekdays: list[int] = [int(val) for val in self.recurring_reservation.weekdays.split(",") if val != ""] - if not weekdays: - weekdays = [self.recurring_reservation.begin_date.weekday()] - - for weekday in weekdays: - delta: int = weekday - self.recurring_reservation.begin_date.weekday() - if delta < 0: - delta += 7 - - begin_date: datetime.date = self.recurring_reservation.begin_date + datetime.timedelta(days=delta) - - periods = get_periods_between( - start_date=begin_date, - end_date=self.recurring_reservation.end_date, - start_time=begin_time, - end_time=end_time, - interval=self.recurring_reservation.recurrence_in_days, - tzinfo=DEFAULT_TIMEZONE, - ) - for begin, end in periods: - if begin.date() in skip_dates: - continue - - reservation_timespan = TimeSpanElement( - start_datetime=begin, - end_datetime=end, - is_reservable=True, - buffer_time_before=( - reservation_unit.actions.get_actual_before_buffer(begin, buffer_time_before) - if check_buffers - else None - ), - buffer_time_after=( - reservation_unit.actions.get_actual_after_buffer(end, buffer_time_after) - if check_buffers - else None - ), - ) - - # Would the reservation timespan overlap with any closing timespans - # that exist due to existing reservations? Checks for: - # 1) Unbuffered reservation timespan overlapping with any buffered closed timespan - # 2) Unbuffered closed timespan overlapping with any buffered reservation timespan - # Note that reservation timespans buffers are only checked if `check_buffers=True`. - if any( - reservation_timespan.overlaps_with(timespan) or timespan.overlaps_with(reservation_timespan) - for timespan in timespans - ): - results.overlapping.append(ReservationPeriod(begin=begin, end=end)) - continue - - # Would the reservation be fully inside any reservable timespans for the resource? - # Ignore buffers for the reservation, since those can be outside reservable times. - if check_opening_hours and not any( - reservation_timespan.fully_inside_of(reservable) for reservable in reservable_timespans - ): - results.not_reservable.append(ReservationPeriod(begin=begin, end=end)) - continue - - # Would the reservation overlap with any explicitly closed opening hours for the resource? - if closed_hours and any( - reservation_timespan.overlaps_with(closed_time_span) for closed_time_span in closed_hours - ): - results.not_reservable.append(ReservationPeriod(begin=begin, end=end)) - continue - - if check_start_interval and not reservation_unit.actions.is_valid_staff_start_interval(begin.timetz()): - results.invalid_start_interval.append(ReservationPeriod(begin=begin, end=end)) - continue - - results.non_overlapping.append(ReservationPeriod(begin=begin, end=end)) - - return results - - def get_reservable_timespans(self) -> list[TimeSpanElement]: - begin_time = self.recurring_reservation.begin_time - end_time = self.recurring_reservation.end_time - hauki_resource = self.recurring_reservation.reservation_unit.origin_hauki_resource - if hauki_resource is None: - return [] - - timespans: Iterable[ReservableTimeSpan] = hauki_resource.reservable_time_spans.all().overlapping_with_period( - start=combine(self.recurring_reservation.begin_date, begin_time, tzinfo=DEFAULT_TIMEZONE), - end=combine(self.recurring_reservation.end_date, end_time, tzinfo=DEFAULT_TIMEZONE), - ) - - return [timespan.as_time_span_element() for timespan in timespans] - - def bulk_create_reservation_for_periods( - self, - periods: Iterable[ReservationPeriod], - reservation_details: ReservationDetails, - ) -> list[Reservation]: - # Pick out the through model for the many-to-many relationship and use if for bulk creation - ThroughModel: type[models.Model] = Reservation.reservation_unit.through # noqa: N806 - - reservations: list[Reservation] = [] - through_models: list[models.Model] = [] - - for period in periods: - if self.recurring_reservation.reservation_unit.reservation_block_whole_day: - reservation_details.setdefault( - "buffer_time_before", - self.recurring_reservation.reservation_unit.actions.get_actual_before_buffer(period["begin"]), - ) - reservation_details.setdefault( - "buffer_time_after", - self.recurring_reservation.reservation_unit.actions.get_actual_after_buffer(period["end"]), - ) - - reservation = Reservation( - begin=period["begin"], - end=period["end"], - recurring_reservation=self.recurring_reservation, - age_group=self.recurring_reservation.age_group, - **reservation_details, - ) - through = ThroughModel( - reservation=reservation, - reservationunit=self.recurring_reservation.reservation_unit, - ) - reservations.append(reservation) - through_models.append(through) - - reservations = Reservation.objects.bulk_create(reservations) - ThroughModel.objects.bulk_create(through_models) - return reservations - - def bulk_create_rejected_occurrences_for_periods( - self, - overlapping: Iterable[ReservationPeriod], - not_reservable: Iterable[ReservationPeriod], - invalid_start_interval: Iterable[ReservationPeriod], - ) -> list[RejectedOccurrence]: - occurrences: list[RejectedOccurrence] = ( - [ - RejectedOccurrence( - begin_datetime=period["begin"], - end_datetime=period["end"], - rejection_reason=RejectionReadinessChoice.OVERLAPPING_RESERVATIONS, - recurring_reservation=self.recurring_reservation, - ) - for period in overlapping - ] - + [ - RejectedOccurrence( - begin_datetime=period["begin"], - end_datetime=period["end"], - rejection_reason=RejectionReadinessChoice.RESERVATION_UNIT_CLOSED, - recurring_reservation=self.recurring_reservation, - ) - for period in not_reservable - ] - + [ - RejectedOccurrence( - begin_datetime=period["begin"], - end_datetime=period["end"], - rejection_reason=RejectionReadinessChoice.INTERVAL_NOT_ALLOWED, - recurring_reservation=self.recurring_reservation, - ) - for period in invalid_start_interval - ] - ) - - return RejectedOccurrence.objects.bulk_create(occurrences) diff --git a/actions/rejected_occurrence.py b/actions/rejected_occurrence.py deleted file mode 100644 index 10a7d3ab7..000000000 --- a/actions/rejected_occurrence.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from reservations.models import RejectedOccurrence - - -class RejectedOccurrenceActions: - def __init__(self, rejected_occurrence: RejectedOccurrence) -> None: - self.rejected_occurrence = rejected_occurrence diff --git a/actions/reservation.py b/actions/reservation.py deleted file mode 100644 index 4829b7c17..000000000 --- a/actions/reservation.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -import datetime -from typing import TYPE_CHECKING - -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import pgettext_lazy -from icalendar import Calendar, Event - -from common.date_utils import DEFAULT_TIMEZONE, local_datetime -from common.utils import get_attr_by_language -from reservations.enums import CalendarProperty, EventProperty - -if TYPE_CHECKING: - from common.typing import Lang - from reservation_units.models import ReservationUnit - from reservations.models import Reservation - from tilavarauspalvelu.models import Location, Unit - - -class ReservationActions: - def __init__(self, reservation: Reservation) -> None: - self.reservation = reservation - - def get_actual_before_buffer(self) -> datetime.timedelta: - buffer_time_before: datetime.timedelta = self.reservation.buffer_time_before or datetime.timedelta() - reservation_unit: ReservationUnit - for reservation_unit in self.reservation.reservation_unit.all(): - before = reservation_unit.actions.get_actual_before_buffer(self.reservation.begin) - buffer_time_before = max(before, buffer_time_before) - return buffer_time_before - - def get_actual_after_buffer(self) -> datetime.timedelta: - buffer_time_after: datetime.timedelta = self.reservation.buffer_time_after or datetime.timedelta() - reservation_unit: ReservationUnit - for reservation_unit in self.reservation.reservation_unit.all(): - after = reservation_unit.actions.get_actual_after_buffer(self.reservation.end) - buffer_time_after = max(after, buffer_time_after) - return buffer_time_after - - def to_ical(self, *, site_name: str) -> bytes: - language: Lang = ( # type: ignore[assignment] - self.reservation.reservee_language or self.reservation.user.get_preferred_language() - ) - - ical_event = Event() - # This should be unique such that if another iCal file is created - # for the same reservation, it will be the same as the previous one. - uid = f"varaamo.reservation.{self.reservation.pk}@{site_name}" - summary = self.get_ical_summary(language=language) - description = self.get_ical_description(site_name=site_name, language=language) - location = self.get_location() - - ical_event.add(name=EventProperty.UID, value=uid) - ical_event.add(name=EventProperty.DTSTAMP, value=local_datetime()) - ical_event.add(name=EventProperty.DTSTART, value=self.reservation.begin.astimezone(DEFAULT_TIMEZONE)) - ical_event.add(name=EventProperty.DTEND, value=self.reservation.end.astimezone(DEFAULT_TIMEZONE)) - - ical_event.add(name=EventProperty.SUMMARY, value=summary) - ical_event.add(name=EventProperty.DESCRIPTION, value=description, parameters={"FMTTYPE": "text/html"}) - ical_event.add(name=EventProperty.X_ALT_DESC, value=description, parameters={"FMTTYPE": "text/html"}) - - if location is not None: - ical_event.add(name=EventProperty.LOCATION, value=location.address) - if location.coordinates is not None: - ical_event.add(name=EventProperty.GEO, value=(location.lat, location.lon)) - - cal = Calendar() - cal.add(CalendarProperty.VERSION, "2.0") - cal.add(CalendarProperty.PRODID, "-//Helsinki City//NONSGML Varaamo//FI") - - cal.add_component(ical_event) - return cal.to_ical() - - def get_ical_summary(self, *, language: Lang = "fi") -> str: - unit: Unit = self.reservation.reservation_unit.first().unit - unit_name = get_attr_by_language(unit, "name", language) - return _("Reservation for %(name)s") % {"name": unit_name} - - def get_ical_description(self, *, site_name: str, language: Lang = "fi") -> str: - reservation_unit: ReservationUnit = self.reservation.reservation_unit.first() - unit: Unit = reservation_unit.unit - begin = self.reservation.begin.astimezone(DEFAULT_TIMEZONE) - end = self.reservation.end.astimezone(DEFAULT_TIMEZONE) - - title = _("Booking details") - reservation_unit_name = get_attr_by_language(reservation_unit, "name", language) - unit_name = get_attr_by_language(unit, "name", language) - location = self.get_location() - address = location.address if location is not None else "" - start_date = begin.date().strftime("%d.%m.%Y") - start_time = begin.time().strftime("%H:%M") - end_date = end.date().strftime("%d.%m.%Y") - end_time = end.time().strftime("%H:%M") - time_delimiter = "klo" if language == "fi" else "kl." if language == "sv" else "at" - if language == "sv": - site_name += "/sv" - elif language == "en": - site_name += "/en" - from_ = pgettext_lazy("ical", "From") - to_ = pgettext_lazy("ical", "To") - footer = _( - "Manage your booking at Varaamo. You can check the details of your booking and Varaamo's " - "terms of contract and cancellation on the '%(bookings)s' page." - ) % { - "bookings": f"" + _("My bookings") + "", - } - - return ( - f"" - f"" - f"" - f"

{title}

" - f"

{reservation_unit_name}, {unit_name}, {address}

" - f"

{from_}: {start_date} {time_delimiter} {start_time}

" - f"

{to_}: {end_date} {time_delimiter} {end_time}

" - f"

{footer}

" - f"" - f"" - ) - - def get_location(self) -> Location | None: - reservation_unit: ReservationUnit = self.reservation.reservation_unit.first() - unit: Unit = reservation_unit.unit - location: Location | None = getattr(unit, "location", None) - if location is None: - return reservation_unit.actions.get_location() - return location diff --git a/actions/reservation_unit.py b/actions/reservation_unit.py index 54c928a45..579634f6b 100644 --- a/actions/reservation_unit.py +++ b/actions/reservation_unit.py @@ -17,8 +17,7 @@ from django.db import models from reservation_units.models import ReservationUnit - from reservations.models import Reservation - from tilavarauspalvelu.models import Building, Location, ReservableTimeSpan + from tilavarauspalvelu.models import Building, Location, ReservableTimeSpan, Reservation __all__ = [ "ReservationUnitActions", @@ -193,8 +192,8 @@ def check_reservation_overlap( end_datetime: datetime.datetime, reservation: Reservation | None = None, ) -> bool: - from reservations.enums import ReservationStateChoice - from reservations.models import Reservation + from tilavarauspalvelu.enums import ReservationStateChoice + from tilavarauspalvelu.models import Reservation qs = Reservation.objects.filter( reservation_unit__in=self.reservation_units_with_common_hierarchy, @@ -214,8 +213,8 @@ def get_next_reservation( reservation: Reservation | None = None, exclude_blocked: bool = False, ) -> Reservation | None: - from reservations.enums import ReservationStateChoice, ReservationTypeChoice - from reservations.models import Reservation + from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice + from tilavarauspalvelu.models import Reservation qs = Reservation.objects.filter( reservation_unit__in=self.reservation_units_with_common_hierarchy, @@ -236,8 +235,8 @@ def get_previous_reservation( reservation: Reservation | None = None, exclude_blocked: bool = False, ) -> Reservation | None: - from reservations.enums import ReservationStateChoice, ReservationTypeChoice - from reservations.models import Reservation + from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice + from tilavarauspalvelu.models import Reservation qs = Reservation.objects.filter( reservation_unit__in=self.reservation_units_with_common_hierarchy, diff --git a/applications/admin/application_section/filters.py b/applications/admin/application_section/filters.py index 0b0c4b8ac..5edb3d648 100644 --- a/applications/admin/application_section/filters.py +++ b/applications/admin/application_section/filters.py @@ -9,7 +9,7 @@ from lookup_property import L from applications.enums import ApplicationRoundStatusChoice, ApplicationSectionStatusChoice, ApplicationStatusChoice -from reservations.models import AgeGroup, ReservationPurpose +from tilavarauspalvelu.models import AgeGroup, ReservationPurpose if TYPE_CHECKING: from django.db.models import QuerySet diff --git a/applications/enums.py b/applications/enums.py index 641cb6096..9b3437dd8 100644 --- a/applications/enums.py +++ b/applications/enums.py @@ -18,7 +18,7 @@ "WeekdayChoice", ] -from reservations.enums import CustomerTypeChoice +from tilavarauspalvelu.enums import CustomerTypeChoice class WeekdayChoice(models.IntegerChoices): diff --git a/applications/migrations/0095_alter_applicationround_purposes_and_more.py b/applications/migrations/0095_alter_applicationround_purposes_and_more.py new file mode 100644 index 000000000..04e07e1b4 --- /dev/null +++ b/applications/migrations/0095_alter_applicationround_purposes_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 5.1.1 on 2024-09-26 14:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("applications", "0094_alter_applicationround_terms_of_use"), + ("tilavarauspalvelu", "0010_migrate_reservations"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="applicationround", + name="purposes", + field=models.ManyToManyField( + related_name="application_rounds", + to="tilavarauspalvelu.reservationpurpose", + ), + ), + migrations.AlterField( + model_name="applicationsection", + name="age_group", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="application_sections", + to="tilavarauspalvelu.agegroup", + ), + ), + migrations.AlterField( + model_name="applicationsection", + name="purpose", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="application_sections", + to="tilavarauspalvelu.reservationpurpose", + ), + ), + ], + database_operations=[], + ), + ] diff --git a/applications/models/application_round.py b/applications/models/application_round.py index 898d58e82..0b09886fa 100644 --- a/applications/models/application_round.py +++ b/applications/models/application_round.py @@ -62,7 +62,7 @@ class ApplicationRound(models.Model): related_name="application_rounds", ) purposes = models.ManyToManyField( - "reservations.ReservationPurpose", + "tilavarauspalvelu.ReservationPurpose", related_name="application_rounds", ) terms_of_use = models.ForeignKey( @@ -215,7 +215,7 @@ def _(self) -> bool: @lookup_property(skip_codegen=True) def reservation_creation_status() -> ApplicationRoundReservationCreationStatusChoice: - from reservations.models import RecurringReservation + from tilavarauspalvelu.models import RecurringReservation timeout = timedelta(minutes=settings.APPLICATION_ROUND_RESERVATION_CREATION_TIMEOUT_MINUTES) @@ -244,7 +244,7 @@ def reservation_creation_status() -> ApplicationRoundReservationCreationStatusCh @reservation_creation_status.override def _(self) -> ApplicationRoundReservationCreationStatusChoice: - from reservations.models import RecurringReservation + from tilavarauspalvelu.models import RecurringReservation now = local_datetime() timeout = timedelta(minutes=settings.APPLICATION_ROUND_RESERVATION_CREATION_TIMEOUT_MINUTES) diff --git a/applications/models/application_section.py b/applications/models/application_section.py index 05db1b68e..b70578821 100644 --- a/applications/models/application_section.py +++ b/applications/models/application_section.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: from applications.models import Application - from reservations.models import AgeGroup, ReservationPurpose + from tilavarauspalvelu.models import AgeGroup, ReservationPurpose __all__ = [ "ApplicationSection", @@ -66,13 +66,13 @@ class ApplicationSection(SerializableMixin, models.Model): # purposes and age groups might get deleted, and the application # section should still remain in the database purpose: ReservationPurpose | None = models.ForeignKey( - "reservations.ReservationPurpose", + "tilavarauspalvelu.ReservationPurpose", null=True, on_delete=models.SET_NULL, related_name="application_sections", ) age_group: AgeGroup | None = models.ForeignKey( - "reservations.AgeGroup", + "tilavarauspalvelu.AgeGroup", null=True, on_delete=models.SET_NULL, related_name="application_sections", diff --git a/applications/tasks.py b/applications/tasks.py index 5fd738f3a..86fc36240 100644 --- a/applications/tasks.py +++ b/applications/tasks.py @@ -4,16 +4,20 @@ from django.db import transaction from django.utils.translation import gettext_lazy as _ -from actions.recurring_reservation import ReservationDetails from applications.enums import ApplicantTypeChoice, Weekday from applications.models import Address, AllocatedTimeSlot, Organisation, Person from common.date_utils import local_end_of_day, local_start_of_day from common.utils import translate_for_user from config.celery import app -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice -from reservations.models import RecurringReservation -from reservations.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task -from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.enums import ( + CustomerTypeChoice, + HaukiResourceState, + ReservationStateChoice, + ReservationTypeChoice, +) +from tilavarauspalvelu.models import RecurringReservation +from tilavarauspalvelu.models.recurring_reservation.actions import ReservationDetails +from tilavarauspalvelu.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement from utils.sentry import SentryLogger diff --git a/common/connectors.py b/common/connectors.py index 070cdf607..4598111f0 100644 --- a/common/connectors.py +++ b/common/connectors.py @@ -5,7 +5,6 @@ "ApplicationActionsConnector", "ApplicationRoundActionsConnector", "ApplicationSectionActionsConnector", - "ReservationActionsConnector", "ReservationUnitActionsConnector", "SuitableTimeRangeActionsConnector", ] @@ -77,27 +76,3 @@ def __get__(self, instance, _): from actions.reservation_unit import ReservationUnitActions return ReservationUnitActions(instance) - - -class ReservationActionsConnector: - def __get__(self, instance, _): - _raise_if_accessed_on_class(instance) - from actions.reservation import ReservationActions - - return ReservationActions(instance) - - -class RecurringReservationActionsConnector: - def __get__(self, instance, _): - _raise_if_accessed_on_class(instance) - from actions.recurring_reservation import RecurringReservationActions - - return RecurringReservationActions(instance) - - -class RejectedOccurrenceActionsConnector: - def __get__(self, instance, _): - _raise_if_accessed_on_class(instance) - from actions.rejected_occurrence import RejectedOccurrenceActions - - return RejectedOccurrenceActions(instance) diff --git a/common/management/commands/data_creation/create_caisa.py b/common/management/commands/data_creation/create_caisa.py index 52fbed79a..05b4ccad2 100644 --- a/common/management/commands/data_creation/create_caisa.py +++ b/common/management/commands/data_creation/create_caisa.py @@ -19,13 +19,13 @@ ReservationUnitType, TaxPercentage, ) -from reservations.models import ReservationMetadataSet from tilavarauspalvelu.enums import TermsOfUseTypeChoices from tilavarauspalvelu.models import ( OriginHaukiResource, PaymentAccounting, PaymentMerchant, PaymentProduct, + ReservationMetadataSet, Space, TermsOfUse, Unit, diff --git a/common/management/commands/data_creation/create_misc.py b/common/management/commands/data_creation/create_misc.py index 243a2a434..97ef5c905 100644 --- a/common/management/commands/data_creation/create_misc.py +++ b/common/management/commands/data_creation/create_misc.py @@ -8,7 +8,11 @@ from common.date_utils import DEFAULT_TIMEZONE from common.enums import BannerNotificationLevel, BannerNotificationTarget from common.models import BannerNotification -from reservations.tasks import prune_reservations_task, update_affecting_time_spans_task, update_expired_orders_task +from tilavarauspalvelu.tasks import ( + prune_reservations_task, + update_affecting_time_spans_task, + update_expired_orders_task, +) from .utils import faker_en, faker_fi, faker_sv, with_logs diff --git a/common/management/commands/data_creation/create_reservation_units.py b/common/management/commands/data_creation/create_reservation_units.py index 190abce37..0190c9904 100644 --- a/common/management/commands/data_creation/create_reservation_units.py +++ b/common/management/commands/data_creation/create_reservation_units.py @@ -26,9 +26,16 @@ ReservationUnitType, TaxPercentage, ) -from reservations.models import ReservationMetadataSet from tilavarauspalvelu.enums import TermsOfUseTypeChoices -from tilavarauspalvelu.models import OriginHaukiResource, ReservableTimeSpan, Resource, Service, TermsOfUse, Unit +from tilavarauspalvelu.models import ( + OriginHaukiResource, + ReservableTimeSpan, + ReservationMetadataSet, + Resource, + Service, + TermsOfUse, + Unit, +) from .create_seasonal_booking import _create_application_round_time_slots from .utils import ( diff --git a/common/management/commands/data_creation/create_reservations.py b/common/management/commands/data_creation/create_reservations.py index 947e57e35..0f674d858 100644 --- a/common/management/commands/data_creation/create_reservations.py +++ b/common/management/commands/data_creation/create_reservations.py @@ -9,8 +9,8 @@ from applications.models import City from common.date_utils import local_start_of_day from reservation_units.models import ReservationUnit, ReservationUnitPricing -from reservations.enums import CustomerTypeChoice, ReservationStateChoice -from reservations.models import ( +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice +from tilavarauspalvelu.models import ( AgeGroup, Reservation, ReservationCancelReason, @@ -18,8 +18,8 @@ ReservationMetadataField, ReservationMetadataSet, ReservationPurpose, + User, ) -from tilavarauspalvelu.models import User from .utils import FieldCombination, SetName, faker_fi, weighted_choice, with_logs diff --git a/common/management/commands/data_creation/create_seasonal_booking.py b/common/management/commands/data_creation/create_seasonal_booking.py index b1c16c314..1e1aa2049 100644 --- a/common/management/commands/data_creation/create_seasonal_booking.py +++ b/common/management/commands/data_creation/create_seasonal_booking.py @@ -20,8 +20,7 @@ ) from applications.typing import TimeSlotDB from reservation_units.models import ReservationUnit -from reservations.models import AgeGroup, ReservationPurpose -from tilavarauspalvelu.models import ServiceSector, Unit, User +from tilavarauspalvelu.models import AgeGroup, ReservationPurpose, ServiceSector, Unit, User from .utils import batched, faker_en, faker_fi, faker_sv, get_paragraphs, random_subset, weighted_choice, with_logs diff --git a/common/management/commands/data_creation/main.py b/common/management/commands/data_creation/main.py index e74ac62fa..dda2d4a57 100644 --- a/common/management/commands/data_creation/main.py +++ b/common/management/commands/data_creation/main.py @@ -4,7 +4,7 @@ from django.core.management import call_command from reservation_units.models import ReservationUnitHierarchy -from reservations.models import AffectingTimeSpan +from tilavarauspalvelu.models import AffectingTimeSpan from .create_caisa import _create_caisa from .create_misc import _create_banner_notifications, _create_periodic_tasks diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index 3d22bd2e4..d0b69def0 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -7,38 +7,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: actions/reservation.py -#, python-format -msgid "Reservation for %(name)s" -msgstr "Varaus kohteeseen %(name)s" - -#: actions/reservation.py -msgid "Booking details" -msgstr "Varauksen tiedot" - -#: actions/reservation.py -msgctxt "ical" -msgid "From" -msgstr "Alkamisaika" - -#: actions/reservation.py -msgctxt "ical" -msgid "To" -msgstr "Päättymisaika" - -#: actions/reservation.py -#, python-format -msgid "" -"Manage your booking at Varaamo. You can check the details of your booking " -"and Varaamo's terms of contract and cancellation on the '%(bookings)s' page." -msgstr "" -"Hallitse varaustasi Varaamossa. Voit perua varauksesi ja tarkistaa varauksen " -"tiedot sekä Varaamon sopimus- ja peruutusehdot '%(bookings)s' -sivulla." - -#: actions/reservation.py -msgid "My bookings" -msgstr "Omat Varaukset" - #: applications/admin/address.py #: tilavarauspalvelu/admin/payment_merchant/admin.py msgid "Street address" @@ -111,14 +79,14 @@ msgstr "Varausyksikkövaihtoehto tälle allokoinnille." #: applications/admin/allocated_time_slot/form.py #: applications/admin/application_section/form.py -#: reservations/admin/reservation/admin.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Begin time" msgstr "Aloitusaika" #: applications/admin/allocated_time_slot/form.py #: applications/admin/application_section/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "End time" msgstr "Lopetusaika" @@ -139,8 +107,8 @@ msgid "Search by user's first name, last name or reservation units name" msgstr "Etsi käyttäjän etu- tai sukunimellä tai varausyksikön nimellä" #: applications/admin/application/admin.py -#: reservations/admin/reservation/admin.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Created at" msgstr "Luotu" @@ -148,7 +116,7 @@ msgstr "Luotu" #: applications/admin/application_round/admin.py #: applications/admin/application_section/admin.py #: reservation_units/admin/reservation_unit/admin.py -#: reservations/admin/reservation/admin.py users/admin/user.py +#: tilavarauspalvelu/admin/reservation/admin.py users/admin/user.py msgid "Basic information" msgstr "Perustiedot" @@ -160,7 +128,7 @@ msgstr "Hakija" #: applications/admin/application/admin.py #: applications/admin/application_round/admin.py #: applications/admin/application_section/admin.py -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Time" msgstr "Aika" @@ -175,7 +143,7 @@ msgstr "Nollaa hakemuksen allokoinnit" #: applications/admin/application/admin.py #: applications/admin/application_round/admin.py -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Are you sure?" msgstr "Oletko varma?" @@ -265,17 +233,17 @@ msgid "Billing address" msgstr "Laskutusosoite" #: applications/admin/application/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Home city" msgstr "Kotikunta" #: applications/admin/application/form.py -#: reservations/admin/reservation/admin.py users/admin/user.py +#: tilavarauspalvelu/admin/reservation/admin.py users/admin/user.py msgid "Additional information" msgstr "Lisätiedot" #: applications/admin/application/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Working memo" msgstr "Käsittelymuistio" @@ -384,8 +352,8 @@ msgstr "" #: reservation_units/admin/reservation_unit/form.py #: reservation_units/models/equipment.py reservation_units/models/keyword.py #: reservation_units/models/reservation_unit_type.py -#: reservations/admin/reservation/form.py -#: reservations/models/reservation_metadata.py +#: tilavarauspalvelu/admin/reservation/form.py +#: tilavarauspalvelu/models/reservation_metadata_set/model.py msgid "Name" msgstr "Nimi" @@ -570,12 +538,12 @@ msgstr "Etsi nimellä, hakijan etu- tai sukunimellä" #: applications/admin/application_section/filters.py #: applications/admin/application_section/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Age group" msgstr "Ikäryhmä" #: applications/admin/application_section/filters.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservation purpose" msgstr "Varaustarkoitukset" @@ -665,7 +633,7 @@ msgstr "" "toistokerrat tälle hakemuksen osalle ovat lukittu tai hylätty.
" #: applications/admin/application_section/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Number of persons" msgstr "Henkilömäärä" @@ -1392,12 +1360,12 @@ msgid "Is the reservation unit closed on this weekday?" msgstr "Onko varausyksikkö suljettu tänä viikonpäivänä?" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "SKU" msgstr "SKU" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Description" msgstr "Kuvaus" @@ -1554,12 +1522,12 @@ msgid "Surface area" msgstr "Pinta-ala" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time before" msgstr "Tauko ennen varausta" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time after" msgstr "Tauko varauksen jälkeen" @@ -1600,7 +1568,7 @@ msgid "Publish ends" msgstr "Julkaisu päättyy" #: reservation_units/admin/reservation_unit/form.py -#: reservations/models/reservation_metadata.py +#: tilavarauspalvelu/models/reservation_metadata_set/model.py msgid "Reservation metadata set" msgstr "Varauslomake" @@ -1743,12 +1711,12 @@ msgid "Minimum number of persons" msgstr "Henkilöiden minimimäärä" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time before reservation" msgstr "Tauko ennen varausta" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time after reservation" msgstr "Tauko varauksen jälkeen" @@ -2068,7 +2036,7 @@ msgid "Category" msgstr "Kategoria" #: reservation_units/models/introduction.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "User" msgstr "Käyttäjä" @@ -2141,727 +2109,583 @@ msgstr "Ylin hinta" msgid "Tax percentage" msgstr "Veroprosentti" -#: reservations/admin/reservation/admin.py +#: templates/admin/base.html +msgid "Change password" +msgstr "Vaihda salasana" + +#: templates/admin/base.html templates/admin/hel_login.html +msgid "Log out" +msgstr "Kirjaudu ulos" + +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +#: templates/email/email_tester.html +msgid "Home" +msgstr "Koti" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Deny reservations" +msgstr "Hylkää varauksia" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Number of unpaid reservations to be denied:" +msgstr "Hylättävien ilmaisten varausten määrä:" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Number of paid reservations to be denied:" +msgstr "Hylättävien maksullisten varausten määrä:" + +#: templates/admin/deny_reservation_confirmation.html +msgid "of which can be refunded:" +msgstr "joista voidaan hyvittää:" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Number of ended reservations, which can't be denied:" +msgstr "Valittujen varausten määrä, jota ei voida hylätä:" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Reason for denying the reservations" +msgstr "Syy varauksen hylkäämiselle" + +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +msgid "Yes, I'm sure" +msgstr "Kyllä, olen varma" + +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +msgid "No, take me back" +msgstr "Ei, vie minut takaisin" + +#: templates/admin/reset_allocation_confirmation.html +msgid "Reset allocations" +msgstr "Nollaa allokoinnit" + +#: templates/email/email_tester.html +msgid "Email Template Testing" +msgstr "Email-pohjan testaus" + +#: tilavarauspalvelu/admin/email_template/tester.py +#, python-format +msgid "Test Email '%s' successfully sent." +msgstr "Testisähköposti '%s' lähetetty onnistuneesti." + +#: tilavarauspalvelu/admin/general_role/admin.py +msgid "Search by username, email, first name or last name" +msgstr "Etsi käyttäjänimellä, sähköpostilla, etu- tai sukunimellä" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"OriginHaukiResources with this specific hash never have any opening hours." +msgstr "" +"OriginHaukiResources, joilla on tämä tietty tarkiste, eivät koskaan sisällä " +"aukioloaikoja." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "ID of the resource in Hauki." +msgstr "Resurssin tunniste Aukiolosovelluksesta." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"Hash of the opening hours. Used to determine if the opening hours have " +"changed." +msgstr "" +"Aukioloaikojen tarkiste. Käytetään määrittämään, ovatko aukioloajat " +"muuttuneet." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"All opening hours have been fetched from Hauki up until this date. Opening " +"hours are fetched until the last day of the month two years from now." +msgstr "" +"Kaikki aukioloajat on haettu Aukiolosovelluksesta tähän päivämäärään asti. " +"Aukioloajat haetaan kaksi vuotta eteenpäin." + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "Reservable Time Spans updated." +msgstr "Varattavat aikavälit päivitetty." + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "The Paytrail Merchant ID should be a six-digit number." +msgstr "Kaupan paytrail-tunnuksen tulisi olla kuusinumeroinen." + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Shop ID" +msgstr "Kaupan tunnus" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Business ID" +msgstr "Y-tunnus" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "ZIP code" +msgstr "Postinumero" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Email address" +msgstr "Sähköpostiosoite" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "URL" +msgstr "Verkko-osoite" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Terms of service URL" +msgstr "Käyttöehtojen URL" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Merchant ID" +msgstr "Kauppiastunniste" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Merchant name" +msgstr "Kauppiaan nimi" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Value comes from the Merchant Experience API" +msgstr "Arvo tulee kauppiaskokemusrajapinnasta" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Reservation" +msgstr "Varaus" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Remote order ID" +msgstr "Ulkoinen tilaustunniste" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Payment ID" +msgstr "Maksutunniste" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Refund ID" +msgstr "Maksun palautuksen tunniste" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Payment type" +msgstr "Maksutyyppi" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Payment status" +msgstr "Maksun tila" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Net amount" +msgstr "Nettomäärä" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "VAT amount" +msgstr "ALV-määrä" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Total amount" +msgstr "Kokonaismäärä" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Processed at" +msgstr "Käsitelty" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Language" +msgstr "Kieli" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Reservation user UUID" +msgstr "Varaajan UUID" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Checkout URL" +msgstr "Kassan URL" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Receipt URL" +msgstr "Kuitin URL" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "The reservation associated with this payment order" +msgstr "Varaus, joka liittyy tähän maksutilaan" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "eCommerce order ID" +msgstr "eCommerce-tilaustunniste" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "eCommerce payment ID" +msgstr "eCommerce-maksutunniste" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Available only when order has been refunded" +msgstr "Saatavilla vain, kun maksu on palautettu" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "" +"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation " +"name, or Reservation Unit name" +msgstr "" +"Etsi maksutunnuksella, varauksen tunnuksella, verkkokaupan tunnuksella, " +"varauksen nimellä tai varausyksikön nimellä" + +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Search by Reservation ID or name" msgstr "Etsi varauksen ID:llä tai nimellä" -#: reservations/admin/reservation/admin.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Price" msgstr "Hinta" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Reservee information" msgstr "Varaajan tiedot" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Billing information" msgstr "Laskutustiedot" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "None of the selected reservations can be denied." msgstr "Yhtään valittua varausta ei voida hylätä." -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Are you sure you want deny these reservations?" msgstr "Oletko varma, että haluat hylätä nämä varaukset?" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Selected reservations have been denied." msgstr "Valitut varaukset on hylätty." -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Deny selected reservations without refund" msgstr "Hylkää valitut varaukset ilman hyvitystä" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Deny selected reservations and refund" msgstr "Hylkää valitut varaukset ja hyvitä" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Refund has been initiated for selected reservations." msgstr "Hyvitys on aloitettu valituille varauksille." -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "No reservations with paid orders to refund." msgstr "Ei varauksia hyvitettävillä maksetuilla tilauksilla." -#: reservations/admin/reservation/filters.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Recurring reservation" msgstr "Toistuva varaus" -#: reservations/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/filters.py msgid "Yes" msgstr "Kyllä" -#: reservations/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/filters.py msgid "No" msgstr "Ei" -#: reservations/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/filters.py msgid "Paid reservation" msgstr "Maksullinen varaus" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "State" msgstr "Tila" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservation type" msgstr "Varauksen tyyppi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Cancel details" msgstr "Perumistiedot" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Handling details" msgstr "Käsittelytiedot" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Handled at" msgstr "Käsitelty" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Confirmed at" msgstr "Vahvistettu" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Price net" msgstr "Nettohinta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Non-subsidised price" msgstr "Subventoimaton hinta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Non-subsidised net price" msgstr "Subventoimaton nettohinta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Unit price" msgstr "Yksikköhinta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Tax percentage value" msgstr "Veroprosentti" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Applying free of charge" msgstr "Hakee alennusta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Free of charge reason" msgstr "Alennuksen haun syy" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee ID" msgstr "Varaajan y-tunnus" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee first name" msgstr "Varaajan etunimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee last name" msgstr "Varaajan sukunimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee email" msgstr "Varaajan sähköposti" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee phone" msgstr "Varaajan puhelinnumero" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee organisation name" msgstr "Varaajan organisaation nimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee address street" msgstr "Varaajan katuosoite" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee address city" msgstr "Varaajan kaupunki" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee address zip code" msgstr "Varaajan postinumero" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee is an unregistered association" msgstr "Varaaja on rekisteröimätön yhdistys" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Preferred language of reservee" msgstr "Varaajan kieli" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Type of reservee" msgstr "Varaajan tyyppi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing first name" msgstr "Varauksen maksajan etunimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing last name" msgstr "Varauksen maksajan sukunimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing email" msgstr "Varauksen maksajan sähköposti" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing phone" msgstr "Varauksen maksajan puhelinnumero" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing address street" msgstr "Varauksen maksajan katuosoite" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing address city" msgstr "Varauksen maksajan postitoimipaikka" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing address zip code" msgstr "Varauksen maksajan postinumero" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for deny" msgstr "Syy hylkäämiselle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for cancellation" msgstr "Syy perumiselle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "SKU for this particular reservation" msgstr "Tämän varauksen SKU" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Name of the reservation" msgstr "Varauksen nimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Description of the reservation" msgstr "Varauksen kuvaus" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Number of persons in the reservation" msgstr "Henkilöiden määrä varauksessa" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "State of the reservation" msgstr "Varauksen tila" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Type of reservation" msgstr "Varauksen tyyppi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Details for this reservation's cancellation" msgstr "Lisätiedot varauksen perumiselle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Additional details for denying or approving the reservation" msgstr "Lisätietoja varauksen hylkäämiseen tai hyväksymiseen" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Working memo for staff users" msgstr "Muistio henkilökunnalle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservation begin date and time" msgstr "Varauksen alkamishetki" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservation end date and time" msgstr "Varauksen loppumishetki" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "When this reservation was handled" msgstr "Milloin tämä varaus käsiteltiin" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "When this reservation was confirmed" msgstr "Milloin tämä varaus vahvistettiin" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "When this reservation was created" msgstr "Milloin tämä varaus luotiin" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The price of this particular reservation including VAT" msgstr "Varauksen hinta ALV:n kanssa" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The price of this particular reservation excluding VAT" msgstr "Varauksen hinta ilman ALV:ta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The non subsidised price of this reservation including VAT" msgstr "Subventoimaton hinta ALV:n kanssa" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The non subsidised price of this reservation excluding VAT" msgstr "Subventoimaton hinta ilman ALV:ta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The unit price of this particular reservation" msgstr "Varauksen yksikköhinta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The value of the tax percentage for this particular reservation" msgstr "ALV:n prosenttiosuus tässä varauksessa" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee is applying for a free-of-charge reservation" msgstr "Varaaja hakee alennuskelpoisuutta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for applying for a free-of-charge reservation" msgstr "Syy alennuskelpoisuuden hakemiselle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's business or association identity code" msgstr "Varaaja yrityksen tai yhdistyksen y-tunnus" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's first name" msgstr "Varaajan etunimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's last name" msgstr "Varaajan sukunimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's email address" msgstr "Varaajan sähköpostiosoite" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's phone number" msgstr "Varaajan puhelinnumero" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's organisation name" msgstr "Varaajan organisaation nimi" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's street address" msgstr "Varaajan katuosoite" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's city" msgstr "Varaajan kaupunki" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's zip code" msgstr "Varaajan postinumero" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's preferred language" msgstr "Varaajan suosima kieli" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "User who made the reservation" msgstr "Varauksen tehnyt käyttäjä" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for denying the reservation" msgstr "Syy varauksen hylkäämiselle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for cancelling the reservation" msgstr "Syy varauksen perumiselle" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Purpose of the reservation" msgstr "Varauksen käyttötarkoitus" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's home city" msgstr "Varaajan kotikunta" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Age group of the group or association" msgstr "Ikäryhmä tälle ryhmälle tai yhdistykselle" -#: reservations/admin/reservation_metadata_set.py +#: tilavarauspalvelu/admin/reservation_metadata_set/admin.py msgid "Required fields must be a subset of supported fields" msgstr "Vaadittujen kenttien täytyy olla tuettujen kenttien alijoukko" -#: reservations/enums.py -msgctxt "CustomerType" -msgid "Business" -msgstr "Yritys" +#: tilavarauspalvelu/admin/space/admin.py +msgid "Search by name or unit name" +msgstr "Etsi nimellä tai toimipisteen nimellä" -#: reservations/enums.py -msgctxt "CustomerType" -msgid "Nonprofit" -msgstr "Voittoa tavoittelematon" - -#: reservations/enums.py -msgctxt "CustomerType" -msgid "Individual" -msgstr "Yksityinen" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Created" -msgstr "Luotu" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Cancelled" -msgstr "Peruutettu" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Requires handling" -msgstr "Käsittelemättä" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Waiting for payment" -msgstr "Odottaa maksua" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Confirmed" -msgstr "Hyväksytty" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Denied" -msgstr "Hylätty" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Normal" -msgstr "Normaali" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Blocked" -msgstr "Suljettu" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Staff" -msgstr "Henkilökunta" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Behalf" -msgstr "Puolesta" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Seasonal" -msgstr "Kausi" - -#: reservations/enums.py -msgctxt "ReservationTypeStaffChoice" -msgid "Blocked" -msgstr "Suljettu" - -#: reservations/enums.py -msgctxt "ReservationTypeStaffChoice" -msgid "Staff" -msgstr "Henkilökunta" - -#: reservations/enums.py -msgctxt "ReservationTypeStaffChoice" -msgid "Behalf" -msgstr "Puolesta" - -#: reservations/enums.py -msgctxt "RejectionReadiness" -msgid "Interval not allowed" -msgstr "Aloitusaika ei sallittu" - -#: reservations/enums.py -msgctxt "RejectionReadiness" -msgid "Overlapping reservations" -msgstr "Päällekkäisiä varauksia" - -#: reservations/enums.py -msgctxt "RejectionReadiness" -msgid "Reservation unit closed" -msgstr "Varausyksikkö suljettu" - -#: reservations/models/affecting_time_span.py -msgid "affecting time span" -msgstr "vaikuttava aikaväli" - -#: reservations/models/affecting_time_span.py -msgid "affecting time spans" -msgstr "vaikuttavat aikavälit" - -#: reservations/models/rejected_occurrence.py -msgid "Rejected occurrence" -msgstr "Hylätty ajankohta" - -#: reservations/models/rejected_occurrence.py -msgid "Rejected occurrences" -msgstr "Hylätyt ajankohdat" - -#: reservations/models/reservation.py -msgid "Closed" -msgstr "Suljettu" - -#: reservations/models/reservation_metadata.py -msgid "Field name" -msgstr "Kentän nimi" - -#: reservations/models/reservation_metadata.py -msgid "Reservation metadata field" -msgstr "Varauslomakkeen kenttä" - -#: reservations/models/reservation_metadata.py -msgid "Reservation metadata fields" -msgstr "Varauslomakkeen kentät" - -#: reservations/models/reservation_metadata.py -msgid "Supported fields" -msgstr "Tuetut kentät" - -#: reservations/models/reservation_metadata.py -msgid "Required fields" -msgstr "Vaaditut kentät" - -#: reservations/models/reservation_metadata.py -msgid "Reservation metadata sets" -msgstr "Varauslomakkeet" - -#: templates/admin/base.html -msgid "Change password" -msgstr "Vaihda salasana" - -#: templates/admin/base.html templates/admin/hel_login.html -msgid "Log out" -msgstr "Kirjaudu ulos" - -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -#: templates/email/email_tester.html -msgid "Home" -msgstr "Koti" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Deny reservations" -msgstr "Hylkää varauksia" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Number of unpaid reservations to be denied:" -msgstr "Hylättävien ilmaisten varausten määrä:" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Number of paid reservations to be denied:" -msgstr "Hylättävien maksullisten varausten määrä:" - -#: templates/admin/deny_reservation_confirmation.html -msgid "of which can be refunded:" -msgstr "joista voidaan hyvittää:" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Number of ended reservations, which can't be denied:" -msgstr "Valittujen varausten määrä, jota ei voida hylätä:" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Reason for denying the reservations" -msgstr "Syy varauksen hylkäämiselle" - -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -msgid "Yes, I'm sure" -msgstr "Kyllä, olen varma" - -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -msgid "No, take me back" -msgstr "Ei, vie minut takaisin" - -#: templates/admin/reset_allocation_confirmation.html -msgid "Reset allocations" -msgstr "Nollaa allokoinnit" - -#: templates/email/email_tester.html -msgid "Email Template Testing" -msgstr "Email-pohjan testaus" - -#: tilavarauspalvelu/admin/email_template/tester.py -#, python-format -msgid "Test Email '%s' successfully sent." -msgstr "Testisähköposti '%s' lähetetty onnistuneesti." - -#: tilavarauspalvelu/admin/general_role/admin.py -msgid "Search by username, email, first name or last name" -msgstr "Etsi käyttäjänimellä, sähköpostilla, etu- tai sukunimellä" - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "" -"OriginHaukiResources with this specific hash never have any opening hours." -msgstr "" -"OriginHaukiResources, joilla on tämä tietty tarkiste, eivät koskaan sisällä " -"aukioloaikoja." - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "ID of the resource in Hauki." -msgstr "Resurssin tunniste Aukiolosovelluksesta." - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "" -"Hash of the opening hours. Used to determine if the opening hours have " -"changed." -msgstr "" -"Aukioloaikojen tarkiste. Käytetään määrittämään, ovatko aukioloajat " -"muuttuneet." - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "" -"All opening hours have been fetched from Hauki up until this date. Opening " -"hours are fetched until the last day of the month two years from now." -msgstr "" -"Kaikki aukioloajat on haettu Aukiolosovelluksesta tähän päivämäärään asti. " -"Aukioloajat haetaan kaksi vuotta eteenpäin." - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "Reservable Time Spans updated." -msgstr "Varattavat aikavälit päivitetty." - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "The Paytrail Merchant ID should be a six-digit number." -msgstr "Kaupan paytrail-tunnuksen tulisi olla kuusinumeroinen." - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Shop ID" -msgstr "Kaupan tunnus" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Business ID" -msgstr "Y-tunnus" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "ZIP code" -msgstr "Postinumero" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Email address" -msgstr "Sähköpostiosoite" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "URL" -msgstr "Verkko-osoite" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Terms of service URL" -msgstr "Käyttöehtojen URL" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Merchant ID" -msgstr "Kauppiastunniste" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Merchant name" -msgstr "Kauppiaan nimi" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Value comes from the Merchant Experience API" -msgstr "Arvo tulee kauppiaskokemusrajapinnasta" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Reservation" -msgstr "Varaus" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Remote order ID" -msgstr "Ulkoinen tilaustunniste" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Payment ID" -msgstr "Maksutunniste" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Refund ID" -msgstr "Maksun palautuksen tunniste" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Payment type" -msgstr "Maksutyyppi" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Payment status" -msgstr "Maksun tila" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Net amount" -msgstr "Nettomäärä" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "VAT amount" -msgstr "ALV-määrä" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Total amount" -msgstr "Kokonaismäärä" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Processed at" -msgstr "Käsitelty" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Language" -msgstr "Kieli" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Reservation user UUID" -msgstr "Varaajan UUID" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Checkout URL" -msgstr "Kassan URL" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Receipt URL" -msgstr "Kuitin URL" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "The reservation associated with this payment order" -msgstr "Varaus, joka liittyy tähän maksutilaan" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "eCommerce order ID" -msgstr "eCommerce-tilaustunniste" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "eCommerce payment ID" -msgstr "eCommerce-maksutunniste" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Available only when order has been refunded" -msgstr "Saatavilla vain, kun maksu on palautettu" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "" -"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation " -"name, or Reservation Unit name" -msgstr "" -"Etsi maksutunnuksella, varauksen tunnuksella, verkkokaupan tunnuksella, " -"varauksen nimellä tai varausyksikön nimellä" - -#: tilavarauspalvelu/admin/space/admin.py -msgid "Search by name or unit name" -msgstr "Etsi nimellä tai toimipisteen nimellä" - -#: tilavarauspalvelu/admin/unit/admin.py -msgid "Search by name or TPREK ID" -msgstr "Etsi nimellä tai TPREK ID:llä" +#: tilavarauspalvelu/admin/unit/admin.py +msgid "Search by name or TPREK ID" +msgstr "Etsi nimellä tai TPREK ID:llä" #: tilavarauspalvelu/admin/unit_group/admin.py msgid "Search by name" @@ -3057,6 +2881,114 @@ msgctxt "HaukiResourceState" msgid "Maintenance" msgstr "Siivoustauko/huoltotauko" +#: tilavarauspalvelu/enums.py +msgctxt "CustomerType" +msgid "Business" +msgstr "Yritys" + +#: tilavarauspalvelu/enums.py +msgctxt "CustomerType" +msgid "Nonprofit" +msgstr "Voittoa tavoittelematon" + +#: tilavarauspalvelu/enums.py +msgctxt "CustomerType" +msgid "Individual" +msgstr "Yksityinen" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Created" +msgstr "Luotu" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Cancelled" +msgstr "Peruutettu" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Requires handling" +msgstr "Käsittelemättä" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Waiting for payment" +msgstr "Odottaa maksua" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Confirmed" +msgstr "Hyväksytty" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Denied" +msgstr "Hylätty" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Normal" +msgstr "Normaali" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Blocked" +msgstr "Suljettu" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Staff" +msgstr "Henkilökunta" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Behalf" +msgstr "Puolesta" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Seasonal" +msgstr "Kausi" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationTypeStaffChoice" +msgid "Blocked" +msgstr "Suljettu" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationTypeStaffChoice" +msgid "Staff" +msgstr "Henkilökunta" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationTypeStaffChoice" +msgid "Behalf" +msgstr "Puolesta" + +#: tilavarauspalvelu/enums.py +msgctxt "RejectionReadiness" +msgid "Interval not allowed" +msgstr "Aloitusaika ei sallittu" + +#: tilavarauspalvelu/enums.py +msgctxt "RejectionReadiness" +msgid "Overlapping reservations" +msgstr "Päällekkäisiä varauksia" + +#: tilavarauspalvelu/enums.py +msgctxt "RejectionReadiness" +msgid "Reservation unit closed" +msgstr "Varausyksikkö suljettu" + +#: tilavarauspalvelu/models/affecting_time_span/model.py +msgid "affecting time span" +msgstr "vaikuttava aikaväli" + +#: tilavarauspalvelu/models/affecting_time_span/model.py +msgid "affecting time spans" +msgstr "vaikuttavat aikavälit" + #: tilavarauspalvelu/models/email_template/model.py msgid "Email type" msgstr "Sähköpostin tyyppi" @@ -3121,10 +3053,70 @@ msgstr "Täytyy olla suurempi kuin 0" msgid "Must be the sum of net and vat amounts" msgstr "Täytyy olla netto- ja ALV-määrien summa" +#: tilavarauspalvelu/models/rejected_occurrence/model.py +msgid "Rejected occurrence" +msgstr "Hylätty ajankohta" + +#: tilavarauspalvelu/models/rejected_occurrence/model.py +msgid "Rejected occurrences" +msgstr "Hylätyt ajankohdat" + #: tilavarauspalvelu/models/reservable_time_span/model.py msgid "`start_datetime` must be before `end_datetime`." msgstr "`start_datetime` täytyy olla ennen `end_datetime`." +#: tilavarauspalvelu/models/reservation/actions.py +#, python-format +msgid "Reservation for %(name)s" +msgstr "Varaus kohteeseen %(name)s" + +#: tilavarauspalvelu/models/reservation/actions.py +msgid "Booking details" +msgstr "Varauksen tiedot" + +#: tilavarauspalvelu/models/reservation/actions.py +msgctxt "ical" +msgid "From" +msgstr "Alkamisaika" + +#: tilavarauspalvelu/models/reservation/actions.py +msgctxt "ical" +msgid "To" +msgstr "Päättymisaika" + +#: tilavarauspalvelu/models/reservation/actions.py +#, python-format +msgid "" +"Manage your booking at Varaamo. You can check the details of your booking " +"and Varaamo's terms of contract and cancellation on the '%(bookings)s' page." +msgstr "" +"Hallitse varaustasi Varaamossa. Voit perua varauksesi ja tarkistaa varauksen " +"tiedot sekä Varaamon sopimus- ja peruutusehdot '%(bookings)s' -sivulla." + +#: tilavarauspalvelu/models/reservation/actions.py +msgid "My bookings" +msgstr "Omat Varaukset" + +#: tilavarauspalvelu/models/reservation/model.py +msgid "Closed" +msgstr "Suljettu" + +#: tilavarauspalvelu/models/reservation_metadata_field/model.py +msgid "Field name" +msgstr "Kentän nimi" + +#: tilavarauspalvelu/models/reservation_metadata_field/model.py +msgid "Reservation metadata field" +msgstr "Varauslomakkeen kenttä" + +#: tilavarauspalvelu/models/reservation_metadata_field/model.py +msgid "Reservation metadata fields" +msgstr "Varauslomakkeen kentät" + +#: tilavarauspalvelu/models/reservation_metadata_set/model.py +msgid "Reservation metadata sets" +msgstr "Varauslomakkeet" + #: tilavarauspalvelu/models/terms_of_use/model.py msgctxt "singular" msgid "terms of use" diff --git a/locale/sv/LC_MESSAGES/django.po b/locale/sv/LC_MESSAGES/django.po index efe62559b..35c6527a2 100644 --- a/locale/sv/LC_MESSAGES/django.po +++ b/locale/sv/LC_MESSAGES/django.po @@ -7,38 +7,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: actions/reservation.py -#, python-format -msgid "Reservation for %(name)s" -msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: actions/reservation.py -msgid "Booking details" -msgstr "Uppgifter om bokningen" - -#: actions/reservation.py -msgctxt "ical" -msgid "From" -msgstr "Börjar" - -#: actions/reservation.py -msgctxt "ical" -msgid "To" -msgstr "Slutar" - -#: actions/reservation.py -#, python-format -msgid "" -"Manage your booking at Varaamo. You can check the details of your booking " -"and Varaamo's terms of contract and cancellation on the '%(bookings)s' page." -msgstr "" -"Hantera din bokning på Varaamo. Du kan kontrollera uppgifterna om din " -"bokning samt Varaamos avtals- och avbokningsvillkor på sidan '%(bookings)s'." - -#: actions/reservation.py -msgid "My bookings" -msgstr "Mina bokningar" - #: applications/admin/address.py #: tilavarauspalvelu/admin/payment_merchant/admin.py msgid "Street address" @@ -109,14 +77,14 @@ msgstr "" #: applications/admin/allocated_time_slot/form.py #: applications/admin/application_section/form.py -#: reservations/admin/reservation/admin.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Begin time" msgstr "" #: applications/admin/allocated_time_slot/form.py #: applications/admin/application_section/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "End time" msgstr "" @@ -137,8 +105,8 @@ msgid "Search by user's first name, last name or reservation units name" msgstr "" #: applications/admin/application/admin.py -#: reservations/admin/reservation/admin.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Created at" msgstr "" @@ -146,7 +114,7 @@ msgstr "" #: applications/admin/application_round/admin.py #: applications/admin/application_section/admin.py #: reservation_units/admin/reservation_unit/admin.py -#: reservations/admin/reservation/admin.py users/admin/user.py +#: tilavarauspalvelu/admin/reservation/admin.py users/admin/user.py msgid "Basic information" msgstr "" @@ -158,7 +126,7 @@ msgstr "" #: applications/admin/application/admin.py #: applications/admin/application_round/admin.py #: applications/admin/application_section/admin.py -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Time" msgstr "" @@ -173,7 +141,7 @@ msgstr "" #: applications/admin/application/admin.py #: applications/admin/application_round/admin.py -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Are you sure?" msgstr "" @@ -258,17 +226,17 @@ msgid "Billing address" msgstr "" #: applications/admin/application/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Home city" msgstr "" #: applications/admin/application/form.py -#: reservations/admin/reservation/admin.py users/admin/user.py +#: tilavarauspalvelu/admin/reservation/admin.py users/admin/user.py msgid "Additional information" msgstr "" #: applications/admin/application/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Working memo" msgstr "" @@ -373,8 +341,8 @@ msgstr "" #: reservation_units/admin/reservation_unit/form.py #: reservation_units/models/equipment.py reservation_units/models/keyword.py #: reservation_units/models/reservation_unit_type.py -#: reservations/admin/reservation/form.py -#: reservations/models/reservation_metadata.py +#: tilavarauspalvelu/admin/reservation/form.py +#: tilavarauspalvelu/models/reservation_metadata_set/model.py msgid "Name" msgstr "" @@ -553,12 +521,12 @@ msgstr "" #: applications/admin/application_section/filters.py #: applications/admin/application_section/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Age group" msgstr "" #: applications/admin/application_section/filters.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservation purpose" msgstr "" @@ -639,7 +607,7 @@ msgid "" msgstr "" #: applications/admin/application_section/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Number of persons" msgstr "" @@ -1352,12 +1320,12 @@ msgid "Is the reservation unit closed on this weekday?" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "SKU" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Description" msgstr "" @@ -1514,12 +1482,12 @@ msgid "Surface area" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time before" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time after" msgstr "" @@ -1560,7 +1528,7 @@ msgid "Publish ends" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/models/reservation_metadata.py +#: tilavarauspalvelu/models/reservation_metadata_set/model.py msgid "Reservation metadata set" msgstr "" @@ -1703,12 +1671,12 @@ msgid "Minimum number of persons" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time before reservation" msgstr "" #: reservation_units/admin/reservation_unit/form.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Buffer time after reservation" msgstr "" @@ -2014,7 +1982,7 @@ msgid "Category" msgstr "" #: reservation_units/models/introduction.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "User" msgstr "" @@ -2091,755 +2059,609 @@ msgstr "" msgid "Tax percentage" msgstr "" -#: reservations/admin/reservation/admin.py +#: templates/admin/base.html +msgid "Change password" +msgstr "" + +#: templates/admin/base.html templates/admin/hel_login.html +msgid "Log out" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +#: templates/email/email_tester.html +msgid "Home" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Deny reservations" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Number of unpaid reservations to be denied:" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Number of paid reservations to be denied:" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +msgid "of which can be refunded:" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Number of ended reservations, which can't be denied:" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +msgid "Reason for denying the reservations" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +msgid "Yes, I'm sure" +msgstr "" + +#: templates/admin/deny_reservation_confirmation.html +#: templates/admin/reset_allocation_confirmation.html +msgid "No, take me back" +msgstr "" + +#: templates/admin/reset_allocation_confirmation.html +msgid "Reset allocations" +msgstr "" + +#: templates/email/email_tester.html +msgid "Email Template Testing" +msgstr "" + +#: tilavarauspalvelu/admin/email_template/tester.py +#, python-format +msgid "Test Email '%s' successfully sent." +msgstr "" + +#: tilavarauspalvelu/admin/general_role/admin.py +msgid "Search by username, email, first name or last name" +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"OriginHaukiResources with this specific hash never have any opening hours." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "ID of the resource in Hauki." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"Hash of the opening hours. Used to determine if the opening hours have " +"changed." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "" +"All opening hours have been fetched from Hauki up until this date. Opening " +"hours are fetched until the last day of the month two years from now." +msgstr "" + +#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py +msgid "Reservable Time Spans updated." +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "The Paytrail Merchant ID should be a six-digit number." +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Shop ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Business ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "ZIP code" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Email address" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "URL" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Terms of service URL" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Merchant ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Merchant name" +msgstr "" + +#: tilavarauspalvelu/admin/payment_merchant/admin.py +msgid "Value comes from the Merchant Experience API" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +#, fuzzy +#| msgid "Reservation for %(name)s" +msgid "Reservation" +msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Remote order ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Payment ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Refund ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Payment type" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Payment status" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Net amount" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "VAT amount" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Total amount" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Processed at" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Language" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +#, fuzzy +#| msgid "Reservation for %(name)s" +msgid "Reservation user UUID" +msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Checkout URL" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Receipt URL" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "The reservation associated with this payment order" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "eCommerce order ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "eCommerce payment ID" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "Available only when order has been refunded" +msgstr "" + +#: tilavarauspalvelu/admin/payment_order/admin.py +msgid "" +"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation " +"name, or Reservation Unit name" +msgstr "" + +#: tilavarauspalvelu/admin/reservation/admin.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Search by Reservation ID or name" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/admin.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Price" msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Reservee information" msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Billing information" msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "None of the selected reservations can be denied." msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Are you sure you want deny these reservations?" msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Selected reservations have been denied." msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Deny selected reservations without refund" msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Deny selected reservations and refund" msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "Refund has been initiated for selected reservations." msgstr "" -#: reservations/admin/reservation/admin.py +#: tilavarauspalvelu/admin/reservation/admin.py msgid "No reservations with paid orders to refund." msgstr "" -#: reservations/admin/reservation/filters.py -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Recurring reservation" msgstr "" -#: reservations/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/filters.py msgid "Yes" msgstr "" -#: reservations/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/filters.py msgid "No" msgstr "" -#: reservations/admin/reservation/filters.py +#: tilavarauspalvelu/admin/reservation/filters.py msgid "Paid reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "State" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservation type" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Booking details" msgid "Cancel details" msgstr "Uppgifter om bokningen" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Booking details" msgid "Handling details" msgstr "Uppgifter om bokningen" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Handled at" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Confirmed at" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Price net" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Non-subsidised price" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Non-subsidised net price" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Unit price" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Tax percentage value" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Applying free of charge" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Free of charge reason" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee ID" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservee first name" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservee last name" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee email" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee phone" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservee organisation name" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee address street" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee address city" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee address zip code" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee is an unregistered association" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Preferred language of reservee" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Type of reservee" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing first name" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing last name" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Booking details" msgid "Billing email" msgstr "Uppgifter om bokningen" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing phone" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing address street" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing address city" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Billing address zip code" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for deny" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for cancellation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "SKU for this particular reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Name of the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Description of the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Number of persons in the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "State of the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Type of reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Details for this reservation's cancellation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Additional details for denying or approving the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Working memo for staff users" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservation begin date and time" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservation end date and time" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "When this reservation was handled" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "When this reservation was confirmed" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "When this reservation was created" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The price of this particular reservation including VAT" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The price of this particular reservation excluding VAT" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The non subsidised price of this reservation including VAT" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The non subsidised price of this reservation excluding VAT" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The unit price of this particular reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "The value of the tax percentage for this particular reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee is applying for a free-of-charge reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for applying for a free-of-charge reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's business or association identity code" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservee's first name" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's last name" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's email address" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's phone number" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py #, fuzzy #| msgid "Reservation for %(name)s" msgid "Reservee's organisation name" msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's street address" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's city" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's zip code" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's preferred language" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "User who made the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for denying the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reason for cancelling the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Purpose of the reservation" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Reservee's home city" msgstr "" -#: reservations/admin/reservation/form.py +#: tilavarauspalvelu/admin/reservation/form.py msgid "Age group of the group or association" msgstr "" -#: reservations/admin/reservation_metadata_set.py +#: tilavarauspalvelu/admin/reservation_metadata_set/admin.py msgid "Required fields must be a subset of supported fields" msgstr "" -#: reservations/enums.py -msgctxt "CustomerType" -msgid "Business" +#: tilavarauspalvelu/admin/space/admin.py +msgid "Search by name or unit name" msgstr "" -#: reservations/enums.py -msgctxt "CustomerType" -msgid "Nonprofit" +#: tilavarauspalvelu/admin/unit/admin.py +msgid "Search by name or TPREK ID" msgstr "" -#: reservations/enums.py -msgctxt "CustomerType" -msgid "Individual" +#: tilavarauspalvelu/admin/unit_group/admin.py +msgid "Search by name" msgstr "" -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Created" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Cancelled" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Requires handling" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Waiting for payment" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Confirmed" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationState" -msgid "Denied" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Normal" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Blocked" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Staff" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Behalf" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationType" -msgid "Seasonal" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationTypeStaffChoice" -msgid "Blocked" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationTypeStaffChoice" -msgid "Staff" -msgstr "" - -#: reservations/enums.py -msgctxt "ReservationTypeStaffChoice" -msgid "Behalf" -msgstr "" - -#: reservations/enums.py -msgctxt "RejectionReadiness" -msgid "Interval not allowed" -msgstr "" - -#: reservations/enums.py -msgctxt "RejectionReadiness" -msgid "Overlapping reservations" -msgstr "" - -#: reservations/enums.py -msgctxt "RejectionReadiness" -msgid "Reservation unit closed" -msgstr "" - -#: reservations/models/affecting_time_span.py -msgid "affecting time span" -msgstr "" - -#: reservations/models/affecting_time_span.py -msgid "affecting time spans" -msgstr "" - -#: reservations/models/rejected_occurrence.py -msgid "Rejected occurrence" -msgstr "" - -#: reservations/models/rejected_occurrence.py -msgid "Rejected occurrences" -msgstr "" - -#: reservations/models/reservation.py -msgid "Closed" -msgstr "" - -#: reservations/models/reservation_metadata.py -msgid "Field name" -msgstr "" - -#: reservations/models/reservation_metadata.py -msgid "Reservation metadata field" -msgstr "" - -#: reservations/models/reservation_metadata.py -msgid "Reservation metadata fields" -msgstr "" - -#: reservations/models/reservation_metadata.py -msgid "Supported fields" -msgstr "" - -#: reservations/models/reservation_metadata.py -msgid "Required fields" -msgstr "" - -#: reservations/models/reservation_metadata.py -#, fuzzy -#| msgid "Reservation for %(name)s" -msgid "Reservation metadata sets" -msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: templates/admin/base.html -msgid "Change password" -msgstr "" - -#: templates/admin/base.html templates/admin/hel_login.html -msgid "Log out" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -#: templates/email/email_tester.html -msgid "Home" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Deny reservations" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Number of unpaid reservations to be denied:" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Number of paid reservations to be denied:" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -msgid "of which can be refunded:" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Number of ended reservations, which can't be denied:" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -msgid "Reason for denying the reservations" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -msgid "Yes, I'm sure" -msgstr "" - -#: templates/admin/deny_reservation_confirmation.html -#: templates/admin/reset_allocation_confirmation.html -msgid "No, take me back" -msgstr "" - -#: templates/admin/reset_allocation_confirmation.html -msgid "Reset allocations" -msgstr "" - -#: templates/email/email_tester.html -msgid "Email Template Testing" -msgstr "" - -#: tilavarauspalvelu/admin/email_template/tester.py -#, python-format -msgid "Test Email '%s' successfully sent." -msgstr "" - -#: tilavarauspalvelu/admin/general_role/admin.py -msgid "Search by username, email, first name or last name" -msgstr "" - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "" -"OriginHaukiResources with this specific hash never have any opening hours." -msgstr "" - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "ID of the resource in Hauki." -msgstr "" - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "" -"Hash of the opening hours. Used to determine if the opening hours have " -"changed." -msgstr "" - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "" -"All opening hours have been fetched from Hauki up until this date. Opening " -"hours are fetched until the last day of the month two years from now." -msgstr "" - -#: tilavarauspalvelu/admin/origin_hauki_resource/admin.py -msgid "Reservable Time Spans updated." -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "The Paytrail Merchant ID should be a six-digit number." -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Shop ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Business ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "ZIP code" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Email address" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "URL" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Terms of service URL" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Merchant ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Merchant name" -msgstr "" - -#: tilavarauspalvelu/admin/payment_merchant/admin.py -msgid "Value comes from the Merchant Experience API" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -#, fuzzy -#| msgid "Reservation for %(name)s" -msgid "Reservation" -msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Remote order ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Payment ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Refund ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Payment type" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Payment status" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Net amount" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "VAT amount" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Total amount" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Processed at" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Language" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -#, fuzzy -#| msgid "Reservation for %(name)s" -msgid "Reservation user UUID" -msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Checkout URL" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Receipt URL" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "The reservation associated with this payment order" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "eCommerce order ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "eCommerce payment ID" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "Available only when order has been refunded" -msgstr "" - -#: tilavarauspalvelu/admin/payment_order/admin.py -msgid "" -"Search by Payment Order ID, Reservation ID, Remote Order ID, Reservation " -"name, or Reservation Unit name" -msgstr "" - -#: tilavarauspalvelu/admin/space/admin.py -msgid "Search by name or unit name" -msgstr "" - -#: tilavarauspalvelu/admin/unit/admin.py -msgid "Search by name or TPREK ID" -msgstr "" - -#: tilavarauspalvelu/admin/unit_group/admin.py -msgid "Search by name" -msgstr "" - -#: tilavarauspalvelu/admin/unit_role/admin.py -msgid "" -"Search by user's username, email, first name, last name, unit or unit group" +#: tilavarauspalvelu/admin/unit_role/admin.py +msgid "" +"Search by user's username, email, first name, last name, unit or unit group" msgstr "" #: tilavarauspalvelu/api/gdpr/views.py @@ -3023,6 +2845,114 @@ msgctxt "HaukiResourceState" msgid "Maintenance" msgstr "" +#: tilavarauspalvelu/enums.py +msgctxt "CustomerType" +msgid "Business" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "CustomerType" +msgid "Nonprofit" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "CustomerType" +msgid "Individual" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Created" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Cancelled" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Requires handling" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Waiting for payment" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Confirmed" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationState" +msgid "Denied" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Normal" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Blocked" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Staff" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Behalf" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationType" +msgid "Seasonal" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationTypeStaffChoice" +msgid "Blocked" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationTypeStaffChoice" +msgid "Staff" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "ReservationTypeStaffChoice" +msgid "Behalf" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "RejectionReadiness" +msgid "Interval not allowed" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "RejectionReadiness" +msgid "Overlapping reservations" +msgstr "" + +#: tilavarauspalvelu/enums.py +msgctxt "RejectionReadiness" +msgid "Reservation unit closed" +msgstr "" + +#: tilavarauspalvelu/models/affecting_time_span/model.py +msgid "affecting time span" +msgstr "" + +#: tilavarauspalvelu/models/affecting_time_span/model.py +msgid "affecting time spans" +msgstr "" + #: tilavarauspalvelu/models/email_template/model.py msgid "Email type" msgstr "" @@ -3081,10 +3011,72 @@ msgstr "" msgid "Must be the sum of net and vat amounts" msgstr "" +#: tilavarauspalvelu/models/rejected_occurrence/model.py +msgid "Rejected occurrence" +msgstr "" + +#: tilavarauspalvelu/models/rejected_occurrence/model.py +msgid "Rejected occurrences" +msgstr "" + #: tilavarauspalvelu/models/reservable_time_span/model.py msgid "`start_datetime` must be before `end_datetime`." msgstr "" +#: tilavarauspalvelu/models/reservation/actions.py +#, python-format +msgid "Reservation for %(name)s" +msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: tilavarauspalvelu/models/reservation/actions.py +msgid "Booking details" +msgstr "Uppgifter om bokningen" + +#: tilavarauspalvelu/models/reservation/actions.py +msgctxt "ical" +msgid "From" +msgstr "Börjar" + +#: tilavarauspalvelu/models/reservation/actions.py +msgctxt "ical" +msgid "To" +msgstr "Slutar" + +#: tilavarauspalvelu/models/reservation/actions.py +#, python-format +msgid "" +"Manage your booking at Varaamo. You can check the details of your booking " +"and Varaamo's terms of contract and cancellation on the '%(bookings)s' page." +msgstr "" +"Hantera din bokning på Varaamo. Du kan kontrollera uppgifterna om din " +"bokning samt Varaamos avtals- och avbokningsvillkor på sidan '%(bookings)s'." + +#: tilavarauspalvelu/models/reservation/actions.py +msgid "My bookings" +msgstr "Mina bokningar" + +#: tilavarauspalvelu/models/reservation/model.py +msgid "Closed" +msgstr "" + +#: tilavarauspalvelu/models/reservation_metadata_field/model.py +msgid "Field name" +msgstr "" + +#: tilavarauspalvelu/models/reservation_metadata_field/model.py +msgid "Reservation metadata field" +msgstr "" + +#: tilavarauspalvelu/models/reservation_metadata_field/model.py +msgid "Reservation metadata fields" +msgstr "" + +#: tilavarauspalvelu/models/reservation_metadata_set/model.py +#, fuzzy +#| msgid "Reservation for %(name)s" +msgid "Reservation metadata sets" +msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" + #: tilavarauspalvelu/models/terms_of_use/model.py msgctxt "singular" msgid "terms of use" diff --git a/reservation_units/migrations/0111_alter_reservationunit_metadata_set.py b/reservation_units/migrations/0111_alter_reservationunit_metadata_set.py new file mode 100644 index 000000000..7d5a07bed --- /dev/null +++ b/reservation_units/migrations/0111_alter_reservationunit_metadata_set.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.1 on 2024-09-26 14:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("reservation_units", "0110_alter_reservationunit_origin_hauki_resource"), + ("tilavarauspalvelu", "0010_migrate_reservations"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name="reservationunit", + name="metadata_set", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_units", + to="tilavarauspalvelu.reservationmetadataset", + ), + ), + ], + database_operations=[], + ), + ] diff --git a/reservation_units/models/reservation_unit.py b/reservation_units/models/reservation_unit.py index 4f2a3f510..8663c023d 100644 --- a/reservation_units/models/reservation_unit.py +++ b/reservation_units/models/reservation_unit.py @@ -26,12 +26,12 @@ if TYPE_CHECKING: from reservation_units.models import ReservationUnitCancellationRule, ReservationUnitType - from reservations.models import ReservationMetadataSet from tilavarauspalvelu.models import ( OriginHaukiResource, PaymentAccounting, PaymentMerchant, PaymentProduct, + ReservationMetadataSet, TermsOfUse, Unit, ) @@ -146,7 +146,7 @@ class ReservationUnit(SearchDocumentMixin, models.Model): on_delete=models.PROTECT, ) metadata_set: ReservationMetadataSet | None = models.ForeignKey( - "reservations.ReservationMetadataSet", + "tilavarauspalvelu.ReservationMetadataSet", related_name="reservation_units", null=True, blank=True, diff --git a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py index 35e887f00..eeb6c6f95 100644 --- a/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py +++ b/reservation_units/utils/first_reservable_time_helper/first_reservable_time_helper.py @@ -17,8 +17,7 @@ from reservation_units.utils.first_reservable_time_helper.first_reservable_time_reservation_unit_helper import ( ReservationUnitFirstReservableTimeHelper, ) -from reservations.models import AffectingTimeSpan -from tilavarauspalvelu.models import ReservableTimeSpan +from tilavarauspalvelu.models import AffectingTimeSpan, ReservableTimeSpan from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement from tilavarauspalvelu.utils.opening_hours.time_span_element_utils import merge_overlapping_time_span_elements diff --git a/reservations/admin/__init__.py b/reservations/admin/__init__.py deleted file mode 100644 index d9920ff92..000000000 --- a/reservations/admin/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from .ability_group import AbilityGroupAdmin -from .age_group import AgeGroupAdmin -from .recurring_reservation import RecurringReservationAdmin -from .reservation.admin import ReservationAdmin -from .reservation_cancel_reason import ReservationCancelReasonAdmin -from .reservation_deny_reason import ReservationDenyReasonAdmin -from .reservation_metadata_field import ReservationMetadataFieldAdmin -from .reservation_metadata_set import ReservationMetadataSetAdmin -from .reservation_purpose import ReservationPurposeAdmin -from .reservation_statistics import ReservationStatisticsAdmin - -__all__ = [ - "AbilityGroupAdmin", - "AgeGroupAdmin", - "RecurringReservationAdmin", - "ReservationAdmin", - "ReservationCancelReasonAdmin", - "ReservationDenyReasonAdmin", - "ReservationMetadataFieldAdmin", - "ReservationMetadataSetAdmin", - "ReservationPurposeAdmin", - "ReservationStatisticsAdmin", -] diff --git a/reservations/admin/age_group.py b/reservations/admin/age_group.py deleted file mode 100644 index ffd60855a..000000000 --- a/reservations/admin/age_group.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.contrib import admin - -from reservations.models import AgeGroup - -__all__ = [ - "AgeGroupAdmin", -] - - -@admin.register(AgeGroup) -class AgeGroupAdmin(admin.ModelAdmin): - pass diff --git a/reservations/admin/recurring_reservation.py b/reservations/admin/recurring_reservation.py deleted file mode 100644 index fb28433eb..000000000 --- a/reservations/admin/recurring_reservation.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib import admin - -from reservations.models import RecurringReservation, Reservation - -__all__ = [ - "RecurringReservationAdmin", -] - - -class ReservationInline(admin.TabularInline): - model = Reservation - extra = 0 - max_num = 0 - show_change_link = True - can_delete = False - fields = [ - "id", - "name", - "begin", - "end", - "state", - "type", - "price", - "price_net", - "unit_price", - ] - readonly_fields = fields - - -@admin.register(RecurringReservation) -class RecurringReservationAdmin(admin.ModelAdmin): - # List - list_display = [ - "name", - "reservation_unit", - "allocated_time_slot", - "begin_date", - "end_date", - "recurrence_in_days", - ] - - # Form - inlines = [ReservationInline] diff --git a/reservations/admin/reservation/__init__.py b/reservations/admin/reservation/__init__.py deleted file mode 100644 index a47475e80..000000000 --- a/reservations/admin/reservation/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .admin import ReservationAdmin - -__all__ = [ - "ReservationAdmin", -] diff --git a/reservations/admin/reservation/admin.py b/reservations/admin/reservation/admin.py deleted file mode 100644 index a16331130..000000000 --- a/reservations/admin/reservation/admin.py +++ /dev/null @@ -1,322 +0,0 @@ -from decimal import Decimal -from typing import Any - -from django.contrib import admin, messages -from django.contrib.admin import helpers -from django.db import models -from django.db.models import QuerySet -from django.template.response import TemplateResponse -from django.utils.translation import gettext_lazy as _ -from more_admin_filters import MultiSelectFilter -from more_admin_filters.filters import MultiSelectRelatedOnlyDropdownFilter -from rangefilter.filters import DateRangeFilterBuilder - -from common.date_utils import local_datetime -from common.typing import WSGIRequest -from reservations.admin.reservation.filters import PaidReservationListFilter, RecurringReservationListFilter -from reservations.admin.reservation.form import ReservationAdminForm -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation, ReservationDenyReason -from reservations.tasks import refund_paid_reservation_task -from tilavarauspalvelu.enums import OrderStatus -from tilavarauspalvelu.models import PaymentOrder -from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender - -__all__ = [ - "ReservationAdmin", -] - - -class PaymentOrderInline(admin.TabularInline): - model = PaymentOrder - extra = 0 - show_change_link = True - can_delete = False - fields = [ - "id", - "payment_type", - "status", - "price_total", - ] - readonly_fields = fields - - -@admin.register(Reservation) -class ReservationAdmin(admin.ModelAdmin): - # Functions - actions = [ - "deny_reservations_without_refund", - "deny_reservations_with_refund", - ] - search_fields = [ - # 'id' handled separately in `get_search_results()` - "name", - ] - search_help_text = _("Search by Reservation ID or name") - - # List - list_display = [ - "id", - "name", - "type", - "state", - "begin", - "reservation_units", - ] - list_filter = [ - ("created_at", DateRangeFilterBuilder(title=_("Created at"))), - ("begin", DateRangeFilterBuilder(title=_("Begin time"))), - ("type", MultiSelectFilter), - ("state", MultiSelectFilter), - RecurringReservationListFilter, - PaidReservationListFilter, - ("reservation_unit__unit", MultiSelectRelatedOnlyDropdownFilter), - ("reservation_unit", MultiSelectRelatedOnlyDropdownFilter), - ] - - # Form - form = ReservationAdminForm - fieldsets = [ - [ - _("Basic information"), - { - "fields": [ - "id", - "sku", - "name", - "description", - "num_persons", - "state", - "type", - "cancel_details", - "handling_details", - "working_memo", - ], - }, - ], - [ - _("Time"), - { - "fields": [ - "begin", - "end", - "buffer_time_before", - "buffer_time_after", - "handled_at", - "confirmed_at", - "created_at", - ], - }, - ], - [ - _("Price"), - { - "fields": [ - "price", - "price_net", - "non_subsidised_price", - "non_subsidised_price_net", - "unit_price", - "tax_percentage_value", - "applying_for_free_of_charge", - "free_of_charge_reason", - ], - }, - ], - [ - _("Reservee information"), - { - "fields": [ - "reservee_id", - "reservee_first_name", - "reservee_last_name", - "reservee_email", - "reservee_phone", - "reservee_organisation_name", - "reservee_address_street", - "reservee_address_city", - "reservee_address_zip", - "reservee_is_unregistered_association", - "reservee_language", - "reservee_type", - ], - }, - ], - [ - _("Billing information"), - { - "fields": [ - "billing_first_name", - "billing_last_name", - "billing_email", - "billing_phone", - "billing_address_street", - "billing_address_city", - "billing_address_zip", - ], - }, - ], - [ - _("Additional information"), - { - "fields": [ - "user", - "recurring_reservation", - "deny_reason", - "cancel_reason", - "purpose", - "home_city", - "age_group", - ], - }, - ], - ] - readonly_fields = [ - "id", - "handled_at", - "confirmed_at", - "created_at", - "price_net", - "non_subsidised_price_net", - ] - inlines = [PaymentOrderInline] - - def get_queryset(self, request): - return super().get_queryset(request).prefetch_related("reservation_unit") - - def get_search_results( - self, - request: WSGIRequest, - queryset: models.QuerySet, - search_term: Any, - ) -> tuple[models.QuerySet, bool]: - queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) - - if str(search_term).isdigit(): - queryset |= self.model.objects.filter(id__exact=int(search_term)) - - return queryset, may_have_duplicates - - @admin.display(ordering="reservation_unit__name") - def reservation_units(self, obj: Reservation) -> str: - return ", ".join([str(reservation_unit) for reservation_unit in obj.reservation_unit.all()]) - - def price_net(self, obj: Reservation) -> Decimal: - return obj.price_net - - def non_subsidised_price_net(self, obj: Reservation) -> Decimal: - return obj.non_subsidised_price_net - - def _deny_reservations_action_confirmation_page( - self, - request: WSGIRequest, - queryset: QuerySet[Reservation], - action_name: str, - ) -> TemplateResponse | None: - if not queryset.exists(): - msg = _("None of the selected reservations can be denied.") - self.message_user(request, msg, level=messages.ERROR) - return None - - queryset = queryset.filter(state__in=ReservationStateChoice.states_that_can_change_to_deny) - queryset_ended_reservation_count = queryset.filter(end__lt=local_datetime()).count() - - queryset = queryset.filter(end__gte=local_datetime()) - queryset_unpaid_reservation_count = queryset.filter(price=0).count() - queryset_paid_reservation_count = queryset.filter(price__gt=0).count() - queryset_refundable_reservation_count = queryset.filter( - price__gt=0, - payment_order__isnull=False, - payment_order__status=OrderStatus.PAID, - payment_order__refund_id__isnull=True, - ).count() - - deny_reasons = ReservationDenyReason.objects.all().order_by("reason") - - context = { - **self.admin_site.each_context(request), - "title": _("Are you sure?"), - "subtitle": _("Are you sure you want deny these reservations?"), - "queryset": queryset, - "queryset_unpaid_reservation_count": queryset_unpaid_reservation_count, - "queryset_paid_reservation_count": queryset_paid_reservation_count, - "queryset_ended_reservation_count": queryset_ended_reservation_count, - "queryset_refundable_reservation_count": queryset_refundable_reservation_count, - "deny_reasons": deny_reasons, - "opts": self.model._meta, - "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME, - "media": self.media, - "action_name": action_name, - } - request.current_app = self.admin_site.name - return TemplateResponse(request, "admin/deny_reservation_confirmation.html", context) - - def _deny_reservations_action_set_denied(self, request: WSGIRequest, queryset: QuerySet[Reservation]) -> None: - deny_reason = request.POST.get("deny_reason") - queryset.filter( - state__in=ReservationStateChoice.states_that_can_change_to_deny, - end__gte=local_datetime(), - ).update( - state=ReservationStateChoice.DENIED, - handled_at=local_datetime(), - deny_reason=deny_reason, - ) - - msg = _("Selected reservations have been denied.") - self.message_user(request, msg, level=messages.INFO) - - for reservation in queryset: - ReservationEmailNotificationSender.send_deny_email(reservation=reservation) - - @admin.action(description=_("Deny selected reservations without refund")) - def deny_reservations_without_refund( - self, - request: WSGIRequest, - queryset: QuerySet[Reservation], - ) -> TemplateResponse | None: - # Confirmation page - if not request.POST.get("confirmed"): - return self._deny_reservations_action_confirmation_page( - request=request, - queryset=queryset, - action_name="deny_reservations_without_refund", - ) - - # Set reservations as denied - self._deny_reservations_action_set_denied(request=request, queryset=queryset) - return None - - @admin.action(description=_("Deny selected reservations and refund")) - def deny_reservations_with_refund( - self, - request: WSGIRequest, - queryset: QuerySet[Reservation], - ) -> TemplateResponse | None: - # Confirmation page - if not request.POST.get("confirmed"): - return self._deny_reservations_action_confirmation_page( - request=request, - queryset=queryset, - action_name="deny_reservations_with_refund", - ) - - # Set reservations as denied - self._deny_reservations_action_set_denied(request=request, queryset=queryset) - - # Refund paid reservations - refund_queryset = queryset.filter( - state=ReservationStateChoice.DENIED, - price__gt=0, - payment_order__isnull=False, - payment_order__status=OrderStatus.PAID, - payment_order__refund_id__isnull=True, - ) - for reservation in refund_queryset: - refund_paid_reservation_task.delay(reservation.pk) - - if refund_queryset.count(): - msg = _("Refund has been initiated for selected reservations.") + f" ({refund_queryset.count()})" - else: - msg = _("No reservations with paid orders to refund.") - self.message_user(request, msg, level=messages.INFO) - return None diff --git a/reservations/admin/reservation_purpose.py b/reservations/admin/reservation_purpose.py deleted file mode 100644 index e7d54d18a..000000000 --- a/reservations/admin/reservation_purpose.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.contrib import admin -from modeltranslation.admin import TranslationAdmin - -from reservations.models import ReservationPurpose - -__all__ = [ - "ReservationPurposeAdmin", -] - - -@admin.register(ReservationPurpose) -class ReservationPurposeAdmin(TranslationAdmin): - pass diff --git a/reservations/admin/reservation_statistics.py b/reservations/admin/reservation_statistics.py deleted file mode 100644 index a03e76918..000000000 --- a/reservations/admin/reservation_statistics.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.contrib import admin -from import_export.admin import ExportMixin -from import_export.formats.base_formats import CSV -from rangefilter.filters import DateRangeFilter - -from reservations.models import ReservationStatistic - -__all__ = [ - "ReservationStatisticsAdmin", -] - - -@admin.register(ReservationStatistic) -class ReservationStatisticsAdmin(ExportMixin, admin.ModelAdmin): - # Functions - formats = [CSV] - - # List - list_filter = ( - ("reservation_created_at", DateRangeFilter), - ("begin", DateRangeFilter), - ) diff --git a/reservations/apps.py b/reservations/apps.py deleted file mode 100644 index e13489b85..000000000 --- a/reservations/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig - - -class ReservationsConfig(AppConfig): - name = "reservations" - - def ready(self) -> None: - import reservations.signals # noqa: F401 diff --git a/reservations/enums.py b/reservations/enums.py deleted file mode 100644 index e1181e268..000000000 --- a/reservations/enums.py +++ /dev/null @@ -1,179 +0,0 @@ -from __future__ import annotations - -from enum import StrEnum - -from django.conf import settings -from django.db import models -from django.utils.functional import classproperty -from django.utils.translation import pgettext_lazy - -__all__ = [ - "CustomerTypeChoice", - "ReservationStateChoice", - "ReservationTypeChoice", - "ReservationTypeStaffChoice", -] - - -class CustomerTypeChoice(models.TextChoices): - BUSINESS = "BUSINESS", pgettext_lazy("CustomerType", "Business") - NONPROFIT = "NONPROFIT", pgettext_lazy("CustomerType", "Nonprofit") - INDIVIDUAL = "INDIVIDUAL", pgettext_lazy("CustomerType", "Individual") - - @classproperty - def organisation(self) -> list[str]: - return [ # type: ignore[return-value] - CustomerTypeChoice.BUSINESS.value, - CustomerTypeChoice.NONPROFIT.value, - ] - - -class ReservationStateChoice(models.TextChoices): - CREATED = "CREATED", pgettext_lazy("ReservationState", "Created") - CANCELLED = "CANCELLED", pgettext_lazy("ReservationState", "Cancelled") - REQUIRES_HANDLING = "REQUIRES_HANDLING", pgettext_lazy("ReservationState", "Requires handling") - WAITING_FOR_PAYMENT = "WAITING_FOR_PAYMENT", pgettext_lazy("ReservationState", "Waiting for payment") - CONFIRMED = "CONFIRMED", pgettext_lazy("ReservationState", "Confirmed") - DENIED = "DENIED", pgettext_lazy("ReservationState", "Denied") - - @classproperty - def states_going_to_occur(self) -> list[str]: - return [ # type: ignore[return-type] - ReservationStateChoice.CREATED.value, - ReservationStateChoice.CONFIRMED.value, - ReservationStateChoice.WAITING_FOR_PAYMENT.value, - ReservationStateChoice.REQUIRES_HANDLING.value, - ] - - @classproperty - def states_that_can_change_to_handling(self) -> list[str]: - return [ # type: ignore[return-type] - ReservationStateChoice.CONFIRMED.value, - ReservationStateChoice.DENIED.value, - ] - - @classproperty - def states_that_can_change_to_deny(self) -> list[str]: - return [ # type: ignore[return-type] - ReservationStateChoice.REQUIRES_HANDLING.value, - ReservationStateChoice.CONFIRMED.value, - ] - - @classproperty - def states_that_can_be_cancelled(self) -> list[str]: - return [ # type: ignore[return-type] - ReservationStateChoice.CREATED.value, - ReservationStateChoice.WAITING_FOR_PAYMENT.value, - ] - - -class ReservationTypeChoice(models.TextChoices): - NORMAL = "NORMAL", pgettext_lazy("ReservationType", "Normal") - BLOCKED = "BLOCKED", pgettext_lazy("ReservationType", "Blocked") - STAFF = "STAFF", pgettext_lazy("ReservationType", "Staff") - BEHALF = "BEHALF", pgettext_lazy("ReservationType", "Behalf") - SEASONAL = "SEASONAL", pgettext_lazy("ReservationType", "Seasonal") - - @classproperty - def allowed_for_user_time_adjust(cls) -> list[str]: - return [ # type: ignore[return-type] - ReservationTypeChoice.NORMAL.value, - ReservationTypeChoice.BEHALF.value, - ] - - -class ReservationTypeStaffChoice(models.TextChoices): - # These are the same as the ones above, but for the staff create endpoint - BLOCKED = "BLOCKED", pgettext_lazy("ReservationTypeStaffChoice", "Blocked") - STAFF = "STAFF", pgettext_lazy("ReservationTypeStaffChoice", "Staff") - BEHALF = "BEHALF", pgettext_lazy("ReservationTypeStaffChoice", "Behalf") - - -RESERVEE_LANGUAGE_CHOICES = (*settings.LANGUAGES, ("", "")) - - -class RejectionReadinessChoice(models.TextChoices): - INTERVAL_NOT_ALLOWED = ( - "INTERVAL_NOT_ALLOWED", - pgettext_lazy("RejectionReadiness", "Interval not allowed"), - ) - OVERLAPPING_RESERVATIONS = ( - "OVERLAPPING_RESERVATIONS", - pgettext_lazy("RejectionReadiness", "Overlapping reservations"), - ) - RESERVATION_UNIT_CLOSED = ( - "RESERVATION_UNIT_CLOSED", - pgettext_lazy("RejectionReadiness", "Reservation unit closed"), - ) - - -class CalendarProperty(StrEnum): - VERSION = "VERSION" # type: str - """ - REQUIRED. Version of the iCalendar specification required to interpret the iCalendar object. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.7.4 - """ - - PRODID = "PRODID" # type: str - """ - REQUIRED. The identifier for the product that created the iCalendar object. - See: https://en.wikipedia.org/wiki/Formal_Public_Identifier - https://datatracker.ietf.org/doc/html/rfc5545#section-3.7.3 - """ - - -class EventProperty(StrEnum): - UID = "UID" # type: str - """ - The unique identifier for the calendar event. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.7 - """ - - DTSTAMP = "DTSTAMP" # type: datetime.datetime - """ - The date and time that the calendar event was created. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.2 - """ - - DTSTART = "DTSTART" # type: datetime.datetime - """ - The date and time that the calendar event begins. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4 - """ - - DTEND = "DTEND" # type: datetime.datetime - """ - The date and time that the calendar event ends. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.2 - """ - - SUMMARY = "SUMMARY" # type: str - """ - A short summary or subject for the event. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.12 - """ - - DESCRIPTION = "DESCRIPTION" # type: str - """ - A more complete description for the event than that provided by "SUMMARY". - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.5 - """ - - LOCATION = "LOCATION" # type: str - """ - The intended venue for the event. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.7 - """ - - GEO = "GEO" # type: tuple[float, float] - """ - Global position for the activity specified by a event. - https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.6 - """ - - X_ALT_DESC = "X-ALT-DESC" # type: str - """ - A more complete description for the event than that provided by "SUMMARY". - Required for Outlook calendars to display HTML descriptions properly. - https://learn.microsoft.com/openspecs/exchange_server_protocols/ms-oxcical/d7f285da-9c7a-4597-803b-b74193c898a8 - """ diff --git a/reservations/migrations/0038_change_staff_event_to_reservation_type.py b/reservations/migrations/0038_change_staff_event_to_reservation_type.py index c7d235553..54bf6652b 100644 --- a/reservations/migrations/0038_change_staff_event_to_reservation_type.py +++ b/reservations/migrations/0038_change_staff_event_to_reservation_type.py @@ -4,11 +4,9 @@ def change_staff_event_types(apps, schema_editor): - from reservations.enums import ReservationTypeChoice - Reservation = apps.get_model("reservations", "Reservation") - Reservation.objects.filter(staff_event=True).update(type=ReservationTypeChoice.STAFF) + Reservation.objects.filter(staff_event=True).update(type="STAFF") class Migration(migrations.Migration): diff --git a/reservations/migrations/0051_refesh_reservation_statistics.py b/reservations/migrations/0051_refesh_reservation_statistics.py index 7517f60a2..9a74b2e1d 100644 --- a/reservations/migrations/0051_refesh_reservation_statistics.py +++ b/reservations/migrations/0051_refesh_reservation_statistics.py @@ -2,7 +2,7 @@ from django.db import migrations -from reservations.tasks import create_or_update_reservation_statistics +from tilavarauspalvelu.tasks import create_or_update_reservation_statistics def refresh_reservation_statistics(apps, schema_editor): diff --git a/reservations/migrations/0071_rejectedoccurrence.py b/reservations/migrations/0071_rejectedoccurrence.py index 481fef4bc..907fb6853 100644 --- a/reservations/migrations/0071_rejectedoccurrence.py +++ b/reservations/migrations/0071_rejectedoccurrence.py @@ -4,7 +4,11 @@ import graphene_django_extensions.fields.model from django.db import migrations, models -import reservations.enums + +class RejectionReadinessChoice(models.TextChoices): + INTERVAL_NOT_ALLOWED = "INTERVAL_NOT_ALLOWED", "Interval not allowed" + OVERLAPPING_RESERVATIONS = "OVERLAPPING_RESERVATIONS", "Overlapping reservations" + RESERVATION_UNIT_CLOSED = "RESERVATION_UNIT_CLOSED", "Reservation unit closed" class Migration(migrations.Migration): @@ -27,7 +31,7 @@ class Migration(migrations.Migration): ("OVERLAPPING_RESERVATIONS", "Overlapping reservations"), ("RESERVATION_UNIT_CLOSED", "Reservation unit closed"), ], - enum=reservations.enums.RejectionReadinessChoice, + enum=RejectionReadinessChoice, max_length=24, ), ), diff --git a/reservations/migrations/0076_uppercase_enums.py b/reservations/migrations/0076_uppercase_enums.py index 8e9ee9db6..a5da178b7 100644 --- a/reservations/migrations/0076_uppercase_enums.py +++ b/reservations/migrations/0076_uppercase_enums.py @@ -1,16 +1,11 @@ -from typing import TYPE_CHECKING - from django.db import migrations, models from django.db.models import F from django.db.models.functions import Lower, Upper -if TYPE_CHECKING: - from reservations import models as rm - def uppercase_enums(apps, schema_editor): - Reservation: rm.Reservation = apps.get_model("reservations", "Reservation") - ReservationStatistic: rm.ReservationStatistic = apps.get_model("reservations", "ReservationStatistic") + Reservation = apps.get_model("reservations", "Reservation") + ReservationStatistic = apps.get_model("reservations", "ReservationStatistic") Reservation.objects.all().update( state=Upper(F("state")), @@ -23,8 +18,8 @@ def uppercase_enums(apps, schema_editor): def lowercase_enums(apps, schema_editor): - Reservation: rm.Reservation = apps.get_model("reservations", "Reservation") - ReservationStatistic: rm.ReservationStatistic = apps.get_model("reservations", "ReservationStatistic") + Reservation = apps.get_model("reservations", "Reservation") + ReservationStatistic = apps.get_model("reservations", "ReservationStatistic") Reservation.objects.all().update( state=Lower(F("state")), diff --git a/reservations/migrations/0079_fix_reservation_price_net.py b/reservations/migrations/0079_fix_reservation_price_net.py index f38f1f872..bbd3042e1 100644 --- a/reservations/migrations/0079_fix_reservation_price_net.py +++ b/reservations/migrations/0079_fix_reservation_price_net.py @@ -1,21 +1,16 @@ -from typing import TYPE_CHECKING from django.db import migrations, models -if TYPE_CHECKING: - from reservations.models import Reservation as ReservationModel - from reservations.models import ReservationStatistic as ReservationStatisticModel - def fix_reservation_price_net(apps, schema_editor): - Reservation: ReservationModel = apps.get_model("reservations", "Reservation") + Reservation = apps.get_model("reservations", "Reservation") Reservation.objects.filter( handled_at__isnull=False, tax_percentage_value__gt=0, price__gt=0, ).update(price_net=models.F("price") / ((100 + models.F("tax_percentage_value")) / 100)) - ReservationStatistic: ReservationStatisticModel = apps.get_model("reservations", "ReservationStatistic") + ReservationStatistic = apps.get_model("reservations", "ReservationStatistic") ReservationStatistic.objects.filter( reservation_handled_at__isnull=False, tax_percentage_value__gt=0, diff --git a/reservations/migrations/0083_remove_recurringreservation_ability_group_and_more.py b/reservations/migrations/0083_remove_recurringreservation_ability_group_and_more.py new file mode 100644 index 000000000..bf48b611b --- /dev/null +++ b/reservations/migrations/0083_remove_recurringreservation_ability_group_and_more.py @@ -0,0 +1,128 @@ +# Generated by Django 5.1.1 on 2024-09-26 14:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("reservations", "0082_remove_net_prices"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name="recurringreservation", + name="ability_group", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="ability_group", + ), + migrations.RemoveField( + model_name="reservation", + name="age_group", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="age_group", + ), + migrations.RemoveField( + model_name="recurringreservation", + name="age_group", + ), + migrations.RemoveField( + model_name="recurringreservation", + name="allocated_time_slot", + ), + migrations.RemoveField( + model_name="recurringreservation", + name="reservation_unit", + ), + migrations.RemoveField( + model_name="recurringreservation", + name="user", + ), + migrations.RemoveField( + model_name="rejectedoccurrence", + name="recurring_reservation", + ), + migrations.RemoveField( + model_name="reservation", + name="recurring_reservation", + ), + migrations.RemoveField( + model_name="reservation", + name="cancel_reason", + ), + migrations.RemoveField( + model_name="reservation", + name="deny_reason", + ), + migrations.RemoveField( + model_name="reservation", + name="home_city", + ), + migrations.RemoveField( + model_name="reservation", + name="purpose", + ), + migrations.RemoveField( + model_name="reservation", + name="reservation_unit", + ), + migrations.RemoveField( + model_name="reservation", + name="user", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="reservation", + ), + migrations.RemoveField( + model_name="affectingtimespan", + name="reservation", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="cancel_reason", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="deny_reason", + ), + migrations.RemoveField( + model_name="reservationmetadataset", + name="supported_fields", + ), + migrations.RemoveField( + model_name="reservationmetadataset", + name="required_fields", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="purpose", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="home_city", + ), + migrations.RemoveField( + model_name="reservationstatistic", + name="primary_reservation_unit", + ), + migrations.RemoveField( + model_name="reservationstatisticsreservationunit", + name="reservation_statistics", + ), + migrations.RemoveField( + model_name="reservationstatisticsreservationunit", + name="reservation_unit", + ), + migrations.DeleteModel( + name="AbilityGroup", + ), + ], + database_operations=[], + ), + ] diff --git a/reservations/migrations/0084_delete_agegroup_delete_rejectedoccurrence_and_more.py b/reservations/migrations/0084_delete_agegroup_delete_rejectedoccurrence_and_more.py new file mode 100644 index 000000000..3db61958c --- /dev/null +++ b/reservations/migrations/0084_delete_agegroup_delete_rejectedoccurrence_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 5.1.1 on 2024-09-26 14:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("applications", "0095_alter_applicationround_purposes_and_more"), + ("reservation_units", "0111_alter_reservationunit_metadata_set"), + ("reservations", "0083_remove_recurringreservation_ability_group_and_more"), + ("tilavarauspalvelu", "0010_migrate_reservations"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.DeleteModel( + name="AgeGroup", + ), + migrations.DeleteModel( + name="RejectedOccurrence", + ), + migrations.DeleteModel( + name="RecurringReservation", + ), + migrations.DeleteModel( + name="Reservation", + ), + migrations.DeleteModel( + name="AffectingTimeSpan", + ), + migrations.DeleteModel( + name="ReservationCancelReason", + ), + migrations.DeleteModel( + name="ReservationDenyReason", + ), + migrations.DeleteModel( + name="ReservationMetadataField", + ), + migrations.DeleteModel( + name="ReservationMetadataSet", + ), + migrations.DeleteModel( + name="ReservationPurpose", + ), + migrations.DeleteModel( + name="ReservationStatistic", + ), + migrations.DeleteModel( + name="ReservationStatisticsReservationUnit", + ), + ], + database_operations=[], + ), + ] diff --git a/reservations/models/__init__.py b/reservations/models/__init__.py deleted file mode 100644 index a93677246..000000000 --- a/reservations/models/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from .ability_group import AbilityGroup -from .affecting_time_span import AffectingTimeSpan -from .age_group import AgeGroup -from .recurring_reservation import RecurringReservation -from .rejected_occurrence import RejectedOccurrence -from .reservation import Reservation -from .reservation_cancel_reason import ReservationCancelReason -from .reservation_deny_reason import ReservationDenyReason -from .reservation_metadata import ReservationMetadataField, ReservationMetadataSet -from .reservation_purpose import ReservationPurpose -from .reservation_statistic import ReservationStatistic, ReservationStatisticsReservationUnit - -__all__ = [ - "AbilityGroup", - "AffectingTimeSpan", - "AgeGroup", - "RecurringReservation", - "RejectedOccurrence", - "Reservation", - "ReservationCancelReason", - "ReservationDenyReason", - "ReservationMetadataField", - "ReservationMetadataSet", - "ReservationPurpose", - "ReservationStatistic", - "ReservationStatisticsReservationUnit", -] diff --git a/reservations/models/ability_group.py b/reservations/models/ability_group.py deleted file mode 100644 index 1e9f0d162..000000000 --- a/reservations/models/ability_group.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.db import models - -__all__ = [ - "AbilityGroup", -] - - -class AbilityGroup(models.Model): - name = models.fields.TextField(null=False, blank=False, unique=True) - - class Meta: - db_table = "ability_group" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return self.name diff --git a/reservations/models/affecting_time_span.py b/reservations/models/affecting_time_span.py deleted file mode 100644 index 241872288..000000000 --- a/reservations/models/affecting_time_span.py +++ /dev/null @@ -1,145 +0,0 @@ -from __future__ import annotations - -import contextlib -import datetime -from typing import TYPE_CHECKING - -from django.conf import settings -from django.contrib.postgres.fields import ArrayField -from django.core.cache import cache -from django.db import models -from django.db.transaction import get_connection -from django.utils.translation import gettext_lazy as _ - -from common.date_utils import DEFAULT_TIMEZONE, local_datetime, timedelta_to_json -from reservations.querysets import AffectingTimeSpanQuerySet -from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement -from utils.sentry import SentryLogger - -if TYPE_CHECKING: - from reservations.models import Reservation - - -__all__ = [ - "AffectingTimeSpan", -] - - -class AffectingTimeSpan(models.Model): - """ - A PostgreSQL materialized view that is used to cache reservations as time spans - for first reservable time calculation. Only future reservations are cached, - and only reservations that are actually going to occur. - - View contains an array of reservation unit ids that the time span affects, so it is possible - to query things like "Give me all time spans that affect reservation units X, Y, and Z". - - This view itself is created through a migration (See: `0073_affectingtimespan.py`.), - and updated through a scheduled task (See `update_affecting_time_spans_task`). - """ - - CACHE_KEY = "affecting_time_spans" - """Key for storing datetime stamp in cache of when the view was last updated.""" - - reservation: Reservation = models.OneToOneField( - "reservations.Reservation", - on_delete=models.DO_NOTHING, - primary_key=True, - db_column="reservation_id", - related_name="affecting_time_span", - ) - - affected_reservation_unit_ids: list[int] = ArrayField(base_field=models.IntegerField()) - buffered_start_datetime: datetime.datetime = models.DateTimeField() - buffered_end_datetime: datetime.datetime = models.DateTimeField() - is_blocking: bool = models.BooleanField() - buffer_time_before: datetime.timedelta = models.DurationField() - buffer_time_after: datetime.timedelta = models.DurationField() - - objects = AffectingTimeSpanQuerySet.as_manager() - - class Meta: - managed = False - db_table = "affecting_time_spans" - verbose_name = _("affecting time span") - verbose_name_plural = _("affecting time spans") - base_manager_name = "objects" - ordering = [ - "buffered_start_datetime", - "reservation_id", - ] - - def __str__(self) -> str: - return self.__repr__() - - def __repr__(self) -> str: - start_buffered = self.buffered_start_datetime.astimezone(DEFAULT_TIMEZONE).replace(tzinfo=None) - end_buffered = self.buffered_end_datetime.astimezone(DEFAULT_TIMEZONE).replace(tzinfo=None) - - start = start_buffered + self.buffer_time_before - end = end_buffered - self.buffer_time_after - - start_str = start.strftime("%Y-%m-%d %H:%M") - end_str = end.strftime("%H:%M") if end.date() == start.date() else end.strftime("%Y-%m-%d %H:%M") - - duration_str = f"{start_str}-{end_str}" - - if self.buffer_time_before: - duration_str += f", -{timedelta_to_json(self.buffer_time_before, timespec='minutes')}" - if self.buffer_time_after: - duration_str += f", +{timedelta_to_json(self.buffer_time_after, timespec='minutes')}" - - return f"" - - @classmethod - def refresh(cls, using: str | None = None) -> None: - """ - Called to refresh the contents of the materialized view. - - The view gets stale quite often, since it's dependent on current time and reservations. - Therefore, this is used as a sort of cache, which is updated as a scheduled task, - but can also be called manually if needed. - - Refreshing updated a value in cache that can be used to check if the view is valid. - """ - try: - with get_connection(using).cursor() as cursor: - cursor.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY affecting_time_spans") - except Exception as error: - # Only raise error in local development, otherwise log to Sentry - if settings.RAISE_ERROR_ON_REFRESH_FAILURE: - raise - SentryLogger.log_exception(error) - else: - last_updated = local_datetime().isoformat() - max_allowed_age = datetime.timedelta(minutes=settings.AFFECTING_TIME_SPANS_UPDATE_INTERVAL_MINUTES) - cache.set(cls.CACHE_KEY, last_updated, timeout=max_allowed_age.total_seconds()) - - @classmethod - @contextlib.contextmanager - def refresh_at_the_end(cls) -> None: - """Refresh the materialized view at the end of the context.""" - try: - yield - finally: - cls.refresh() - - @classmethod - def is_valid(cls) -> bool: - """Check last update datetime against a set max allowed age..""" - cached_value: str | None = cache.get(cls.CACHE_KEY) - if cached_value is None: - return False - last_updated = datetime.datetime.fromisoformat(cached_value) - max_allowed_age = datetime.timedelta(minutes=settings.AFFECTING_TIME_SPANS_UPDATE_INTERVAL_MINUTES) - return local_datetime() - last_updated < max_allowed_age - - def as_time_span_element(self) -> TimeSpanElement: - return TimeSpanElement( - start_datetime=self.buffered_start_datetime + self.buffer_time_before, - end_datetime=self.buffered_end_datetime - self.buffer_time_after, - is_reservable=False, - # Buffers are ignored for blocking reservation even if set. - buffer_time_before=None if self.is_blocking else self.buffer_time_before, - buffer_time_after=None if self.is_blocking else self.buffer_time_after, - ) diff --git a/reservations/models/age_group.py b/reservations/models/age_group.py deleted file mode 100644 index 69b7dcae0..000000000 --- a/reservations/models/age_group.py +++ /dev/null @@ -1,22 +0,0 @@ -from django.db import models - -__all__ = [ - "AgeGroup", -] - - -class AgeGroup(models.Model): - minimum = models.fields.PositiveIntegerField(null=False, blank=False) - maximum = models.fields.PositiveIntegerField(null=True, blank=True) - - class Meta: - db_table = "age_group" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - if self.maximum is None: - return f"{self.minimum}+" - return f"{self.minimum} - {self.maximum}" diff --git a/reservations/models/recurring_reservation.py b/reservations/models/recurring_reservation.py deleted file mode 100644 index c5d448824..000000000 --- a/reservations/models/recurring_reservation.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -import uuid as uuid_ -from typing import TYPE_CHECKING - -from django.core.validators import validate_comma_separated_integer_list -from django.db import models - -from common.connectors import RecurringReservationActionsConnector -from config.utils.commons import WEEKDAYS -from reservations.enums import ReservationStateChoice -from reservations.querysets.recurring_reservation import RecurringReservationQuerySet - -if TYPE_CHECKING: - import datetime - - from applications.models import AllocatedTimeSlot - from reservation_units.models import ReservationUnit - from reservations.models import AbilityGroup, AgeGroup, Reservation - from tilavarauspalvelu.models import User - -__all__ = [ - "RecurringReservation", -] - - -class RecurringReservation(models.Model): - name: str = models.CharField(max_length=255, blank=True, default="") - description: str = models.CharField(max_length=500, blank=True, default="") - uuid: uuid_.UUID = models.UUIDField(default=uuid_.uuid4, editable=False, unique=True) - created: datetime.datetime = models.DateTimeField(auto_now_add=True) - - begin_date: datetime.date | None = models.DateField(null=True) - begin_time: datetime.time | None = models.TimeField(null=True) - end_date: datetime.date | None = models.DateField(null=True) - end_time: datetime.time | None = models.TimeField(null=True) - - recurrence_in_days: int | None = models.PositiveIntegerField(null=True, blank=True) - - weekdays: str = models.CharField( - max_length=16, - validators=[validate_comma_separated_integer_list], - choices=WEEKDAYS.CHOICES, - blank=True, - default="", - ) - - # Relations - - reservation_unit: ReservationUnit = models.ForeignKey( - "reservation_units.ReservationUnit", - on_delete=models.PROTECT, - related_name="recurring_reservations", - ) - user: User | None = models.ForeignKey( - "tilavarauspalvelu.User", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - allocated_time_slot: AllocatedTimeSlot | None = models.OneToOneField( - "applications.AllocatedTimeSlot", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="recurring_reservation", - ) - age_group: AgeGroup | None = models.ForeignKey( - "reservations.AgeGroup", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="recurring_reservations", - ) - - # TODO: Remove these fields - ability_group: AbilityGroup | None = models.ForeignKey( - "reservations.AbilityGroup", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="recurring_reservations", - ) - - objects = RecurringReservationQuerySet.as_manager() - actions = RecurringReservationActionsConnector() - - class Meta: - db_table = "recurring_reservation" - base_manager_name = "objects" - ordering = [ - "begin_date", - "begin_time", - "reservation_unit", - ] - - def __str__(self) -> str: - return f"{self.name}" - - @property - def denied_reservations(self): # DEPRECATED - """Used in `api.legacy_rest_api.serializers.RecurringReservationSerializer`""" - # Avoid a query to the database if we have fetched list already - reservation: Reservation # noqa: F842 - if "reservations" in self._prefetched_objects_cache: - return [ - reservation - for reservation in self.reservations.all() - if reservation.state == ReservationStateChoice.DENIED - ] - - return self.reservations.filter(state=ReservationStateChoice.DENIED) diff --git a/reservations/models/rejected_occurrence.py b/reservations/models/rejected_occurrence.py deleted file mode 100644 index 5582e0fa9..000000000 --- a/reservations/models/rejected_occurrence.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -from django.db import models -from django.utils.translation import gettext_lazy as _ -from graphene_django_extensions.fields.model import StrChoiceField - -from common.connectors import RejectedOccurrenceActionsConnector -from reservations.enums import RejectionReadinessChoice -from reservations.querysets import RejectedOccurrenceQuerySet - -if TYPE_CHECKING: - import datetime - - from reservations.models import RecurringReservation - - -class RejectedOccurrence(models.Model): - begin_datetime: datetime.datetime = models.DateTimeField() - end_datetime: datetime.datetime = models.DateTimeField() - rejection_reason: str = StrChoiceField(enum=RejectionReadinessChoice) - created_at: datetime.datetime = models.DateTimeField(auto_now_add=True) - - recurring_reservation: RecurringReservation = models.ForeignKey( - "reservations.RecurringReservation", - on_delete=models.CASCADE, - related_name="rejected_occurrences", - ) - - objects = RejectedOccurrenceQuerySet.as_manager() - actions = RejectedOccurrenceActionsConnector() - - class Meta: - db_table = "rejected_occurrence" - base_manager_name = "objects" - verbose_name = _("Rejected occurrence") - verbose_name_plural = _("Rejected occurrences") - ordering = [ - "begin_datetime", - "end_datetime", - ] - - def __str__(self) -> str: - return f"{_("Rejected occurrence")} ({self.begin_datetime.isoformat()} - {self.end_datetime.isoformat()})" diff --git a/reservations/models/reservation.py b/reservations/models/reservation.py deleted file mode 100644 index e9e276aaa..000000000 --- a/reservations/models/reservation.py +++ /dev/null @@ -1,323 +0,0 @@ -from __future__ import annotations - -import datetime -from decimal import Decimal -from typing import TYPE_CHECKING - -from django.db import models -from django.db.models.functions import Concat -from django.db.models.functions.text import Trim -from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ -from helsinki_gdpr.models import SerializableMixin -from lookup_property import lookup_property - -from common.connectors import ReservationActionsConnector -from config.utils.auditlog_util import AuditLogger -from reservations.enums import ( - RESERVEE_LANGUAGE_CHOICES, - CustomerTypeChoice, - ReservationStateChoice, - ReservationTypeChoice, -) -from reservations.querysets import ReservationQuerySet -from utils.decimal_utils import round_decimal - -if TYPE_CHECKING: - from applications.models import City - from reservations.models import ( - AgeGroup, - RecurringReservation, - ReservationCancelReason, - ReservationDenyReason, - ReservationPurpose, - ) - from tilavarauspalvelu.models import Unit, User - - -__all__ = [ - "Reservation", -] - - -class ReservationManager(SerializableMixin.SerializableManager, models.Manager.from_queryset(ReservationQuerySet)): - """Contains custom queryset methods and GDPR serialization.""" - - -class Reservation(SerializableMixin, models.Model): - # Basic information - sku: str = models.CharField(max_length=255, blank=True, default="") - name: str = models.CharField(max_length=255, blank=True, default="") - description: str = models.CharField(max_length=255, blank=True, default="") - num_persons: int | None = models.fields.PositiveIntegerField(null=True, blank=True) - state: str = models.CharField( - max_length=32, - choices=ReservationStateChoice.choices, - default=ReservationStateChoice.CREATED, - db_index=True, - ) - type: str | None = models.CharField( - max_length=50, - null=True, - blank=False, - choices=ReservationTypeChoice.choices, - default=ReservationTypeChoice.NORMAL, - ) - cancel_details: str = models.TextField(blank=True, default="") - handling_details: str = models.TextField(blank=True, default="") - working_memo: str = models.TextField(null=True, blank=True, default="") - - # Time information - begin: datetime.datetime = models.DateTimeField(db_index=True) - end: datetime.datetime = models.DateTimeField(db_index=True) - buffer_time_before: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) - buffer_time_after: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) - handled_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) - confirmed_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) - created_at: datetime.datetime | None = models.DateTimeField(null=True, default=now) - - # Pricing details - price: Decimal = models.DecimalField(max_digits=10, decimal_places=2, default=0) - non_subsidised_price: Decimal = models.DecimalField(max_digits=20, decimal_places=2, default=0) - unit_price: Decimal = models.DecimalField(max_digits=10, decimal_places=2, default=0) - tax_percentage_value: Decimal = models.DecimalField(max_digits=5, decimal_places=2, default=0) - - # Free of charge information - applying_for_free_of_charge: bool = models.BooleanField(default=False, blank=True) - free_of_charge_reason: bool | None = models.TextField(null=True, blank=True) - - # Reservee information - reservee_id: str = models.CharField(max_length=255, blank=True, default="") - reservee_first_name: str = models.CharField(max_length=255, blank=True, default="") - reservee_last_name: str = models.CharField(max_length=255, blank=True, default="") - reservee_email: str | None = models.EmailField(null=True, blank=True) - reservee_phone: str = models.CharField(max_length=255, blank=True, default="") - reservee_organisation_name: str = models.CharField(max_length=255, blank=True, default="") - reservee_address_street: str = models.CharField(max_length=255, blank=True, default="") - reservee_address_city: str = models.CharField(max_length=255, blank=True, default="") - reservee_address_zip: str = models.CharField(max_length=255, blank=True, default="") - reservee_is_unregistered_association: bool = models.BooleanField(default=False, blank=True) - reservee_used_ad_login: bool = models.BooleanField(default=False, blank=True) - reservee_language: str = models.CharField( - max_length=255, - blank=True, - default="", - choices=RESERVEE_LANGUAGE_CHOICES, - ) - reservee_type: str | None = models.CharField( - max_length=50, - choices=CustomerTypeChoice.choices, - null=True, - blank=True, - ) - - # Billing information - billing_first_name: str = models.CharField(max_length=255, blank=True, default="") - billing_last_name: str = models.CharField(max_length=255, blank=True, default="") - billing_email: str | None = models.EmailField(null=True, blank=True) - billing_phone: str = models.CharField(max_length=255, blank=True, default="") - billing_address_street: str = models.CharField(max_length=255, blank=True, default="") - billing_address_city: str = models.CharField(max_length=255, blank=True, default="") - billing_address_zip: str = models.CharField(max_length=255, blank=True, default="") - - # Relations - reservation_unit = models.ManyToManyField("reservation_units.ReservationUnit") - - user: User | None = models.ForeignKey( - "tilavarauspalvelu.User", - related_name="reservations", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - recurring_reservation: RecurringReservation | None = models.ForeignKey( - "reservations.RecurringReservation", - related_name="reservations", - on_delete=models.PROTECT, - null=True, - blank=True, - ) - deny_reason: ReservationDenyReason | None = models.ForeignKey( - "reservations.ReservationDenyReason", - related_name="reservations", - on_delete=models.PROTECT, - null=True, - blank=True, - ) - cancel_reason: ReservationCancelReason | None = models.ForeignKey( - "reservations.ReservationCancelReason", - related_name="reservations", - on_delete=models.PROTECT, - null=True, - blank=True, - ) - purpose: ReservationPurpose | None = models.ForeignKey( - "reservations.ReservationPurpose", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - home_city: City | None = models.ForeignKey( - "applications.City", - related_name="home_city_reservation", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - age_group: AgeGroup | None = models.ForeignKey( - "reservations.AgeGroup", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - - objects = ReservationManager() - actions = ReservationActionsConnector() - - class Meta: - db_table = "reservation" - base_manager_name = "objects" - ordering = [ - "begin", - ] - - # For GDPR API - serialize_fields = ( - {"name": "name"}, - {"name": "description"}, - {"name": "begin"}, - {"name": "end"}, - {"name": "reservee_first_name"}, - {"name": "reservee_last_name"}, - {"name": "reservee_email"}, - {"name": "reservee_phone"}, - {"name": "reservee_address_zip"}, - {"name": "reservee_address_city"}, - {"name": "reservee_address_street"}, - {"name": "billing_first_name"}, - {"name": "billing_last_name"}, - {"name": "billing_email"}, - {"name": "billing_phone"}, - {"name": "billing_address_zip"}, - {"name": "billing_address_city"}, - {"name": "billing_address_street"}, - {"name": "reservee_id"}, - {"name": "reservee_organisation_name"}, - {"name": "free_of_charge_reason"}, - {"name": "cancel_details"}, - ) - - def __str__(self) -> str: - return f"{self.name} ({self.type})" - - @property - def price_net(self) -> Decimal: - """Return the net price of the reservation. (Price without VAT)""" - return round_decimal(self.price / (1 + self.tax_percentage_value / Decimal(100)), 2) - - @property - def price_vat_amount(self) -> Decimal: - """Return the VAT amount of the reservation.""" - return round_decimal(self.price - self.price_net, 2) - - @property - def non_subsidised_price_net(self) -> Decimal: - return round_decimal(self.non_subsidised_price / (1 + self.tax_percentage_value / Decimal(100)), 2) - - @lookup_property(joins=["recurring_reservation", "user"]) - def reservee_name() -> str: - return models.Case( # type: ignore[return-value] - # Blocking reservation - models.When( - condition=( - models.Q(type=ReservationTypeChoice.BLOCKED.value) # - ), - then=models.Value(str(_("Closed"))), - ), - # Internal reservations created by STAFF - models.When( - condition=( - models.Q(type=ReservationTypeChoice.STAFF.value) # - & models.Q(recurring_reservation__isnull=False) - & ~models.Q(recurring_reservation__name="") - ), - then=models.F("recurring_reservation__name"), - ), - models.When( - condition=( - models.Q(type=ReservationTypeChoice.STAFF.value) # - & (models.Q(recurring_reservation__isnull=True) | models.Q(recurring_reservation__name="")) - & ~models.Q(name="") - ), - then=models.F("name"), - ), - # Organisation reservee - models.When( - condition=( - models.Q(reservee_type__in=CustomerTypeChoice.organisation) # - & ~models.Q(reservee_organisation_name="") - ), - then=models.F("reservee_organisation_name"), - ), - # Individual reservee - models.When( - condition=( - ~models.Q(reservee_type__in=CustomerTypeChoice.organisation) # - & (~models.Q(reservee_first_name="") | ~models.Q(reservee_last_name="")) - ), - then=Trim(Concat("reservee_first_name", models.Value(" "), "reservee_last_name")), - ), - # Use reservation name when reservee name as first fallback - models.When( - condition=~models.Q(name=""), - then=models.F("name"), - ), - # Use the name of the User who made the reservation as the last fallback - models.When( - condition=( - models.Q(user__isnull=False) # - & ( - ~models.Q(user__first_name="") # - | ~models.Q(user__last_name="") - ) - ), - then=Trim(Concat("user__first_name", models.Value(" "), "user__last_name")), - ), - default=models.Value(""), - output_field=models.CharField(), - ) - - @property - def requires_handling(self) -> bool: - return ( - self.reservation_unit.filter(require_reservation_handling=True).exists() or self.applying_for_free_of_charge - ) - - @property - def units_for_permissions(self) -> list[Unit]: - from tilavarauspalvelu.models import Unit - - if hasattr(self, "_units_for_permissions"): - return self._units_for_permissions - - self._units_for_permissions = list( - Unit.objects.filter(reservationunit__reservation=self).prefetch_related("unit_groups").distinct() - ) - return self._units_for_permissions - - @units_for_permissions.setter - def units_for_permissions(self, value: list[Unit]) -> None: - # The setter is used by ReservationQuerySet to pre-evaluate units for multiple Reservations. - # Should not be used by anything else! - self._units_for_permissions = value - - -AuditLogger.register( - Reservation, - # Exclude lookup properties, since they are calculated values. - exclude_fields=[ - "_reservee_name", - "_unit_ids_for_perms", - "_unit_group_ids_for_perms", - ], -) diff --git a/reservations/models/reservation_cancel_reason.py b/reservations/models/reservation_cancel_reason.py deleted file mode 100644 index 4255d6177..000000000 --- a/reservations/models/reservation_cancel_reason.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db import models - -__all__ = [ - "ReservationCancelReason", -] - - -class ReservationCancelReason(models.Model): - reason = models.CharField(max_length=255, null=False, blank=False) - - # Translated field hints - reason_fi: str | None - reason_en: str | None - reason_sv: str | None - - class Meta: - db_table = "reservation_cancel_reason" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return self.reason diff --git a/reservations/models/reservation_deny_reason.py b/reservations/models/reservation_deny_reason.py deleted file mode 100644 index 1dbee4d26..000000000 --- a/reservations/models/reservation_deny_reason.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import models - -__all__ = [ - "ReservationDenyReason", -] - - -class ReservationDenyReason(models.Model): - rank: int | None = models.PositiveBigIntegerField(null=True, blank=True, db_index=True) - reason: str = models.CharField(max_length=255) - - # Translated field hints - reason_fi: str | None - reason_sv: str | None - reason_en: str | None - - class Meta: - db_table = "reservation_deny_reason" - base_manager_name = "objects" - ordering = [ - "rank", - ] - - def __str__(self) -> str: - return self.reason diff --git a/reservations/models/reservation_metadata.py b/reservations/models/reservation_metadata.py deleted file mode 100644 index 9c57d2f3d..000000000 --- a/reservations/models/reservation_metadata.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - -__all__ = [ - "ReservationMetadataField", - "ReservationMetadataSet", -] - - -class ReservationMetadataField(models.Model): - field_name = models.CharField(max_length=100, verbose_name=_("Field name"), unique=True) - - class Meta: - db_table = "reservation_metadata_field" - base_manager_name = "objects" - verbose_name = _("Reservation metadata field") - verbose_name_plural = _("Reservation metadata fields") - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return self.field_name - - -class ReservationMetadataSet(models.Model): - name = models.CharField(max_length=100, verbose_name=_("Name"), unique=True) - supported_fields = models.ManyToManyField( - "reservations.ReservationMetadataField", - verbose_name=_("Supported fields"), - related_name="metadata_sets_supported", - ) - required_fields = models.ManyToManyField( - "reservations.ReservationMetadataField", - verbose_name=_("Required fields"), - related_name="metadata_sets_required", - blank=True, - ) - - class Meta: - db_table = "reservation_metadata_set" - base_manager_name = "objects" - verbose_name = _("Reservation metadata set") - verbose_name_plural = _("Reservation metadata sets") - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return self.name diff --git a/reservations/models/reservation_purpose.py b/reservations/models/reservation_purpose.py deleted file mode 100644 index b86c866e4..000000000 --- a/reservations/models/reservation_purpose.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db import models - -__all__ = [ - "ReservationPurpose", -] - - -class ReservationPurpose(models.Model): - name = models.CharField(max_length=200) - - # Translated field hints - name_fi: str | None - name_sv: str | None - name_en: str | None - - class Meta: - db_table = "reservation_purpose" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return self.name diff --git a/reservations/models/reservation_statistic.py b/reservations/models/reservation_statistic.py deleted file mode 100644 index 0a3a0d6a6..000000000 --- a/reservations/models/reservation_statistic.py +++ /dev/null @@ -1,280 +0,0 @@ -from __future__ import annotations - -import datetime -from typing import TYPE_CHECKING - -from django.db import models, transaction -from django.utils import timezone - -from reservations.enums import CustomerTypeChoice - -if TYPE_CHECKING: - from decimal import Decimal - - from reservations.models import Reservation - - -__all__ = [ - "ReservationStatistic", - "ReservationStatisticsReservationUnit", -] - - -class ReservationStatistic(models.Model): - # Copied from Reservation - - num_persons: int | None = models.fields.PositiveIntegerField(null=True, blank=True) - state: str = models.CharField(max_length=255) - reservation_type: str | None = models.CharField(max_length=255, null=True) - - begin: datetime.datetime = models.DateTimeField() - end: datetime.datetime = models.DateTimeField() - buffer_time_before: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) - buffer_time_after: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) - reservation_handled_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) - reservation_confirmed_at: datetime.datetime | None = models.DateTimeField(null=True) - reservation_created_at: datetime.datetime | None = models.DateTimeField(null=True, default=timezone.now) - - price: Decimal = models.DecimalField(max_digits=10, decimal_places=2, default=0) - price_net: Decimal = models.DecimalField(max_digits=20, decimal_places=6, default=0) - non_subsidised_price: Decimal = models.DecimalField(max_digits=20, decimal_places=2, default=0) - non_subsidised_price_net: Decimal = models.DecimalField(max_digits=20, decimal_places=6, default=0) - tax_percentage_value: Decimal = models.DecimalField(max_digits=5, decimal_places=2, default=0) - - applying_for_free_of_charge: bool = models.BooleanField(default=False, blank=True) - - reservee_id: str = models.CharField(max_length=255, blank=True, default="") - reservee_organisation_name: str = models.CharField(max_length=255, blank=True, default="") - reservee_address_zip: str = models.CharField(max_length=255, blank=True, default="") - reservee_is_unregistered_association: bool = models.BooleanField(null=True, default=False, blank=True) - reservee_language: str = models.CharField(max_length=255, blank=True, default="") - reservee_type: str | None = models.CharField(max_length=255, null=True, blank=True) - - # Relations and static copies of their values - - primary_reservation_unit = models.ForeignKey( - "reservation_units.ReservationUnit", - null=True, - on_delete=models.SET_NULL, - ) - primary_reservation_unit_name: str = models.CharField(max_length=255) - primary_unit_tprek_id: str | None = models.CharField(max_length=255, null=True) - primary_unit_name: str = models.CharField(max_length=255) - - deny_reason = models.ForeignKey( - "reservations.ReservationDenyReason", - related_name="reservation_statistics", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - deny_reason_text: str = models.CharField(max_length=255) - - cancel_reason = models.ForeignKey( - "reservations.ReservationCancelReason", - related_name="reservation_statistics", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - cancel_reason_text: str = models.CharField(max_length=255) - - purpose = models.ForeignKey( - "reservations.ReservationPurpose", - related_name="reservation_statistics", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - purpose_name: str = models.CharField(max_length=255, default="", blank=True) - - home_city = models.ForeignKey( - "applications.City", - related_name="reservation_statistics", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - home_city_name: str = models.CharField(max_length=255, default="", blank=True) - home_city_municipality_code: str = models.CharField(max_length=255, default="") - - age_group = models.ForeignKey( - "reservations.AgeGroup", - related_name="reservation_statistics", - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - age_group_name: str = models.fields.CharField(max_length=255, default="", blank=True) - - # From RecurringReservation - ability_group = models.ForeignKey( - "reservations.AbilityGroup", - null=True, - blank=True, - on_delete=models.SET_NULL, - ) - ability_group_name: str = models.fields.TextField() - - reservation = models.OneToOneField( - "reservations.Reservation", - on_delete=models.SET_NULL, - null=True, - ) - - # Reservation statistics specific - - updated_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True, auto_now=True) - priority: int | None = models.IntegerField(null=True, blank=True) - priority_name: str = models.CharField(max_length=255, default="", blank=True) - duration_minutes: int = models.IntegerField() - is_subsidised: bool = models.BooleanField(default=False) - is_recurring: bool = models.BooleanField(default=False) - recurrence_begin_date: datetime.date | None = models.DateField(null=True) - recurrence_end_date: datetime.date | None = models.DateField(null=True) - recurrence_uuid: str = models.CharField(max_length=255, default="", blank=True) - reservee_uuid: str = models.CharField(max_length=255, default="", blank=True) - reservee_used_ad_login: bool = models.BooleanField(default=False, blank=True) - is_applied: bool = models.BooleanField(default=False, blank=True) - """Is the reservation done through application process.""" - - class Meta: - db_table = "reservation_statistic" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return f"{self.reservee_uuid} - {self.begin} - {self.end}" - - @classmethod - def for_reservation(cls, reservation: Reservation, *, save: bool = True) -> ReservationStatistic: # noqa: PLR0915 - recurring_reservation = getattr(reservation, "recurring_reservation", None) - ability_group = getattr(recurring_reservation, "ability_group", None) - allocated_time_slot = getattr(recurring_reservation, "allocated_time_slot", None) - - requires_org_name = reservation.reservee_type != CustomerTypeChoice.INDIVIDUAL - requires_org_id = not reservation.reservee_is_unregistered_association and requires_org_name - by_profile_user = bool(getattr(reservation.user, "profile_id", "")) - - statistic = ( # - ReservationStatistic.objects.filter(reservation=reservation).first() - or ReservationStatistic(reservation=reservation) - ) - - statistic.ability_group = ability_group - statistic.age_group = reservation.age_group - statistic.age_group_name = str(reservation.age_group) - statistic.applying_for_free_of_charge = reservation.applying_for_free_of_charge - statistic.begin = reservation.begin - statistic.buffer_time_after = reservation.buffer_time_after - statistic.buffer_time_before = reservation.buffer_time_before - statistic.cancel_reason = reservation.cancel_reason - statistic.cancel_reason_text = getattr(reservation.cancel_reason, "reason", "") - statistic.deny_reason = reservation.deny_reason - statistic.deny_reason_text = getattr(reservation.deny_reason, "reason", "") - statistic.duration_minutes = (reservation.end - reservation.begin).total_seconds() / 60 - statistic.end = reservation.end - statistic.home_city = reservation.home_city - statistic.home_city_municipality_code = getattr(reservation.home_city, "municipality_code", "") - statistic.home_city_name = reservation.home_city.name if reservation.home_city else "" - statistic.is_applied = allocated_time_slot is not None - statistic.is_recurring = recurring_reservation is not None - statistic.is_subsidised = reservation.price < reservation.non_subsidised_price - statistic.non_subsidised_price = reservation.non_subsidised_price - statistic.non_subsidised_price_net = reservation.non_subsidised_price_net - statistic.num_persons = reservation.num_persons - statistic.price = reservation.price - statistic.price_net = reservation.price_net - statistic.purpose = reservation.purpose - statistic.purpose_name = reservation.purpose.name if reservation.purpose else "" - statistic.recurrence_begin_date = getattr(recurring_reservation, "begin_date", None) - statistic.recurrence_end_date = getattr(recurring_reservation, "end_date", None) - statistic.recurrence_uuid = getattr(recurring_reservation, "uuid", "") - statistic.reservation = reservation - statistic.reservation_confirmed_at = reservation.confirmed_at - statistic.reservation_created_at = reservation.created_at - statistic.reservation_handled_at = reservation.handled_at - statistic.reservation_type = reservation.type - statistic.reservee_address_zip = reservation.reservee_address_zip if by_profile_user else "" - statistic.reservee_id = reservation.reservee_id if requires_org_id else "" - statistic.reservee_is_unregistered_association = reservation.reservee_is_unregistered_association - statistic.reservee_language = reservation.reservee_language - statistic.reservee_organisation_name = reservation.reservee_organisation_name if requires_org_name else "" - statistic.reservee_type = reservation.reservee_type - statistic.reservee_used_ad_login = reservation.reservee_used_ad_login - statistic.reservee_uuid = str(reservation.user.tvp_uuid) if reservation.user else "" - statistic.state = reservation.state - statistic.tax_percentage_value = reservation.tax_percentage_value - - for res_unit in reservation.reservation_unit.all(): - statistic.primary_reservation_unit = res_unit - statistic.primary_reservation_unit_name = res_unit.name - statistic.primary_unit_name = getattr(res_unit.unit, "name", "") - statistic.primary_unit_tprek_id = getattr(res_unit.unit, "tprek_id", "") - break - - if statistic.is_applied and ability_group: - statistic.ability_group_name = ability_group.name - - if save: - statistic.save() - - return statistic - - -class ReservationStatisticsReservationUnit(models.Model): - name = models.CharField(max_length=255) - unit_name = models.CharField(max_length=255) - unit_tprek_id = models.CharField(max_length=255, null=True) - - reservation_statistics = models.ForeignKey( - "reservations.ReservationStatistic", - on_delete=models.CASCADE, - related_name="reservation_stats_reservation_units", - ) - reservation_unit = models.ForeignKey( - "reservation_units.ReservationUnit", - null=True, - on_delete=models.SET_NULL, - ) - - class Meta: - db_table = "reservation_statistics_reservation_unit" - base_manager_name = "objects" - ordering = [ - "pk", - ] - - def __str__(self) -> str: - return f"{self.reservation_statistics} - {self.reservation_unit}" - - @classmethod - def for_statistic( - cls, - statistic: ReservationStatistic, - *, - save: bool = True, - ) -> list[ReservationStatisticsReservationUnit]: - to_save: list[ReservationStatisticsReservationUnit] = [] - for reservation_unit in statistic.reservation.reservation_unit.all(): - stat_unit = ReservationStatisticsReservationUnit( - reservation_statistics=statistic, - reservation_unit=reservation_unit, - ) - - stat_unit.name = reservation_unit.name - stat_unit.reservation_statistics = statistic - stat_unit.reservation_unit = reservation_unit - stat_unit.unit_name = getattr(reservation_unit.unit, "name", "") - stat_unit.unit_tprek_id = getattr(reservation_unit.unit, "tprek_id", "") - - to_save.append(stat_unit) - - if save: - with transaction.atomic(): - ReservationStatisticsReservationUnit.objects.filter(reservation_statistics=statistic).delete() - ReservationStatisticsReservationUnit.objects.bulk_create(to_save) - - return to_save diff --git a/reservations/querysets/__init__.py b/reservations/querysets/__init__.py deleted file mode 100644 index 0b09d7cd7..000000000 --- a/reservations/querysets/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .affecting_time_span import AffectingTimeSpanQuerySet -from .recurring_reservation import RecurringReservationQuerySet -from .rejected_occurrence import RejectedOccurrenceQuerySet -from .reservation import ReservationQuerySet - -__all__ = [ - "AffectingTimeSpanQuerySet", - "RecurringReservationQuerySet", - "RejectedOccurrenceQuerySet", - "ReservationQuerySet", -] diff --git a/reservations/querysets/affecting_time_span.py b/reservations/querysets/affecting_time_span.py deleted file mode 100644 index a03047f97..000000000 --- a/reservations/querysets/affecting_time_span.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import models - -__all__ = [ - "AffectingTimeSpanQuerySet", -] - - -class AffectingTimeSpanQuerySet(models.QuerySet): - pass diff --git a/reservations/querysets/recurring_reservation.py b/reservations/querysets/recurring_reservation.py deleted file mode 100644 index 4cef05a40..000000000 --- a/reservations/querysets/recurring_reservation.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.db import models - - -class RecurringReservationQuerySet(models.QuerySet): - pass diff --git a/reservations/querysets/rejected_occurrence.py b/reservations/querysets/rejected_occurrence.py deleted file mode 100644 index 2c5076f2c..000000000 --- a/reservations/querysets/rejected_occurrence.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Self - -from django.db import models -from lookup_property import L - -from reservations.enums import RejectionReadinessChoice - - -class RejectedOccurrenceQuerySet(models.QuerySet): - def order_by_applicant(self, *, desc: bool = False) -> Self: - applicant_ref = ( - "recurring_reservation" - "__allocated_time_slot" - "__reservation_unit_option" - "__application_section" - "__application" - "__applicant" - ) - return self.order_by(L(applicant_ref).order_by(descending=desc)) - - def order_by_rejection_reason(self, *, desc: bool = False) -> Self: - return self.alias( - rejection_reason_order=models.Case( - models.When( - rejection_reason=models.Value(RejectionReadinessChoice.INTERVAL_NOT_ALLOWED), - then=models.Value(0), - ), - models.When( - rejection_reason=models.Value(RejectionReadinessChoice.OVERLAPPING_RESERVATIONS), - then=models.Value(1), - ), - models.When( - rejection_reason=models.Value(RejectionReadinessChoice.RESERVATION_UNIT_CLOSED), - then=models.Value(2), - ), - default=models.Value(3), - output_field=models.IntegerField(), - ), - ).order_by(models.OrderBy(models.F("rejection_reason_order"), descending=desc)) diff --git a/reservations/querysets/reservation.py b/reservations/querysets/reservation.py deleted file mode 100644 index 2da033720..000000000 --- a/reservations/querysets/reservation.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -import datetime -from typing import TYPE_CHECKING, Self - -from django.conf import settings -from django.contrib.postgres.aggregates import ArrayAgg -from django.db import models -from django.db.models.functions import Coalesce - -from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from tilavarauspalvelu.enums import OrderStatus - -if TYPE_CHECKING: - from applications.models import ApplicationRound - from reservations.models import Reservation - - -class ReservationQuerySet(models.QuerySet): - def with_buffered_begin_and_end(self: Self) -> Self: - """Annotate the queryset with buffered begin and end times.""" - return self.annotate( - buffered_begin=models.F("begin") - models.F("buffer_time_before"), - buffered_end=models.F("end") + models.F("buffer_time_after"), - ) - - def filter_buffered_reservations_period(self: Self, start_date: datetime.date, end_date: datetime.date) -> Self: - """Filter reservations that are on the given period.""" - return ( - self.with_buffered_begin_and_end() - .filter( - buffered_begin__date__lte=end_date, - buffered_end__date__gte=start_date, - ) - .distinct() - .order_by("buffered_begin") - ) - - def total_duration(self: Self) -> datetime.timedelta: - return ( - self.annotate(duration=models.F("end") - models.F("begin")) - .aggregate(total_duration=models.Sum("duration")) - .get("total_duration") - ) or datetime.timedelta() - - def total_seconds(self: Self) -> int: - return int(self.total_duration().total_seconds()) - - def within_application_round_period(self: Self, app_round: ApplicationRound) -> Self: - return self.within_period( - app_round.reservation_period_begin, - app_round.reservation_period_end, - ) - - def within_period(self: Self, period_start: datetime.date, period_end: datetime.date) -> Self: - """All reservation fully withing a period.""" - return self.filter( - begin__date__gte=period_start, - end__date__lte=period_end, - ) - - def overlapping_period(self: Self, period_start: datetime.date, period_end: datetime.date) -> Self: - """All reservations that overlap with a period, even partially.""" - return self.filter( - begin__date__lte=period_end, - end__date__gte=period_start, - ) - - def going_to_occur(self: Self): - return self.filter(state__in=ReservationStateChoice.states_going_to_occur) - - def active(self: Self) -> Self: - """ - Filter reservations that have not ended yet. - - Note: - - There might be older reservations with buffers that are still active, - even if the reservation itself is not returned by this queryset. - - Returned data may contain some 'Inactive' reservations, before they are deleted by a periodic task. - """ - return self.going_to_occur().filter(end__gte=local_datetime()) - - def inactive(self: Self, older_than_minutes: int) -> Self: - """Filter 'draft' reservations, which are older than X minutes old, and can be assumed to be inactive.""" - return self.filter( - state=ReservationStateChoice.CREATED, - created_at__lte=local_datetime() - datetime.timedelta(minutes=older_than_minutes), - ) - - def with_inactive_payments(self: Self) -> Self: - expiration_time = local_datetime() - datetime.timedelta(minutes=settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES) - return self.filter( - state=ReservationStateChoice.WAITING_FOR_PAYMENT, - payment_order__remote_id__isnull=False, - payment_order__status__in=[OrderStatus.EXPIRED, OrderStatus.CANCELLED], - payment_order__created_at__lte=expiration_time, - ) - - def affecting_reservations(self: Self, units: list[int] = (), reservation_units: list[int] = ()) -> Self: - """Filter reservations that affect other reservations in the given units and/or reservation units.""" - from reservation_units.models import ReservationUnit - - qs = ReservationUnit.objects.all() - if units: - qs = qs.filter(unit__in=units) - if reservation_units: - qs = qs.filter(pk__in=reservation_units) - - return self.filter( - reservation_unit__in=models.Subquery(qs.affected_reservation_unit_ids), - ).exclude( - # Cancelled or denied reservations never affect any reservations - state__in=[ - ReservationStateChoice.CANCELLED, - ReservationStateChoice.DENIED, - ] - ) - - def _fetch_all(self) -> None: - super()._fetch_all() - if "FETCH_UNITS_FOR_PERMISSIONS_FLAG" in self._hints: - self._hints.pop("FETCH_UNITS_FOR_PERMISSIONS_FLAG", None) - self._add_units_for_permissions() - - def with_permissions(self) -> Self: - """Indicates that we need to fetch units for permissions checks when the queryset is evaluated.""" - self._hints["FETCH_UNITS_FOR_PERMISSIONS_FLAG"] = True - return self - - def _add_units_for_permissions(self) -> None: - # This works sort of like a 'prefetch_related', since it makes another query - # to fetch units and unit groups for the permission checks when the queryset is evaluated, - # and 'joins' them to the correct model instances in python. - from tilavarauspalvelu.models import Unit - - items: list[Reservation] = list(self) - if not items: - return - - units = ( - Unit.objects.prefetch_related("unit_groups") - .filter(reservationunit__reservation__in=items) - .annotate( - reservation_ids=Coalesce( - ArrayAgg( - "reservationunit__reservation", - distinct=True, - filter=( - models.Q(reservationunit__isnull=False) - & models.Q(reservationunit__reservation__isnull=False) - ), - ), - models.Value([]), - ) - ) - .distinct() - ) - - for item in items: - item.units_for_permissions = [unit for unit in units if item.pk in unit.reservation_ids] diff --git a/reservations/signals.py b/reservations/signals.py deleted file mode 100644 index 28d1e0556..000000000 --- a/reservations/signals.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Any, Literal - -from django.conf import settings -from django.db.models.signals import m2m_changed, post_delete, post_save -from django.dispatch import receiver - -from reservations.models import Reservation -from reservations.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task - -type M2MAction = Literal["pre_add", "post_add", "pre_remove", "post_remove", "pre_clear", "post_clear"] - - -@receiver(post_save, sender=Reservation, dispatch_uid="reservation_create") -def reservation_create( - sender: type[Reservation], - instance: Reservation, - raw: bool = False, - **kwargs, -) -> None: - if not raw and settings.SAVE_RESERVATION_STATISTICS: - # Note that many-to-many relationships are not yet available at this time so - # statistics might be saved with the previous reservation unit value. That is why - # we also have m2m_changed signal below. - # - # It gets triggered when relationships are being changed and happens always after post_save signal. - # It will update the reservation unit with the new value. - # - # Current implementation allows reservation to have multiple reservation units, but in practise, only - # one can be defined. If in the future we truly support reservations with multiple reservation units, - # we need to change this implementation so that we either have multiple reservation units in the statistics - # or we have better way to indicate which one is the primary unit. - create_or_update_reservation_statistics([instance.pk]) - - if not raw and settings.UPDATE_AFFECTING_TIME_SPANS: - update_affecting_time_spans_task.delay() - - -@receiver(post_delete, sender=Reservation, dispatch_uid="reservation_delete") -def reservation_delete( - sender: type[Reservation], - **kwargs, -) -> None: - if settings.UPDATE_AFFECTING_TIME_SPANS: - update_affecting_time_spans_task.delay() - - -@receiver( - m2m_changed, - sender=Reservation.reservation_unit.through, - dispatch_uid="reservations_reservation_units_m2m", -) -def reservations_reservation_units_m2m( - action: M2MAction, - instance: Reservation, - reverse: bool = False, - raw: bool = False, - **kwargs: Any, -) -> None: - if action == "post_add" and not raw and not reverse and settings.SAVE_RESERVATION_STATISTICS: - create_or_update_reservation_statistics([instance.pk]) - - if not raw and settings.UPDATE_AFFECTING_TIME_SPANS: - update_affecting_time_spans_task.delay() diff --git a/reservations/tasks.py b/reservations/tasks.py deleted file mode 100644 index 1223df07a..000000000 --- a/reservations/tasks.py +++ /dev/null @@ -1,143 +0,0 @@ -import datetime -import uuid -from contextlib import suppress - -from django.conf import settings -from django.db import transaction -from django.db.models import Prefetch -from django.db.transaction import atomic - -from common.date_utils import local_datetime -from config.celery import app -from reservation_units.models import ReservationUnit -from reservations.models import ( - AffectingTimeSpan, - Reservation, - ReservationStatistic, - ReservationStatisticsReservationUnit, -) -from reservations.pruning import ( - prune_inactive_reservations, - prune_recurring_reservations, - prune_reservation_statistics, - prune_reservation_with_inactive_payments, -) -from tilavarauspalvelu.enums import OrderStatus -from tilavarauspalvelu.models import PaymentOrder -from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError -from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError -from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient - - -@app.task(name="prune_reservations") -def prune_reservations_task() -> None: - prune_inactive_reservations() - prune_reservation_with_inactive_payments() - - -@app.task(name="update_expired_orders") -def update_expired_orders_task() -> None: - older_than_minutes = settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES - expired_datetime = local_datetime() - datetime.timedelta(minutes=older_than_minutes) - expired_orders = PaymentOrder.objects.filter( - status=OrderStatus.DRAFT, - created_at__lte=expired_datetime, - remote_id__isnull=False, - ).all() - - for payment_order in expired_orders: - # Do not update the PaymentOrder status if an error occurs - with suppress(GetPaymentError, CancelOrderError), atomic(): - payment_order.refresh_order_status_from_webshop() - - if payment_order.status == OrderStatus.EXPIRED: - payment_order.cancel_order_in_webshop() - - -@app.task(name="prune_reservation_statistics") -def prune_reservation_statistics_task() -> None: - prune_reservation_statistics() - - -@app.task(name="prune_recurring_reservations") -def prune_recurring_reservations_task() -> None: - prune_recurring_reservations() - - -@app.task( - name="refund_paid_reservation", - autoretry_for=(Exception,), - max_retries=5, - retry_backoff=True, -) -def refund_paid_reservation_task(reservation_pk: int) -> None: - reservation = Reservation.objects.filter(pk=reservation_pk).first() - if not reservation: - return - - payment_order: PaymentOrder | None = PaymentOrder.objects.filter(reservation=reservation).first() - if not payment_order: - return - - if not settings.MOCK_VERKKOKAUPPA_API_ENABLED: - refund = VerkkokauppaAPIClient.refund_order(order_uuid=payment_order.remote_id) - payment_order.refund_id = refund.refund_id - else: - payment_order.refund_id = uuid.uuid4() - payment_order.status = OrderStatus.REFUNDED - payment_order.save(update_fields=["refund_id", "status"]) - - -@app.task(name="update_affecting_time_spans") -def update_affecting_time_spans_task() -> None: - AffectingTimeSpan.refresh() - - -@app.task(name="create_statistics_for_reservations") -def create_or_update_reservation_statistics(reservation_pks: list[int]) -> None: - new_statistics: list[ReservationStatistic] = [] - new_statistics_units: list[ReservationStatisticsReservationUnit] = [] - - reservations = ( - Reservation.objects.filter(pk__in=reservation_pks) - .select_related( - "user", - "recurring_reservation", - "recurring_reservation__ability_group", - "recurring_reservation__allocated_time_slot", - "deny_reason", - "cancel_reason", - "purpose", - "home_city", - "age_group", - ) - .prefetch_related( - Prefetch( - "reservation_unit", - queryset=ReservationUnit.objects.select_related("unit"), - ), - ) - ) - - for reservation in reservations: - statistic = ReservationStatistic.for_reservation(reservation, save=False) - statistic_units = ReservationStatisticsReservationUnit.for_statistic(statistic, save=False) - new_statistics.append(statistic) - new_statistics_units.extend(statistic_units) - - fields_to_update: list[str] = [ - field.name - for field in ReservationStatistic._meta.get_fields() - # Update all fields that can be updated - if field.concrete and not field.many_to_many and not field.primary_key - ] - - with transaction.atomic(): - new_statistics = ReservationStatistic.objects.bulk_create( - new_statistics, - update_conflicts=True, - update_fields=fields_to_update, - unique_fields=["reservation"], - ) - ReservationStatisticsReservationUnit.objects.filter(reservation_statistics__in=new_statistics).delete() - ReservationStatisticsReservationUnit.objects.bulk_create(new_statistics_units) diff --git a/reservations/translation.py b/reservations/translation.py deleted file mode 100644 index 6bf15c2db..000000000 --- a/reservations/translation.py +++ /dev/null @@ -1,23 +0,0 @@ -from modeltranslation.translator import TranslationOptions, register - -from reservations.models import AbilityGroup, ReservationCancelReason, ReservationDenyReason, ReservationPurpose - - -@register(AbilityGroup) -class AbilityGroupTranslationOptions(TranslationOptions): - fields = ["name"] - - -@register(ReservationPurpose) -class ReservationPurposeTranslationOptions(TranslationOptions): - fields = ["name"] - - -@register(ReservationCancelReason) -class ReservationCancelReasonTranslationOptions(TranslationOptions): - fields = ["reason"] - - -@register(ReservationDenyReason) -class ReservationDenyReasonTranslationOptions(TranslationOptions): - fields = ["reason"] diff --git a/tests/factories/ability_group.py b/tests/factories/ability_group.py index d5654a5c4..bd65fabf3 100644 --- a/tests/factories/ability_group.py +++ b/tests/factories/ability_group.py @@ -1,6 +1,6 @@ from factory import fuzzy -from reservations.models import AbilityGroup +from tilavarauspalvelu.models import AbilityGroup from ._base import GenericDjangoModelFactory diff --git a/tests/factories/age_group.py b/tests/factories/age_group.py index b105be75b..3117d7930 100644 --- a/tests/factories/age_group.py +++ b/tests/factories/age_group.py @@ -1,6 +1,6 @@ from factory import fuzzy -from reservations.models import AgeGroup +from tilavarauspalvelu.models import AgeGroup from ._base import GenericDjangoModelFactory diff --git a/tests/factories/recurring_reservation.py b/tests/factories/recurring_reservation.py index adc2c685c..2951280af 100644 --- a/tests/factories/recurring_reservation.py +++ b/tests/factories/recurring_reservation.py @@ -4,7 +4,7 @@ from factory import fuzzy from common.date_utils import DEFAULT_TIMEZONE -from reservations.models import RecurringReservation +from tilavarauspalvelu.models import RecurringReservation from ._base import GenericDjangoModelFactory, OneToManyFactory diff --git a/tests/factories/rejected_occurrence.py b/tests/factories/rejected_occurrence.py index 12578dabc..00a3ddd16 100644 --- a/tests/factories/rejected_occurrence.py +++ b/tests/factories/rejected_occurrence.py @@ -3,8 +3,8 @@ import factory from factory import fuzzy -from reservations.enums import RejectionReadinessChoice -from reservations.models import RejectedOccurrence +from tilavarauspalvelu.enums import RejectionReadinessChoice +from tilavarauspalvelu.models import RejectedOccurrence from ._base import GenericDjangoModelFactory diff --git a/tests/factories/reservation.py b/tests/factories/reservation.py index 171a10fc6..4e5307131 100644 --- a/tests/factories/reservation.py +++ b/tests/factories/reservation.py @@ -7,9 +7,8 @@ from common.date_utils import local_start_of_day, next_hour from reservation_units.enums import PricingType from reservation_units.models import ReservationUnit -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation -from tilavarauspalvelu.enums import OrderStatus, PaymentType +from tilavarauspalvelu.enums import OrderStatus, PaymentType, ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import Reservation from ._base import GenericDjangoModelFactory, ManyToManyFactory, NullableSubFactory, OneToManyFactory diff --git a/tests/factories/reservation_cancel_reason.py b/tests/factories/reservation_cancel_reason.py index b2461ad7d..edd54d838 100644 --- a/tests/factories/reservation_cancel_reason.py +++ b/tests/factories/reservation_cancel_reason.py @@ -1,6 +1,6 @@ from factory import fuzzy -from reservations.models import ReservationCancelReason +from tilavarauspalvelu.models import ReservationCancelReason from ._base import GenericDjangoModelFactory, OneToManyFactory diff --git a/tests/factories/reservation_deny_reason.py b/tests/factories/reservation_deny_reason.py index 603dc3c0d..af19390d5 100644 --- a/tests/factories/reservation_deny_reason.py +++ b/tests/factories/reservation_deny_reason.py @@ -1,5 +1,5 @@ -from reservations.models import ReservationDenyReason from tests.factories._base import GenericDjangoModelFactory +from tilavarauspalvelu.models import ReservationDenyReason __all__ = [ "ReservationDenyReasonFactory", diff --git a/tests/factories/reservation_metadata.py b/tests/factories/reservation_metadata.py index 5c06f8caf..2ac187331 100644 --- a/tests/factories/reservation_metadata.py +++ b/tests/factories/reservation_metadata.py @@ -2,7 +2,7 @@ from factory import fuzzy -from reservations.models import ReservationMetadataField, ReservationMetadataSet +from tilavarauspalvelu.models import ReservationMetadataField, ReservationMetadataSet from ._base import GenericDjangoModelFactory, ManyToManyFactory diff --git a/tests/factories/reservation_purpose.py b/tests/factories/reservation_purpose.py index 38ef3b91a..2d15bd3aa 100644 --- a/tests/factories/reservation_purpose.py +++ b/tests/factories/reservation_purpose.py @@ -1,7 +1,7 @@ from factory import fuzzy -from reservations.models import ReservationPurpose from tests.factories._base import GenericDjangoModelFactory +from tilavarauspalvelu.models import ReservationPurpose __all__ = [ "ReservationPurposeFactory", diff --git a/tests/test_actions/test_application_round_actions.py b/tests/test_actions/test_application_round_actions.py index e4d9c2c73..f7165a749 100644 --- a/tests/test_actions/test_application_round_actions.py +++ b/tests/test_actions/test_application_round_actions.py @@ -2,9 +2,9 @@ from applications.enums import ApplicationRoundStatusChoice from applications.models import AllocatedTimeSlot, ReservationUnitOption -from reservations.enums import ReservationTypeChoice -from reservations.models import RecurringReservation, Reservation from tests.factories import AllocatedTimeSlotFactory, ApplicationRoundFactory, ReservationFactory +from tilavarauspalvelu.enums import ReservationTypeChoice +from tilavarauspalvelu.models import RecurringReservation, Reservation # Applied to all tests pytestmark = [ diff --git a/tests/test_actions/test_reservation_unit_actions_check_reservation_overlap.py b/tests/test_actions/test_reservation_unit_actions_check_reservation_overlap.py index 7a1502dc5..c2aa237c7 100644 --- a/tests/test_actions/test_reservation_unit_actions_check_reservation_overlap.py +++ b/tests/test_actions/test_reservation_unit_actions_check_reservation_overlap.py @@ -4,8 +4,8 @@ from common.date_utils import local_datetime from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice from tests.factories import ReservationFactory, ReservationUnitFactory, ServiceFactory, SpaceFactory +from tilavarauspalvelu.enums import ReservationStateChoice # Applied to all tests pytestmark = [ diff --git a/tests/test_actions/test_reservation_unit_actions_get_next_reservation.py b/tests/test_actions/test_reservation_unit_actions_get_next_reservation.py index 13a1cb570..3a990a36d 100644 --- a/tests/test_actions/test_reservation_unit_actions_get_next_reservation.py +++ b/tests/test_actions/test_reservation_unit_actions_get_next_reservation.py @@ -5,8 +5,8 @@ from freezegun import freeze_time from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import ReservationFactory, ReservationUnitFactory, SpaceFactory +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice if TYPE_CHECKING: from reservation_units.models import ReservationUnit diff --git a/tests/test_actions/test_reservation_unit_actions_unit_get_previous_reservation.py b/tests/test_actions/test_reservation_unit_actions_unit_get_previous_reservation.py index d7a06228f..dd114a639 100644 --- a/tests/test_actions/test_reservation_unit_actions_unit_get_previous_reservation.py +++ b/tests/test_actions/test_reservation_unit_actions_unit_get_previous_reservation.py @@ -4,8 +4,8 @@ from freezegun import freeze_time from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import ReservationFactory, ReservationUnitFactory, SpaceFactory +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice # Applied to all tests pytestmark = [ diff --git a/tests/test_email/test_email_builder_render_reservation.py b/tests/test_email/test_email_builder_render_reservation.py index c25ecce0c..7c19558b2 100644 --- a/tests/test_email/test_email_builder_render_reservation.py +++ b/tests/test_email/test_email_builder_render_reservation.py @@ -6,11 +6,10 @@ import pytest from django.core.files.uploadedfile import SimpleUploadedFile -from reservations.models import Reservation from tests.factories import EmailTemplateFactory, ReservationFactory, ReservationUnitFactory from tilavarauspalvelu.enums import EmailType from tilavarauspalvelu.exceptions import EmailTemplateValidationError -from tilavarauspalvelu.models import EmailTemplate +from tilavarauspalvelu.models import EmailTemplate, Reservation from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailBuilder, ReservationEmailContext pytestmark = [ diff --git a/tests/test_email/test_email_context_reservation.py b/tests/test_email/test_email_context_reservation.py index bfc511e05..f5025b47d 100644 --- a/tests/test_email/test_email_context_reservation.py +++ b/tests/test_email/test_email_context_reservation.py @@ -4,8 +4,6 @@ from django.utils.timezone import get_default_timezone from common.utils import get_attr_by_language -from reservations.enums import CustomerTypeChoice -from reservations.models import Reservation from tests.factories import ( LocationFactory, ReservationCancelReasonFactory, @@ -14,7 +12,8 @@ ReservationUnitFactory, UserFactory, ) -from tilavarauspalvelu.models import Location +from tilavarauspalvelu.enums import CustomerTypeChoice +from tilavarauspalvelu.models import Location, Reservation from tilavarauspalvelu.utils.email.email_builder_reservation import ReservationEmailContext if TYPE_CHECKING: diff --git a/tests/test_email/test_email_sender_reservation.py b/tests/test_email/test_email_sender_reservation.py index 65f25e73c..4db30384f 100644 --- a/tests/test_email/test_email_sender_reservation.py +++ b/tests/test_email/test_email_sender_reservation.py @@ -13,7 +13,7 @@ from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender if TYPE_CHECKING: - from reservations.models import Reservation + from tilavarauspalvelu.models import Reservation # Applied to all tests pytestmark = [ diff --git a/tests/test_external_services/test_verkkokauppa/test_create_order_params.py b/tests/test_external_services/test_verkkokauppa/test_create_order_params.py index 61c36c5b5..a8344e094 100644 --- a/tests/test_external_services/test_verkkokauppa/test_create_order_params.py +++ b/tests/test_external_services/test_verkkokauppa/test_create_order_params.py @@ -6,8 +6,8 @@ from django.conf import settings from common.date_utils import local_datetime -from reservations.enums import CustomerTypeChoice from tests.factories import PaymentProductFactory, ReservationFactory, ReservationUnitFactory, UserFactory +from tilavarauspalvelu.enums import CustomerTypeChoice from tilavarauspalvelu.utils.verkkokauppa.helpers import get_verkkokauppa_order_params # Applied to all tests diff --git a/tests/test_external_services/test_verkkokauppa/test_helpers.py b/tests/test_external_services/test_verkkokauppa/test_helpers.py index 3fd7d68b1..cd736bd3e 100644 --- a/tests/test_external_services/test_verkkokauppa/test_helpers.py +++ b/tests/test_external_services/test_verkkokauppa/test_helpers.py @@ -4,8 +4,8 @@ from django.utils.timezone import get_default_timezone from freezegun import freeze_time -from reservations.enums import CustomerTypeChoice from tests.factories import PaymentProductFactory, ReservationFactory, ReservationUnitFactory, UserFactory +from tilavarauspalvelu.enums import CustomerTypeChoice from tilavarauspalvelu.utils.verkkokauppa.exceptions import UnsupportedMetaKeyError from tilavarauspalvelu.utils.verkkokauppa.helpers import ( get_formatted_reservation_time, diff --git a/tests/test_external_services/test_verkkokauppa/test_pruning.py b/tests/test_external_services/test_verkkokauppa/test_pruning.py index f101658cf..5d13461a2 100644 --- a/tests/test_external_services/test_verkkokauppa/test_pruning.py +++ b/tests/test_external_services/test_verkkokauppa/test_pruning.py @@ -4,11 +4,10 @@ from freezegun import freeze_time from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from reservations.tasks import update_expired_orders_task from tests.factories import PaymentFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.tasks import update_expired_orders_task from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError diff --git a/tests/test_external_services/test_verkkokauppa/test_tasks.py b/tests/test_external_services/test_verkkokauppa/test_tasks.py index 7700446db..a6cfa1a32 100644 --- a/tests/test_external_services/test_verkkokauppa/test_tasks.py +++ b/tests/test_external_services/test_verkkokauppa/test_tasks.py @@ -3,9 +3,9 @@ import pytest -from reservations.tasks import refund_paid_reservation_task from tests.factories import PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method +from tilavarauspalvelu.tasks import refund_paid_reservation_task from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient # Applied to all tests diff --git a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py index 49f46eaac..75324b5d7 100644 --- a/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py +++ b/tests/test_external_services/test_verkkokauppa/test_webhooks/test_order_payment_webhooks.py @@ -3,11 +3,10 @@ import pytest from django.urls import reverse -from reservations.enums import ReservationStateChoice from tests.factories import PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method from tests.test_external_services.test_verkkokauppa.test_webhooks.helpers import get_mock_order_payment_api -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient diff --git a/tests/test_gdpr_api/test_gdpr_api.py b/tests/test_gdpr_api/test_gdpr_api.py index 82570caf5..a87b001b9 100644 --- a/tests/test_gdpr_api/test_gdpr_api.py +++ b/tests/test_gdpr_api/test_gdpr_api.py @@ -7,16 +7,15 @@ from django.urls import reverse from django.utils import timezone -from reservations.enums import ReservationStateChoice from tests.factories import ApplicationFactory, PaymentOrderFactory, ReservationFactory, UserFactory -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice from .helpers import get_gdpr_auth_header, patch_oidc_config if TYPE_CHECKING: from applications.models import Application, ApplicationSection - from reservations.models import Reservation - from tilavarauspalvelu.models import User + from tilavarauspalvelu.models import Reservation, User + # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_order/test_refresh.py b/tests/test_graphql_api/test_order/test_refresh.py index 972fe7cdb..a5ff601f6 100644 --- a/tests/test_graphql_api/test_order/test_refresh.py +++ b/tests/test_graphql_api/test_order/test_refresh.py @@ -2,10 +2,9 @@ import pytest -from reservations.enums import ReservationStateChoice from tests.factories import PaymentFactory from tests.helpers import patch_method -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus diff --git a/tests/test_graphql_api/test_recurring_reservation/helpers.py b/tests/test_graphql_api/test_recurring_reservation/helpers.py index 705b5444f..5885128e2 100644 --- a/tests/test_graphql_api/test_recurring_reservation/helpers.py +++ b/tests/test_graphql_api/test_recurring_reservation/helpers.py @@ -5,7 +5,7 @@ from graphene_django_extensions.testing import build_mutation, build_query from reservation_units.models import ReservationUnit -from reservations.enums import ReservationTypeChoice +from tilavarauspalvelu.enums import ReservationTypeChoice from tilavarauspalvelu.models import User recurring_reservations_query = partial(build_query, "recurringReservations", connection=True, order_by="nameAsc") diff --git a/tests/test_graphql_api/test_recurring_reservation/test_create.py b/tests/test_graphql_api/test_recurring_reservation/test_create.py index cb17b1232..68ebf3f1e 100644 --- a/tests/test_graphql_api/test_recurring_reservation/test_create.py +++ b/tests/test_graphql_api/test_recurring_reservation/test_create.py @@ -4,8 +4,8 @@ from common.date_utils import local_date from reservation_units.enums import ReservationStartInterval -from reservations.models import RecurringReservation from tests.factories import AbilityGroupFactory, AgeGroupFactory, ReservationUnitFactory +from tilavarauspalvelu.models import RecurringReservation from .helpers import CREATE_MUTATION, get_minimal_create_date diff --git a/tests/test_graphql_api/test_recurring_reservation/test_create_permissions.py b/tests/test_graphql_api/test_recurring_reservation/test_create_permissions.py index 749a57ddd..41f0c5c94 100644 --- a/tests/test_graphql_api/test_recurring_reservation/test_create_permissions.py +++ b/tests/test_graphql_api/test_recurring_reservation/test_create_permissions.py @@ -1,7 +1,7 @@ import pytest -from reservations.models import RecurringReservation from tests.factories import ReservationUnitFactory, UserFactory +from tilavarauspalvelu.models import RecurringReservation from .helpers import CREATE_MUTATION, get_minimal_create_date diff --git a/tests/test_graphql_api/test_recurring_reservation/test_create_series.py b/tests/test_graphql_api/test_recurring_reservation/test_create_series.py index bd53f69ed..865fedeb0 100644 --- a/tests/test_graphql_api/test_recurring_reservation/test_create_series.py +++ b/tests/test_graphql_api/test_recurring_reservation/test_create_series.py @@ -4,13 +4,6 @@ from common.date_utils import DEFAULT_TIMEZONE, combine, local_date, local_end_of_day, local_start_of_day from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ( - CustomerTypeChoice, - ReservationStateChoice, - ReservationTypeChoice, - ReservationTypeStaffChoice, -) -from reservations.models import AffectingTimeSpan, RecurringReservation, Reservation from tests.factories import ( AbilityGroupFactory, AgeGroupFactory, @@ -22,6 +15,13 @@ SpaceFactory, UserFactory, ) +from tilavarauspalvelu.enums import ( + CustomerTypeChoice, + ReservationStateChoice, + ReservationTypeChoice, + ReservationTypeStaffChoice, +) +from tilavarauspalvelu.models import AffectingTimeSpan, RecurringReservation, Reservation from .helpers import CREATE_SERIES_MUTATION, get_minimal_series_data diff --git a/tests/test_graphql_api/test_recurring_reservation/test_query.py b/tests/test_graphql_api/test_recurring_reservation/test_query.py index e34e92216..6d69474db 100644 --- a/tests/test_graphql_api/test_recurring_reservation/test_query.py +++ b/tests/test_graphql_api/test_recurring_reservation/test_query.py @@ -1,8 +1,8 @@ import freezegun import pytest -from reservations.enums import RejectionReadinessChoice from tests.factories import RecurringReservationFactory +from tilavarauspalvelu.enums import RejectionReadinessChoice from .helpers import recurring_reservations_query diff --git a/tests/test_graphql_api/test_rejected_occurrence/test_query.py b/tests/test_graphql_api/test_rejected_occurrence/test_query.py index c9b8c1890..4e9ce5199 100644 --- a/tests/test_graphql_api/test_rejected_occurrence/test_query.py +++ b/tests/test_graphql_api/test_rejected_occurrence/test_query.py @@ -2,8 +2,8 @@ import pytest -from reservations.enums import RejectionReadinessChoice from tests.factories import RejectedOccurrenceFactory +from tilavarauspalvelu.enums import RejectionReadinessChoice from .helpers import rejected_occurrence_query diff --git a/tests/test_graphql_api/test_reservation/helpers.py b/tests/test_graphql_api/test_reservation/helpers.py index 848235e37..74bf34d38 100644 --- a/tests/test_graphql_api/test_reservation/helpers.py +++ b/tests/test_graphql_api/test_reservation/helpers.py @@ -7,11 +7,11 @@ from common.date_utils import next_hour from reservation_units.models import ReservationUnit -from reservations.enums import ReservationTypeChoice -from reservations.models import Reservation from tests.factories import ReservationCancelReasonFactory, ReservationDenyReasonFactory from tests.factories.helsinki_profile import MyProfileDataFactory from tests.helpers import ResponseMock, patch_method +from tilavarauspalvelu.enums import ReservationTypeChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.helauth.clients import HelsinkiProfileClient reservation_query = partial(build_query, "reservation") diff --git a/tests/test_graphql_api/test_reservation/test_adjust_time.py b/tests/test_graphql_api/test_reservation/test_adjust_time.py index 02b094d1d..df5965b44 100644 --- a/tests/test_graphql_api/test_reservation/test_adjust_time.py +++ b/tests/test_graphql_api/test_reservation/test_adjust_time.py @@ -9,8 +9,6 @@ from common.date_utils import local_date, local_datetime from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tests.factories import ( ApplicationRoundFactory, EmailTemplateFactory, @@ -22,7 +20,8 @@ SpaceFactory, UserFactory, ) -from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.enums import EmailType, ReservationStateChoice +from tilavarauspalvelu.models import Reservation from .helpers import ADJUST_MUTATION, get_adjust_data diff --git a/tests/test_graphql_api/test_reservation/test_affecting_reservations.py b/tests/test_graphql_api/test_reservation/test_affecting_reservations.py index a91bc1297..9bfc1b6ec 100644 --- a/tests/test_graphql_api/test_reservation/test_affecting_reservations.py +++ b/tests/test_graphql_api/test_reservation/test_affecting_reservations.py @@ -6,8 +6,8 @@ from graphene_django_extensions.testing import build_query from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice from tests.factories import ReservationFactory, ReservationUnitFactory, SpaceFactory, UnitFactory +from tilavarauspalvelu.enums import ReservationStateChoice # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_reservation/test_approve.py b/tests/test_graphql_api/test_reservation/test_approve.py index 2732f033e..bb1b10ac4 100644 --- a/tests/test_graphql_api/test_reservation/test_approve.py +++ b/tests/test_graphql_api/test_reservation/test_approve.py @@ -1,7 +1,7 @@ import pytest -from reservations.enums import ReservationStateChoice from tests.factories import ReservationFactory, ReservationUnitFactory +from tilavarauspalvelu.enums import ReservationStateChoice from .helpers import APPROVE_MUTATION, get_approve_data diff --git a/tests/test_graphql_api/test_reservation/test_approve_permissions.py b/tests/test_graphql_api/test_reservation/test_approve_permissions.py index 985bec21a..cd1535b1c 100644 --- a/tests/test_graphql_api/test_reservation/test_approve_permissions.py +++ b/tests/test_graphql_api/test_reservation/test_approve_permissions.py @@ -1,8 +1,7 @@ import pytest -from reservations.enums import ReservationStateChoice from tests.factories import ReservationFactory -from tilavarauspalvelu.enums import UserRoleChoice +from tilavarauspalvelu.enums import ReservationStateChoice, UserRoleChoice from .helpers import APPROVE_MUTATION, get_approve_data diff --git a/tests/test_graphql_api/test_reservation/test_cancel.py b/tests/test_graphql_api/test_reservation/test_cancel.py index e30e271ca..c782b598b 100644 --- a/tests/test_graphql_api/test_reservation/test_cancel.py +++ b/tests/test_graphql_api/test_reservation/test_cancel.py @@ -6,11 +6,10 @@ import pytest from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from reservations.models import ReservationCancelReason from tests.factories import EmailTemplateFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method -from tilavarauspalvelu.enums import EmailType, OrderStatus, PaymentType +from tilavarauspalvelu.enums import EmailType, OrderStatus, PaymentType, ReservationStateChoice +from tilavarauspalvelu.models import ReservationCancelReason from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from .helpers import CANCEL_MUTATION, get_cancel_data diff --git a/tests/test_graphql_api/test_reservation/test_confirm.py b/tests/test_graphql_api/test_reservation/test_confirm.py index 7c3873688..cd763ad04 100644 --- a/tests/test_graphql_api/test_reservation/test_confirm.py +++ b/tests/test_graphql_api/test_reservation/test_confirm.py @@ -4,7 +4,6 @@ from graphene_django_extensions.testing import build_mutation from reservation_units.enums import PricingType -from reservations.enums import ReservationStateChoice from tests.factories import ( EmailTemplateFactory, OrderFactory, @@ -14,7 +13,7 @@ UserFactory, ) from tests.helpers import patch_method -from tilavarauspalvelu.enums import EmailType, OrderStatus, PaymentType, ReservationNotification +from tilavarauspalvelu.enums import EmailType, OrderStatus, PaymentType, ReservationNotification, ReservationStateChoice from tilavarauspalvelu.models import PaymentOrder from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CreateOrderError from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient diff --git a/tests/test_graphql_api/test_reservation/test_create.py b/tests/test_graphql_api/test_reservation/test_create.py index 1f3b47541..c3a9cb77c 100644 --- a/tests/test_graphql_api/test_reservation/test_create.py +++ b/tests/test_graphql_api/test_reservation/test_create.py @@ -10,8 +10,6 @@ from common.date_utils import local_datetime, local_end_of_day, local_start_of_day, next_hour from reservation_units.enums import PriceUnit, PricingStatus, ReservationKind from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation from tests.factories import ( AgeGroupFactory, ApplicationRoundFactory, @@ -26,6 +24,8 @@ UserFactory, ) from tests.helpers import ResponseMock, patch_method +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.helauth.clients import HelsinkiProfileClient from tilavarauspalvelu.utils.helauth.typing import ADLoginAMR from utils.decimal_utils import round_decimal diff --git a/tests/test_graphql_api/test_reservation/test_delete.py b/tests/test_graphql_api/test_reservation/test_delete.py index 416f894f4..de721390a 100644 --- a/tests/test_graphql_api/test_reservation/test_delete.py +++ b/tests/test_graphql_api/test_reservation/test_delete.py @@ -2,11 +2,10 @@ import pytest -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tests.factories import OrderFactory, PaymentFactory, PaymentOrderFactory, ReservationFactory from tests.helpers import patch_method -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from tilavarauspalvelu.utils.verkkokauppa.payment.types import PaymentStatus diff --git a/tests/test_graphql_api/test_reservation/test_delete_permissions.py b/tests/test_graphql_api/test_reservation/test_delete_permissions.py index 8270417ba..3bc3db4db 100644 --- a/tests/test_graphql_api/test_reservation/test_delete_permissions.py +++ b/tests/test_graphql_api/test_reservation/test_delete_permissions.py @@ -1,7 +1,7 @@ import pytest -from reservations.models import Reservation from tests.factories import ReservationFactory, UserFactory +from tilavarauspalvelu.models import Reservation from .helpers import DELETE_MUTATION, get_delete_data diff --git a/tests/test_graphql_api/test_reservation/test_deny.py b/tests/test_graphql_api/test_reservation/test_deny.py index a08652edb..ee2dabafa 100644 --- a/tests/test_graphql_api/test_reservation/test_deny.py +++ b/tests/test_graphql_api/test_reservation/test_deny.py @@ -3,9 +3,8 @@ import pytest from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import EmailTemplateFactory, ReservationFactory -from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.enums import EmailType, ReservationStateChoice, ReservationTypeChoice from .helpers import DENY_MUTATION, get_deny_data diff --git a/tests/test_graphql_api/test_reservation/test_filtering.py b/tests/test_graphql_api/test_reservation/test_filtering.py index 52edba442..aceda4c50 100644 --- a/tests/test_graphql_api/test_reservation/test_filtering.py +++ b/tests/test_graphql_api/test_reservation/test_filtering.py @@ -5,7 +5,6 @@ from freezegun import freeze_time from common.date_utils import DEFAULT_TIMEZONE -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import ( PaymentOrderFactory, RecurringReservationFactory, @@ -17,7 +16,7 @@ UserFactory, ) from tests.test_graphql_api.test_reservation.helpers import reservations_query -from tilavarauspalvelu.enums import OrderStatus, UserRoleChoice +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice, ReservationTypeChoice, UserRoleChoice # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_reservation/test_ordering.py b/tests/test_graphql_api/test_reservation/test_ordering.py index df4510211..ab25ac9ea 100644 --- a/tests/test_graphql_api/test_reservation/test_ordering.py +++ b/tests/test_graphql_api/test_reservation/test_ordering.py @@ -3,10 +3,9 @@ import pytest from django.utils import timezone -from reservations.enums import CustomerTypeChoice from tests.factories import PaymentOrderFactory, ReservationFactory, ReservationUnitFactory from tests.test_graphql_api.test_reservation.helpers import reservations_query -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import CustomerTypeChoice, OrderStatus # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_reservation/test_query.py b/tests/test_graphql_api/test_reservation/test_query.py index effbd430d..7b7b6d72f 100644 --- a/tests/test_graphql_api/test_reservation/test_query.py +++ b/tests/test_graphql_api/test_reservation/test_query.py @@ -5,7 +5,6 @@ import pytest from graphql_relay import to_global_id -from reservations.enums import CustomerTypeChoice, ReservationTypeChoice from tests.factories import ( PaymentOrderFactory, ReservationFactory, @@ -14,12 +13,13 @@ UnitGroupFactory, UserFactory, ) +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationTypeChoice from tilavarauspalvelu.models import PersonalInfoViewLog from .helpers import reservation_query, reservations_query if TYPE_CHECKING: - from reservations.models import Reservation + from tilavarauspalvelu.models import Reservation # Applied to all tests pytestmark = [ diff --git a/tests/test_graphql_api/test_reservation/test_query_permissions.py b/tests/test_graphql_api/test_reservation/test_query_permissions.py index 018d63f2c..d43298d1a 100644 --- a/tests/test_graphql_api/test_reservation/test_query_permissions.py +++ b/tests/test_graphql_api/test_reservation/test_query_permissions.py @@ -2,7 +2,6 @@ import pytest -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from tests.factories import ( AgeGroupFactory, CityFactory, @@ -13,6 +12,7 @@ UnitFactory, UserFactory, ) +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from .helpers import reservations_query diff --git a/tests/test_graphql_api/test_reservation/test_refund.py b/tests/test_graphql_api/test_reservation/test_refund.py index deca3e6c4..f52b32b4b 100644 --- a/tests/test_graphql_api/test_reservation/test_refund.py +++ b/tests/test_graphql_api/test_reservation/test_refund.py @@ -6,10 +6,9 @@ import pytest from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice from tests.factories import ReservationFactory, UserFactory from tests.helpers import patch_method -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from .helpers import REFUND_MUTATION, get_refund_data diff --git a/tests/test_graphql_api/test_reservation/test_require_handling.py b/tests/test_graphql_api/test_reservation/test_require_handling.py index efc94db03..04ea47cc1 100644 --- a/tests/test_graphql_api/test_reservation/test_require_handling.py +++ b/tests/test_graphql_api/test_reservation/test_require_handling.py @@ -1,8 +1,7 @@ import pytest -from reservations.enums import ReservationStateChoice from tests.factories import EmailTemplateFactory, ReservationFactory, UserFactory -from tilavarauspalvelu.enums import EmailType, ReservationNotification +from tilavarauspalvelu.enums import EmailType, ReservationNotification, ReservationStateChoice from .helpers import REQUIRE_HANDLING_MUTATION, get_require_handling_data diff --git a/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py b/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py index 30136f445..7e3222332 100644 --- a/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py +++ b/tests/test_graphql_api/test_reservation/test_staff_adjust_time.py @@ -6,9 +6,8 @@ from common.date_utils import DEFAULT_TIMEZONE, local_datetime, next_hour from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import EmailTemplateFactory, ReservationFactory -from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.enums import EmailType, ReservationStateChoice, ReservationTypeChoice from .helpers import ADJUST_STAFF_MUTATION, get_staff_adjust_data diff --git a/tests/test_graphql_api/test_reservation/test_staff_create.py b/tests/test_graphql_api/test_reservation/test_staff_create.py index 51f90d60d..ab6658c6e 100644 --- a/tests/test_graphql_api/test_reservation/test_staff_create.py +++ b/tests/test_graphql_api/test_reservation/test_staff_create.py @@ -6,8 +6,6 @@ from common.date_utils import local_datetime, next_hour, timedelta_to_json from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation from tests.factories import ( AgeGroupFactory, CityFactory, @@ -20,6 +18,8 @@ SpaceFactory, UserFactory, ) +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import Reservation from .helpers import CREATE_STAFF_MUTATION, get_staff_create_data diff --git a/tests/test_graphql_api/test_reservation/test_staff_modify.py b/tests/test_graphql_api/test_reservation/test_staff_modify.py index a6f61964f..0c1c2a229 100644 --- a/tests/test_graphql_api/test_reservation/test_staff_modify.py +++ b/tests/test_graphql_api/test_reservation/test_staff_modify.py @@ -3,8 +3,8 @@ import pytest from common.date_utils import next_hour -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import ReservationFactory +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice from .helpers import UPDATE_STAFF_MUTATION, get_staff_modify_data diff --git a/tests/test_graphql_api/test_reservation/test_staff_update.py b/tests/test_graphql_api/test_reservation/test_staff_update.py index 9bf43a76b..6aca34c21 100644 --- a/tests/test_graphql_api/test_reservation/test_staff_update.py +++ b/tests/test_graphql_api/test_reservation/test_staff_update.py @@ -5,8 +5,6 @@ from django.utils.timezone import get_default_timezone from common.date_utils import timedelta_to_json -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tests.factories import ( OriginHaukiResourceFactory, ReservableTimeSpanFactory, @@ -16,6 +14,8 @@ UserFactory, ) from tests.test_graphql_api.test_reservation.helpers import ADJUST_STAFF_MUTATION +from tilavarauspalvelu.enums import ReservationStateChoice +from tilavarauspalvelu.models import Reservation DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tests/test_graphql_api/test_reservation/test_update.py b/tests/test_graphql_api/test_reservation/test_update.py index 2e88ebe38..9ba045a83 100644 --- a/tests/test_graphql_api/test_reservation/test_update.py +++ b/tests/test_graphql_api/test_reservation/test_update.py @@ -6,7 +6,6 @@ from common.date_utils import local_datetime from reservation_units.enums import PriceUnit, PricingStatus from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from tests.factories import ( ApplicationRoundFactory, CityFactory, @@ -14,6 +13,7 @@ ReservationMetadataSetFactory, ReservationUnitPricingFactory, ) +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice from utils.decimal_utils import round_decimal from .helpers import UPDATE_MUTATION, get_update_data, mock_profile_reader diff --git a/tests/test_graphql_api/test_reservation_unit/test_query.py b/tests/test_graphql_api/test_reservation_unit/test_query.py index 611e8370b..ca8e06fb8 100644 --- a/tests/test_graphql_api/test_reservation_unit/test_query.py +++ b/tests/test_graphql_api/test_reservation_unit/test_query.py @@ -7,7 +7,6 @@ from applications.enums import WeekdayChoice from common.date_utils import local_datetime, next_hour from reservation_units.enums import PricingType, ReservationUnitPublishingState -from reservations.enums import ReservationStateChoice, ReservationTypeChoice from tests.factories import ( ApplicationRoundFactory, ApplicationRoundTimeSlotFactory, @@ -27,7 +26,7 @@ TermsOfUseFactory, UserFactory, ) -from tilavarauspalvelu.enums import TermsOfUseTypeChoices +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice, TermsOfUseTypeChoices from .helpers import reservation_unit_query, reservation_units_query diff --git a/tests/test_graphql_api/test_reservation_unit/test_query_first_reservable_time.py b/tests/test_graphql_api/test_reservation_unit/test_query_first_reservable_time.py index 1b8c8531e..87dd73a30 100644 --- a/tests/test_graphql_api/test_reservation_unit/test_query_first_reservable_time.py +++ b/tests/test_graphql_api/test_reservation_unit/test_query_first_reservable_time.py @@ -15,8 +15,6 @@ from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnit, ReservationUnitHierarchy from reservation_units.utils.first_reservable_time_helper.first_reservable_time_helper import CachedReservableTime -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import AffectingTimeSpan from tests.factories import ( ApplicationRoundFactory, OriginHaukiResourceFactory, @@ -26,6 +24,8 @@ ResourceFactory, SpaceFactory, ) +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import AffectingTimeSpan from .helpers import reservation_units_query diff --git a/tests/test_models/test_reservation_reservee_name.py b/tests/test_models/test_reservation_reservee_name.py index 627185a26..e1be411a8 100644 --- a/tests/test_models/test_reservation_reservee_name.py +++ b/tests/test_models/test_reservation_reservee_name.py @@ -3,8 +3,8 @@ import pytest from graphene_django_extensions.testing.utils import parametrize_helper -from reservations.enums import CustomerTypeChoice, ReservationTypeChoice -from tests.factories import RecurringReservationFactory, ReservationFactory +from tests.factories import ReservationFactory +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationTypeChoice class Params(NamedTuple): @@ -26,7 +26,7 @@ class Params(NamedTuple): "Type: STAFF | Recurring Reservation": Params( fields={ "type": ReservationTypeChoice.STAFF, - "recurring_reservation": RecurringReservationFactory.build(name="Recurring"), + "recurring_reservation__name": "Recurring", "name": "Reservation Name", "reservee_first_name": "First", "reservee_last_name": "Last", @@ -36,7 +36,7 @@ class Params(NamedTuple): "Type: STAFF | Recurring Reservation has no name": Params( fields={ "type": ReservationTypeChoice.STAFF, - "recurring_reservation": RecurringReservationFactory.build(name=""), + "recurring_reservation__name": "", "name": "Reservation Name", "reservee_first_name": "First", "reservee_last_name": "Last", diff --git a/tests/test_models/test_reservation_statistics.py b/tests/test_models/test_reservation_statistics.py index b2f39ba9e..1c3392d80 100644 --- a/tests/test_models/test_reservation_statistics.py +++ b/tests/test_models/test_reservation_statistics.py @@ -5,8 +5,6 @@ from django.utils.timezone import get_default_timezone from applications.models import City -from reservations.enums import CustomerTypeChoice, ReservationStateChoice -from reservations.models import AgeGroup, ReservationStatistic from tests.factories import ( RecurringReservationFactory, ReservationCancelReasonFactory, @@ -15,6 +13,8 @@ ReservationUnitFactory, UnitFactory, ) +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice +from tilavarauspalvelu.models import AgeGroup, ReservationStatistic # Applied to all tests pytestmark = [ diff --git a/tests/test_models/test_reservation_unit_reservation_scheduler.py b/tests/test_models/test_reservation_unit_reservation_scheduler.py index 2ad74224e..0fb3bc0ea 100644 --- a/tests/test_models/test_reservation_unit_reservation_scheduler.py +++ b/tests/test_models/test_reservation_unit_reservation_scheduler.py @@ -5,7 +5,6 @@ from freezegun import freeze_time from reservation_units.models import ReservationUnit -from reservations.enums import ReservationStateChoice from tests.factories import ( ApplicationRoundFactory, OriginHaukiResourceFactory, @@ -14,6 +13,7 @@ ReservationUnitFactory, SpaceFactory, ) +from tilavarauspalvelu.enums import ReservationStateChoice DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tests/test_querysets/test_reservation_querysets.py b/tests/test_querysets/test_reservation_querysets.py index 5645309b3..fe12e7e64 100644 --- a/tests/test_querysets/test_reservation_querysets.py +++ b/tests/test_querysets/test_reservation_querysets.py @@ -5,9 +5,9 @@ from common.date_utils import DEFAULT_TIMEZONE, local_date from reservation_units.models import ReservationUnit, ReservationUnitHierarchy -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import AffectingTimeSpan, Reservation from tests.factories import ReservationFactory, ReservationUnitFactory, ResourceFactory, SpaceFactory, UnitFactory +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import AffectingTimeSpan, Reservation from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement # Applied to all tests diff --git a/tests/test_tasks/test_prune_inactive_reservations.py b/tests/test_tasks/test_prune_inactive_reservations.py index bd8ad71d4..7379e2813 100644 --- a/tests/test_tasks/test_prune_inactive_reservations.py +++ b/tests/test_tasks/test_prune_inactive_reservations.py @@ -3,10 +3,10 @@ import pytest from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation -from reservations.pruning import prune_inactive_reservations from tests.factories import ReservationFactory +from tilavarauspalvelu.enums import ReservationStateChoice +from tilavarauspalvelu.models import Reservation +from tilavarauspalvelu.utils.pruning import prune_inactive_reservations # Applied to all tests pytestmark = [ diff --git a/tests/test_tasks/test_prune_recurring_reservations.py b/tests/test_tasks/test_prune_recurring_reservations.py index e3cf2cf9d..9d5a4adea 100644 --- a/tests/test_tasks/test_prune_recurring_reservations.py +++ b/tests/test_tasks/test_prune_recurring_reservations.py @@ -4,9 +4,9 @@ from freezegun import freeze_time from common.date_utils import local_datetime -from reservations.models import RecurringReservation -from reservations.pruning import prune_recurring_reservations from tests.factories import RecurringReservationFactory +from tilavarauspalvelu.models import RecurringReservation +from tilavarauspalvelu.utils.pruning import prune_recurring_reservations # Applied to all tests pytestmark = [ diff --git a/tests/test_tasks/test_prune_reservation_statistics.py b/tests/test_tasks/test_prune_reservation_statistics.py index 12ac85aa7..b0786ce81 100644 --- a/tests/test_tasks/test_prune_reservation_statistics.py +++ b/tests/test_tasks/test_prune_reservation_statistics.py @@ -2,9 +2,9 @@ from dateutil.relativedelta import relativedelta from common.date_utils import local_datetime -from reservations.models import ReservationStatistic -from reservations.pruning import prune_reservation_statistics from tests.factories import ReservationFactory +from tilavarauspalvelu.models import ReservationStatistic +from tilavarauspalvelu.utils.pruning import prune_reservation_statistics # Applied to all tests pytestmark = [ diff --git a/tests/test_tasks/test_prune_reservation_with_inactive_payments.py b/tests/test_tasks/test_prune_reservation_with_inactive_payments.py index 00c226a29..be18b6ff8 100644 --- a/tests/test_tasks/test_prune_reservation_with_inactive_payments.py +++ b/tests/test_tasks/test_prune_reservation_with_inactive_payments.py @@ -5,11 +5,10 @@ from freezegun import freeze_time from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation -from reservations.pruning import prune_reservation_with_inactive_payments from tests.factories import PaymentOrderFactory, ReservationFactory -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import Reservation +from tilavarauspalvelu.utils.pruning import prune_reservation_with_inactive_payments # Applied to all tests pytestmark = [ diff --git a/tests/test_utils/test_create_test_data.py b/tests/test_utils/test_create_test_data.py index c0a379808..fc6a198b4 100644 --- a/tests/test_utils/test_create_test_data.py +++ b/tests/test_utils/test_create_test_data.py @@ -14,16 +14,20 @@ ReservationUnitPaymentType, TaxPercentage, ) -from reservations.models import ( +from tilavarauspalvelu.models import ( AbilityGroup, AffectingTimeSpan, + Building, + EmailTemplate, + PaymentOrder, + PersonalInfoViewLog, + RealEstate, RecurringReservation, RejectedOccurrence, ReservationMetadataField, ReservationStatistic, ReservationStatisticsReservationUnit, ) -from tilavarauspalvelu.models import Building, EmailTemplate, PaymentOrder, PersonalInfoViewLog, RealEstate apps_to_check: list[str] = [ "common", diff --git a/tests/test_utils/test_generate_reservations_from_allocations.py b/tests/test_utils/test_generate_reservations_from_allocations.py index b225d474b..572bf13ee 100644 --- a/tests/test_utils/test_generate_reservations_from_allocations.py +++ b/tests/test_utils/test_generate_reservations_from_allocations.py @@ -1,5 +1,6 @@ import datetime import re +from typing import TYPE_CHECKING import freezegun import pytest @@ -8,20 +9,23 @@ from applications.tasks import generate_reservation_series_from_allocations from common.date_utils import DEFAULT_TIMEZONE, combine, local_date, local_datetime, local_iso_format from reservation_units.models import ReservationUnitHierarchy -from reservations.enums import ( +from tests.factories import AllocatedTimeSlotFactory, ReservationFactory +from tests.helpers import patch_method +from tilavarauspalvelu.enums import ( CustomerTypeChoice, + HaukiResourceState, RejectionReadinessChoice, ReservationStateChoice, ReservationTypeChoice, ) -from reservations.models import AffectingTimeSpan, RecurringReservation, RejectedOccurrence, Reservation -from tests.factories import AllocatedTimeSlotFactory, ReservationFactory -from tests.helpers import patch_method -from tilavarauspalvelu.enums import HaukiResourceState +from tilavarauspalvelu.models import AffectingTimeSpan, RecurringReservation, RejectedOccurrence from tilavarauspalvelu.utils.opening_hours.hauki_api_client import HaukiAPIClient from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIDatePeriod from utils.sentry import SentryLogger +if TYPE_CHECKING: + from tilavarauspalvelu.models import Reservation + pytestmark = [ pytest.mark.django_db, ] diff --git a/tilavarauspalvelu/admin/__init__.py b/tilavarauspalvelu/admin/__init__.py index 6cc1b760c..21fa94007 100644 --- a/tilavarauspalvelu/admin/__init__.py +++ b/tilavarauspalvelu/admin/__init__.py @@ -1,9 +1,19 @@ +from .ability_group.admin import AbilityGroupAdmin +from .age_group.admin import AgeGroupAdmin from .email_template.admin import EmailTemplateAdmin from .general_role.admin import GeneralRoleAdmin from .origin_hauki_resource.admin import OriginHaukiResourceAdmin from .payment_accounting.admin import PaymentAccountingAdmin from .payment_merchant.admin import PaymentMerchantAdmin from .payment_order.admin import PaymentOrderAdmin +from .recurring_reservation.admin import RecurringReservationAdmin +from .reservation.admin import ReservationAdmin +from .reservation_cancel_reason.admin import ReservationCancelReasonAdmin +from .reservation_deny_reason.admin import ReservationDenyReasonAdmin +from .reservation_metadata_field.admin import ReservationMetadataFieldAdmin +from .reservation_metadata_set.admin import ReservationMetadataSetAdmin +from .reservation_purpose.admin import ReservationPurposeAdmin +from .reservation_statistic.admin import ReservationStatisticAdmin from .resource.admin import ResourceAdmin from .service.admin import ServiceAdmin from .service_sector.admin import ServiceSectorAdmin @@ -15,12 +25,22 @@ from .user.admin import UserAdmin __all__ = [ + "AbilityGroupAdmin", + "AgeGroupAdmin", "EmailTemplateAdmin", "GeneralRoleAdmin", "OriginHaukiResourceAdmin", "PaymentAccountingAdmin", "PaymentMerchantAdmin", "PaymentOrderAdmin", + "RecurringReservationAdmin", + "ReservationAdmin", + "ReservationCancelReasonAdmin", + "ReservationDenyReasonAdmin", + "ReservationMetadataFieldAdmin", + "ReservationMetadataSetAdmin", + "ReservationPurposeAdmin", + "ReservationStatisticAdmin", "ResourceAdmin", "ServiceAdmin", "ServiceSectorAdmin", diff --git a/reservations/management/__init__.py b/tilavarauspalvelu/admin/ability_group/__init__.py similarity index 100% rename from reservations/management/__init__.py rename to tilavarauspalvelu/admin/ability_group/__init__.py diff --git a/reservations/admin/ability_group.py b/tilavarauspalvelu/admin/ability_group/admin.py similarity index 66% rename from reservations/admin/ability_group.py rename to tilavarauspalvelu/admin/ability_group/admin.py index 2e7374cc4..7147e3489 100644 --- a/reservations/admin/ability_group.py +++ b/tilavarauspalvelu/admin/ability_group/admin.py @@ -1,11 +1,7 @@ from django.contrib import admin from modeltranslation.admin import TranslationAdmin -from reservations.models import AbilityGroup - -__all__ = [ - "AbilityGroupAdmin", -] +from tilavarauspalvelu.models import AbilityGroup @admin.register(AbilityGroup) diff --git a/tilavarauspalvelu/admin/age_group/admin.py b/tilavarauspalvelu/admin/age_group/admin.py index e69de29bb..4c0cf5683 100644 --- a/tilavarauspalvelu/admin/age_group/admin.py +++ b/tilavarauspalvelu/admin/age_group/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from tilavarauspalvelu.models import AgeGroup + + +@admin.register(AgeGroup) +class AgeGroupAdmin(admin.ModelAdmin): + pass diff --git a/tilavarauspalvelu/admin/deny_reason/admin.py b/tilavarauspalvelu/admin/deny_reason/admin.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/admin/metadata_field/admin.py b/tilavarauspalvelu/admin/metadata_field/admin.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/admin/metadata_set/admin.py b/tilavarauspalvelu/admin/metadata_set/admin.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/admin/recurring_reservation/admin.py b/tilavarauspalvelu/admin/recurring_reservation/admin.py index e69de29bb..b7376559d 100644 --- a/tilavarauspalvelu/admin/recurring_reservation/admin.py +++ b/tilavarauspalvelu/admin/recurring_reservation/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin + +from tilavarauspalvelu.admin.reservation.admin import ReservationInline +from tilavarauspalvelu.models import RecurringReservation + + +@admin.register(RecurringReservation) +class RecurringReservationAdmin(admin.ModelAdmin): + # List + list_display = [ + "name", + "reservation_unit", + "allocated_time_slot", + "begin_date", + "end_date", + "recurrence_in_days", + ] + + # Form + inlines = [ReservationInline] diff --git a/tilavarauspalvelu/admin/reservation/admin.py b/tilavarauspalvelu/admin/reservation/admin.py index e69de29bb..495e5dac4 100644 --- a/tilavarauspalvelu/admin/reservation/admin.py +++ b/tilavarauspalvelu/admin/reservation/admin.py @@ -0,0 +1,337 @@ +from decimal import Decimal +from typing import Any + +from django.contrib import admin, messages +from django.contrib.admin import helpers +from django.db import models +from django.db.models import QuerySet +from django.template.response import TemplateResponse +from django.utils.translation import gettext_lazy as _ +from more_admin_filters import MultiSelectFilter +from more_admin_filters.filters import MultiSelectRelatedOnlyDropdownFilter +from rangefilter.filters import DateRangeFilterBuilder + +from common.date_utils import local_datetime +from common.typing import WSGIRequest +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import PaymentOrder, Reservation, ReservationDenyReason +from tilavarauspalvelu.tasks import refund_paid_reservation_task +from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender + +from .filters import PaidReservationListFilter, RecurringReservationListFilter +from .form import ReservationAdminForm + + +class ReservationInline(admin.TabularInline): + model = Reservation + extra = 0 + max_num = 0 + show_change_link = True + can_delete = False + fields = [ + "id", + "name", + "begin", + "end", + "state", + "type", + "price", + "price_net", + "unit_price", + ] + readonly_fields = fields + + +class PaymentOrderInline(admin.TabularInline): + model = PaymentOrder + extra = 0 + show_change_link = True + can_delete = False + fields = [ + "id", + "payment_type", + "status", + "price_total", + ] + readonly_fields = fields + + +@admin.register(Reservation) +class ReservationAdmin(admin.ModelAdmin): + # Functions + actions = [ + "deny_reservations_without_refund", + "deny_reservations_with_refund", + ] + search_fields = [ + # 'id' handled separately in `get_search_results()` + "name", + ] + search_help_text = _("Search by Reservation ID or name") + + # List + list_display = [ + "id", + "name", + "type", + "state", + "begin", + "reservation_units", + ] + list_filter = [ + ("created_at", DateRangeFilterBuilder(title=_("Created at"))), + ("begin", DateRangeFilterBuilder(title=_("Begin time"))), + ("type", MultiSelectFilter), + ("state", MultiSelectFilter), + RecurringReservationListFilter, + PaidReservationListFilter, + ("reservation_unit__unit", MultiSelectRelatedOnlyDropdownFilter), + ("reservation_unit", MultiSelectRelatedOnlyDropdownFilter), + ] + + # Form + form = ReservationAdminForm + fieldsets = [ + [ + _("Basic information"), + { + "fields": [ + "id", + "sku", + "name", + "description", + "num_persons", + "state", + "type", + "cancel_details", + "handling_details", + "working_memo", + ], + }, + ], + [ + _("Time"), + { + "fields": [ + "begin", + "end", + "buffer_time_before", + "buffer_time_after", + "handled_at", + "confirmed_at", + "created_at", + ], + }, + ], + [ + _("Price"), + { + "fields": [ + "price", + "price_net", + "non_subsidised_price", + "non_subsidised_price_net", + "unit_price", + "tax_percentage_value", + "applying_for_free_of_charge", + "free_of_charge_reason", + ], + }, + ], + [ + _("Reservee information"), + { + "fields": [ + "reservee_id", + "reservee_first_name", + "reservee_last_name", + "reservee_email", + "reservee_phone", + "reservee_organisation_name", + "reservee_address_street", + "reservee_address_city", + "reservee_address_zip", + "reservee_is_unregistered_association", + "reservee_language", + "reservee_type", + ], + }, + ], + [ + _("Billing information"), + { + "fields": [ + "billing_first_name", + "billing_last_name", + "billing_email", + "billing_phone", + "billing_address_street", + "billing_address_city", + "billing_address_zip", + ], + }, + ], + [ + _("Additional information"), + { + "fields": [ + "user", + "recurring_reservation", + "deny_reason", + "cancel_reason", + "purpose", + "home_city", + "age_group", + ], + }, + ], + ] + readonly_fields = [ + "id", + "handled_at", + "confirmed_at", + "created_at", + "price_net", + "non_subsidised_price_net", + ] + inlines = [PaymentOrderInline] + + def get_queryset(self, request): + return super().get_queryset(request).prefetch_related("reservation_unit") + + def get_search_results( + self, + request: WSGIRequest, + queryset: models.QuerySet, + search_term: Any, + ) -> tuple[models.QuerySet, bool]: + queryset, may_have_duplicates = super().get_search_results(request, queryset, search_term) + + if str(search_term).isdigit(): + queryset |= self.model.objects.filter(id__exact=int(search_term)) + + return queryset, may_have_duplicates + + @admin.display(ordering="reservation_unit__name") + def reservation_units(self, obj: Reservation) -> str: + return ", ".join([str(reservation_unit) for reservation_unit in obj.reservation_unit.all()]) + + def price_net(self, obj: Reservation) -> Decimal: + return obj.price_net + + def non_subsidised_price_net(self, obj: Reservation) -> Decimal: + return obj.non_subsidised_price_net + + def _deny_reservations_action_confirmation_page( + self, + request: WSGIRequest, + queryset: QuerySet[Reservation], + action_name: str, + ) -> TemplateResponse | None: + if not queryset.exists(): + msg = _("None of the selected reservations can be denied.") + self.message_user(request, msg, level=messages.ERROR) + return None + + queryset = queryset.filter(state__in=ReservationStateChoice.states_that_can_change_to_deny) + queryset_ended_reservation_count = queryset.filter(end__lt=local_datetime()).count() + + queryset = queryset.filter(end__gte=local_datetime()) + queryset_unpaid_reservation_count = queryset.filter(price=0).count() + queryset_paid_reservation_count = queryset.filter(price__gt=0).count() + queryset_refundable_reservation_count = queryset.filter( + price__gt=0, + payment_order__isnull=False, + payment_order__status=OrderStatus.PAID, + payment_order__refund_id__isnull=True, + ).count() + + deny_reasons = ReservationDenyReason.objects.all().order_by("reason") + + context = { + **self.admin_site.each_context(request), + "title": _("Are you sure?"), + "subtitle": _("Are you sure you want deny these reservations?"), + "queryset": queryset, + "queryset_unpaid_reservation_count": queryset_unpaid_reservation_count, + "queryset_paid_reservation_count": queryset_paid_reservation_count, + "queryset_ended_reservation_count": queryset_ended_reservation_count, + "queryset_refundable_reservation_count": queryset_refundable_reservation_count, + "deny_reasons": deny_reasons, + "opts": self.model._meta, + "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME, + "media": self.media, + "action_name": action_name, + } + request.current_app = self.admin_site.name + return TemplateResponse(request, "admin/deny_reservation_confirmation.html", context) + + def _deny_reservations_action_set_denied(self, request: WSGIRequest, queryset: QuerySet[Reservation]) -> None: + deny_reason = request.POST.get("deny_reason") + queryset.filter( + state__in=ReservationStateChoice.states_that_can_change_to_deny, + end__gte=local_datetime(), + ).update( + state=ReservationStateChoice.DENIED, + handled_at=local_datetime(), + deny_reason=deny_reason, + ) + + msg = _("Selected reservations have been denied.") + self.message_user(request, msg, level=messages.INFO) + + for reservation in queryset: + ReservationEmailNotificationSender.send_deny_email(reservation=reservation) + + @admin.action(description=_("Deny selected reservations without refund")) + def deny_reservations_without_refund( + self, + request: WSGIRequest, + queryset: QuerySet[Reservation], + ) -> TemplateResponse | None: + # Confirmation page + if not request.POST.get("confirmed"): + return self._deny_reservations_action_confirmation_page( + request=request, + queryset=queryset, + action_name="deny_reservations_without_refund", + ) + + # Set reservations as denied + self._deny_reservations_action_set_denied(request=request, queryset=queryset) + return None + + @admin.action(description=_("Deny selected reservations and refund")) + def deny_reservations_with_refund( + self, + request: WSGIRequest, + queryset: QuerySet[Reservation], + ) -> TemplateResponse | None: + # Confirmation page + if not request.POST.get("confirmed"): + return self._deny_reservations_action_confirmation_page( + request=request, + queryset=queryset, + action_name="deny_reservations_with_refund", + ) + + # Set reservations as denied + self._deny_reservations_action_set_denied(request=request, queryset=queryset) + + # Refund paid reservations + refund_queryset = queryset.filter( + state=ReservationStateChoice.DENIED, + price__gt=0, + payment_order__isnull=False, + payment_order__status=OrderStatus.PAID, + payment_order__refund_id__isnull=True, + ) + for reservation in refund_queryset: + refund_paid_reservation_task.delay(reservation.pk) + + if refund_queryset.count(): + msg = _("Refund has been initiated for selected reservations.") + f" ({refund_queryset.count()})" + else: + msg = _("No reservations with paid orders to refund.") + self.message_user(request, msg, level=messages.INFO) + return None diff --git a/reservations/admin/reservation/filters.py b/tilavarauspalvelu/admin/reservation/filters.py similarity index 96% rename from reservations/admin/reservation/filters.py rename to tilavarauspalvelu/admin/reservation/filters.py index d22f5d02f..79d02bde3 100644 --- a/reservations/admin/reservation/filters.py +++ b/tilavarauspalvelu/admin/reservation/filters.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from common.typing import WSGIRequest -from reservations.models import Reservation +from tilavarauspalvelu.models import Reservation class RecurringReservationListFilter(admin.SimpleListFilter): diff --git a/reservations/admin/reservation/form.py b/tilavarauspalvelu/admin/reservation/form.py similarity index 99% rename from reservations/admin/reservation/form.py rename to tilavarauspalvelu/admin/reservation/form.py index 7b781ad1f..2d09708a3 100644 --- a/reservations/admin/reservation/form.py +++ b/tilavarauspalvelu/admin/reservation/form.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from reservations.models import Reservation +from tilavarauspalvelu.models import Reservation class ReservationAdminForm(forms.ModelForm): diff --git a/reservations/management/commands/__init__.py b/tilavarauspalvelu/admin/reservation_cancel_reason/__init__.py similarity index 100% rename from reservations/management/commands/__init__.py rename to tilavarauspalvelu/admin/reservation_cancel_reason/__init__.py diff --git a/reservations/admin/reservation_cancel_reason.py b/tilavarauspalvelu/admin/reservation_cancel_reason/admin.py similarity index 64% rename from reservations/admin/reservation_cancel_reason.py rename to tilavarauspalvelu/admin/reservation_cancel_reason/admin.py index d697fb9b4..e768d1396 100644 --- a/reservations/admin/reservation_cancel_reason.py +++ b/tilavarauspalvelu/admin/reservation_cancel_reason/admin.py @@ -1,11 +1,7 @@ from django.contrib import admin from modeltranslation.admin import TranslationAdmin -from reservations.models import ReservationCancelReason - -__all__ = [ - "ReservationCancelReasonAdmin", -] +from tilavarauspalvelu.models import ReservationCancelReason @admin.register(ReservationCancelReason) diff --git a/tilavarauspalvelu/admin/cancel_reason/__init__.py b/tilavarauspalvelu/admin/reservation_deny_reason/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/cancel_reason/__init__.py rename to tilavarauspalvelu/admin/reservation_deny_reason/__init__.py diff --git a/reservations/admin/reservation_deny_reason.py b/tilavarauspalvelu/admin/reservation_deny_reason/admin.py similarity index 71% rename from reservations/admin/reservation_deny_reason.py rename to tilavarauspalvelu/admin/reservation_deny_reason/admin.py index 32b28eb83..bee584747 100644 --- a/reservations/admin/reservation_deny_reason.py +++ b/tilavarauspalvelu/admin/reservation_deny_reason/admin.py @@ -2,11 +2,7 @@ from django.contrib import admin from modeltranslation.admin import TranslationAdmin -from reservations.models import ReservationDenyReason - -__all__ = [ - "ReservationDenyReasonAdmin", -] +from tilavarauspalvelu.models import ReservationDenyReason @admin.register(ReservationDenyReason) diff --git a/tilavarauspalvelu/admin/cancellation_rule/__init__.py b/tilavarauspalvelu/admin/reservation_metadata_field/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/cancellation_rule/__init__.py rename to tilavarauspalvelu/admin/reservation_metadata_field/__init__.py diff --git a/reservations/admin/reservation_metadata_field.py b/tilavarauspalvelu/admin/reservation_metadata_field/admin.py similarity index 86% rename from reservations/admin/reservation_metadata_field.py rename to tilavarauspalvelu/admin/reservation_metadata_field/admin.py index 9538f22df..90a6ad809 100644 --- a/reservations/admin/reservation_metadata_field.py +++ b/tilavarauspalvelu/admin/reservation_metadata_field/admin.py @@ -1,11 +1,7 @@ from django import forms from django.contrib import admin -from reservations.models import ReservationMetadataField - -__all__ = [ - "ReservationMetadataFieldAdmin", -] +from tilavarauspalvelu.models import ReservationMetadataField class ReservationMetadataFieldForm(forms.ModelForm): diff --git a/tilavarauspalvelu/admin/deny_reason/__init__.py b/tilavarauspalvelu/admin/reservation_metadata_set/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/deny_reason/__init__.py rename to tilavarauspalvelu/admin/reservation_metadata_set/__init__.py diff --git a/reservations/admin/reservation_metadata_set.py b/tilavarauspalvelu/admin/reservation_metadata_set/admin.py similarity index 89% rename from reservations/admin/reservation_metadata_set.py rename to tilavarauspalvelu/admin/reservation_metadata_set/admin.py index 97fc1ee81..7f514e86b 100644 --- a/reservations/admin/reservation_metadata_set.py +++ b/tilavarauspalvelu/admin/reservation_metadata_set/admin.py @@ -3,11 +3,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from reservations.models import ReservationMetadataSet - -__all__ = [ - "ReservationMetadataSetAdmin", -] +from tilavarauspalvelu.models import ReservationMetadataSet class ReservationMetadataSetForm(forms.ModelForm): diff --git a/tilavarauspalvelu/admin/reservation_purpose/admin.py b/tilavarauspalvelu/admin/reservation_purpose/admin.py index e69de29bb..6a968afef 100644 --- a/tilavarauspalvelu/admin/reservation_purpose/admin.py +++ b/tilavarauspalvelu/admin/reservation_purpose/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from modeltranslation.admin import TranslationAdmin + +from tilavarauspalvelu.models import ReservationPurpose + + +@admin.register(ReservationPurpose) +class ReservationPurposeAdmin(TranslationAdmin): + pass diff --git a/tilavarauspalvelu/admin/reservation_statistic/admin.py b/tilavarauspalvelu/admin/reservation_statistic/admin.py index e69de29bb..7ed236002 100644 --- a/tilavarauspalvelu/admin/reservation_statistic/admin.py +++ b/tilavarauspalvelu/admin/reservation_statistic/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from import_export.admin import ExportMixin +from import_export.formats.base_formats import CSV +from rangefilter.filters import DateRangeFilter + +from tilavarauspalvelu.models import ReservationStatistic + + +@admin.register(ReservationStatistic) +class ReservationStatisticAdmin(ExportMixin, admin.ModelAdmin): + # Functions + formats = [CSV] + + # List + list_filter = ( + ("reservation_created_at", DateRangeFilter), + ("begin", DateRangeFilter), + ) diff --git a/tilavarauspalvelu/admin/metadata_field/__init__.py b/tilavarauspalvelu/admin/reservation_unit_cancellation_rule/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/metadata_field/__init__.py rename to tilavarauspalvelu/admin/reservation_unit_cancellation_rule/__init__.py diff --git a/tilavarauspalvelu/admin/cancel_reason/admin.py b/tilavarauspalvelu/admin/reservation_unit_cancellation_rule/admin.py similarity index 100% rename from tilavarauspalvelu/admin/cancel_reason/admin.py rename to tilavarauspalvelu/admin/reservation_unit_cancellation_rule/admin.py diff --git a/tilavarauspalvelu/api/graphql/schema.py b/tilavarauspalvelu/api/graphql/schema.py index c1d67b00e..90e6e8989 100644 --- a/tilavarauspalvelu/api/graphql/schema.py +++ b/tilavarauspalvelu/api/graphql/schema.py @@ -14,9 +14,8 @@ from applications.models import AllocatedTimeSlot from common.models import BannerNotification from common.typing import AnyUser, GQLInfo -from reservations.models import Reservation from tilavarauspalvelu.enums import UserPermissionChoice -from tilavarauspalvelu.models import PaymentOrder, User +from tilavarauspalvelu.models import PaymentOrder, Reservation, User from tilavarauspalvelu.utils.helauth.clients import HelsinkiProfileClient from tilavarauspalvelu.utils.helauth.typing import UserProfileInfo diff --git a/tilavarauspalvelu/api/graphql/types/ability_group/types.py b/tilavarauspalvelu/api/graphql/types/ability_group/types.py index 09dde971a..8448e8f7d 100644 --- a/tilavarauspalvelu/api/graphql/types/ability_group/types.py +++ b/tilavarauspalvelu/api/graphql/types/ability_group/types.py @@ -1,6 +1,6 @@ from graphene_django_extensions import DjangoNode -from reservations.models import AbilityGroup +from tilavarauspalvelu.models import AbilityGroup from .permissions import AbilityGroupPermission diff --git a/tilavarauspalvelu/api/graphql/types/age_group/types.py b/tilavarauspalvelu/api/graphql/types/age_group/types.py index 88dd72681..77f9d2763 100644 --- a/tilavarauspalvelu/api/graphql/types/age_group/types.py +++ b/tilavarauspalvelu/api/graphql/types/age_group/types.py @@ -1,6 +1,6 @@ from graphene_django_extensions import DjangoNode -from reservations.models import AgeGroup +from tilavarauspalvelu.models import AgeGroup from .permissions import AgeGroupPermission diff --git a/tilavarauspalvelu/api/graphql/types/helsinki_profile/types.py b/tilavarauspalvelu/api/graphql/types/helsinki_profile/types.py index 35d5260ac..ef81cfa4a 100644 --- a/tilavarauspalvelu/api/graphql/types/helsinki_profile/types.py +++ b/tilavarauspalvelu/api/graphql/types/helsinki_profile/types.py @@ -7,9 +7,8 @@ from applications.models import Application from common.typing import AnyUser, GQLInfo -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes -from tilavarauspalvelu.models import User +from tilavarauspalvelu.models import Reservation, User from tilavarauspalvelu.tasks import save_personal_info_view_log __all__ = [ diff --git a/tilavarauspalvelu/api/graphql/types/recurring_reservation/filtersets.py b/tilavarauspalvelu/api/graphql/types/recurring_reservation/filtersets.py index 1fe00c71c..a0d277be8 100644 --- a/tilavarauspalvelu/api/graphql/types/recurring_reservation/filtersets.py +++ b/tilavarauspalvelu/api/graphql/types/recurring_reservation/filtersets.py @@ -3,8 +3,7 @@ from graphene_django_extensions import ModelFilterSet from reservation_units.models import ReservationUnit, ReservationUnitType -from reservations.models import RecurringReservation -from tilavarauspalvelu.models import Unit, User +from tilavarauspalvelu.models import RecurringReservation, Unit, User __all__ = [ "RecurringReservationFilterSet", diff --git a/tilavarauspalvelu/api/graphql/types/recurring_reservation/permissions.py b/tilavarauspalvelu/api/graphql/types/recurring_reservation/permissions.py index 39fe4a93f..0ba97afbb 100644 --- a/tilavarauspalvelu/api/graphql/types/recurring_reservation/permissions.py +++ b/tilavarauspalvelu/api/graphql/types/recurring_reservation/permissions.py @@ -6,8 +6,8 @@ from common.typing import AnyUser from reservation_units.models import ReservationUnit -from reservations.models import RecurringReservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.models import RecurringReservation __all__ = [ "RecurringReservationPermission", diff --git a/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py b/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py index 95e77cb6c..3793f04d4 100644 --- a/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py +++ b/tilavarauspalvelu/api/graphql/types/recurring_reservation/serializers.py @@ -9,16 +9,16 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from actions.recurring_reservation import ReservationDetails from applications.enums import WeekdayChoice from common.date_utils import local_date from common.fields.serializer import CurrentUserDefaultNullable, input_only_field from reservation_units.enums import ReservationStartInterval from reservation_units.models import ReservationUnit -from reservations.enums import ReservationStateChoice, ReservationTypeStaffChoice -from reservations.models import RecurringReservation, Reservation -from reservations.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeStaffChoice +from tilavarauspalvelu.models import RecurringReservation, Reservation +from tilavarauspalvelu.models.recurring_reservation.actions import ReservationDetails +from tilavarauspalvelu.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task __all__ = [ "RecurringReservationCreateSerializer", diff --git a/tilavarauspalvelu/api/graphql/types/recurring_reservation/types.py b/tilavarauspalvelu/api/graphql/types/recurring_reservation/types.py index 6a1405a54..85dee2623 100644 --- a/tilavarauspalvelu/api/graphql/types/recurring_reservation/types.py +++ b/tilavarauspalvelu/api/graphql/types/recurring_reservation/types.py @@ -3,7 +3,7 @@ from graphene_django_extensions import DjangoNode from common.typing import GQLInfo -from reservations.models import RecurringReservation +from tilavarauspalvelu.models import RecurringReservation from .filtersets import RecurringReservationFilterSet from .permissions import RecurringReservationPermission diff --git a/tilavarauspalvelu/api/graphql/types/rejected_occurrence/filtersets.py b/tilavarauspalvelu/api/graphql/types/rejected_occurrence/filtersets.py index fe1aad320..c0798a150 100644 --- a/tilavarauspalvelu/api/graphql/types/rejected_occurrence/filtersets.py +++ b/tilavarauspalvelu/api/graphql/types/rejected_occurrence/filtersets.py @@ -6,13 +6,14 @@ from common.db import text_search from common.utils import log_text_search -from reservations.models import RejectedOccurrence -from reservations.querysets import RejectedOccurrenceQuerySet +from tilavarauspalvelu.models import RejectedOccurrence __all__ = [ "RejectedOccurrenceFilterSet", ] +from tilavarauspalvelu.models.rejected_occurrence.queryset import RejectedOccurrenceQuerySet + class RejectedOccurrenceFilterSet(ModelFilterSet): pk = IntMultipleChoiceFilter() diff --git a/tilavarauspalvelu/api/graphql/types/rejected_occurrence/types.py b/tilavarauspalvelu/api/graphql/types/rejected_occurrence/types.py index 212e056d9..562cb7048 100644 --- a/tilavarauspalvelu/api/graphql/types/rejected_occurrence/types.py +++ b/tilavarauspalvelu/api/graphql/types/rejected_occurrence/types.py @@ -1,6 +1,6 @@ from graphene_django_extensions import DjangoNode -from reservations.models import RejectedOccurrence +from tilavarauspalvelu.models import RejectedOccurrence from .filtersets import RejectedOccurrenceFilterSet from .permissions import RejectedOccurrencePermission diff --git a/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py b/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py index 66596d6e9..307af8755 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/filtersets.py @@ -10,10 +10,15 @@ from common.db import text_search from common.utils import log_text_search -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.filters import TimezoneAwareDateFilter -from tilavarauspalvelu.enums import OrderStatusWithFree, UserRoleChoice +from tilavarauspalvelu.enums import ( + CustomerTypeChoice, + OrderStatusWithFree, + ReservationStateChoice, + ReservationTypeChoice, + UserRoleChoice, +) +from tilavarauspalvelu.models import Reservation if TYPE_CHECKING: from common.typing import AnyUser diff --git a/tilavarauspalvelu/api/graphql/types/reservation/mutations.py b/tilavarauspalvelu/api/graphql/types/reservation/mutations.py index 5f6867cd6..1fc5fee5f 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/mutations.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/mutations.py @@ -6,10 +6,9 @@ from common.date_utils import local_datetime from common.typing import AnyUser -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.types.merchants.types import PaymentOrderNode -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from .permissions import ( diff --git a/tilavarauspalvelu/api/graphql/types/reservation/permissions.py b/tilavarauspalvelu/api/graphql/types/reservation/permissions.py index ce96f8a8c..7d8c2525e 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/permissions.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/permissions.py @@ -5,8 +5,8 @@ from common.typing import AnyUser from reservation_units.models import ReservationUnit -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.models import Reservation __all__ = [ "ReservationCommentPermission", diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py index 1b255b120..634b86046 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/adjust_time_serializers.py @@ -5,14 +5,14 @@ from graphene_django_extensions.fields import EnumFriendlyChoiceField from reservation_units.models import ReservationUnit -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeyUpdateSerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ( ReservationPriceMixin, ReservationSchedulingMixin, ) +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py index 24e26a27d..1057e9bde 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/approve_serializers.py @@ -5,9 +5,9 @@ from rest_framework.exceptions import ValidationError from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.enums import ReservationStateChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py index ef8a582d7..1264162a5 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/cancellation_serializers.py @@ -5,11 +5,10 @@ from rest_framework.exceptions import ValidationError from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation -from reservations.tasks import refund_paid_reservation_task from tilavarauspalvelu.api.graphql.extensions import error_codes -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import Reservation +from tilavarauspalvelu.tasks import refund_paid_reservation_task from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender if TYPE_CHECKING: diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py index ae5263953..7311059a6 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/confirm_serializers.py @@ -5,12 +5,10 @@ from reservation_units.enums import PaymentType, PricingType from reservation_units.utils.reservation_unit_pricing_helper import ReservationUnitPricingHelper -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.serializers.update_serializers import ReservationUpdateSerializer -from tilavarauspalvelu.enums import Language, OrderStatus -from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.enums import Language, OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import PaymentOrder, Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender from tilavarauspalvelu.utils.verkkokauppa.helpers import create_mock_verkkokauppa_order, get_verkkokauppa_order_params from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CreateOrderError diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializers.py index 23320c5d5..6ec456a28 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/create_serializers.py @@ -10,13 +10,6 @@ from common.typing import AnyUser, WSGIRequest from reservation_units.enums import ReservationKind from reservation_units.models import ReservationUnit -from reservations.enums import ( - RESERVEE_LANGUAGE_CHOICES, - CustomerTypeChoice, - ReservationStateChoice, - ReservationTypeChoice, -) -from reservations.models import AgeGroup, Reservation, ReservationPurpose from tilavarauspalvelu.api.graphql.extensions.fields import DurationField, OldChoiceCharField from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeySerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode @@ -24,6 +17,13 @@ ReservationPriceMixin, ReservationSchedulingMixin, ) +from tilavarauspalvelu.enums import ( + RESERVEE_LANGUAGE_CHOICES, + CustomerTypeChoice, + ReservationStateChoice, + ReservationTypeChoice, +) +from tilavarauspalvelu.models import AgeGroup, Reservation, ReservationPurpose from tilavarauspalvelu.utils.helauth.clients import HelsinkiProfileClient from utils.external_service.errors import ExternalServiceError from utils.sentry import SentryLogger diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py index d40700cc0..73223d5c6 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/deny_serializers.py @@ -6,9 +6,9 @@ from common.date_utils import local_datetime from common.utils import comma_sep_str -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.enums import ReservationStateChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py index 12d70a48b..e4ba16ff8 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/handling_required_serializers.py @@ -5,9 +5,9 @@ from rest_framework.exceptions import ValidationError from common.utils import comma_sep_str -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions import error_codes +from tilavarauspalvelu.enums import ReservationStateChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender __all__ = [ diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/memo_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/memo_serializers.py index 9f68ee198..e085074d5 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/memo_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/memo_serializers.py @@ -1,5 +1,5 @@ -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeySerializer +from tilavarauspalvelu.models import Reservation class ReservationWorkingMemoSerializer(OldPrimaryKeySerializer): diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/mixins.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/mixins.py index 96576e26e..5a0d652de 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/mixins.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/mixins.py @@ -11,10 +11,10 @@ from reservation_units.enums import PriceUnit, PricingType, ReservationStartInterval, ReservationUnitPublishingState from reservation_units.models import ReservationUnit, ReservationUnitPricing from reservation_units.utils.reservation_unit_pricing_helper import ReservationUnitPricingHelper -from reservations.enums import ReservationTypeChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.types import ReservationNode +from tilavarauspalvelu.enums import ReservationTypeChoice +from tilavarauspalvelu.models import Reservation class PriceCalculationResult: diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py index 10efdf6ee..f071de508 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/refund_serializers.py @@ -4,13 +4,11 @@ from django.utils.timezone import get_default_timezone from common.utils import comma_sep_str -from reservations.enums import ReservationStateChoice -from reservations.models import Reservation -from reservations.tasks import refund_paid_reservation_task from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeySerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode -from tilavarauspalvelu.enums import OrderStatus -from tilavarauspalvelu.models import PaymentOrder +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice +from tilavarauspalvelu.models import PaymentOrder, Reservation +from tilavarauspalvelu.tasks import refund_paid_reservation_task DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py index 3f4dd9ff3..9cd11d268 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_adjust_time_serializers.py @@ -5,11 +5,11 @@ from graphene_django_extensions.fields import EnumFriendlyChoiceField from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeyUpdateSerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ReservationSchedulingMixin +from tilavarauspalvelu.enums import ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender if TYPE_CHECKING: diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_create_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_create_serializers.py index e2e8bd4c9..81ce499fb 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_create_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_create_serializers.py @@ -8,17 +8,17 @@ from applications.models import City from common.date_utils import local_datetime from reservation_units.models import ReservationUnit -from reservations.enums import ( +from tilavarauspalvelu.api.graphql.extensions.fields import OldChoiceCharField +from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeySerializer +from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode +from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ReservationSchedulingMixin +from tilavarauspalvelu.enums import ( RESERVEE_LANGUAGE_CHOICES, CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice, ) -from reservations.models import AgeGroup, RecurringReservation, Reservation, ReservationPurpose -from tilavarauspalvelu.api.graphql.extensions.fields import OldChoiceCharField -from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeySerializer -from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode -from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ReservationSchedulingMixin +from tilavarauspalvelu.models import AgeGroup, RecurringReservation, Reservation, ReservationPurpose if TYPE_CHECKING: from common.typing import AnyUser diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_reservation_modify_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_reservation_modify_serializers.py index 6c0c206a1..cdbcca9e0 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_reservation_modify_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/staff_reservation_modify_serializers.py @@ -7,17 +7,17 @@ from applications.models import City from reservation_units.models import ReservationUnit -from reservations.enums import ( +from tilavarauspalvelu.api.graphql.extensions.fields import DurationField +from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeyUpdateSerializer +from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode +from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ReservationSchedulingMixin +from tilavarauspalvelu.enums import ( RESERVEE_LANGUAGE_CHOICES, CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice, ) -from reservations.models import AgeGroup, Reservation, ReservationPurpose -from tilavarauspalvelu.api.graphql.extensions.fields import DurationField -from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeyUpdateSerializer -from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode -from tilavarauspalvelu.api.graphql.types.reservation.serializers.mixins import ReservationSchedulingMixin +from tilavarauspalvelu.models import AgeGroup, Reservation, ReservationPurpose DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tilavarauspalvelu/api/graphql/types/reservation/serializers/update_serializers.py b/tilavarauspalvelu/api/graphql/types/reservation/serializers/update_serializers.py index 7bf38621e..f4201cbc7 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/serializers/update_serializers.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/serializers/update_serializers.py @@ -3,11 +3,11 @@ from django.utils.timezone import get_default_timezone from graphene.utils.str_converters import to_camel_case -from reservations.enums import CustomerTypeChoice, ReservationStateChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.extensions.serializers import OldPrimaryKeyUpdateSerializer from tilavarauspalvelu.api.graphql.extensions.validation_errors import ValidationErrorCodes, ValidationErrorWithCode from tilavarauspalvelu.api.graphql.types.reservation.serializers.create_serializers import ReservationCreateSerializer +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice +from tilavarauspalvelu.models import Reservation DEFAULT_TIMEZONE = get_default_timezone() diff --git a/tilavarauspalvelu/api/graphql/types/reservation/types.py b/tilavarauspalvelu/api/graphql/types/reservation/types.py index 7e4f7e0d8..437dceca8 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation/types.py @@ -11,12 +11,10 @@ from common.typing import AnyUser, GQLInfo from common.utils import ical_hmac_signature from reservation_units.models import ReservationUnit -from reservations.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice -from reservations.enums import ReservationTypeChoice as ReservationTypeField -from reservations.models import Reservation -from reservations.querysets import ReservationQuerySet from tilavarauspalvelu.api.graphql.types.merchants.types import PaymentOrderNode -from tilavarauspalvelu.models import PaymentOrder, User +from tilavarauspalvelu.enums import CustomerTypeChoice, ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import PaymentOrder, Reservation, User +from tilavarauspalvelu.models.reservation.queryset import ReservationQuerySet from .filtersets import ReservationFilterSet from .permissions import ReservationPermission @@ -41,12 +39,12 @@ class ReservationNode(DjangoNode): reservee_name = AnnotatedField(graphene.String, expression=L("reservee_name")) - is_blocked = AnnotatedField(graphene.Boolean, expression=models.Q(type=ReservationTypeField.BLOCKED.value)) + is_blocked = AnnotatedField(graphene.Boolean, expression=models.Q(type=ReservationTypeChoice.BLOCKED.value)) is_handled = AnnotatedField(graphene.Boolean, expression=models.Q(handled_at__isnull=False)) staff_event = AnnotatedField( graphene.Boolean, - expression=models.Q(type=ReservationTypeField.STAFF.value), + expression=models.Q(type=ReservationTypeChoice.STAFF.value), deprecation_reason="Please use to 'type' instead.", ) diff --git a/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/filersets.py b/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/filersets.py index d42b54404..f43468075 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/filersets.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/filersets.py @@ -1,7 +1,7 @@ from graphene_django_extensions import ModelFilterSet from graphene_django_extensions.filters import IntMultipleChoiceFilter -from reservations.models import ReservationPurpose +from tilavarauspalvelu.models import ReservationPurpose __all__ = [ "ReservationCancelReasonFilterSet", diff --git a/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/types.py b/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/types.py index 10d2165ff..53330bba3 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_cancel_reason/types.py @@ -1,7 +1,7 @@ from graphene_django_extensions import DjangoNode from graphene_django_extensions.permissions import AllowAuthenticated -from reservations.models import ReservationCancelReason +from tilavarauspalvelu.models import ReservationCancelReason from .filersets import ReservationCancelReasonFilterSet diff --git a/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/filtersets.py b/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/filtersets.py index ce6d4273b..6000cf17f 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/filtersets.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/filtersets.py @@ -2,7 +2,7 @@ from graphene_django_extensions import ModelFilterSet from graphene_django_extensions.filters import IntMultipleChoiceFilter -from reservations.models import ReservationDenyReason +from tilavarauspalvelu.models import ReservationDenyReason __all__ = [ "ReservationDenyReasonFilterSet", diff --git a/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/types.py b/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/types.py index 795792389..618d56c66 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_deny_reason/types.py @@ -1,7 +1,7 @@ from graphene_django_extensions import DjangoNode from graphene_django_extensions.permissions import AllowAuthenticated -from reservations.models import ReservationDenyReason +from tilavarauspalvelu.models import ReservationDenyReason from .filtersets import ReservationDenyReasonFilterSet diff --git a/tilavarauspalvelu/api/graphql/types/reservation_metadata/types.py b/tilavarauspalvelu/api/graphql/types/reservation_metadata/types.py index 4c5089f29..5150e7e61 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_metadata/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_metadata/types.py @@ -1,6 +1,6 @@ from graphene_django_extensions import DjangoNode -from reservations.models import ReservationMetadataField, ReservationMetadataSet +from tilavarauspalvelu.models import ReservationMetadataField, ReservationMetadataSet from .permissions import ReservationMetadataSetPermission diff --git a/tilavarauspalvelu/api/graphql/types/reservation_purpose/filtersets.py b/tilavarauspalvelu/api/graphql/types/reservation_purpose/filtersets.py index 287873c55..d51121865 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_purpose/filtersets.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_purpose/filtersets.py @@ -1,7 +1,7 @@ from graphene_django_extensions import ModelFilterSet from graphene_django_extensions.filters import IntMultipleChoiceFilter -from reservations.models import ReservationPurpose +from tilavarauspalvelu.models import ReservationPurpose __all__ = [ "ReservationPurposeFilterSet", diff --git a/tilavarauspalvelu/api/graphql/types/reservation_purpose/types.py b/tilavarauspalvelu/api/graphql/types/reservation_purpose/types.py index cc123460f..f1d5e009b 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_purpose/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_purpose/types.py @@ -1,6 +1,6 @@ from graphene_django_extensions import DjangoNode -from reservations.models import ReservationPurpose +from tilavarauspalvelu.models import ReservationPurpose from .filtersets import ReservationPurposeFilterSet from .permissions import ReservationPurposePermission diff --git a/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py b/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py index 2607bc674..27bb33478 100644 --- a/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py +++ b/tilavarauspalvelu/api/graphql/types/reservation_unit/types.py @@ -15,11 +15,10 @@ from common.typing import GQLInfo from reservation_units.enums import ReservationUnitPublishingState, ReservationUnitReservationState from reservation_units.models import ReservationUnit -from reservations.enums import ReservationTypeChoice -from reservations.models import Reservation from tilavarauspalvelu.api.graphql.types.location.types import LocationNode from tilavarauspalvelu.api.graphql.types.reservation.types import ReservationNode -from tilavarauspalvelu.models import Location, OriginHaukiResource, PaymentMerchant, Space, Unit +from tilavarauspalvelu.enums import ReservationTypeChoice +from tilavarauspalvelu.models import Location, OriginHaukiResource, PaymentMerchant, Reservation, Space, Unit from tilavarauspalvelu.utils.opening_hours.hauki_link_generator import generate_hauki_link from .filtersets import ReservationUnitFilterSet diff --git a/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py b/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py index b29a818dc..e756bb72b 100644 --- a/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py +++ b/tilavarauspalvelu/api/mock_verkkokauppa_api/views.py @@ -9,13 +9,11 @@ from common.date_utils import local_datetime from common.typing import WSGIRequest -from reservations.enums import ReservationStateChoice -from tilavarauspalvelu.enums import OrderStatus +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice from tilavarauspalvelu.models import PaymentOrder if TYPE_CHECKING: - from reservations.models import Reservation - + from tilavarauspalvelu.models import Reservation __all__ = [ "MockVerkkokauppaView", diff --git a/tilavarauspalvelu/api/rest/views.py b/tilavarauspalvelu/api/rest/views.py index 0ff60db32..89c665564 100644 --- a/tilavarauspalvelu/api/rest/views.py +++ b/tilavarauspalvelu/api/rest/views.py @@ -9,8 +9,7 @@ from common.pdf import render_to_pdf from common.utils import ical_hmac_signature -from reservations.models import Reservation -from tilavarauspalvelu.models import TermsOfUse +from tilavarauspalvelu.models import Reservation, TermsOfUse __all__ = [ "reservation_ical", diff --git a/tilavarauspalvelu/enums.py b/tilavarauspalvelu/enums.py index 153e4738a..bab2d2962 100644 --- a/tilavarauspalvelu/enums.py +++ b/tilavarauspalvelu/enums.py @@ -1,9 +1,11 @@ from __future__ import annotations import enum +from enum import StrEnum from inspect import cleandoc from types import DynamicClassAttribute +from django.conf import settings from django.db import models from django.utils.functional import classproperty from django.utils.translation import gettext_lazy as _ @@ -13,11 +15,19 @@ from tilavarauspalvelu.typing import permission __all__ = [ + "RESERVEE_LANGUAGE_CHOICES", + "CalendarProperty", + "CustomerTypeChoice", + "EventProperty", "Language", "OrderStatus", "OrderStatusWithFree", "PaymentType", + "RejectionReadinessChoice", "ReservationNotification", + "ReservationStateChoice", + "ReservationTypeChoice", + "ReservationTypeStaffChoice", "ResourceLocationType", "ServiceTypeChoices", "TermsOfUseTypeChoices", @@ -316,3 +326,167 @@ def get(cls, state): return HaukiResourceState(state) except ValueError: return HaukiResourceState.UNDEFINED + + +class CustomerTypeChoice(models.TextChoices): + BUSINESS = "BUSINESS", pgettext_lazy("CustomerType", "Business") + NONPROFIT = "NONPROFIT", pgettext_lazy("CustomerType", "Nonprofit") + INDIVIDUAL = "INDIVIDUAL", pgettext_lazy("CustomerType", "Individual") + + @classproperty + def organisation(self) -> list[str]: + return [ # type: ignore[return-value] + CustomerTypeChoice.BUSINESS.value, + CustomerTypeChoice.NONPROFIT.value, + ] + + +class ReservationStateChoice(models.TextChoices): + CREATED = "CREATED", pgettext_lazy("ReservationState", "Created") + CANCELLED = "CANCELLED", pgettext_lazy("ReservationState", "Cancelled") + REQUIRES_HANDLING = "REQUIRES_HANDLING", pgettext_lazy("ReservationState", "Requires handling") + WAITING_FOR_PAYMENT = "WAITING_FOR_PAYMENT", pgettext_lazy("ReservationState", "Waiting for payment") + CONFIRMED = "CONFIRMED", pgettext_lazy("ReservationState", "Confirmed") + DENIED = "DENIED", pgettext_lazy("ReservationState", "Denied") + + @classproperty + def states_going_to_occur(self) -> list[str]: + return [ # type: ignore[return-type] + ReservationStateChoice.CREATED.value, + ReservationStateChoice.CONFIRMED.value, + ReservationStateChoice.WAITING_FOR_PAYMENT.value, + ReservationStateChoice.REQUIRES_HANDLING.value, + ] + + @classproperty + def states_that_can_change_to_handling(self) -> list[str]: + return [ # type: ignore[return-type] + ReservationStateChoice.CONFIRMED.value, + ReservationStateChoice.DENIED.value, + ] + + @classproperty + def states_that_can_change_to_deny(self) -> list[str]: + return [ # type: ignore[return-type] + ReservationStateChoice.REQUIRES_HANDLING.value, + ReservationStateChoice.CONFIRMED.value, + ] + + @classproperty + def states_that_can_be_cancelled(self) -> list[str]: + return [ # type: ignore[return-type] + ReservationStateChoice.CREATED.value, + ReservationStateChoice.WAITING_FOR_PAYMENT.value, + ] + + +class ReservationTypeChoice(models.TextChoices): + NORMAL = "NORMAL", pgettext_lazy("ReservationType", "Normal") + BLOCKED = "BLOCKED", pgettext_lazy("ReservationType", "Blocked") + STAFF = "STAFF", pgettext_lazy("ReservationType", "Staff") + BEHALF = "BEHALF", pgettext_lazy("ReservationType", "Behalf") + SEASONAL = "SEASONAL", pgettext_lazy("ReservationType", "Seasonal") + + @classproperty + def allowed_for_user_time_adjust(cls) -> list[str]: + return [ # type: ignore[return-type] + ReservationTypeChoice.NORMAL.value, + ReservationTypeChoice.BEHALF.value, + ] + + +class ReservationTypeStaffChoice(models.TextChoices): + # These are the same as the ones above, but for the staff create endpoint + BLOCKED = "BLOCKED", pgettext_lazy("ReservationTypeStaffChoice", "Blocked") + STAFF = "STAFF", pgettext_lazy("ReservationTypeStaffChoice", "Staff") + BEHALF = "BEHALF", pgettext_lazy("ReservationTypeStaffChoice", "Behalf") + + +RESERVEE_LANGUAGE_CHOICES = (*settings.LANGUAGES, ("", "")) + + +class RejectionReadinessChoice(models.TextChoices): + INTERVAL_NOT_ALLOWED = ( + "INTERVAL_NOT_ALLOWED", + pgettext_lazy("RejectionReadiness", "Interval not allowed"), + ) + OVERLAPPING_RESERVATIONS = ( + "OVERLAPPING_RESERVATIONS", + pgettext_lazy("RejectionReadiness", "Overlapping reservations"), + ) + RESERVATION_UNIT_CLOSED = ( + "RESERVATION_UNIT_CLOSED", + pgettext_lazy("RejectionReadiness", "Reservation unit closed"), + ) + + +class CalendarProperty(StrEnum): + VERSION = "VERSION" # type: str + """ + REQUIRED. Version of the iCalendar specification required to interpret the iCalendar object. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.7.4 + """ + + PRODID = "PRODID" # type: str + """ + REQUIRED. The identifier for the product that created the iCalendar object. + See: https://en.wikipedia.org/wiki/Formal_Public_Identifier + https://datatracker.ietf.org/doc/html/rfc5545#section-3.7.3 + """ + + +class EventProperty(StrEnum): + UID = "UID" # type: str + """ + The unique identifier for the calendar event. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.4.7 + """ + + DTSTAMP = "DTSTAMP" # type: datetime.datetime + """ + The date and time that the calendar event was created. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.7.2 + """ + + DTSTART = "DTSTART" # type: datetime.datetime + """ + The date and time that the calendar event begins. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.4 + """ + + DTEND = "DTEND" # type: datetime.datetime + """ + The date and time that the calendar event ends. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.2.2 + """ + + SUMMARY = "SUMMARY" # type: str + """ + A short summary or subject for the event. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.12 + """ + + DESCRIPTION = "DESCRIPTION" # type: str + """ + A more complete description for the event than that provided by "SUMMARY". + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.5 + """ + + LOCATION = "LOCATION" # type: str + """ + The intended venue for the event. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.7 + """ + + GEO = "GEO" # type: tuple[float, float] + """ + Global position for the activity specified by a event. + https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.6 + """ + + X_ALT_DESC = "X-ALT-DESC" # type: str + """ + A more complete description for the event than that provided by "SUMMARY". + Required for Outlook calendars to display HTML descriptions properly. + https://learn.microsoft.com/openspecs/exchange_server_protocols/ms-oxcical/d7f285da-9c7a-4597-803b-b74193c898a8 + """ diff --git a/reservations/management/commands/create_missing_statistics.py b/tilavarauspalvelu/management/commands/create_missing_statistics.py similarity index 87% rename from reservations/management/commands/create_missing_statistics.py rename to tilavarauspalvelu/management/commands/create_missing_statistics.py index ad00aa422..421720c02 100644 --- a/reservations/management/commands/create_missing_statistics.py +++ b/tilavarauspalvelu/management/commands/create_missing_statistics.py @@ -5,8 +5,8 @@ from django.core.management.base import BaseCommand from common.date_utils import local_datetime -from reservations.models import Reservation -from reservations.tasks import create_or_update_reservation_statistics +from tilavarauspalvelu.models import Reservation +from tilavarauspalvelu.tasks import create_or_update_reservation_statistics class Command(BaseCommand): diff --git a/tilavarauspalvelu/migrations/0010_migrate_reservations.py b/tilavarauspalvelu/migrations/0010_migrate_reservations.py new file mode 100644 index 000000000..23b864592 --- /dev/null +++ b/tilavarauspalvelu/migrations/0010_migrate_reservations.py @@ -0,0 +1,697 @@ +# Generated by Django 5.1.1 on 2024-09-26 14:07 + +import datetime +import re +import uuid + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import graphene_django_extensions.fields.model +from django.conf import settings +from django.db import migrations, models + +import tilavarauspalvelu.enums + + +class Migration(migrations.Migration): + dependencies = [ + ("applications", "0094_alter_applicationround_terms_of_use"), + ("reservation_units", "0110_alter_reservationunit_origin_hauki_resource"), + ("tilavarauspalvelu", "0009_migrate_opening_hours"), + ] + + run_before = [ + ("social_django", "0013_migrate_extra_data"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name="Reservation", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("sku", models.CharField(blank=True, default="", max_length=255)), + ("name", models.CharField(blank=True, default="", max_length=255)), + ("description", models.CharField(blank=True, default="", max_length=255)), + ("num_persons", models.PositiveIntegerField(blank=True, null=True)), + ( + "state", + models.CharField( + choices=[ + ("CREATED", "Created"), + ("CANCELLED", "Cancelled"), + ("REQUIRES_HANDLING", "Requires handling"), + ("WAITING_FOR_PAYMENT", "Waiting for payment"), + ("CONFIRMED", "Confirmed"), + ("DENIED", "Denied"), + ], + db_index=True, + default="CREATED", + max_length=32, + ), + ), + ( + "type", + models.CharField( + choices=[ + ("NORMAL", "Normal"), + ("BLOCKED", "Blocked"), + ("STAFF", "Staff"), + ("BEHALF", "Behalf"), + ("SEASONAL", "Seasonal"), + ], + default="NORMAL", + max_length=50, + null=True, + ), + ), + ("cancel_details", models.TextField(blank=True, default="")), + ("handling_details", models.TextField(blank=True, default="")), + ("working_memo", models.TextField(blank=True, default="", null=True)), + ("begin", models.DateTimeField(db_index=True)), + ("end", models.DateTimeField(db_index=True)), + ("buffer_time_before", models.DurationField(blank=True, default=datetime.timedelta(0))), + ("buffer_time_after", models.DurationField(blank=True, default=datetime.timedelta(0))), + ("handled_at", models.DateTimeField(blank=True, null=True)), + ("confirmed_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(default=django.utils.timezone.now, null=True)), + ("price", models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ("non_subsidised_price", models.DecimalField(decimal_places=2, default=0, max_digits=20)), + ("unit_price", models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ("tax_percentage_value", models.DecimalField(decimal_places=2, default=0, max_digits=5)), + ("applying_for_free_of_charge", models.BooleanField(blank=True, default=False)), + ("free_of_charge_reason", models.TextField(blank=True, null=True)), + ("reservee_id", models.CharField(blank=True, default="", max_length=255)), + ("reservee_first_name", models.CharField(blank=True, default="", max_length=255)), + ("reservee_last_name", models.CharField(blank=True, default="", max_length=255)), + ("reservee_email", models.EmailField(blank=True, max_length=254, null=True)), + ("reservee_phone", models.CharField(blank=True, default="", max_length=255)), + ("reservee_organisation_name", models.CharField(blank=True, default="", max_length=255)), + ("reservee_address_street", models.CharField(blank=True, default="", max_length=255)), + ("reservee_address_city", models.CharField(blank=True, default="", max_length=255)), + ("reservee_address_zip", models.CharField(blank=True, default="", max_length=255)), + ("reservee_is_unregistered_association", models.BooleanField(blank=True, default=False)), + ("reservee_used_ad_login", models.BooleanField(blank=True, default=False)), + ( + "reservee_language", + models.CharField( + blank=True, + choices=[("fi", "Finnish"), ("en", "English"), ("sv", "Swedish"), ("", "")], + default="", + max_length=255, + ), + ), + ( + "reservee_type", + models.CharField( + blank=True, + choices=[ + ("BUSINESS", "Business"), + ("NONPROFIT", "Nonprofit"), + ("INDIVIDUAL", "Individual"), + ], + max_length=50, + null=True, + ), + ), + ("billing_first_name", models.CharField(blank=True, default="", max_length=255)), + ("billing_last_name", models.CharField(blank=True, default="", max_length=255)), + ("billing_email", models.EmailField(blank=True, max_length=254, null=True)), + ("billing_phone", models.CharField(blank=True, default="", max_length=255)), + ("billing_address_street", models.CharField(blank=True, default="", max_length=255)), + ("billing_address_city", models.CharField(blank=True, default="", max_length=255)), + ("billing_address_zip", models.CharField(blank=True, default="", max_length=255)), + ( + "home_city", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="home_city_reservation", + to="applications.city", + ), + ), + ("reservation_unit", models.ManyToManyField(to="reservation_units.reservationunit")), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservations", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "reservation", + "ordering": ["begin"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="AbilityGroup", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("name", models.TextField(unique=True)), + ("name_fi", models.TextField(null=True, unique=True)), + ("name_en", models.TextField(null=True, unique=True)), + ("name_sv", models.TextField(null=True, unique=True)), + ], + options={ + "db_table": "ability_group", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="AgeGroup", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("minimum", models.PositiveIntegerField()), + ("maximum", models.PositiveIntegerField(blank=True, null=True)), + ], + options={ + "db_table": "age_group", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="RecurringReservation", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("name", models.CharField(blank=True, default="", max_length=255)), + ("description", models.CharField(blank=True, default="", max_length=500)), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("begin_date", models.DateField(null=True)), + ("begin_time", models.TimeField(null=True)), + ("end_date", models.DateField(null=True)), + ("end_time", models.TimeField(null=True)), + ("recurrence_in_days", models.PositiveIntegerField(blank=True, null=True)), + ( + "weekdays", + models.CharField( + blank=True, + choices=[ + (0, "Monday"), + (1, "Tuesday"), + (2, "Wednesday"), + (3, "Thursday"), + (4, "Friday"), + (5, "Saturday"), + (6, "Sunday"), + ], + default="", + max_length=16, + validators=[ + django.core.validators.RegexValidator( + re.compile("^\\d+(?:,\\d+)*\\Z"), + code="invalid", + message="Enter only digits separated by commas.", + ) + ], + ), + ), + ( + "ability_group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="recurring_reservations", + to="tilavarauspalvelu.abilitygroup", + ), + ), + ( + "age_group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="recurring_reservations", + to="tilavarauspalvelu.agegroup", + ), + ), + ( + "allocated_time_slot", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="recurring_reservation", + to="applications.allocatedtimeslot", + ), + ), + ( + "reservation_unit", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="recurring_reservations", + to="reservation_units.reservationunit", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "recurring_reservation", + "ordering": ["begin_date", "begin_time", "reservation_unit"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="ReservationCancelReason", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("reason", models.CharField(max_length=255)), + ("reason_fi", models.CharField(max_length=255, null=True)), + ("reason_en", models.CharField(max_length=255, null=True)), + ("reason_sv", models.CharField(max_length=255, null=True)), + ], + options={ + "db_table": "reservation_cancel_reason", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="ReservationDenyReason", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("rank", models.PositiveBigIntegerField(blank=True, db_index=True, null=True)), + ("reason", models.CharField(max_length=255)), + ("reason_fi", models.CharField(max_length=255, null=True)), + ("reason_en", models.CharField(max_length=255, null=True)), + ("reason_sv", models.CharField(max_length=255, null=True)), + ], + options={ + "db_table": "reservation_deny_reason", + "ordering": ["rank"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="ReservationMetadataField", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("field_name", models.CharField(max_length=100, unique=True)), + ], + options={ + "verbose_name": "Reservation metadata field", + "verbose_name_plural": "Reservation metadata fields", + "db_table": "reservation_metadata_field", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="ReservationPurpose", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("name", models.CharField(max_length=200)), + ("name_fi", models.CharField(max_length=200, null=True)), + ("name_en", models.CharField(max_length=200, null=True)), + ("name_sv", models.CharField(max_length=200, null=True)), + ], + options={ + "db_table": "reservation_purpose", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="AffectingTimeSpan", + fields=[ + ( + "reservation", + models.OneToOneField( + db_column="reservation_id", + on_delete=django.db.models.deletion.DO_NOTHING, + primary_key=True, + related_name="affecting_time_span", + serialize=False, + to="tilavarauspalvelu.reservation", + ), + ), + ( + "affected_reservation_unit_ids", + django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), size=None), + ), + ("buffered_start_datetime", models.DateTimeField()), + ("buffered_end_datetime", models.DateTimeField()), + ("is_blocking", models.BooleanField()), + ("buffer_time_before", models.DurationField()), + ("buffer_time_after", models.DurationField()), + ], + options={ + "verbose_name": "affecting time span", + "verbose_name_plural": "affecting time spans", + "db_table": "affecting_time_spans", + "ordering": ["buffered_start_datetime", "reservation_id"], + "managed": False, + "base_manager_name": "objects", + }, + ), + migrations.AlterField( + model_name="paymentorder", + name="reservation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="payment_order", + to="tilavarauspalvelu.reservation", + ), + ), + migrations.AddField( + model_name="reservation", + name="age_group", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="tilavarauspalvelu.agegroup", + ), + ), + migrations.AddField( + model_name="reservation", + name="recurring_reservation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="reservations", + to="tilavarauspalvelu.recurringreservation", + ), + ), + migrations.CreateModel( + name="RejectedOccurrence", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("begin_datetime", models.DateTimeField()), + ("end_datetime", models.DateTimeField()), + ( + "rejection_reason", + graphene_django_extensions.fields.model.StrChoiceField( + choices=[ + ("INTERVAL_NOT_ALLOWED", "Interval not allowed"), + ("OVERLAPPING_RESERVATIONS", "Overlapping reservations"), + ("RESERVATION_UNIT_CLOSED", "Reservation unit closed"), + ], + enum=tilavarauspalvelu.enums.RejectionReadinessChoice, + max_length=24, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "recurring_reservation", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="rejected_occurrences", + to="tilavarauspalvelu.recurringreservation", + ), + ), + ], + options={ + "verbose_name": "Rejected occurrence", + "verbose_name_plural": "Rejected occurrences", + "db_table": "rejected_occurrence", + "ordering": ["begin_datetime", "end_datetime"], + "base_manager_name": "objects", + }, + ), + migrations.AddField( + model_name="reservation", + name="cancel_reason", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="reservations", + to="tilavarauspalvelu.reservationcancelreason", + ), + ), + migrations.AddField( + model_name="reservation", + name="deny_reason", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="reservations", + to="tilavarauspalvelu.reservationdenyreason", + ), + ), + migrations.CreateModel( + name="ReservationMetadataSet", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "required_fields", + models.ManyToManyField( + blank=True, + related_name="metadata_sets_required", + to="tilavarauspalvelu.reservationmetadatafield", + ), + ), + ( + "supported_fields", + models.ManyToManyField( + related_name="metadata_sets_supported", to="tilavarauspalvelu.reservationmetadatafield" + ), + ), + ], + options={ + "verbose_name": "Reservation metadata set", + "verbose_name_plural": "Reservation metadata sets", + "db_table": "reservation_metadata_set", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.AddField( + model_name="reservation", + name="purpose", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="tilavarauspalvelu.reservationpurpose", + ), + ), + migrations.CreateModel( + name="ReservationStatistic", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("num_persons", models.PositiveIntegerField(blank=True, null=True)), + ("state", models.CharField(max_length=255)), + ("reservation_type", models.CharField(max_length=255, null=True)), + ("begin", models.DateTimeField()), + ("end", models.DateTimeField()), + ("buffer_time_before", models.DurationField(blank=True, default=datetime.timedelta(0))), + ("buffer_time_after", models.DurationField(blank=True, default=datetime.timedelta(0))), + ("reservation_handled_at", models.DateTimeField(blank=True, null=True)), + ("reservation_confirmed_at", models.DateTimeField(null=True)), + ("reservation_created_at", models.DateTimeField(default=django.utils.timezone.now, null=True)), + ("price", models.DecimalField(decimal_places=2, default=0, max_digits=10)), + ("price_net", models.DecimalField(decimal_places=6, default=0, max_digits=20)), + ("non_subsidised_price", models.DecimalField(decimal_places=2, default=0, max_digits=20)), + ("non_subsidised_price_net", models.DecimalField(decimal_places=6, default=0, max_digits=20)), + ("tax_percentage_value", models.DecimalField(decimal_places=2, default=0, max_digits=5)), + ("applying_for_free_of_charge", models.BooleanField(blank=True, default=False)), + ("reservee_id", models.CharField(blank=True, default="", max_length=255)), + ("reservee_organisation_name", models.CharField(blank=True, default="", max_length=255)), + ("reservee_address_zip", models.CharField(blank=True, default="", max_length=255)), + ( + "reservee_is_unregistered_association", + models.BooleanField(blank=True, default=False, null=True), + ), + ("reservee_language", models.CharField(blank=True, default="", max_length=255)), + ("reservee_type", models.CharField(blank=True, max_length=255, null=True)), + ("primary_reservation_unit_name", models.CharField(max_length=255)), + ("primary_unit_tprek_id", models.CharField(max_length=255, null=True)), + ("primary_unit_name", models.CharField(max_length=255)), + ("deny_reason_text", models.CharField(max_length=255)), + ("cancel_reason_text", models.CharField(max_length=255)), + ("purpose_name", models.CharField(blank=True, default="", max_length=255)), + ("home_city_name", models.CharField(blank=True, default="", max_length=255)), + ("home_city_municipality_code", models.CharField(default="", max_length=255)), + ("age_group_name", models.CharField(blank=True, default="", max_length=255)), + ("ability_group_name", models.TextField()), + ("updated_at", models.DateTimeField(auto_now=True, null=True)), + ("priority", models.IntegerField(blank=True, null=True)), + ("priority_name", models.CharField(blank=True, default="", max_length=255)), + ("duration_minutes", models.IntegerField()), + ("is_subsidised", models.BooleanField(default=False)), + ("is_recurring", models.BooleanField(default=False)), + ("recurrence_begin_date", models.DateField(null=True)), + ("recurrence_end_date", models.DateField(null=True)), + ("recurrence_uuid", models.CharField(blank=True, default="", max_length=255)), + ("reservee_uuid", models.CharField(blank=True, default="", max_length=255)), + ("reservee_used_ad_login", models.BooleanField(blank=True, default=False)), + ("is_applied", models.BooleanField(blank=True, default=False)), + ( + "ability_group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="tilavarauspalvelu.abilitygroup", + ), + ), + ( + "age_group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_statistics", + to="tilavarauspalvelu.agegroup", + ), + ), + ( + "cancel_reason", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_statistics", + to="tilavarauspalvelu.reservationcancelreason", + ), + ), + ( + "deny_reason", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_statistics", + to="tilavarauspalvelu.reservationdenyreason", + ), + ), + ( + "home_city", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_statistics", + to="applications.city", + ), + ), + ( + "primary_reservation_unit", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="reservation_units.reservationunit", + ), + ), + ( + "purpose", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reservation_statistics", + to="tilavarauspalvelu.reservationpurpose", + ), + ), + ( + "reservation", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="tilavarauspalvelu.reservation", + ), + ), + ], + options={ + "db_table": "reservation_statistic", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + migrations.CreateModel( + name="ReservationStatisticsReservationUnit", + fields=[ + ( + "id", + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + ), + ("name", models.CharField(max_length=255)), + ("unit_name", models.CharField(max_length=255)), + ("unit_tprek_id", models.CharField(max_length=255, null=True)), + ( + "reservation_statistics", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="reservation_stats_reservation_units", + to="tilavarauspalvelu.reservationstatistic", + ), + ), + ( + "reservation_unit", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="reservation_units.reservationunit", + ), + ), + ], + options={ + "db_table": "reservation_statistics_reservation_unit", + "ordering": ["pk"], + "base_manager_name": "objects", + }, + ), + ], + database_operations=[], + ), + ] diff --git a/tilavarauspalvelu/models/__init__.py b/tilavarauspalvelu/models/__init__.py index 0c68bfa8e..a136be24a 100644 --- a/tilavarauspalvelu/models/__init__.py +++ b/tilavarauspalvelu/models/__init__.py @@ -1,3 +1,6 @@ +from .ability_group.model import AbilityGroup +from .affecting_time_span.model import AffectingTimeSpan +from .age_group.model import AgeGroup from .building.model import Building from .email_template.model import EmailTemplate from .general_role.model import GeneralRole @@ -9,7 +12,17 @@ from .payment_product.model import PaymentProduct from .personal_info_view_log.model import PersonalInfoViewLog from .real_estate.model import RealEstate +from .recurring_reservation.model import RecurringReservation +from .rejected_occurrence.model import RejectedOccurrence from .reservable_time_span.model import ReservableTimeSpan +from .reservation.model import Reservation +from .reservation_cancel_reason.model import ReservationCancelReason +from .reservation_deny_reason.model import ReservationDenyReason +from .reservation_metadata_field.model import ReservationMetadataField +from .reservation_metadata_set.model import ReservationMetadataSet +from .reservation_purpose.model import ReservationPurpose +from .reservation_statistic.model import ReservationStatistic +from .reservation_statistic_unit.model import ReservationStatisticsReservationUnit from .resource.model import Resource from .service.model import Service from .service_sector.model import ServiceSector @@ -21,6 +34,9 @@ from .user.model import ProfileUser, User __all__ = [ + "AbilityGroup", + "AffectingTimeSpan", + "AgeGroup", "Building", "EmailTemplate", "GeneralRole", @@ -33,7 +49,17 @@ "PersonalInfoViewLog", "ProfileUser", "RealEstate", + "RecurringReservation", + "RejectedOccurrence", "ReservableTimeSpan", + "Reservation", + "ReservationCancelReason", + "ReservationDenyReason", + "ReservationMetadataField", + "ReservationMetadataSet", + "ReservationPurpose", + "ReservationStatistic", + "ReservationStatisticsReservationUnit", "Resource", "Service", "ServiceSector", diff --git a/tilavarauspalvelu/admin/metadata_set/__init__.py b/tilavarauspalvelu/models/ability_group/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/metadata_set/__init__.py rename to tilavarauspalvelu/models/ability_group/__init__.py diff --git a/tilavarauspalvelu/models/ability_group/actions.py b/tilavarauspalvelu/models/ability_group/actions.py new file mode 100644 index 000000000..5feee30c8 --- /dev/null +++ b/tilavarauspalvelu/models/ability_group/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import AbilityGroup + + +class AbilityGroupActions: + def __init__(self, ability_group: "AbilityGroup") -> None: + self.ability_group = ability_group diff --git a/tilavarauspalvelu/models/ability_group/model.py b/tilavarauspalvelu/models/ability_group/model.py new file mode 100644 index 000000000..5bd765591 --- /dev/null +++ b/tilavarauspalvelu/models/ability_group/model.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import AbilityGroupQuerySet + +if TYPE_CHECKING: + from .actions import AbilityGroupActions + + +__all__ = [ + "AbilityGroup", +] + + +class AbilityGroup(models.Model): + name = models.fields.TextField(null=False, blank=False, unique=True) + + objects = AbilityGroupQuerySet.as_manager() + + class Meta: + db_table = "ability_group" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return self.name + + @cached_property + def actions(self) -> AbilityGroupActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import AbilityGroupActions + + return AbilityGroupActions(self) diff --git a/tilavarauspalvelu/models/ability_group/queryset.py b/tilavarauspalvelu/models/ability_group/queryset.py new file mode 100644 index 000000000..3b251b242 --- /dev/null +++ b/tilavarauspalvelu/models/ability_group/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "AbilityGroupQuerySet", +] + + +class AbilityGroupQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/affecting_time_span/actions.py b/tilavarauspalvelu/models/affecting_time_span/actions.py index e69de29bb..efb16f2bc 100644 --- a/tilavarauspalvelu/models/affecting_time_span/actions.py +++ b/tilavarauspalvelu/models/affecting_time_span/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import AffectingTimeSpan + + +class AffectingTimeSpanActions: + def __init__(self, affecting_time_span: "AffectingTimeSpan") -> None: + self.affecting_time_span = affecting_time_span diff --git a/tilavarauspalvelu/models/affecting_time_span/model.py b/tilavarauspalvelu/models/affecting_time_span/model.py index e69de29bb..2a2f6e033 100644 --- a/tilavarauspalvelu/models/affecting_time_span/model.py +++ b/tilavarauspalvelu/models/affecting_time_span/model.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import contextlib +import datetime +from functools import cached_property +from typing import TYPE_CHECKING + +from django.conf import settings +from django.contrib.postgres.fields import ArrayField +from django.core.cache import cache +from django.db import models +from django.db.transaction import get_connection +from django.utils.translation import gettext_lazy as _ + +from common.date_utils import DEFAULT_TIMEZONE, local_datetime, timedelta_to_json +from utils.sentry import SentryLogger + +from .queryset import AffectingTimeSpanQuerySet + +if TYPE_CHECKING: + from tilavarauspalvelu.models import Reservation + from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement + + from .actions import AffectingTimeSpanActions + + +class AffectingTimeSpan(models.Model): + """ + A PostgreSQL materialized view that is used to cache reservations as time spans + for first reservable time calculation. Only future reservations are cached, + and only reservations that are actually going to occur. + + View contains an array of reservation unit ids that the time span affects, so it is possible + to query things like "Give me all time spans that affect reservation units X, Y, and Z". + + This view itself is created through a migration (See: `0073_affectingtimespan.py`.), + and updated through a scheduled task (See `update_affecting_time_spans_task`). + """ + + CACHE_KEY = "affecting_time_spans" + """Key for storing datetime stamp in cache of when the view was last updated.""" + + reservation: Reservation = models.OneToOneField( + "tilavarauspalvelu.Reservation", + on_delete=models.DO_NOTHING, + primary_key=True, + db_column="reservation_id", + related_name="affecting_time_span", + ) + + affected_reservation_unit_ids: list[int] = ArrayField(base_field=models.IntegerField()) + buffered_start_datetime: datetime.datetime = models.DateTimeField() + buffered_end_datetime: datetime.datetime = models.DateTimeField() + is_blocking: bool = models.BooleanField() + buffer_time_before: datetime.timedelta = models.DurationField() + buffer_time_after: datetime.timedelta = models.DurationField() + + objects = AffectingTimeSpanQuerySet.as_manager() + + class Meta: + managed = False + db_table = "affecting_time_spans" + verbose_name = _("affecting time span") + verbose_name_plural = _("affecting time spans") + base_manager_name = "objects" + ordering = [ + "buffered_start_datetime", + "reservation_id", + ] + + def __str__(self) -> str: + return self.__repr__() + + def __repr__(self) -> str: + start_buffered = self.buffered_start_datetime.astimezone(DEFAULT_TIMEZONE).replace(tzinfo=None) + end_buffered = self.buffered_end_datetime.astimezone(DEFAULT_TIMEZONE).replace(tzinfo=None) + + start = start_buffered + self.buffer_time_before + end = end_buffered - self.buffer_time_after + + start_str = start.strftime("%Y-%m-%d %H:%M") + end_str = end.strftime("%H:%M") if end.date() == start.date() else end.strftime("%Y-%m-%d %H:%M") + + duration_str = f"{start_str}-{end_str}" + + if self.buffer_time_before: + duration_str += f", -{timedelta_to_json(self.buffer_time_before, timespec='minutes')}" + if self.buffer_time_after: + duration_str += f", +{timedelta_to_json(self.buffer_time_after, timespec='minutes')}" + + return f"" + + @cached_property + def actions(self) -> AffectingTimeSpanActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import AffectingTimeSpanActions + + return AffectingTimeSpanActions(self) + + @classmethod + def refresh(cls, using: str | None = None) -> None: + """ + Called to refresh the contents of the materialized view. + + The view gets stale quite often, since it's dependent on current time and reservations. + Therefore, this is used as a sort of cache, which is updated as a scheduled task, + but can also be called manually if needed. + + Refreshing updated a value in cache that can be used to check if the view is valid. + """ + try: + with get_connection(using).cursor() as cursor: + cursor.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY affecting_time_spans") + except Exception as error: + # Only raise error in local development, otherwise log to Sentry + if settings.RAISE_ERROR_ON_REFRESH_FAILURE: + raise + SentryLogger.log_exception(error) + else: + last_updated = local_datetime().isoformat() + max_allowed_age = datetime.timedelta(minutes=settings.AFFECTING_TIME_SPANS_UPDATE_INTERVAL_MINUTES) + cache.set(cls.CACHE_KEY, last_updated, timeout=max_allowed_age.total_seconds()) + + @classmethod + @contextlib.contextmanager + def refresh_at_the_end(cls) -> None: + """Refresh the materialized view at the end of the context.""" + try: + yield + finally: + cls.refresh() + + @classmethod + def is_valid(cls) -> bool: + """Check last update datetime against a set max allowed age..""" + cached_value: str | None = cache.get(cls.CACHE_KEY) + if cached_value is None: + return False + last_updated = datetime.datetime.fromisoformat(cached_value) + max_allowed_age = datetime.timedelta(minutes=settings.AFFECTING_TIME_SPANS_UPDATE_INTERVAL_MINUTES) + return local_datetime() - last_updated < max_allowed_age + + def as_time_span_element(self) -> TimeSpanElement: + from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement + + return TimeSpanElement( + start_datetime=self.buffered_start_datetime + self.buffer_time_before, + end_datetime=self.buffered_end_datetime - self.buffer_time_after, + is_reservable=False, + # Buffers are ignored for blocking reservation even if set. + buffer_time_before=None if self.is_blocking else self.buffer_time_before, + buffer_time_after=None if self.is_blocking else self.buffer_time_after, + ) diff --git a/tilavarauspalvelu/models/affecting_time_span/queryset.py b/tilavarauspalvelu/models/affecting_time_span/queryset.py index e69de29bb..746f45dba 100644 --- a/tilavarauspalvelu/models/affecting_time_span/queryset.py +++ b/tilavarauspalvelu/models/affecting_time_span/queryset.py @@ -0,0 +1,4 @@ +from django.db import models + + +class AffectingTimeSpanQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/age_group/actions.py b/tilavarauspalvelu/models/age_group/actions.py index e69de29bb..f88db5f63 100644 --- a/tilavarauspalvelu/models/age_group/actions.py +++ b/tilavarauspalvelu/models/age_group/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import AgeGroup + + +class AgeGroupActions: + def __init__(self, age_group: "AgeGroup") -> None: + self.age_group = age_group diff --git a/tilavarauspalvelu/models/age_group/model.py b/tilavarauspalvelu/models/age_group/model.py index e69de29bb..62f40f0c4 100644 --- a/tilavarauspalvelu/models/age_group/model.py +++ b/tilavarauspalvelu/models/age_group/model.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import AgeGroupQuerySet + +if TYPE_CHECKING: + from .actions import AgeGroupActions + +__all__ = [ + "AgeGroup", +] + + +class AgeGroup(models.Model): + minimum = models.fields.PositiveIntegerField(null=False, blank=False) + maximum = models.fields.PositiveIntegerField(null=True, blank=True) + + objects = AgeGroupQuerySet.as_manager() + + class Meta: + db_table = "age_group" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + if self.maximum is None: + return f"{self.minimum}+" + return f"{self.minimum} - {self.maximum}" + + @cached_property + def actions(self) -> AgeGroupActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import AgeGroupActions + + return AgeGroupActions(self) diff --git a/tilavarauspalvelu/models/age_group/queryset.py b/tilavarauspalvelu/models/age_group/queryset.py index e69de29bb..4c4997ae4 100644 --- a/tilavarauspalvelu/models/age_group/queryset.py +++ b/tilavarauspalvelu/models/age_group/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "AgeGroupQuerySet", +] + + +class AgeGroupQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/cancel_reason/actions.py b/tilavarauspalvelu/models/cancel_reason/actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/cancel_reason/model.py b/tilavarauspalvelu/models/cancel_reason/model.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/cancel_reason/queryset.py b/tilavarauspalvelu/models/cancel_reason/queryset.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/deny_reason/actions.py b/tilavarauspalvelu/models/deny_reason/actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/deny_reason/model.py b/tilavarauspalvelu/models/deny_reason/model.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/deny_reason/queryset.py b/tilavarauspalvelu/models/deny_reason/queryset.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/metadata_field/actions.py b/tilavarauspalvelu/models/metadata_field/actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/metadata_field/model.py b/tilavarauspalvelu/models/metadata_field/model.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/metadata_field/queryset.py b/tilavarauspalvelu/models/metadata_field/queryset.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/metadata_set/actions.py b/tilavarauspalvelu/models/metadata_set/actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/metadata_set/model.py b/tilavarauspalvelu/models/metadata_set/model.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/metadata_set/queryset.py b/tilavarauspalvelu/models/metadata_set/queryset.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tilavarauspalvelu/models/payment_order/model.py b/tilavarauspalvelu/models/payment_order/model.py index 91359a248..0a94e8ba2 100644 --- a/tilavarauspalvelu/models/payment_order/model.py +++ b/tilavarauspalvelu/models/payment_order/model.py @@ -11,9 +11,7 @@ from django.utils.translation import gettext_lazy as _ from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice -from tilavarauspalvelu.enums import Language, OrderStatus, PaymentType -from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ReservationEmailNotificationSender +from tilavarauspalvelu.enums import Language, OrderStatus, PaymentType, ReservationStateChoice from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError from tilavarauspalvelu.utils.verkkokauppa.payment.types import Payment @@ -26,7 +24,7 @@ if TYPE_CHECKING: import uuid - from reservations.models import Reservation + from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.verkkokauppa.order.types import Order from .actions import PaymentOrderActions @@ -39,7 +37,7 @@ class PaymentOrder(models.Model): reservation: Reservation | None = models.ForeignKey( - "reservations.Reservation", + "tilavarauspalvelu.Reservation", related_name="payment_order", on_delete=models.SET_NULL, null=True, @@ -172,6 +170,10 @@ def update_order_status(self, new_status: OrderStatus, payment_id: str = "") -> and self.reservation is not None and self.reservation.state == ReservationStateChoice.WAITING_FOR_PAYMENT ): + from tilavarauspalvelu.utils.email.reservation_email_notification_sender import ( + ReservationEmailNotificationSender, + ) + self.reservation.state = ReservationStateChoice.CONFIRMED self.reservation.save(update_fields=["state"]) ReservationEmailNotificationSender.send_confirmation_email(reservation=self.reservation) diff --git a/tilavarauspalvelu/models/recurring_reservation/actions.py b/tilavarauspalvelu/models/recurring_reservation/actions.py index e69de29bb..defb17f81 100644 --- a/tilavarauspalvelu/models/recurring_reservation/actions.py +++ b/tilavarauspalvelu/models/recurring_reservation/actions.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import dataclasses +import datetime +from itertools import chain +from typing import TYPE_CHECKING, Any, TypedDict + +from common.date_utils import DEFAULT_TIMEZONE, combine, get_periods_between +from tilavarauspalvelu.enums import ( + CustomerTypeChoice, + RejectionReadinessChoice, + ReservationStateChoice, + ReservationTypeChoice, + ReservationTypeStaffChoice, +) +from tilavarauspalvelu.models import ( + AffectingTimeSpan, + RecurringReservation, + RejectedOccurrence, + ReservableTimeSpan, + Reservation, + ReservationPurpose, +) +from tilavarauspalvelu.utils.opening_hours.time_span_element import TimeSpanElement + +if TYPE_CHECKING: + from collections.abc import Collection, Iterable + + from django.db import models + + from applications.models import City + from users.models import User + + +class ReservationPeriod(TypedDict): + begin: datetime.datetime + end: datetime.datetime + + +@dataclasses.dataclass +class ReservationSeriesCalculationResults: + non_overlapping: list[ReservationPeriod] = dataclasses.field(default_factory=list) + overlapping: list[ReservationPeriod] = dataclasses.field(default_factory=list) + not_reservable: list[ReservationPeriod] = dataclasses.field(default_factory=list) + invalid_start_interval: list[ReservationPeriod] = dataclasses.field(default_factory=list) + + def as_json(self, periods: list[ReservationPeriod]) -> list[dict[str, Any]]: + return [ + { + "begin": period["begin"].isoformat(timespec="seconds"), + "end": period["end"].isoformat(timespec="seconds"), + } + for period in periods + ] + + @property + def overlapping_json(self) -> list[dict[str, Any]]: + return self.as_json(self.overlapping) + + @property + def not_reservable_json(self) -> list[dict[str, Any]]: + return self.as_json(self.not_reservable) + + @property + def invalid_start_interval_json(self) -> list[dict[str, Any]]: + return self.as_json(self.invalid_start_interval) + + @property + def possible(self) -> Iterable[ReservationPeriod]: + return self.non_overlapping + + @property + def not_possible(self) -> Iterable[ReservationPeriod]: + return chain(self.overlapping, self.not_reservable, self.invalid_start_interval) + + +class ReservationDetails(TypedDict, total=False): + name: str + description: str + num_persons: int + state: ReservationStateChoice + type: ReservationTypeChoice | ReservationTypeStaffChoice + working_memo: str + + buffer_time_before: datetime.timedelta + buffer_time_after: datetime.timedelta + handled_at: datetime.datetime + confirmed_at: datetime.datetime + + applying_for_free_of_charge: bool + free_of_charge_reason: bool + + reservee_id: str + reservee_first_name: str + reservee_last_name: str + reservee_email: str + reservee_phone: str + reservee_organisation_name: str + reservee_address_street: str + reservee_address_city: str + reservee_address_zip: str + reservee_is_unregistered_association: bool + reservee_language: str + reservee_type: CustomerTypeChoice + + billing_first_name: str + billing_last_name: str + billing_email: str + billing_phone: str + billing_address_street: str + billing_address_city: str + billing_address_zip: str + + user: int | User + purpose: int | ReservationPurpose + home_city: int | City + + +class RecurringReservationActions: + def __init__(self, recurring_reservation: RecurringReservation) -> None: + self.recurring_reservation = recurring_reservation + + def pre_calculate_slots( + self, + *, + check_opening_hours: bool = False, + check_buffers: bool = False, + check_start_interval: bool = False, + skip_dates: Collection[datetime.date] = (), + closed_hours: Collection[TimeSpanElement] = (), + buffer_time_before: datetime.timedelta | None = None, + buffer_time_after: datetime.timedelta | None = None, + ) -> ReservationSeriesCalculationResults: + """ + Pre-calculate slots for reservations for the recurring reservation. + + :param check_opening_hours: Whether to check if the reservation falls within reservable times. + :param check_buffers: Whether to check if the reservation overlaps with other reservations' buffers. + :param check_start_interval: Whether to check if the reservation starts at the correct interval. + :param skip_dates: Dates to skip when calculating slots. + :param closed_hours: Explicitly closed opening hours for the resource. + :param buffer_time_before: Used buffer time before the reservation. + :param buffer_time_after: Used buffer time after the reservation. + """ + pk = self.recurring_reservation.reservation_unit.pk + + timespans = [ + timespan.as_time_span_element() + for timespan in AffectingTimeSpan.objects.filter( + affected_reservation_unit_ids__contains=[pk], + buffered_start_datetime__date__lte=self.recurring_reservation.end_date, + buffered_end_datetime__date__gte=self.recurring_reservation.begin_date, + ) + ] + + reservable_timespans = self.get_reservable_timespans() if check_opening_hours else [] + + results = ReservationSeriesCalculationResults() + + begin_time: datetime.time = self.recurring_reservation.begin_time + end_time: datetime.time = self.recurring_reservation.end_time + reservation_unit = self.recurring_reservation.reservation_unit + + weekdays: list[int] = [int(val) for val in self.recurring_reservation.weekdays.split(",") if val != ""] + if not weekdays: + weekdays = [self.recurring_reservation.begin_date.weekday()] + + for weekday in weekdays: + delta: int = weekday - self.recurring_reservation.begin_date.weekday() + if delta < 0: + delta += 7 + + begin_date: datetime.date = self.recurring_reservation.begin_date + datetime.timedelta(days=delta) + + periods = get_periods_between( + start_date=begin_date, + end_date=self.recurring_reservation.end_date, + start_time=begin_time, + end_time=end_time, + interval=self.recurring_reservation.recurrence_in_days, + tzinfo=DEFAULT_TIMEZONE, + ) + for begin, end in periods: + if begin.date() in skip_dates: + continue + + reservation_timespan = TimeSpanElement( + start_datetime=begin, + end_datetime=end, + is_reservable=True, + buffer_time_before=( + reservation_unit.actions.get_actual_before_buffer(begin, buffer_time_before) + if check_buffers + else None + ), + buffer_time_after=( + reservation_unit.actions.get_actual_after_buffer(end, buffer_time_after) + if check_buffers + else None + ), + ) + + # Would the reservation timespan overlap with any closing timespans + # that exist due to existing reservations? Checks for: + # 1) Unbuffered reservation timespan overlapping with any buffered closed timespan + # 2) Unbuffered closed timespan overlapping with any buffered reservation timespan + # Note that reservation timespans buffers are only checked if `check_buffers=True`. + if any( + reservation_timespan.overlaps_with(timespan) or timespan.overlaps_with(reservation_timespan) + for timespan in timespans + ): + results.overlapping.append(ReservationPeriod(begin=begin, end=end)) + continue + + # Would the reservation be fully inside any reservable timespans for the resource? + # Ignore buffers for the reservation, since those can be outside reservable times. + if check_opening_hours and not any( + reservation_timespan.fully_inside_of(reservable) for reservable in reservable_timespans + ): + results.not_reservable.append(ReservationPeriod(begin=begin, end=end)) + continue + + # Would the reservation overlap with any explicitly closed opening hours for the resource? + if closed_hours and any( + reservation_timespan.overlaps_with(closed_time_span) for closed_time_span in closed_hours + ): + results.not_reservable.append(ReservationPeriod(begin=begin, end=end)) + continue + + if check_start_interval and not reservation_unit.actions.is_valid_staff_start_interval(begin.timetz()): + results.invalid_start_interval.append(ReservationPeriod(begin=begin, end=end)) + continue + + results.non_overlapping.append(ReservationPeriod(begin=begin, end=end)) + + return results + + def get_reservable_timespans(self) -> list[TimeSpanElement]: + begin_time = self.recurring_reservation.begin_time + end_time = self.recurring_reservation.end_time + hauki_resource = self.recurring_reservation.reservation_unit.origin_hauki_resource + if hauki_resource is None: + return [] + + timespans: Iterable[ReservableTimeSpan] = hauki_resource.reservable_time_spans.all().overlapping_with_period( + start=combine(self.recurring_reservation.begin_date, begin_time, tzinfo=DEFAULT_TIMEZONE), + end=combine(self.recurring_reservation.end_date, end_time, tzinfo=DEFAULT_TIMEZONE), + ) + + return [timespan.as_time_span_element() for timespan in timespans] + + def bulk_create_reservation_for_periods( + self, + periods: Iterable[ReservationPeriod], + reservation_details: ReservationDetails, + ) -> list[Reservation]: + # Pick out the through model for the many-to-many relationship and use if for bulk creation + ThroughModel: type[models.Model] = Reservation.reservation_unit.through # noqa: N806 + + reservations: list[Reservation] = [] + through_models: list[models.Model] = [] + + for period in periods: + if self.recurring_reservation.reservation_unit.reservation_block_whole_day: + reservation_details.setdefault( + "buffer_time_before", + self.recurring_reservation.reservation_unit.actions.get_actual_before_buffer(period["begin"]), + ) + reservation_details.setdefault( + "buffer_time_after", + self.recurring_reservation.reservation_unit.actions.get_actual_after_buffer(period["end"]), + ) + + reservation = Reservation( + begin=period["begin"], + end=period["end"], + recurring_reservation=self.recurring_reservation, + age_group=self.recurring_reservation.age_group, + **reservation_details, + ) + through = ThroughModel( + reservation=reservation, + reservationunit=self.recurring_reservation.reservation_unit, + ) + reservations.append(reservation) + through_models.append(through) + + reservations = Reservation.objects.bulk_create(reservations) + ThroughModel.objects.bulk_create(through_models) + return reservations + + def bulk_create_rejected_occurrences_for_periods( + self, + overlapping: Iterable[ReservationPeriod], + not_reservable: Iterable[ReservationPeriod], + invalid_start_interval: Iterable[ReservationPeriod], + ) -> list[RejectedOccurrence]: + occurrences: list[RejectedOccurrence] = ( + [ + RejectedOccurrence( + begin_datetime=period["begin"], + end_datetime=period["end"], + rejection_reason=RejectionReadinessChoice.OVERLAPPING_RESERVATIONS, + recurring_reservation=self.recurring_reservation, + ) + for period in overlapping + ] + + [ + RejectedOccurrence( + begin_datetime=period["begin"], + end_datetime=period["end"], + rejection_reason=RejectionReadinessChoice.RESERVATION_UNIT_CLOSED, + recurring_reservation=self.recurring_reservation, + ) + for period in not_reservable + ] + + [ + RejectedOccurrence( + begin_datetime=period["begin"], + end_datetime=period["end"], + rejection_reason=RejectionReadinessChoice.INTERVAL_NOT_ALLOWED, + recurring_reservation=self.recurring_reservation, + ) + for period in invalid_start_interval + ] + ) + + return RejectedOccurrence.objects.bulk_create(occurrences) diff --git a/tilavarauspalvelu/models/recurring_reservation/model.py b/tilavarauspalvelu/models/recurring_reservation/model.py index e69de29bb..77f1981de 100644 --- a/tilavarauspalvelu/models/recurring_reservation/model.py +++ b/tilavarauspalvelu/models/recurring_reservation/model.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import uuid as uuid_ +from functools import cached_property +from typing import TYPE_CHECKING + +from django.core.validators import validate_comma_separated_integer_list +from django.db import models + +from config.utils.commons import WEEKDAYS +from tilavarauspalvelu.enums import ReservationStateChoice + +from .queryset import RecurringReservationQuerySet + +if TYPE_CHECKING: + import datetime + + from applications.models import AllocatedTimeSlot + from reservation_units.models import ReservationUnit + from tilavarauspalvelu.models import AbilityGroup, AgeGroup, Reservation + from users.models import User + + from .actions import RecurringReservationActions + + +__all__ = [ + "RecurringReservation", +] + + +class RecurringReservation(models.Model): + name: str = models.CharField(max_length=255, blank=True, default="") + description: str = models.CharField(max_length=500, blank=True, default="") + uuid: uuid_.UUID = models.UUIDField(default=uuid_.uuid4, editable=False, unique=True) + created: datetime.datetime = models.DateTimeField(auto_now_add=True) + + begin_date: datetime.date | None = models.DateField(null=True) + begin_time: datetime.time | None = models.TimeField(null=True) + end_date: datetime.date | None = models.DateField(null=True) + end_time: datetime.time | None = models.TimeField(null=True) + + recurrence_in_days: int | None = models.PositiveIntegerField(null=True, blank=True) + + weekdays: str = models.CharField( + max_length=16, + validators=[validate_comma_separated_integer_list], + choices=WEEKDAYS.CHOICES, + blank=True, + default="", + ) + + # Relations + + reservation_unit: ReservationUnit = models.ForeignKey( + "reservation_units.ReservationUnit", + on_delete=models.PROTECT, + related_name="recurring_reservations", + ) + user: User | None = models.ForeignKey( + "tilavarauspalvelu.User", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + allocated_time_slot: AllocatedTimeSlot | None = models.OneToOneField( + "applications.AllocatedTimeSlot", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="recurring_reservation", + ) + age_group: AgeGroup | None = models.ForeignKey( + "tilavarauspalvelu.AgeGroup", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="recurring_reservations", + ) + + # TODO: Remove these fields + ability_group: AbilityGroup | None = models.ForeignKey( + "tilavarauspalvelu.AbilityGroup", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="recurring_reservations", + ) + + objects = RecurringReservationQuerySet.as_manager() + + class Meta: + db_table = "recurring_reservation" + base_manager_name = "objects" + ordering = [ + "begin_date", + "begin_time", + "reservation_unit", + ] + + def __str__(self) -> str: + return f"{self.name}" + + @cached_property + def actions(self) -> RecurringReservationActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import RecurringReservationActions + + return RecurringReservationActions(self) + + @property + def denied_reservations(self): # DEPRECATED + """Used in `api.legacy_rest_api.serializers.RecurringReservationSerializer`""" + # Avoid a query to the database if we have fetched list already + reservation: Reservation # noqa: F842 + if "reservations" in self._prefetched_objects_cache: + return [ + reservation + for reservation in self.reservations.all() + if reservation.state == ReservationStateChoice.DENIED + ] + + return self.reservations.filter(state=ReservationStateChoice.DENIED) diff --git a/tilavarauspalvelu/models/recurring_reservation/queryset.py b/tilavarauspalvelu/models/recurring_reservation/queryset.py index e69de29bb..79a4b33bd 100644 --- a/tilavarauspalvelu/models/recurring_reservation/queryset.py +++ b/tilavarauspalvelu/models/recurring_reservation/queryset.py @@ -0,0 +1,4 @@ +from django.db import models + + +class RecurringReservationQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/rejected_occurrence/actions.py b/tilavarauspalvelu/models/rejected_occurrence/actions.py index e69de29bb..c6cd45fa8 100644 --- a/tilavarauspalvelu/models/rejected_occurrence/actions.py +++ b/tilavarauspalvelu/models/rejected_occurrence/actions.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from tilavarauspalvelu.models import RejectedOccurrence + +__all__ = [ + "RejectedOccurrenceActions", +] + + +class RejectedOccurrenceActions: + def __init__(self, rejected_occurrence: RejectedOccurrence) -> None: + self.rejected_occurrence = rejected_occurrence diff --git a/tilavarauspalvelu/models/rejected_occurrence/model.py b/tilavarauspalvelu/models/rejected_occurrence/model.py index e69de29bb..ce1a781e7 100644 --- a/tilavarauspalvelu/models/rejected_occurrence/model.py +++ b/tilavarauspalvelu/models/rejected_occurrence/model.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from graphene_django_extensions.fields.model import StrChoiceField + +from tilavarauspalvelu.enums import RejectionReadinessChoice + +from .queryset import RejectedOccurrenceQuerySet + +if TYPE_CHECKING: + import datetime + + from tilavarauspalvelu.models import RecurringReservation + + from .actions import RejectedOccurrenceActions + + +__all__ = [ + "RejectedOccurrence", +] + + +class RejectedOccurrence(models.Model): + begin_datetime: datetime.datetime = models.DateTimeField() + end_datetime: datetime.datetime = models.DateTimeField() + rejection_reason: str = StrChoiceField(enum=RejectionReadinessChoice) + created_at: datetime.datetime = models.DateTimeField(auto_now_add=True) + + recurring_reservation: RecurringReservation = models.ForeignKey( + "tilavarauspalvelu.RecurringReservation", + on_delete=models.CASCADE, + related_name="rejected_occurrences", + ) + + objects = RejectedOccurrenceQuerySet.as_manager() + + class Meta: + db_table = "rejected_occurrence" + base_manager_name = "objects" + verbose_name = _("Rejected occurrence") + verbose_name_plural = _("Rejected occurrences") + ordering = [ + "begin_datetime", + "end_datetime", + ] + + def __str__(self) -> str: + return f"{_("Rejected occurrence")} ({self.begin_datetime.isoformat()} - {self.end_datetime.isoformat()})" + + @cached_property + def actions(self) -> RejectedOccurrenceActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import RejectedOccurrenceActions + + return RejectedOccurrenceActions(self) diff --git a/tilavarauspalvelu/models/rejected_occurrence/queryset.py b/tilavarauspalvelu/models/rejected_occurrence/queryset.py index e69de29bb..0cbb5ded4 100644 --- a/tilavarauspalvelu/models/rejected_occurrence/queryset.py +++ b/tilavarauspalvelu/models/rejected_occurrence/queryset.py @@ -0,0 +1,39 @@ +from typing import Self + +from django.db import models +from lookup_property import L + +from tilavarauspalvelu.enums import RejectionReadinessChoice + + +class RejectedOccurrenceQuerySet(models.QuerySet): + def order_by_applicant(self, *, desc: bool = False) -> Self: + applicant_ref = ( + "recurring_reservation" + "__allocated_time_slot" + "__reservation_unit_option" + "__application_section" + "__application" + "__applicant" + ) + return self.order_by(L(applicant_ref).order_by(descending=desc)) + + def order_by_rejection_reason(self, *, desc: bool = False) -> Self: + return self.alias( + rejection_reason_order=models.Case( + models.When( + rejection_reason=models.Value(RejectionReadinessChoice.INTERVAL_NOT_ALLOWED), + then=models.Value(0), + ), + models.When( + rejection_reason=models.Value(RejectionReadinessChoice.OVERLAPPING_RESERVATIONS), + then=models.Value(1), + ), + models.When( + rejection_reason=models.Value(RejectionReadinessChoice.RESERVATION_UNIT_CLOSED), + then=models.Value(2), + ), + default=models.Value(3), + output_field=models.IntegerField(), + ), + ).order_by(models.OrderBy(models.F("rejection_reason_order"), descending=desc)) diff --git a/tilavarauspalvelu/models/reservation/actions.py b/tilavarauspalvelu/models/reservation/actions.py index e69de29bb..071b76c05 100644 --- a/tilavarauspalvelu/models/reservation/actions.py +++ b/tilavarauspalvelu/models/reservation/actions.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING + +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import pgettext_lazy +from icalendar import Calendar, Event + +from common.date_utils import DEFAULT_TIMEZONE, local_datetime +from common.utils import get_attr_by_language +from tilavarauspalvelu.enums import CalendarProperty, EventProperty + +if TYPE_CHECKING: + from common.typing import Lang + from reservation_units.models import ReservationUnit + from tilavarauspalvelu.models import Location, Reservation, Unit + + +__all__ = [ + "ReservationActions", +] + + +class ReservationActions: + def __init__(self, reservation: Reservation) -> None: + self.reservation = reservation + + def get_actual_before_buffer(self) -> datetime.timedelta: + buffer_time_before: datetime.timedelta = self.reservation.buffer_time_before or datetime.timedelta() + reservation_unit: ReservationUnit + for reservation_unit in self.reservation.reservation_unit.all(): + before = reservation_unit.actions.get_actual_before_buffer(self.reservation.begin) + buffer_time_before = max(before, buffer_time_before) + return buffer_time_before + + def get_actual_after_buffer(self) -> datetime.timedelta: + buffer_time_after: datetime.timedelta = self.reservation.buffer_time_after or datetime.timedelta() + reservation_unit: ReservationUnit + for reservation_unit in self.reservation.reservation_unit.all(): + after = reservation_unit.actions.get_actual_after_buffer(self.reservation.end) + buffer_time_after = max(after, buffer_time_after) + return buffer_time_after + + def to_ical(self, *, site_name: str) -> bytes: + language: Lang = ( # type: ignore[assignment] + self.reservation.reservee_language or self.reservation.user.get_preferred_language() + ) + + ical_event = Event() + # This should be unique such that if another iCal file is created + # for the same reservation, it will be the same as the previous one. + uid = f"varaamo.reservation.{self.reservation.pk}@{site_name}" + summary = self.get_ical_summary(language=language) + description = self.get_ical_description(site_name=site_name, language=language) + location = self.get_location() + + ical_event.add(name=EventProperty.UID, value=uid) + ical_event.add(name=EventProperty.DTSTAMP, value=local_datetime()) + ical_event.add(name=EventProperty.DTSTART, value=self.reservation.begin.astimezone(DEFAULT_TIMEZONE)) + ical_event.add(name=EventProperty.DTEND, value=self.reservation.end.astimezone(DEFAULT_TIMEZONE)) + + ical_event.add(name=EventProperty.SUMMARY, value=summary) + ical_event.add(name=EventProperty.DESCRIPTION, value=description, parameters={"FMTTYPE": "text/html"}) + ical_event.add(name=EventProperty.X_ALT_DESC, value=description, parameters={"FMTTYPE": "text/html"}) + + if location is not None: + ical_event.add(name=EventProperty.LOCATION, value=location.address) + if location.coordinates is not None: + ical_event.add(name=EventProperty.GEO, value=(location.lat, location.lon)) + + cal = Calendar() + cal.add(CalendarProperty.VERSION, "2.0") + cal.add(CalendarProperty.PRODID, "-//Helsinki City//NONSGML Varaamo//FI") + + cal.add_component(ical_event) + return cal.to_ical() + + def get_ical_summary(self, *, language: Lang = "fi") -> str: + unit: Unit = self.reservation.reservation_unit.first().unit + unit_name = get_attr_by_language(unit, "name", language) + return _("Reservation for %(name)s") % {"name": unit_name} + + def get_ical_description(self, *, site_name: str, language: Lang = "fi") -> str: + reservation_unit: ReservationUnit = self.reservation.reservation_unit.first() + unit: Unit = reservation_unit.unit + begin = self.reservation.begin.astimezone(DEFAULT_TIMEZONE) + end = self.reservation.end.astimezone(DEFAULT_TIMEZONE) + + title = _("Booking details") + reservation_unit_name = get_attr_by_language(reservation_unit, "name", language) + unit_name = get_attr_by_language(unit, "name", language) + location = self.get_location() + address = location.address if location is not None else "" + start_date = begin.date().strftime("%d.%m.%Y") + start_time = begin.time().strftime("%H:%M") + end_date = end.date().strftime("%d.%m.%Y") + end_time = end.time().strftime("%H:%M") + time_delimiter = "klo" if language == "fi" else "kl." if language == "sv" else "at" + if language == "sv": + site_name += "/sv" + elif language == "en": + site_name += "/en" + from_ = pgettext_lazy("ical", "From") + to_ = pgettext_lazy("ical", "To") + footer = _( + "Manage your booking at Varaamo. You can check the details of your booking and Varaamo's " + "terms of contract and cancellation on the '%(bookings)s' page." + ) % { + "bookings": f"" + _("My bookings") + "", + } + + return ( + f"" + f"" + f"" + f"

{title}

" + f"

{reservation_unit_name}, {unit_name}, {address}

" + f"

{from_}: {start_date} {time_delimiter} {start_time}

" + f"

{to_}: {end_date} {time_delimiter} {end_time}

" + f"

{footer}

" + f"" + f"" + ) + + def get_location(self) -> Location | None: + reservation_unit: ReservationUnit = self.reservation.reservation_unit.first() + unit: Unit = reservation_unit.unit + location: Location | None = getattr(unit, "location", None) + if location is None: + return reservation_unit.actions.get_location() + return location diff --git a/tilavarauspalvelu/models/reservation/model.py b/tilavarauspalvelu/models/reservation/model.py index e69de29bb..e0b4a8b5e 100644 --- a/tilavarauspalvelu/models/reservation/model.py +++ b/tilavarauspalvelu/models/reservation/model.py @@ -0,0 +1,324 @@ +from __future__ import annotations + +import datetime +from decimal import Decimal +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models +from django.db.models.functions import Concat, Trim +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from helsinki_gdpr.models import SerializableMixin +from lookup_property import lookup_property + +from config.utils.auditlog_util import AuditLogger +from tilavarauspalvelu.enums import ( + RESERVEE_LANGUAGE_CHOICES, + CustomerTypeChoice, + ReservationStateChoice, + ReservationTypeChoice, +) +from utils.decimal_utils import round_decimal + +from .queryset import ReservationQuerySet + +if TYPE_CHECKING: + from applications.models import City + from tilavarauspalvelu.models import ( + AgeGroup, + RecurringReservation, + ReservationCancelReason, + ReservationDenyReason, + ReservationPurpose, + Unit, + ) + from users.models import User + + from .actions import ReservationActions + + +class ReservationManager(SerializableMixin.SerializableManager, models.Manager.from_queryset(ReservationQuerySet)): + """Contains custom queryset methods and GDPR serialization.""" + + +class Reservation(SerializableMixin, models.Model): + # Basic information + sku: str = models.CharField(max_length=255, blank=True, default="") + name: str = models.CharField(max_length=255, blank=True, default="") + description: str = models.CharField(max_length=255, blank=True, default="") + num_persons: int | None = models.fields.PositiveIntegerField(null=True, blank=True) + state: str = models.CharField( + max_length=32, + choices=ReservationStateChoice.choices, + default=ReservationStateChoice.CREATED, + db_index=True, + ) + type: str | None = models.CharField( + max_length=50, + null=True, + blank=False, + choices=ReservationTypeChoice.choices, + default=ReservationTypeChoice.NORMAL, + ) + cancel_details: str = models.TextField(blank=True, default="") + handling_details: str = models.TextField(blank=True, default="") + working_memo: str = models.TextField(null=True, blank=True, default="") + + # Time information + begin: datetime.datetime = models.DateTimeField(db_index=True) + end: datetime.datetime = models.DateTimeField(db_index=True) + buffer_time_before: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) + buffer_time_after: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) + handled_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) + confirmed_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) + created_at: datetime.datetime | None = models.DateTimeField(null=True, default=now) + + # Pricing details + price: Decimal = models.DecimalField(max_digits=10, decimal_places=2, default=0) + non_subsidised_price: Decimal = models.DecimalField(max_digits=20, decimal_places=2, default=0) + unit_price: Decimal = models.DecimalField(max_digits=10, decimal_places=2, default=0) + tax_percentage_value: Decimal = models.DecimalField(max_digits=5, decimal_places=2, default=0) + + # Free of charge information + applying_for_free_of_charge: bool = models.BooleanField(default=False, blank=True) + free_of_charge_reason: bool | None = models.TextField(null=True, blank=True) + + # Reservee information + reservee_id: str = models.CharField(max_length=255, blank=True, default="") + reservee_first_name: str = models.CharField(max_length=255, blank=True, default="") + reservee_last_name: str = models.CharField(max_length=255, blank=True, default="") + reservee_email: str | None = models.EmailField(null=True, blank=True) + reservee_phone: str = models.CharField(max_length=255, blank=True, default="") + reservee_organisation_name: str = models.CharField(max_length=255, blank=True, default="") + reservee_address_street: str = models.CharField(max_length=255, blank=True, default="") + reservee_address_city: str = models.CharField(max_length=255, blank=True, default="") + reservee_address_zip: str = models.CharField(max_length=255, blank=True, default="") + reservee_is_unregistered_association: bool = models.BooleanField(default=False, blank=True) + reservee_used_ad_login: bool = models.BooleanField(default=False, blank=True) + reservee_language: str = models.CharField( + max_length=255, + blank=True, + default="", + choices=RESERVEE_LANGUAGE_CHOICES, + ) + reservee_type: str | None = models.CharField( + max_length=50, + choices=CustomerTypeChoice.choices, + null=True, + blank=True, + ) + + # Billing information + billing_first_name: str = models.CharField(max_length=255, blank=True, default="") + billing_last_name: str = models.CharField(max_length=255, blank=True, default="") + billing_email: str | None = models.EmailField(null=True, blank=True) + billing_phone: str = models.CharField(max_length=255, blank=True, default="") + billing_address_street: str = models.CharField(max_length=255, blank=True, default="") + billing_address_city: str = models.CharField(max_length=255, blank=True, default="") + billing_address_zip: str = models.CharField(max_length=255, blank=True, default="") + + # Relations + reservation_unit = models.ManyToManyField("reservation_units.ReservationUnit") + + user: User | None = models.ForeignKey( + "tilavarauspalvelu.User", + related_name="reservations", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + recurring_reservation: RecurringReservation | None = models.ForeignKey( + "tilavarauspalvelu.RecurringReservation", + related_name="reservations", + on_delete=models.PROTECT, + null=True, + blank=True, + ) + deny_reason: ReservationDenyReason | None = models.ForeignKey( + "tilavarauspalvelu.ReservationDenyReason", + related_name="reservations", + on_delete=models.PROTECT, + null=True, + blank=True, + ) + cancel_reason: ReservationCancelReason | None = models.ForeignKey( + "tilavarauspalvelu.ReservationCancelReason", + related_name="reservations", + on_delete=models.PROTECT, + null=True, + blank=True, + ) + purpose: ReservationPurpose | None = models.ForeignKey( + "tilavarauspalvelu.ReservationPurpose", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + home_city: City | None = models.ForeignKey( + "applications.City", + related_name="home_city_reservation", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + age_group: AgeGroup | None = models.ForeignKey( + "tilavarauspalvelu.AgeGroup", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + + objects = ReservationManager() + + class Meta: + db_table = "reservation" + base_manager_name = "objects" + ordering = ["begin"] + + # For GDPR API + serialize_fields = ( + {"name": "name"}, + {"name": "description"}, + {"name": "begin"}, + {"name": "end"}, + {"name": "reservee_first_name"}, + {"name": "reservee_last_name"}, + {"name": "reservee_email"}, + {"name": "reservee_phone"}, + {"name": "reservee_address_zip"}, + {"name": "reservee_address_city"}, + {"name": "reservee_address_street"}, + {"name": "billing_first_name"}, + {"name": "billing_last_name"}, + {"name": "billing_email"}, + {"name": "billing_phone"}, + {"name": "billing_address_zip"}, + {"name": "billing_address_city"}, + {"name": "billing_address_street"}, + {"name": "reservee_id"}, + {"name": "reservee_organisation_name"}, + {"name": "free_of_charge_reason"}, + {"name": "cancel_details"}, + ) + + def __str__(self) -> str: + return f"{self.name} ({self.type})" + + @cached_property + def actions(self) -> ReservationActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationActions + + return ReservationActions(self) + + @property + def price_net(self) -> Decimal: + """Return the net price of the reservation. (Price without VAT)""" + return round_decimal(self.price / (1 + self.tax_percentage_value / Decimal(100)), 2) + + @property + def price_vat_amount(self) -> Decimal: + """Return the VAT amount of the reservation.""" + return round_decimal(self.price - self.price_net, 2) + + @property + def non_subsidised_price_net(self) -> Decimal: + return round_decimal(self.non_subsidised_price / (1 + self.tax_percentage_value / Decimal(100)), 2) + + @lookup_property(joins=["recurring_reservation", "user"]) + def reservee_name() -> str: + return models.Case( # type: ignore[return-value] + # Blocking reservation + models.When( + condition=( + models.Q(type=ReservationTypeChoice.BLOCKED.value) # + ), + then=models.Value(str(_("Closed"))), + ), + # Internal reservations created by STAFF + models.When( + condition=( + models.Q(type=ReservationTypeChoice.STAFF.value) # + & models.Q(recurring_reservation__isnull=False) + & ~models.Q(recurring_reservation__name="") + ), + then=models.F("recurring_reservation__name"), + ), + models.When( + condition=( + models.Q(type=ReservationTypeChoice.STAFF.value) # + & (models.Q(recurring_reservation__isnull=True) | models.Q(recurring_reservation__name="")) + & ~models.Q(name="") + ), + then=models.F("name"), + ), + # Organisation reservee + models.When( + condition=( + models.Q(reservee_type__in=CustomerTypeChoice.organisation) # + & ~models.Q(reservee_organisation_name="") + ), + then=models.F("reservee_organisation_name"), + ), + # Individual reservee + models.When( + condition=( + ~models.Q(reservee_type__in=CustomerTypeChoice.organisation) # + & (~models.Q(reservee_first_name="") | ~models.Q(reservee_last_name="")) + ), + then=Trim(Concat("reservee_first_name", models.Value(" "), "reservee_last_name")), + ), + # Use reservation name when reservee name as first fallback + models.When( + condition=~models.Q(name=""), + then=models.F("name"), + ), + # Use the name of the User who made the reservation as the last fallback + models.When( + condition=( + models.Q(user__isnull=False) # + & ( + ~models.Q(user__first_name="") # + | ~models.Q(user__last_name="") + ) + ), + then=Trim(Concat("user__first_name", models.Value(" "), "user__last_name")), + ), + default=models.Value(""), + output_field=models.CharField(), + ) + + @property + def requires_handling(self) -> bool: + return ( + self.reservation_unit.filter(require_reservation_handling=True).exists() or self.applying_for_free_of_charge + ) + + @property + def units_for_permissions(self) -> list[Unit]: + from tilavarauspalvelu.models import Unit + + if hasattr(self, "_units_for_permissions"): + return self._units_for_permissions + + self._units_for_permissions = list( + Unit.objects.filter(reservationunit__reservation=self).prefetch_related("unit_groups").distinct() + ) + return self._units_for_permissions + + @units_for_permissions.setter + def units_for_permissions(self, value: list[Unit]) -> None: + # The setter is used by ReservationQuerySet to pre-evaluate units for multiple Reservations. + # Should not be used by anything else! + self._units_for_permissions = value + + +AuditLogger.register( + Reservation, + # Exclude lookup properties, since they are calculated values. + exclude_fields=[ + "_reservee_name", + ], +) diff --git a/tilavarauspalvelu/models/reservation/queryset.py b/tilavarauspalvelu/models/reservation/queryset.py index e69de29bb..0ff7d25be 100644 --- a/tilavarauspalvelu/models/reservation/queryset.py +++ b/tilavarauspalvelu/models/reservation/queryset.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Self + +from django.conf import settings +from django.contrib.postgres.aggregates import ArrayAgg +from django.db import models +from django.db.models.functions import Coalesce + +from common.date_utils import local_datetime +from tilavarauspalvelu.enums import OrderStatus, ReservationStateChoice + +if TYPE_CHECKING: + from applications.models import ApplicationRound + from tilavarauspalvelu.models import Reservation + + +class ReservationQuerySet(models.QuerySet): + def with_buffered_begin_and_end(self: Self) -> Self: + """Annotate the queryset with buffered begin and end times.""" + return self.annotate( + buffered_begin=models.F("begin") - models.F("buffer_time_before"), + buffered_end=models.F("end") + models.F("buffer_time_after"), + ) + + def filter_buffered_reservations_period(self: Self, start_date: datetime.date, end_date: datetime.date) -> Self: + """Filter reservations that are on the given period.""" + return ( + self.with_buffered_begin_and_end() + .filter( + buffered_begin__date__lte=end_date, + buffered_end__date__gte=start_date, + ) + .distinct() + .order_by("buffered_begin") + ) + + def total_duration(self: Self) -> datetime.timedelta: + return ( + self.annotate(duration=models.F("end") - models.F("begin")) + .aggregate(total_duration=models.Sum("duration")) + .get("total_duration") + ) or datetime.timedelta() + + def total_seconds(self: Self) -> int: + return int(self.total_duration().total_seconds()) + + def within_application_round_period(self: Self, app_round: ApplicationRound) -> Self: + return self.within_period( + app_round.reservation_period_begin, + app_round.reservation_period_end, + ) + + def within_period(self: Self, period_start: datetime.date, period_end: datetime.date) -> Self: + """All reservation fully withing a period.""" + return self.filter( + begin__date__gte=period_start, + end__date__lte=period_end, + ) + + def overlapping_period(self: Self, period_start: datetime.date, period_end: datetime.date) -> Self: + """All reservations that overlap with a period, even partially.""" + return self.filter( + begin__date__lte=period_end, + end__date__gte=period_start, + ) + + def going_to_occur(self: Self): + return self.filter(state__in=ReservationStateChoice.states_going_to_occur) + + def active(self: Self) -> Self: + """ + Filter reservations that have not ended yet. + + Note: + - There might be older reservations with buffers that are still active, + even if the reservation itself is not returned by this queryset. + - Returned data may contain some 'Inactive' reservations, before they are deleted by a periodic task. + """ + return self.going_to_occur().filter(end__gte=local_datetime()) + + def inactive(self: Self, older_than_minutes: int) -> Self: + """Filter 'draft' reservations, which are older than X minutes old, and can be assumed to be inactive.""" + return self.filter( + state=ReservationStateChoice.CREATED, + created_at__lte=local_datetime() - datetime.timedelta(minutes=older_than_minutes), + ) + + def with_inactive_payments(self: Self) -> Self: + expiration_time = local_datetime() - datetime.timedelta(minutes=settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES) + return self.filter( + state=ReservationStateChoice.WAITING_FOR_PAYMENT, + payment_order__remote_id__isnull=False, + payment_order__status__in=[OrderStatus.EXPIRED, OrderStatus.CANCELLED], + payment_order__created_at__lte=expiration_time, + ) + + def affecting_reservations(self: Self, units: list[int] = (), reservation_units: list[int] = ()) -> Self: + """Filter reservations that affect other reservations in the given units and/or reservation units.""" + from reservation_units.models import ReservationUnit + + qs = ReservationUnit.objects.all() + if units: + qs = qs.filter(unit__in=units) + if reservation_units: + qs = qs.filter(pk__in=reservation_units) + + return self.filter( + reservation_unit__in=models.Subquery(qs.affected_reservation_unit_ids), + ).exclude( + # Cancelled or denied reservations never affect any reservations + state__in=[ + ReservationStateChoice.CANCELLED, + ReservationStateChoice.DENIED, + ] + ) + + def _fetch_all(self) -> None: + super()._fetch_all() + if "FETCH_UNITS_FOR_PERMISSIONS_FLAG" in self._hints: + self._hints.pop("FETCH_UNITS_FOR_PERMISSIONS_FLAG", None) + self._add_units_for_permissions() + + def with_permissions(self) -> Self: + """Indicates that we need to fetch units for permissions checks when the queryset is evaluated.""" + self._hints["FETCH_UNITS_FOR_PERMISSIONS_FLAG"] = True + return self + + def _add_units_for_permissions(self) -> None: + # This works sort of like a 'prefetch_related', since it makes another query + # to fetch units and unit groups for the permission checks when the queryset is evaluated, + # and 'joins' them to the correct model instances in python. + from tilavarauspalvelu.models import Unit + + items: list[Reservation] = list(self) + if not items: + return + + units = ( + Unit.objects.prefetch_related("unit_groups") + .filter(reservationunit__reservation__in=items) + .annotate( + reservation_ids=Coalesce( + ArrayAgg( + "reservationunit__reservation", + distinct=True, + filter=( + models.Q(reservationunit__isnull=False) + & models.Q(reservationunit__reservation__isnull=False) + ), + ), + models.Value([]), + ) + ) + .distinct() + ) + + for item in items: + item.units_for_permissions = [unit for unit in units if item.pk in unit.reservation_ids] diff --git a/tilavarauspalvelu/models/cancel_reason/__init__.py b/tilavarauspalvelu/models/reservation_cancel_reason/__init__.py similarity index 100% rename from tilavarauspalvelu/models/cancel_reason/__init__.py rename to tilavarauspalvelu/models/reservation_cancel_reason/__init__.py diff --git a/tilavarauspalvelu/models/reservation_cancel_reason/actions.py b/tilavarauspalvelu/models/reservation_cancel_reason/actions.py new file mode 100644 index 000000000..67d7491ad --- /dev/null +++ b/tilavarauspalvelu/models/reservation_cancel_reason/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationCancelReason + + +class ReservationCancelReasonActions: + def __init__(self, reservation_cancel_reason: "ReservationCancelReason") -> None: + self.reservation_cancel_reason = reservation_cancel_reason diff --git a/tilavarauspalvelu/models/reservation_cancel_reason/model.py b/tilavarauspalvelu/models/reservation_cancel_reason/model.py new file mode 100644 index 000000000..6e11cdc99 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_cancel_reason/model.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import ReservationCancelReasonQuerySet + +if TYPE_CHECKING: + from .actions import ReservationCancelReasonActions + +__all__ = [ + "ReservationCancelReason", +] + + +class ReservationCancelReason(models.Model): + reason = models.CharField(max_length=255, null=False, blank=False) + + # Translated field hints + reason_fi: str | None + reason_en: str | None + reason_sv: str | None + + objects = ReservationCancelReasonQuerySet.as_manager() + + class Meta: + db_table = "reservation_cancel_reason" + base_manager_name = "objects" + ordering = [ + "pk", + ] + + def __str__(self) -> str: + return self.reason + + @cached_property + def actions(self) -> ReservationCancelReasonActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationCancelReasonActions + + return ReservationCancelReasonActions(self) diff --git a/tilavarauspalvelu/models/reservation_cancel_reason/queryset.py b/tilavarauspalvelu/models/reservation_cancel_reason/queryset.py new file mode 100644 index 000000000..a85ff2cb5 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_cancel_reason/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationCancelReasonQuerySet", +] + + +class ReservationCancelReasonQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/deny_reason/__init__.py b/tilavarauspalvelu/models/reservation_deny_reason/__init__.py similarity index 100% rename from tilavarauspalvelu/models/deny_reason/__init__.py rename to tilavarauspalvelu/models/reservation_deny_reason/__init__.py diff --git a/tilavarauspalvelu/models/reservation_deny_reason/actions.py b/tilavarauspalvelu/models/reservation_deny_reason/actions.py new file mode 100644 index 000000000..5843d0da7 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_deny_reason/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationDenyReason + + +class ReservationDenyReasonActions: + def __init__(self, reservation_deny_reason: "ReservationDenyReason") -> None: + self.reservation_deny_reason = reservation_deny_reason diff --git a/tilavarauspalvelu/models/reservation_deny_reason/model.py b/tilavarauspalvelu/models/reservation_deny_reason/model.py new file mode 100644 index 000000000..2bff9af43 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_deny_reason/model.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import ReservationDenyReasonQuerySet + +if TYPE_CHECKING: + from .actions import ReservationDenyReasonActions + + +class ReservationDenyReason(models.Model): + rank: int | None = models.PositiveBigIntegerField(null=True, blank=True, db_index=True) + reason: str = models.CharField(max_length=255) + + # Translated field hints + reason_fi: str | None + reason_sv: str | None + reason_en: str | None + + objects = ReservationDenyReasonQuerySet.as_manager() + + class Meta: + db_table = "reservation_deny_reason" + base_manager_name = "objects" + ordering = ["rank"] + + def __str__(self) -> str: + return self.reason + + @cached_property + def actions(self) -> ReservationDenyReasonActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationDenyReasonActions + + return ReservationDenyReasonActions(self) diff --git a/tilavarauspalvelu/models/reservation_deny_reason/queryset.py b/tilavarauspalvelu/models/reservation_deny_reason/queryset.py new file mode 100644 index 000000000..1baadf463 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_deny_reason/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationDenyReasonQuerySet", +] + + +class ReservationDenyReasonQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/metadata_field/__init__.py b/tilavarauspalvelu/models/reservation_metadata_field/__init__.py similarity index 100% rename from tilavarauspalvelu/models/metadata_field/__init__.py rename to tilavarauspalvelu/models/reservation_metadata_field/__init__.py diff --git a/tilavarauspalvelu/models/reservation_metadata_field/actions.py b/tilavarauspalvelu/models/reservation_metadata_field/actions.py new file mode 100644 index 000000000..b8543d2fc --- /dev/null +++ b/tilavarauspalvelu/models/reservation_metadata_field/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationMetadataField + + +class ReservationMetadataFieldActions: + def __init__(self, reservation_unit_metadata_field: "ReservationMetadataField") -> None: + self.reservation_unit_metadata_field = reservation_unit_metadata_field diff --git a/tilavarauspalvelu/models/reservation_metadata_field/model.py b/tilavarauspalvelu/models/reservation_metadata_field/model.py new file mode 100644 index 000000000..065e6a3c8 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_metadata_field/model.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .queryset import ReservationMetadataFieldQuerySet + +if TYPE_CHECKING: + from .actions import ReservationMetadataFieldActions + + +class ReservationMetadataField(models.Model): + field_name = models.CharField(max_length=100, unique=True) + + objects = ReservationMetadataFieldQuerySet.as_manager() + + class Meta: + db_table = "reservation_metadata_field" + base_manager_name = "objects" + verbose_name = _("Reservation metadata field") + verbose_name_plural = _("Reservation metadata fields") + ordering = ["pk"] + + def __str__(self) -> str: + return self.field_name + + @cached_property + def actions(self) -> ReservationMetadataFieldActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationMetadataFieldActions + + return ReservationMetadataFieldActions(self) diff --git a/tilavarauspalvelu/models/reservation_metadata_field/queryset.py b/tilavarauspalvelu/models/reservation_metadata_field/queryset.py new file mode 100644 index 000000000..4f7e22973 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_metadata_field/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationMetadataFieldQuerySet", +] + + +class ReservationMetadataFieldQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/metadata_set/__init__.py b/tilavarauspalvelu/models/reservation_metadata_set/__init__.py similarity index 100% rename from tilavarauspalvelu/models/metadata_set/__init__.py rename to tilavarauspalvelu/models/reservation_metadata_set/__init__.py diff --git a/tilavarauspalvelu/models/reservation_metadata_set/actions.py b/tilavarauspalvelu/models/reservation_metadata_set/actions.py new file mode 100644 index 000000000..da4a121e0 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_metadata_set/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationMetadataSet + + +class ReservationMetadataSetActions: + def __init__(self, reservation_unit_metadata_set: "ReservationMetadataSet") -> None: + self.reservation_unit_metadata_set = reservation_unit_metadata_set diff --git a/tilavarauspalvelu/models/reservation_metadata_set/model.py b/tilavarauspalvelu/models/reservation_metadata_set/model.py new file mode 100644 index 000000000..9ef39bdd9 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_metadata_set/model.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .queryset import ReservationMetadataSetQuerySet + +if TYPE_CHECKING: + from .actions import ReservationMetadataSetActions + + +__all__ = [ + "ReservationMetadataSet", +] + + +class ReservationMetadataSet(models.Model): + name = models.CharField(max_length=100, unique=True) + supported_fields = models.ManyToManyField( + "tilavarauspalvelu.ReservationMetadataField", + related_name="metadata_sets_supported", + ) + required_fields = models.ManyToManyField( + "tilavarauspalvelu.ReservationMetadataField", + related_name="metadata_sets_required", + blank=True, + ) + + objects = ReservationMetadataSetQuerySet.as_manager() + + class Meta: + db_table = "reservation_metadata_set" + base_manager_name = "objects" + verbose_name = _("Reservation metadata set") + verbose_name_plural = _("Reservation metadata sets") + ordering = ["pk"] + + def __str__(self) -> str: + return self.name + + @cached_property + def actions(self) -> ReservationMetadataSetActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationMetadataSetActions + + return ReservationMetadataSetActions(self) diff --git a/tilavarauspalvelu/models/reservation_metadata_set/queryset.py b/tilavarauspalvelu/models/reservation_metadata_set/queryset.py new file mode 100644 index 000000000..4721ffa5f --- /dev/null +++ b/tilavarauspalvelu/models/reservation_metadata_set/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationMetadataSetQuerySet", +] + + +class ReservationMetadataSetQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/reservation_purpose/actions.py b/tilavarauspalvelu/models/reservation_purpose/actions.py index e69de29bb..dd4dfcdbb 100644 --- a/tilavarauspalvelu/models/reservation_purpose/actions.py +++ b/tilavarauspalvelu/models/reservation_purpose/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationPurpose + + +class ReservationPurposeActions: + def __init__(self, reservation_purpose: "ReservationPurpose") -> None: + self.reservation_purpose = reservation_purpose diff --git a/tilavarauspalvelu/models/reservation_purpose/model.py b/tilavarauspalvelu/models/reservation_purpose/model.py index e69de29bb..e4099d0bb 100644 --- a/tilavarauspalvelu/models/reservation_purpose/model.py +++ b/tilavarauspalvelu/models/reservation_purpose/model.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models + +from .queryset import ReservationPurposeQuerySet + +if TYPE_CHECKING: + from .actions import ReservationPurposeActions + + +class ReservationPurpose(models.Model): + name = models.CharField(max_length=200) + + # Translated field hints + name_fi: str | None + name_sv: str | None + name_en: str | None + + objects = ReservationPurposeQuerySet.as_manager() + + class Meta: + db_table = "reservation_purpose" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return self.name + + @cached_property + def actions(self) -> ReservationPurposeActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationPurposeActions + + return ReservationPurposeActions(self) diff --git a/tilavarauspalvelu/models/reservation_purpose/queryset.py b/tilavarauspalvelu/models/reservation_purpose/queryset.py index e69de29bb..fe84895b4 100644 --- a/tilavarauspalvelu/models/reservation_purpose/queryset.py +++ b/tilavarauspalvelu/models/reservation_purpose/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationPurposeQuerySet", +] + + +class ReservationPurposeQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/reservation_statistic/actions.py b/tilavarauspalvelu/models/reservation_statistic/actions.py index e69de29bb..2ac554143 100644 --- a/tilavarauspalvelu/models/reservation_statistic/actions.py +++ b/tilavarauspalvelu/models/reservation_statistic/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationStatistic + + +class ReservationStatisticActions: + def __init__(self, reservation_statistic: "ReservationStatistic") -> None: + self.reservation_statistic = reservation_statistic diff --git a/tilavarauspalvelu/models/reservation_statistic/model.py b/tilavarauspalvelu/models/reservation_statistic/model.py index e69de29bb..720598f88 100644 --- a/tilavarauspalvelu/models/reservation_statistic/model.py +++ b/tilavarauspalvelu/models/reservation_statistic/model.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import datetime +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models +from django.utils import timezone + +from tilavarauspalvelu.enums import CustomerTypeChoice + +from .queryset import ReservationStatisticQuerySet + +if TYPE_CHECKING: + from decimal import Decimal + + from tilavarauspalvelu.models import Reservation + + from .actions import ReservationStatisticActions + + +class ReservationStatistic(models.Model): + # Copied from Reservation + + num_persons: int | None = models.fields.PositiveIntegerField(null=True, blank=True) + state: str = models.CharField(max_length=255) + reservation_type: str | None = models.CharField(max_length=255, null=True) + + begin: datetime.datetime = models.DateTimeField() + end: datetime.datetime = models.DateTimeField() + buffer_time_before: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) + buffer_time_after: datetime.timedelta = models.DurationField(default=datetime.timedelta(), blank=True) + reservation_handled_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True) + reservation_confirmed_at: datetime.datetime | None = models.DateTimeField(null=True) + reservation_created_at: datetime.datetime | None = models.DateTimeField(null=True, default=timezone.now) + + price: Decimal = models.DecimalField(max_digits=10, decimal_places=2, default=0) + price_net: Decimal = models.DecimalField(max_digits=20, decimal_places=6, default=0) + non_subsidised_price: Decimal = models.DecimalField(max_digits=20, decimal_places=2, default=0) + non_subsidised_price_net: Decimal = models.DecimalField(max_digits=20, decimal_places=6, default=0) + tax_percentage_value: Decimal = models.DecimalField(max_digits=5, decimal_places=2, default=0) + + applying_for_free_of_charge: bool = models.BooleanField(default=False, blank=True) + + reservee_id: str = models.CharField(max_length=255, blank=True, default="") + reservee_organisation_name: str = models.CharField(max_length=255, blank=True, default="") + reservee_address_zip: str = models.CharField(max_length=255, blank=True, default="") + reservee_is_unregistered_association: bool = models.BooleanField(null=True, default=False, blank=True) + reservee_language: str = models.CharField(max_length=255, blank=True, default="") + reservee_type: str | None = models.CharField(max_length=255, null=True, blank=True) + + # Relations and static copies of their values + + primary_reservation_unit = models.ForeignKey( + "reservation_units.ReservationUnit", + null=True, + on_delete=models.SET_NULL, + ) + primary_reservation_unit_name: str = models.CharField(max_length=255) + primary_unit_tprek_id: str | None = models.CharField(max_length=255, null=True) + primary_unit_name: str = models.CharField(max_length=255) + + deny_reason = models.ForeignKey( + "tilavarauspalvelu.ReservationDenyReason", + related_name="reservation_statistics", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + deny_reason_text: str = models.CharField(max_length=255) + + cancel_reason = models.ForeignKey( + "tilavarauspalvelu.ReservationCancelReason", + related_name="reservation_statistics", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + cancel_reason_text: str = models.CharField(max_length=255) + + purpose = models.ForeignKey( + "tilavarauspalvelu.ReservationPurpose", + related_name="reservation_statistics", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + purpose_name: str = models.CharField(max_length=255, default="", blank=True) + + home_city = models.ForeignKey( + "applications.City", + related_name="reservation_statistics", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + home_city_name: str = models.CharField(max_length=255, default="", blank=True) + home_city_municipality_code: str = models.CharField(max_length=255, default="") + + age_group = models.ForeignKey( + "tilavarauspalvelu.AgeGroup", + related_name="reservation_statistics", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + age_group_name: str = models.fields.CharField(max_length=255, default="", blank=True) + + # From RecurringReservation + ability_group = models.ForeignKey( + "tilavarauspalvelu.AbilityGroup", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + ability_group_name: str = models.fields.TextField() + + reservation = models.OneToOneField( + "tilavarauspalvelu.Reservation", + on_delete=models.SET_NULL, + null=True, + ) + + # Reservation statistics specific + + updated_at: datetime.datetime | None = models.DateTimeField(null=True, blank=True, auto_now=True) + priority: int | None = models.IntegerField(null=True, blank=True) + priority_name: str = models.CharField(max_length=255, default="", blank=True) + duration_minutes: int = models.IntegerField() + is_subsidised: bool = models.BooleanField(default=False) + is_recurring: bool = models.BooleanField(default=False) + recurrence_begin_date: datetime.date | None = models.DateField(null=True) + recurrence_end_date: datetime.date | None = models.DateField(null=True) + recurrence_uuid: str = models.CharField(max_length=255, default="", blank=True) + reservee_uuid: str = models.CharField(max_length=255, default="", blank=True) + reservee_used_ad_login: bool = models.BooleanField(default=False, blank=True) + is_applied: bool = models.BooleanField(default=False, blank=True) + """Is the reservation done through application process.""" + + objects = ReservationStatisticQuerySet.as_manager() + + class Meta: + db_table = "reservation_statistic" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return f"{self.reservee_uuid} - {self.begin} - {self.end}" + + @cached_property + def actions(self) -> ReservationStatisticActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationStatisticActions + + return ReservationStatisticActions(self) + + @classmethod + def for_reservation(cls, reservation: Reservation, *, save: bool = True) -> ReservationStatistic: # noqa: PLR0915 + recurring_reservation = getattr(reservation, "recurring_reservation", None) + ability_group = getattr(recurring_reservation, "ability_group", None) + allocated_time_slot = getattr(recurring_reservation, "allocated_time_slot", None) + + requires_org_name = reservation.reservee_type != CustomerTypeChoice.INDIVIDUAL + requires_org_id = not reservation.reservee_is_unregistered_association and requires_org_name + by_profile_user = bool(getattr(reservation.user, "profile_id", "")) + + statistic = ( # + ReservationStatistic.objects.filter(reservation=reservation).first() + or ReservationStatistic(reservation=reservation) + ) + + statistic.ability_group = ability_group + statistic.age_group = reservation.age_group + statistic.age_group_name = str(reservation.age_group) + statistic.applying_for_free_of_charge = reservation.applying_for_free_of_charge + statistic.begin = reservation.begin + statistic.buffer_time_after = reservation.buffer_time_after + statistic.buffer_time_before = reservation.buffer_time_before + statistic.cancel_reason = reservation.cancel_reason + statistic.cancel_reason_text = getattr(reservation.cancel_reason, "reason", "") + statistic.deny_reason = reservation.deny_reason + statistic.deny_reason_text = getattr(reservation.deny_reason, "reason", "") + statistic.duration_minutes = (reservation.end - reservation.begin).total_seconds() / 60 + statistic.end = reservation.end + statistic.home_city = reservation.home_city + statistic.home_city_municipality_code = getattr(reservation.home_city, "municipality_code", "") + statistic.home_city_name = reservation.home_city.name if reservation.home_city else "" + statistic.is_applied = allocated_time_slot is not None + statistic.is_recurring = recurring_reservation is not None + statistic.is_subsidised = reservation.price < reservation.non_subsidised_price + statistic.non_subsidised_price = reservation.non_subsidised_price + statistic.non_subsidised_price_net = reservation.non_subsidised_price_net + statistic.num_persons = reservation.num_persons + statistic.price = reservation.price + statistic.price_net = reservation.price_net + statistic.purpose = reservation.purpose + statistic.purpose_name = reservation.purpose.name if reservation.purpose else "" + statistic.recurrence_begin_date = getattr(recurring_reservation, "begin_date", None) + statistic.recurrence_end_date = getattr(recurring_reservation, "end_date", None) + statistic.recurrence_uuid = getattr(recurring_reservation, "uuid", "") + statistic.reservation = reservation + statistic.reservation_confirmed_at = reservation.confirmed_at + statistic.reservation_created_at = reservation.created_at + statistic.reservation_handled_at = reservation.handled_at + statistic.reservation_type = reservation.type + statistic.reservee_address_zip = reservation.reservee_address_zip if by_profile_user else "" + statistic.reservee_id = reservation.reservee_id if requires_org_id else "" + statistic.reservee_is_unregistered_association = reservation.reservee_is_unregistered_association + statistic.reservee_language = reservation.reservee_language + statistic.reservee_organisation_name = reservation.reservee_organisation_name if requires_org_name else "" + statistic.reservee_type = reservation.reservee_type + statistic.reservee_used_ad_login = reservation.reservee_used_ad_login + statistic.reservee_uuid = str(reservation.user.tvp_uuid) if reservation.user else "" + statistic.state = reservation.state + statistic.tax_percentage_value = reservation.tax_percentage_value + + for res_unit in reservation.reservation_unit.all(): + statistic.primary_reservation_unit = res_unit + statistic.primary_reservation_unit_name = res_unit.name + statistic.primary_unit_name = getattr(res_unit.unit, "name", "") + statistic.primary_unit_tprek_id = getattr(res_unit.unit, "tprek_id", "") + break + + if statistic.is_applied and ability_group: + statistic.ability_group_name = ability_group.name + + if save: + statistic.save() + + return statistic diff --git a/tilavarauspalvelu/models/reservation_statistic/queryset.py b/tilavarauspalvelu/models/reservation_statistic/queryset.py index e69de29bb..71fd6901a 100644 --- a/tilavarauspalvelu/models/reservation_statistic/queryset.py +++ b/tilavarauspalvelu/models/reservation_statistic/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationStatisticQuerySet", +] + + +class ReservationStatisticQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/admin/cancellation_rule/admin.py b/tilavarauspalvelu/models/reservation_statistic_unit/__init__.py similarity index 100% rename from tilavarauspalvelu/admin/cancellation_rule/admin.py rename to tilavarauspalvelu/models/reservation_statistic_unit/__init__.py diff --git a/tilavarauspalvelu/models/reservation_statistic_unit/actions.py b/tilavarauspalvelu/models/reservation_statistic_unit/actions.py new file mode 100644 index 000000000..e77698688 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_statistic_unit/actions.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .model import ReservationStatisticsReservationUnit + + +class ReservationStatisticsReservationUnitActions: + def __init__(self, reservation_statistics_unit: "ReservationStatisticsReservationUnit") -> None: + self.reservation_statistics_unit = reservation_statistics_unit diff --git a/tilavarauspalvelu/models/reservation_statistic_unit/model.py b/tilavarauspalvelu/models/reservation_statistic_unit/model.py new file mode 100644 index 000000000..b208f49e8 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_statistic_unit/model.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from django.db import models, transaction + +from .queryset import ReservationStatisticsReservationUnitQuerySet + +if TYPE_CHECKING: + from tilavarauspalvelu.models import ReservationStatistic + + from .actions import ReservationStatisticsReservationUnitActions + + +class ReservationStatisticsReservationUnit(models.Model): + name = models.CharField(max_length=255) + unit_name = models.CharField(max_length=255) + unit_tprek_id = models.CharField(max_length=255, null=True) + + reservation_statistics = models.ForeignKey( + "tilavarauspalvelu.ReservationStatistic", + on_delete=models.CASCADE, + related_name="reservation_stats_reservation_units", + ) + reservation_unit = models.ForeignKey( + "reservation_units.ReservationUnit", + null=True, + on_delete=models.SET_NULL, + ) + + objects = ReservationStatisticsReservationUnitQuerySet.as_manager() + + class Meta: + db_table = "reservation_statistics_reservation_unit" + base_manager_name = "objects" + ordering = ["pk"] + + def __str__(self) -> str: + return f"{self.reservation_statistics} - {self.reservation_unit}" + + @cached_property + def actions(self) -> ReservationStatisticsReservationUnitActions: + # Import actions inline to defer loading them. + # This allows us to avoid circular imports. + from .actions import ReservationStatisticsReservationUnitActions + + return ReservationStatisticsReservationUnitActions(self) + + @classmethod + def for_statistic( + cls, + statistic: ReservationStatistic, + *, + save: bool = True, + ) -> list[ReservationStatisticsReservationUnit]: + to_save: list[ReservationStatisticsReservationUnit] = [] + for reservation_unit in statistic.reservation.reservation_unit.all(): + stat_unit = ReservationStatisticsReservationUnit( + reservation_statistics=statistic, + reservation_unit=reservation_unit, + ) + + stat_unit.name = reservation_unit.name + stat_unit.reservation_statistics = statistic + stat_unit.reservation_unit = reservation_unit + stat_unit.unit_name = getattr(reservation_unit.unit, "name", "") + stat_unit.unit_tprek_id = getattr(reservation_unit.unit, "tprek_id", "") + + to_save.append(stat_unit) + + if save: + with transaction.atomic(): + ReservationStatisticsReservationUnit.objects.filter(reservation_statistics=statistic).delete() + ReservationStatisticsReservationUnit.objects.bulk_create(to_save) + + return to_save diff --git a/tilavarauspalvelu/models/reservation_statistic_unit/queryset.py b/tilavarauspalvelu/models/reservation_statistic_unit/queryset.py new file mode 100644 index 000000000..12f1f01e9 --- /dev/null +++ b/tilavarauspalvelu/models/reservation_statistic_unit/queryset.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from django.db import models + +__all__ = [ + "ReservationStatisticsReservationUnitQuerySet", +] + + +class ReservationStatisticsReservationUnitQuerySet(models.QuerySet): ... diff --git a/tilavarauspalvelu/models/unit/queryset.py b/tilavarauspalvelu/models/unit/queryset.py index c4f7e9c1c..15e9b5856 100644 --- a/tilavarauspalvelu/models/unit/queryset.py +++ b/tilavarauspalvelu/models/unit/queryset.py @@ -37,7 +37,7 @@ def order_by_reservation_units_count(self, *, desc: bool = False) -> Self: ).order_by(models.OrderBy(models.F("reservation_units_count"), descending=desc)) def order_by_reservation_count(self, *, desc: bool = False) -> Self: - from reservations.models import Reservation + from tilavarauspalvelu.models import Reservation return self.alias( reservation_count=SubqueryCount( diff --git a/tilavarauspalvelu/signals.py b/tilavarauspalvelu/signals.py index 6ecc54c75..cf5d7bf80 100644 --- a/tilavarauspalvelu/signals.py +++ b/tilavarauspalvelu/signals.py @@ -1,10 +1,12 @@ from typing import Any from django.conf import settings -from django.db.models.signals import post_delete, post_save +from django.db.models.signals import m2m_changed, post_delete, post_save from django.dispatch import receiver -from tilavarauspalvelu.models import Space +from tilavarauspalvelu.models import Reservation, Space +from tilavarauspalvelu.tasks import create_or_update_reservation_statistics, update_affecting_time_spans_task +from tilavarauspalvelu.typing import M2MAction @receiver([post_save, post_delete], sender=Space, dispatch_uid="space_modify") @@ -23,3 +25,56 @@ def space_modify(instance: Space, *args: Any, **kwargs: Any) -> None: from reservation_units.models import ReservationUnitHierarchy ReservationUnitHierarchy.refresh(kwargs.get("using")) + + +@receiver(post_save, sender=Reservation, dispatch_uid="reservation_create") +def reservation_create( + sender: type[Reservation], + instance: Reservation, + raw: bool = False, + **kwargs, +) -> None: + if not raw and settings.SAVE_RESERVATION_STATISTICS: + # Note that many-to-many relationships are not yet available at this time so + # statistics might be saved with the previous reservation unit value. That is why + # we also have m2m_changed signal below. + # + # It gets triggered when relationships are being changed and happens always after post_save signal. + # It will update the reservation unit with the new value. + # + # Current implementation allows reservation to have multiple reservation units, but in practise, only + # one can be defined. If in the future we truly support reservations with multiple reservation units, + # we need to change this implementation so that we either have multiple reservation units in the statistics + # or we have better way to indicate which one is the primary unit. + create_or_update_reservation_statistics([instance.pk]) + + if not raw and settings.UPDATE_AFFECTING_TIME_SPANS: + update_affecting_time_spans_task.delay() + + +@receiver(post_delete, sender=Reservation, dispatch_uid="reservation_delete") +def reservation_delete( + sender: type[Reservation], + **kwargs, +) -> None: + if settings.UPDATE_AFFECTING_TIME_SPANS: + update_affecting_time_spans_task.delay() + + +@receiver( + m2m_changed, + sender=Reservation.reservation_unit.through, + dispatch_uid="reservations_reservation_units_m2m", +) +def reservations_reservation_units_m2m( + action: M2MAction, + instance: Reservation, + reverse: bool = False, + raw: bool = False, + **kwargs: Any, +) -> None: + if action == "post_add" and not raw and not reverse and settings.SAVE_RESERVATION_STATISTICS: + create_or_update_reservation_statistics([instance.pk]) + + if not raw and settings.UPDATE_AFFECTING_TIME_SPANS: + update_affecting_time_spans_task.delay() diff --git a/tilavarauspalvelu/tasks.py b/tilavarauspalvelu/tasks.py index 769108d7c..f6f57e9f2 100644 --- a/tilavarauspalvelu/tasks.py +++ b/tilavarauspalvelu/tasks.py @@ -1,7 +1,14 @@ +from __future__ import annotations + +import datetime import logging +import uuid +from contextlib import suppress from django.conf import settings from django.contrib.auth import get_user_model +from django.db import transaction +from django.db.models import Prefetch from django.db.transaction import atomic from django.utils import timezone from lookup_property import L @@ -10,10 +17,25 @@ from applications.models import Application from common.date_utils import local_datetime from config.celery import app -from reservations.models import Reservation -from tilavarauspalvelu.enums import EmailType, ReservationNotification +from tilavarauspalvelu.enums import EmailType, OrderStatus, ReservationNotification from tilavarauspalvelu.exceptions import SendEmailNotificationError +from tilavarauspalvelu.models import ( + AffectingTimeSpan, + PaymentOrder, + Reservation, + ReservationStatistic, + ReservationStatisticsReservationUnit, +) from tilavarauspalvelu.utils.email.email_sender import EmailNotificationSender +from tilavarauspalvelu.utils.pruning import ( + prune_inactive_reservations, + prune_recurring_reservations, + prune_reservation_statistics, + prune_reservation_with_inactive_payments, +) +from tilavarauspalvelu.utils.verkkokauppa.order.exceptions import CancelOrderError +from tilavarauspalvelu.utils.verkkokauppa.payment.exceptions import GetPaymentError +from tilavarauspalvelu.utils.verkkokauppa.verkkokauppa_api_client import VerkkokauppaAPIClient from utils.sentry import SentryLogger logger = logging.getLogger(__name__) @@ -215,3 +237,119 @@ def update_origin_hauki_resource_reservable_time_spans() -> None: logger.info("Updating OriginHaukiResource reservable time spans...") HaukiResourceHashUpdater().run() + + +@app.task(name="prune_reservations") +def prune_reservations_task() -> None: + prune_inactive_reservations() + prune_reservation_with_inactive_payments() + + +@app.task(name="update_expired_orders") +def update_expired_orders_task() -> None: + older_than_minutes = settings.VERKKOKAUPPA_ORDER_EXPIRATION_MINUTES + expired_datetime = local_datetime() - datetime.timedelta(minutes=older_than_minutes) + expired_orders = PaymentOrder.objects.filter( + status=OrderStatus.DRAFT, + created_at__lte=expired_datetime, + remote_id__isnull=False, + ).all() + + for payment_order in expired_orders: + # Do not update the PaymentOrder status if an error occurs + with suppress(GetPaymentError, CancelOrderError), atomic(): + payment_order.refresh_order_status_from_webshop() + + if payment_order.status == OrderStatus.EXPIRED: + payment_order.cancel_order_in_webshop() + + +@app.task(name="prune_reservation_statistics") +def prune_reservation_statistics_task() -> None: + prune_reservation_statistics() + + +@app.task(name="prune_recurring_reservations") +def prune_recurring_reservations_task() -> None: + prune_recurring_reservations() + + +@app.task( + name="refund_paid_reservation", + autoretry_for=(Exception,), + max_retries=5, + retry_backoff=True, +) +def refund_paid_reservation_task(reservation_pk: int) -> None: + reservation = Reservation.objects.filter(pk=reservation_pk).first() + if not reservation: + return + + payment_order: PaymentOrder | None = PaymentOrder.objects.filter(reservation=reservation).first() + if not payment_order: + return + + if not settings.MOCK_VERKKOKAUPPA_API_ENABLED: + refund = VerkkokauppaAPIClient.refund_order(order_uuid=payment_order.remote_id) + payment_order.refund_id = refund.refund_id + else: + payment_order.refund_id = uuid.uuid4() + payment_order.status = OrderStatus.REFUNDED + payment_order.save(update_fields=["refund_id", "status"]) + + +@app.task(name="update_affecting_time_spans") +def update_affecting_time_spans_task() -> None: + AffectingTimeSpan.refresh() + + +@app.task(name="create_statistics_for_reservations") +def create_or_update_reservation_statistics(reservation_pks: list[int]) -> None: + from reservation_units.models import ReservationUnit + + new_statistics: list[ReservationStatistic] = [] + new_statistics_units: list[ReservationStatisticsReservationUnit] = [] + + reservations = ( + Reservation.objects.filter(pk__in=reservation_pks) + .select_related( + "user", + "recurring_reservation", + "recurring_reservation__ability_group", + "recurring_reservation__allocated_time_slot", + "deny_reason", + "cancel_reason", + "purpose", + "home_city", + "age_group", + ) + .prefetch_related( + Prefetch( + "reservation_unit", + queryset=ReservationUnit.objects.select_related("unit"), + ), + ) + ) + + for reservation in reservations: + statistic = ReservationStatistic.for_reservation(reservation, save=False) + statistic_units = ReservationStatisticsReservationUnit.for_statistic(statistic, save=False) + new_statistics.append(statistic) + new_statistics_units.extend(statistic_units) + + fields_to_update: list[str] = [ + field.name + for field in ReservationStatistic._meta.get_fields() + # Update all fields that can be updated + if field.concrete and not field.many_to_many and not field.primary_key + ] + + with transaction.atomic(): + new_statistics = ReservationStatistic.objects.bulk_create( + new_statistics, + update_conflicts=True, + update_fields=fields_to_update, + unique_fields=["reservation"], + ) + ReservationStatisticsReservationUnit.objects.filter(reservation_statistics__in=new_statistics).delete() + ReservationStatisticsReservationUnit.objects.bulk_create(new_statistics_units) diff --git a/tilavarauspalvelu/translation.py b/tilavarauspalvelu/translation.py index 7aafaf3c5..51ea51464 100644 --- a/tilavarauspalvelu/translation.py +++ b/tilavarauspalvelu/translation.py @@ -1,7 +1,16 @@ +from __future__ import annotations + from modeltranslation.decorators import register from modeltranslation.translator import TranslationOptions -from .models import Service, TermsOfUse +from .models import ( + AbilityGroup, + ReservationCancelReason, + ReservationDenyReason, + ReservationPurpose, + Service, + TermsOfUse, +) from .models.building.model import Building from .models.email_template.model import EmailTemplate from .models.location.model import Location @@ -66,3 +75,23 @@ class LocationTranslationOptions(TranslationOptions): @register(EmailTemplate) class EmailTemplateTranslationOptions(TranslationOptions): fields = ["subject", "content", "html_content"] + + +@register(AbilityGroup) +class AbilityGroupTranslationOptions(TranslationOptions): + fields = ["name"] + + +@register(ReservationPurpose) +class ReservationPurposeTranslationOptions(TranslationOptions): + fields = ["name"] + + +@register(ReservationCancelReason) +class ReservationCancelReasonTranslationOptions(TranslationOptions): + fields = ["reason"] + + +@register(ReservationDenyReason) +class ReservationDenyReasonTranslationOptions(TranslationOptions): + fields = ["reason"] diff --git a/tilavarauspalvelu/typing.py b/tilavarauspalvelu/typing.py index 959412898..9ad9b745b 100644 --- a/tilavarauspalvelu/typing.py +++ b/tilavarauspalvelu/typing.py @@ -1,4 +1,9 @@ from __future__ import annotations +from typing import Literal + class permission(classmethod): ... # noqa: N801 + + +type M2MAction = Literal["pre_add", "post_add", "pre_remove", "post_remove", "pre_clear", "post_clear"] diff --git a/tilavarauspalvelu/utils/anonymisation.py b/tilavarauspalvelu/utils/anonymisation.py index 815623041..dc7811ad0 100644 --- a/tilavarauspalvelu/utils/anonymisation.py +++ b/tilavarauspalvelu/utils/anonymisation.py @@ -8,10 +8,8 @@ from applications.enums import ApplicationStatusChoice from applications.models import Address, Application, ApplicationSection, Person -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation -from tilavarauspalvelu.enums import OrderStatus, ReservationNotification -from tilavarauspalvelu.models import GeneralRole, UnitRole, User +from tilavarauspalvelu.enums import OrderStatus, ReservationNotification, ReservationStateChoice, ReservationTypeChoice +from tilavarauspalvelu.models import GeneralRole, Reservation, UnitRole, User ANONYMIZED = "Anonymized" SENSITIVE_RESERVATION = "Sensitive data of this reservation has been anonymized by a script" diff --git a/tilavarauspalvelu/utils/email/email_builder_reservation.py b/tilavarauspalvelu/utils/email/email_builder_reservation.py index 706bc6227..923549905 100644 --- a/tilavarauspalvelu/utils/email/email_builder_reservation.py +++ b/tilavarauspalvelu/utils/email/email_builder_reservation.py @@ -10,15 +10,13 @@ from django.utils.timezone import get_default_timezone from common.utils import get_attr_by_language -from reservations.enums import CustomerTypeChoice -from tilavarauspalvelu.enums import EmailType +from tilavarauspalvelu.enums import CustomerTypeChoice, EmailType from tilavarauspalvelu.utils.email.email_builder_base import BaseEmailBuilder, BaseEmailContext if TYPE_CHECKING: from config.utils.commons import LanguageType - from reservations.models import Reservation from tilavarauspalvelu.admin.email_template.tester import EmailTemplateTesterForm - from tilavarauspalvelu.models import EmailTemplate, Location + from tilavarauspalvelu.models import EmailTemplate, Location, Reservation type InstructionNameType = Literal["confirmed", "pending", "cancelled"] diff --git a/tilavarauspalvelu/utils/email/email_sender.py b/tilavarauspalvelu/utils/email/email_sender.py index e3e90ae3f..49ac5464e 100644 --- a/tilavarauspalvelu/utils/email/email_sender.py +++ b/tilavarauspalvelu/utils/email/email_sender.py @@ -18,9 +18,9 @@ from applications.models import Application from config.utils.commons import LanguageType - from reservations.models import Reservation from tilavarauspalvelu.admin.email_template.tester import EmailTemplateTesterForm from tilavarauspalvelu.enums import EmailType + from tilavarauspalvelu.models import Reservation from tilavarauspalvelu.utils.email.email_builder_base import BaseEmailBuilder diff --git a/tilavarauspalvelu/utils/email/reservation_email_notification_sender.py b/tilavarauspalvelu/utils/email/reservation_email_notification_sender.py index 45df98b3e..214724de5 100644 --- a/tilavarauspalvelu/utils/email/reservation_email_notification_sender.py +++ b/tilavarauspalvelu/utils/email/reservation_email_notification_sender.py @@ -1,9 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from common.date_utils import local_datetime -from reservations.enums import ReservationStateChoice, ReservationTypeChoice -from reservations.models import Reservation -from tilavarauspalvelu.enums import EmailType, ReservationNotification +from tilavarauspalvelu.enums import EmailType, ReservationNotification, ReservationStateChoice, ReservationTypeChoice from tilavarauspalvelu.tasks import send_reservation_email_task, send_staff_reservation_email_task +if TYPE_CHECKING: + from tilavarauspalvelu.models import Reservation + class ReservationEmailNotificationSender: """Helper class for triggering reservation email sending tasks.""" diff --git a/tilavarauspalvelu/utils/helauth/pipeline.py b/tilavarauspalvelu/utils/helauth/pipeline.py index 5b53e30a0..e39a000e1 100644 --- a/tilavarauspalvelu/utils/helauth/pipeline.py +++ b/tilavarauspalvelu/utils/helauth/pipeline.py @@ -146,8 +146,7 @@ def migrate_from_tunnistamo_to_keycloak(*, email: str) -> None: new_user.save() from applications.models import Application - from reservations.models import RecurringReservation, Reservation - from tilavarauspalvelu.models import GeneralRole, UnitRole + from tilavarauspalvelu.models import GeneralRole, RecurringReservation, Reservation, UnitRole # Migrate general roles. GeneralRole.objects.filter(user=old_user).update(user=new_user) diff --git a/tilavarauspalvelu/utils/opening_hours/time_span_element.py b/tilavarauspalvelu/utils/opening_hours/time_span_element.py index ef8a0ce11..86543a303 100644 --- a/tilavarauspalvelu/utils/opening_hours/time_span_element.py +++ b/tilavarauspalvelu/utils/opening_hours/time_span_element.py @@ -1,11 +1,16 @@ +from __future__ import annotations + import datetime -import zoneinfo from dataclasses import dataclass, field -from typing import Optional +from typing import TYPE_CHECKING from common.date_utils import DEFAULT_TIMEZONE, combine, local_start_of_day from tilavarauspalvelu.enums import HaukiResourceState -from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIOpeningHoursResponseTime + +if TYPE_CHECKING: + import zoneinfo + + from tilavarauspalvelu.utils.opening_hours.hauki_api_types import HaukiAPIOpeningHoursResponseTime @dataclass(order=True, frozen=False) @@ -49,7 +54,7 @@ def _get_datetime_str(self) -> str: return duration_str - def __copy__(self) -> "TimeSpanElement": + def __copy__(self) -> TimeSpanElement: return TimeSpanElement( start_datetime=self.start_datetime, end_datetime=self.end_datetime, @@ -75,7 +80,7 @@ def create_from_time_element( date: datetime.date, timezone: zoneinfo.ZoneInfo, time_element: HaukiAPIOpeningHoursResponseTime, - ) -> Optional["TimeSpanElement"]: + ) -> TimeSpanElement | None: # We only care if the resource is reservable or closed on the time frame. # That means we can ignore all other states (OPEN, SELF_SERVICE, WEATHER_PERMITTING, etc.) time_element_state = HaukiResourceState.get(time_element["resource_state"]) @@ -158,7 +163,7 @@ def round_start_time_to_next_minute(self) -> None: if self.start_datetime.microsecond > 0 or self.start_datetime.second > 0: self.start_datetime = self.start_datetime.replace(second=0, microsecond=0) + datetime.timedelta(minutes=1) - def overlaps_with(self, other: "TimeSpanElement") -> bool: + def overlaps_with(self, other: TimeSpanElement) -> bool: """ Does this time spans overlap with the other time span? @@ -180,7 +185,7 @@ def overlaps_with(self, other: "TimeSpanElement") -> bool: """ return self.start_datetime < other.buffered_end_datetime and self.end_datetime > other.buffered_start_datetime - def fully_inside_of(self, other: "TimeSpanElement") -> bool: + def fully_inside_of(self, other: TimeSpanElement) -> bool: """ Does this time spans fully overlap with the other time span? @@ -202,7 +207,7 @@ def fully_inside_of(self, other: "TimeSpanElement") -> bool: """ return self.start_datetime >= other.buffered_start_datetime and self.end_datetime <= other.buffered_end_datetime - def starts_inside_of(self, other: "TimeSpanElement") -> bool: + def starts_inside_of(self, other: TimeSpanElement) -> bool: """ Does this time spans start inside the other time span? @@ -224,7 +229,7 @@ def starts_inside_of(self, other: "TimeSpanElement") -> bool: """ return other.buffered_start_datetime <= self.start_datetime < other.buffered_end_datetime - def ends_inside_of(self, other: "TimeSpanElement") -> bool: + def ends_inside_of(self, other: TimeSpanElement) -> bool: """ Does this time spans end inside the other time span? @@ -250,7 +255,7 @@ def generate_closed_time_spans_outside_filter( self, filter_time_start: datetime.time | None, filter_time_end: datetime.time | None, - ) -> list["TimeSpanElement"]: + ) -> list[TimeSpanElement]: """ Generate a list of closed time spans for this time span based on given filter time values. diff --git a/tilavarauspalvelu/utils/permission_resolver.py b/tilavarauspalvelu/utils/permission_resolver.py index 46162c45b..5b8a476f2 100644 --- a/tilavarauspalvelu/utils/permission_resolver.py +++ b/tilavarauspalvelu/utils/permission_resolver.py @@ -13,8 +13,7 @@ from applications.models import Application, ApplicationRound from common.typing import AnyUser from reservation_units.models import ReservationUnit - from reservations.models import RecurringReservation, Reservation - from tilavarauspalvelu.models import Space, Unit, User + from tilavarauspalvelu.models import RecurringReservation, Reservation, Space, Unit, User class PermissionResolver: diff --git a/reservations/pruning.py b/tilavarauspalvelu/utils/pruning.py similarity index 95% rename from reservations/pruning.py rename to tilavarauspalvelu/utils/pruning.py index f2f1ff27f..808814612 100644 --- a/reservations/pruning.py +++ b/tilavarauspalvelu/utils/pruning.py @@ -5,7 +5,7 @@ from django.conf import settings from django.utils.timezone import get_default_timezone -from reservations.models import RecurringReservation, Reservation, ReservationStatistic +from tilavarauspalvelu.models import RecurringReservation, Reservation, ReservationStatistic logger = logging.getLogger(__name__) diff --git a/tilavarauspalvelu/utils/verkkokauppa/helpers.py b/tilavarauspalvelu/utils/verkkokauppa/helpers.py index 919599dc8..86f7410cc 100644 --- a/tilavarauspalvelu/utils/verkkokauppa/helpers.py +++ b/tilavarauspalvelu/utils/verkkokauppa/helpers.py @@ -9,8 +9,7 @@ from common.date_utils import local_datetime from config.utils.date_util import localized_short_weekday from reservation_units.utils.reservation_unit_payment_helper import ReservationUnitPaymentHelper -from reservations.models import Reservation -from tilavarauspalvelu.models import PaymentMerchant, PaymentProduct +from tilavarauspalvelu.models import PaymentMerchant, PaymentProduct, Reservation from tilavarauspalvelu.utils.verkkokauppa.exceptions import UnsupportedMetaKeyError from tilavarauspalvelu.utils.verkkokauppa.order.types import ( CreateOrderParams,