diff --git a/pms_long_stay/README.rst b/pms_long_stay/README.rst new file mode 100644 index 00000000..a0e2936c --- /dev/null +++ b/pms_long_stay/README.rst @@ -0,0 +1,133 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================== +PMS Long Stay Reservations +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:395bdcdf5b0f56dd19fe0fed9c08bccb5219c106d24a8e0f78056c346ad06cd0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github + :target: https://github.com/OCA/pms/tree/16.0/pms_long_stay + :alt: OCA/pms +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pms-16-0/pms-16-0-pms_long_stay + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pms&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds support for long-stay reservations in the PMS. + +A new reservation type (``long_stay``) is introduced. When a reservation +of this type is created, it is automatically split into weekly or monthly +segments according to the configuration defined on the room type. The +reservation initially created by the user becomes the first segment, and +the remaining segments are generated automatically. + +All segments are linked through a long-stay group, allowing coherent +management of the whole stay while preserving operational independence +of each segment. + +For each segment, a corresponding long-stay service line is generated +automatically. The service is based on a dedicated product configured on +the room type, and its price is computed using the standard PMS service +pricing logic, including consumption-date rules. The date used for the +generated service line is configurable on the PMS Property, allowing the +service to be invoiced either at the start of the period or on the last +night of the segment. + +The room type form is extended with long-stay configuration fields: + +* Period type (weekly or monthly) +* Base period price +* Taxes for the long-stay product + +The module integrates cleanly with the PMS pricing architecture and +provides extension hooks to allow other modules to introduce additional +reservation types or pricing rules. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To create a long-stay reservation, set the reservation type to +``long_stay`` on a PMS reservation and save it. The reservation will be +automatically split into weekly or monthly segments depending on the +configuration of the related room type. The original reservation becomes +the first segment. + +Each generated segment has its own check-in and check-out dates and is +linked to the others through a long-stay group. + +For each segment, a long-stay service is created automatically: + +* It uses the long-stay product configured on the room type. +* The price is computed via the standard PMS service pricing logic. +* The consumption date is the last night of the segment. + +The date used for the generated service line depends on the +``long_stay_billing_timing`` field on the PMS Property: + +* ``start``: the service line date is the segment check-in date. +* ``end``: the service line date is the last night of the segment + (checkout minus one day). + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Roomdoo + +Contributors +~~~~~~~~~~~~ + +* Dario Lodeiros + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pms `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pms_long_stay/__init__.py b/pms_long_stay/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/pms_long_stay/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pms_long_stay/__manifest__.py b/pms_long_stay/__manifest__.py new file mode 100644 index 00000000..4c2f87f2 --- /dev/null +++ b/pms_long_stay/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2020-21 Jose Luis Algara (Alda Hotels ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "PMS Long Stay Reservations", + "version": "16.0.1.0.0", + "summary": "Adds long stay reservation type and configuration per room type.", + "category": "Hotel/PMS", + "author": "Roomdoo, Odoo Community Association (OCA)", + "website": "https://roomdoo.com", + "license": "AGPL-3", + "depends": [ + "pms", + "product", + ], + "data": [ + "views/pms_room_type_views.xml", + "views/pms_property_views.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": False, +} diff --git a/pms_long_stay/models/__init__.py b/pms_long_stay/models/__init__.py new file mode 100644 index 00000000..673f6a0c --- /dev/null +++ b/pms_long_stay/models/__init__.py @@ -0,0 +1,6 @@ +from . import pms_reservation +from . import pms_room_type +from . import product_template +from . import pms_reservation_long_stay_group +from . import pms_property +from . import pms_folio diff --git a/pms_long_stay/models/pms_folio.py b/pms_long_stay/models/pms_folio.py new file mode 100644 index 00000000..93be79ad --- /dev/null +++ b/pms_long_stay/models/pms_folio.py @@ -0,0 +1,22 @@ +from odoo import fields, models + + +class PmsFolio(models.Model): + _inherit = "pms.folio" + + reservation_type = fields.Selection( + selection_add=[("long_stay", "Long Stay")], + ) + + # --------------------------------------------------------- + # EXTEND SERVICE PRICING TYPES + # --------------------------------------------------------- + def _get_reservation_types_with_service_pricing(self): + """ + Extend base service pricing types to include 'long_stay' so that + long stay reservations also use standard service pricing logic. + """ + types = list(super()._get_reservation_types_with_service_pricing()) + if "long_stay" not in types: + types.append("long_stay") + return tuple(types) diff --git a/pms_long_stay/models/pms_property.py b/pms_long_stay/models/pms_property.py new file mode 100644 index 00000000..dd57eeeb --- /dev/null +++ b/pms_long_stay/models/pms_property.py @@ -0,0 +1,29 @@ +from odoo import fields, models + + +class PmsProperty(models.Model): + _inherit = "pms.property" + + week_start_day = fields.Selection( + [ + ("monday", "Monday"), + ("sunday", "Sunday"), + ("saturday", "Saturday"), + ], + string="Week Start Day", + default="monday", + help="Defines the first day of the week for long-stay splitting.", + ) + long_stay_billing_timing = fields.Selection( + selection=[ + ("start", "Invoice at period start"), + ("end", "Invoice at period end"), + ], + string="Long Stay Billing Timing", + default="end", + help=( + "Defines whether long stay periods are invoiced at the beginning " + "or at the end of each period. This controls the date of the " + "generated long stay service lines." + ), + ) diff --git a/pms_long_stay/models/pms_reservation.py b/pms_long_stay/models/pms_reservation.py new file mode 100644 index 00000000..7814623d --- /dev/null +++ b/pms_long_stay/models/pms_reservation.py @@ -0,0 +1,337 @@ +from datetime import date, datetime, timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.misc import format_date + + +class PmsReservation(models.Model): + _inherit = "pms.reservation" + + reservation_type = fields.Selection( + selection_add=[("long_stay", "Long Stay")], + ) + + long_stay_group_id = fields.Many2one( + comodel_name="pms.reservation.long.stay.group", + string="Long Stay Group", + help="Links all reservations that belong to the same long stay block.", + ) + + is_long_stay_master = fields.Boolean( + string="Long Stay Master", + help="Technical flag used to identify the main reservation " + "for a long stay group.", + ) + + # --------------------------------------------------------- + # CREATE OVERRIDE — AUTO-SPLITTING LONG STAY RESERVATIONS + # --------------------------------------------------------- + @api.model + def create(self, vals): + """ + Intercepts creation of long stay reservations to automatically split + the stay into period-based blocks (weekly or monthly). + + The record explicitly created by the user is reused as the first + period, so no "full-range" orphan reservation is left. + """ + if vals.get("reservation_type") != "long_stay": + return super().create(vals) + + if not vals.get("checkin") or not vals.get("checkout"): + raise ValidationError( + _("Check-in and Check-out are required for long stay reservations.") + ) + if not vals.get("room_type_id"): + raise ValidationError( + _("Room type is required for long stay reservations.") + ) + + # Create the initial reservation (will become the first segment) + master_reservation = super().create(vals) + + room_type = master_reservation.room_type_id + if not room_type.long_stay_period: + raise ValidationError( + _("This room type has no long stay period configured.") + ) + + period = room_type.long_stay_period + start = master_reservation.checkin + end = master_reservation.checkout + + # Create the group representing the whole original stay + group = self.env["pms.reservation.long.stay.group"].create( + { + "name": "Long Stay %s" + % (master_reservation.name or master_reservation.id), + "period": period, + "original_checkin": start, + "original_checkout": end, + } + ) + + # Link master to the group and mark as master + master_reservation.write( + { + "long_stay_group_id": group.id, + "is_long_stay_master": True, + } + ) + + # Split the reservation: reuse master as first block, create the rest + master_reservation._split_long_stay_into_periods( + period=period, + start=start, + end=end, + group=group, + ) + + # The caller keeps working with the first segment (the reused master) + return master_reservation + + # --------------------------------------------------------- + # HELPERS FOR PERIOD BOUNDARIES + # --------------------------------------------------------- + + def _to_date(self, value): + """ + Ensure we always work with date objects (not datetime). + """ + if isinstance(value, datetime): + return value.date() + if isinstance(value, date): + return value + raise ValueError("Unsupported type for date conversion: %s" % type(value)) + + def _get_next_week_boundary_date(self, start_date): + """ + Returns the end date of the weekly block based on hotel's configuration + week_start_day. All logic is purely date-based (no timezone). + + Week boundaries: + - week_start = monday -> week ends on Sunday (6) + - week_start = sunday -> week ends on Saturday (5) + - week_start = saturday -> week ends on Friday (4) + """ + self.ensure_one() + + week_start = self.pms_property_id.week_start_day or "monday" + + # Python weekday(): Monday=0 ... Sunday=6 + target_end_day = { + "monday": 6, # ends Sunday + "sunday": 5, # ends Saturday + "saturday": 4, # ends Friday + }[week_start] + + weekday = start_date.weekday() + days_to_boundary = (target_end_day - weekday) % 7 + if days_to_boundary == 0: + days_to_boundary = 7 # avoid zero-length interval + + return start_date + timedelta(days=days_to_boundary) + + def _get_next_month_boundary_date(self, start_date): + """ + Returns the end date of the monthly block. + The boundary is always the 1st of the next month. + Example: + - start 23 Jan -> boundary 1 Feb + - start 5 Mar -> boundary 1 Apr + """ + self.ensure_one() + + base = start_date.replace(day=1) + next_month_first = base + relativedelta(months=1) + return next_month_first + + # --------------------------------------------------------- + # SPLIT LOGIC + # --------------------------------------------------------- + def _split_long_stay_into_periods(self, period, start, end, group): + """ + Reuses the current reservation as the first period and creates + additional reservations for subsequent periods. + + All calculations are date-based (no time, no timezone). + Additionally, each segment gets an automatic long stay service + line using the room type's long stay product. + """ + self.ensure_one() + + start_date = self._to_date(start) + end_date = self._to_date(end) + + current_start = start_date + segment_index = 0 + + while current_start < end_date: + # Compute candidate boundary based on period type + if period == "weekly": + current_end_candidate = self._get_next_week_boundary_date(current_start) + elif period == "monthly": + current_end_candidate = self._get_next_month_boundary_date( + current_start + ) + else: + current_end_candidate = end_date + + # Clip to final checkout + current_end = min(current_end_candidate, end_date) + + # Safety guard to avoid zero-length loops + if current_end <= current_start: + break + + if segment_index == 0: + # First segment: reuse current reservation + self.write( + { + "checkin": current_start, + "checkout": current_end, + } + ) + # Create long stay service for this segment + self._create_long_stay_service_for_segment() + else: + # Subsequent segments: create new reservations + child_vals = self._prepare_long_stay_child_vals( + checkin=current_start, + checkout=current_end, + group=group, + ) + child_res = super().create(child_vals) + # Create long stay service for the new segment + child_res._create_long_stay_service_for_segment() + + current_start = current_end + segment_index += 1 + + def _prepare_long_stay_child_vals(self, checkin, checkout, group): + """ + Prepare values for child reservations based on the master reservation. + """ + self.ensure_one() + + return { + "reservation_type": "long_stay", + "long_stay_group_id": group.id, + "room_type_id": self.room_type_id.id, + "folio_id": self.folio_id.id, + "partner_id": self.partner_id.id, + "pms_property_id": self.pms_property_id.id, + "checkin": checkin, + "checkout": checkout, + "is_long_stay_master": False, + } + + # --------------------------------------------------------- + # SERVICE LONG STAY + # -------------------------------------------------------- + def _get_long_stay_service_description(self): + self.ensure_one() + + room_type = self.room_type_id + checkin_date = self._to_date(self.checkin) + period = room_type.long_stay_period or "monthly" + + env_lang = self.env(context=dict(self.env.context, lang=self.lang)) + + month_label = format_date(env_lang, checkin_date) + room_name = room_type.display_name or "" + + if period == "monthly": + return f"{month_label} - {room_name}" + + week_index = ((checkin_date.day - 1) // 7) + 1 + return f"S{week_index} {month_label} - {room_name}" + + def _create_long_stay_service_for_segment(self): + """ + Creates the long stay service for this reservation segment. + + - Uses the room type long stay product. + - Creates a pms.service with one service line. + - Line date is controlled by property.long_stay_billing_timing: + * 'start' -> segment check-in date + * 'end' -> last night of the segment (checkout - 1 day) + - Price is computed using pms.service._get_price_unit_line() + with the consumption_date set to the last night. + """ + self.ensure_one() + + room_type = self.room_type_id + product_tmpl = room_type.long_stay_product_id + if not product_tmpl: + # No long stay product configured for this room type + return + + product = product_tmpl.product_variant_id + if not product: + return + + property_rec = self.pms_property_id + + checkin_date = self._to_date(self.checkin) + checkout_date = self._to_date(self.checkout) + last_night_date = checkout_date - timedelta(days=1) + + # Date used in the service line depends on billing timing configuration + billing_timing = property_rec.long_stay_billing_timing or "end" + if billing_timing == "start": + line_date = checkin_date + else: + # 'end' -> use the last night of the interval + line_date = last_night_date + + # Consumption date is always the last night of the stay + consumption_date = last_night_date + + description = self._get_long_stay_service_description() + + # Try to keep sale channel consistent with the reservation/folio + sale_channel_id = ( + ( + getattr(self, "sale_channel_origin_id", False) + and self.sale_channel_origin_id.id + ) + or ( + self.folio_id + and getattr(self.folio_id, "sale_channel_origin_id", False) + and self.folio_id.sale_channel_origin_id.id + ) + or False + ) + + # Create the service with a single service line + service = self.env["pms.service"].create( + { + "product_id": product.id, + "folio_id": self.folio_id.id, + "reservation_id": self.id, + "name": description, + "sale_channel_origin_id": sale_channel_id, + "service_line_ids": [ + ( + 0, + 0, + { + "product_id": product.id, + "day_qty": 1, + "price_unit": 0.0, # temporary, updated below + "date": line_date, + }, + ) + ], + } + ) + + # Compute price using existing pricing method, passing the consumption_date + price = service._get_price_unit_line(date=consumption_date) + + # Update line price with computed value + service.service_line_ids.write({"price_unit": price}) diff --git a/pms_long_stay/models/pms_reservation_long_stay_group.py b/pms_long_stay/models/pms_reservation_long_stay_group.py new file mode 100644 index 00000000..9bed5361 --- /dev/null +++ b/pms_long_stay/models/pms_reservation_long_stay_group.py @@ -0,0 +1,37 @@ +from odoo import fields, models + + +class PmsReservationLongStayGroup(models.Model): + _name = "pms.reservation.long.stay.group" + _description = "Long Stay Reservation Group" + + name = fields.Char( + string="Reference", + required=True, + help="Human-readable reference for the long stay group.", + ) + + period = fields.Selection( + selection=[ + ("weekly", "Weekly"), + ("monthly", "Monthly"), + ], + string="Period", + help="Base period used for splitting long stay reservations.", + ) + + original_checkin = fields.Datetime( + string="Original Check-in", + help="Original check-in date before splitting into periods.", + ) + + original_checkout = fields.Datetime( + string="Original Check-out", + help="Original check-out date before splitting into periods.", + ) + + reservation_ids = fields.One2many( + comodel_name="pms.reservation", + inverse_name="long_stay_group_id", + string="Reservations", + ) diff --git a/pms_long_stay/models/pms_room_type.py b/pms_long_stay/models/pms_room_type.py new file mode 100644 index 00000000..52bfdba9 --- /dev/null +++ b/pms_long_stay/models/pms_room_type.py @@ -0,0 +1,133 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PmsRoomType(models.Model): + _inherit = "pms.room.type" + + long_stay_period = fields.Selection( + selection=[ + ("weekly", "Weekly"), + ("monthly", "Monthly"), + ], + string="Long Stay Period", + help="Defines the base duration of the long stay reservation.", + ) + + long_stay_price = fields.Monetary( + string="Long Stay Base Price", + help="Base price for the selected long stay period.", + ) + + long_stay_product_id = fields.Many2one( + comodel_name="product.template", + string="Long Stay Product", + readonly=True, + help="Internal product automatically created for long stay pricing.", + ) + + long_stay_tax_ids = fields.Many2many( + comodel_name="account.tax", + string="Long Stay Taxes", + help="Taxes that will be assigned to the internal long stay product.", + ) + + @api.constrains("long_stay_period", "long_stay_price") + def _check_long_stay_fields(self): + """ + Ensure that both long_stay_period and long_stay_price + are set together. Partial configuration is not allowed. + """ + for room_type in self: + if room_type.long_stay_period and not room_type.long_stay_price: + raise ValidationError( + _( + "You must set a Long Stay Base Price when a Long Stay " + "Period is defined for room type '%s'." + ) + % room_type.display_name + ) + if room_type.long_stay_price and not room_type.long_stay_period: + raise ValidationError( + _( + "You must set a Long Stay Period when a Long Stay " + "Base Price is defined for room type '%s'." + ) + % room_type.display_name + ) + + def _get_long_stay_product_name(self): + """ + Generate a default product name based on the room type + and the selected long stay period. + + Example: + "Double Room long stay monthly" + """ + self.ensure_one() + period_label = dict(self._fields["long_stay_period"].selection).get( + self.long_stay_period, "" + ) + return f"{self.display_name} long stay {period_label.lower()}" + + def _create_or_update_long_stay_product(self): + """ + Automatically create or update the product.template used for + long stay pricing. + + If the long stay configuration is incomplete, the product is + deactivated. If both fields are set, the product is created + or updated accordingly. + """ + ProductTemplate = self.env["product.template"] + + for room_type in self: + # If long stay configuration is incomplete, deactivate product + if not room_type.long_stay_period or not room_type.long_stay_price: + if room_type.long_stay_product_id: + room_type.long_stay_product_id.active = False + continue + + # Build product values + vals = { + "name": room_type._get_long_stay_product_name(), + "is_long_stay_product": True, + "sale_ok": False, # Not visible as a sellable service + "list_price": room_type.long_stay_price, + "type": "service", + "active": True, + "taxes_id": [(6, 0, room_type.long_stay_tax_ids.ids)], + "categ_id": self.env.ref("pms.product_category_service").id, + } + + # Update existing product + if room_type.long_stay_product_id: + room_type.long_stay_product_id.write(vals) + + # Create new product + else: + product = ProductTemplate.create(vals) + room_type.long_stay_product_id = product.id + + @api.model + def create(self, vals): + """ + Extend create() to auto-generate long stay products + if the long stay fields are set at creation. + """ + room_types = super().create(vals) + room_types._create_or_update_long_stay_product() + return room_types + + def write(self, vals): + """ + Extend write() to update or create the long stay product + whenever the relevant configuration changes. + """ + res = super().write(vals) + + tracked_fields = {"long_stay_period", "long_stay_price", "long_stay_tax_ids"} + if tracked_fields.intersection(vals.keys()): + self._create_or_update_long_stay_product() + + return res diff --git a/pms_long_stay/models/product_template.py b/pms_long_stay/models/product_template.py new file mode 100644 index 00000000..c2450ddd --- /dev/null +++ b/pms_long_stay/models/product_template.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_long_stay_product = fields.Boolean( + string="Long Stay Product", + help="If enabled, this product can be used as a long stay product " + "for a room type.", + ) diff --git a/pms_long_stay/readme/CONTRIBUTORS.rst b/pms_long_stay/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..93e44db3 --- /dev/null +++ b/pms_long_stay/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Dario Lodeiros diff --git a/pms_long_stay/readme/DESCRIPTION.rst b/pms_long_stay/readme/DESCRIPTION.rst new file mode 100644 index 00000000..44e840f0 --- /dev/null +++ b/pms_long_stay/readme/DESCRIPTION.rst @@ -0,0 +1,29 @@ +This module adds support for long-stay reservations in the PMS. + +A new reservation type (``long_stay``) is introduced. When a reservation +of this type is created, it is automatically split into weekly or monthly +segments according to the configuration defined on the room type. The +reservation initially created by the user becomes the first segment, and +the remaining segments are generated automatically. + +All segments are linked through a long-stay group, allowing coherent +management of the whole stay while preserving operational independence +of each segment. + +For each segment, a corresponding long-stay service line is generated +automatically. The service is based on a dedicated product configured on +the room type, and its price is computed using the standard PMS service +pricing logic, including consumption-date rules. The date used for the +generated service line is configurable on the PMS Property, allowing the +service to be invoiced either at the start of the period or on the last +night of the segment. + +The room type form is extended with long-stay configuration fields: + +* Period type (weekly or monthly) +* Base period price +* Taxes for the long-stay product + +The module integrates cleanly with the PMS pricing architecture and +provides extension hooks to allow other modules to introduce additional +reservation types or pricing rules. diff --git a/pms_long_stay/readme/USAGE.rst b/pms_long_stay/readme/USAGE.rst new file mode 100644 index 00000000..83427fc3 --- /dev/null +++ b/pms_long_stay/readme/USAGE.rst @@ -0,0 +1,21 @@ +To create a long-stay reservation, set the reservation type to +``long_stay`` on a PMS reservation and save it. The reservation will be +automatically split into weekly or monthly segments depending on the +configuration of the related room type. The original reservation becomes +the first segment. + +Each generated segment has its own check-in and check-out dates and is +linked to the others through a long-stay group. + +For each segment, a long-stay service is created automatically: + +* It uses the long-stay product configured on the room type. +* The price is computed via the standard PMS service pricing logic. +* The consumption date is the last night of the segment. + +The date used for the generated service line depends on the +``long_stay_billing_timing`` field on the PMS Property: + +* ``start``: the service line date is the segment check-in date. +* ``end``: the service line date is the last night of the segment + (checkout minus one day). diff --git a/pms_long_stay/security/ir.model.access.csv b/pms_long_stay/security/ir.model.access.csv new file mode 100644 index 00000000..6d1692fb --- /dev/null +++ b/pms_long_stay/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_pms_reservation_long_stay_group_user,pms.reservation.long.stay.group.user,model_pms_reservation_long_stay_group,pms.group_pms_user,1,1,1,1 diff --git a/pms_long_stay/static/description/index.html b/pms_long_stay/static/description/index.html new file mode 100644 index 00000000..8344b885 --- /dev/null +++ b/pms_long_stay/static/description/index.html @@ -0,0 +1,477 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

PMS Long Stay Reservations

+ +

Beta License: AGPL-3 OCA/pms Translate me on Weblate Try me on Runboat

+

This module adds support for long-stay reservations in the PMS.

+

A new reservation type (long_stay) is introduced. When a reservation +of this type is created, it is automatically split into weekly or monthly +segments according to the configuration defined on the room type. The +reservation initially created by the user becomes the first segment, and +the remaining segments are generated automatically.

+

All segments are linked through a long-stay group, allowing coherent +management of the whole stay while preserving operational independence +of each segment.

+

For each segment, a corresponding long-stay service line is generated +automatically. The service is based on a dedicated product configured on +the room type, and its price is computed using the standard PMS service +pricing logic, including consumption-date rules. The date used for the +generated service line is configurable on the PMS Property, allowing the +service to be invoiced either at the start of the period or on the last +night of the segment.

+

The room type form is extended with long-stay configuration fields:

+
    +
  • Period type (weekly or monthly)
  • +
  • Base period price
  • +
  • Taxes for the long-stay product
  • +
+

The module integrates cleanly with the PMS pricing architecture and +provides extension hooks to allow other modules to introduce additional +reservation types or pricing rules.

+

Table of contents

+ +
+

Usage

+

To create a long-stay reservation, set the reservation type to +long_stay on a PMS reservation and save it. The reservation will be +automatically split into weekly or monthly segments depending on the +configuration of the related room type. The original reservation becomes +the first segment.

+

Each generated segment has its own check-in and check-out dates and is +linked to the others through a long-stay group.

+

For each segment, a long-stay service is created automatically:

+
    +
  • It uses the long-stay product configured on the room type.
  • +
  • The price is computed via the standard PMS service pricing logic.
  • +
  • The consumption date is the last night of the segment.
  • +
+

The date used for the generated service line depends on the +long_stay_billing_timing field on the PMS Property:

+
    +
  • start: the service line date is the segment check-in date.
  • +
  • end: the service line date is the last night of the segment +(checkout minus one day).
  • +
+
+
+

Bug Tracker

+

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

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Roomdoo
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

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

+

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

+
+
+
+
+ + diff --git a/pms_long_stay/views/pms_property_views.xml b/pms_long_stay/views/pms_property_views.xml new file mode 100644 index 00000000..262c4c32 --- /dev/null +++ b/pms_long_stay/views/pms_property_views.xml @@ -0,0 +1,22 @@ + + + + pms.property.form.week.start.day + pms.property + + + + + + + + + + + + + + + + + diff --git a/pms_long_stay/views/pms_room_type_views.xml b/pms_long_stay/views/pms_room_type_views.xml new file mode 100644 index 00000000..5fff593d --- /dev/null +++ b/pms_long_stay/views/pms_room_type_views.xml @@ -0,0 +1,30 @@ + + + + pms.room.type.form.long.stay + pms.room.type + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/pms_long_stay/odoo/addons/pms_long_stay b/setup/pms_long_stay/odoo/addons/pms_long_stay new file mode 120000 index 00000000..4d00d7d4 --- /dev/null +++ b/setup/pms_long_stay/odoo/addons/pms_long_stay @@ -0,0 +1 @@ +../../../../pms_long_stay \ No newline at end of file diff --git a/setup/pms_long_stay/setup.py b/setup/pms_long_stay/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/pms_long_stay/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)