From edcfad2e68c5ef962af20b6e4ff79adf1dee39fc Mon Sep 17 00:00:00 2001 From: DarioLodeiros Date: Sun, 30 Nov 2025 11:22:29 +0100 Subject: [PATCH 1/3] [ADD]pms:long_stay: add new module --- pms_long_stay/__init__.py | 1 + pms_long_stay/__manifest__.py | 23 + pms_long_stay/models/__init__.py | 6 + pms_long_stay/models/pms_folio.py | 22 + pms_long_stay/models/pms_property.py | 29 ++ pms_long_stay/models/pms_reservation.py | 371 +++++++++++++++ .../models/pms_reservation_long_stay_group.py | 37 ++ pms_long_stay/models/pms_room_type.py | 133 ++++++ pms_long_stay/models/product_template.py | 11 + pms_long_stay/readme/CONTRIBUTORS.rst | 1 + pms_long_stay/readme/DESCRIPTION.rst | 29 ++ pms_long_stay/readme/USAGE.rst | 50 ++ pms_long_stay/security/ir.model.access.csv | 2 + pms_long_stay/static/description/index.html | 428 ++++++++++++++++++ pms_long_stay/views/pms_property_views.xml | 22 + pms_long_stay/views/pms_room_type_views.xml | 30 ++ 16 files changed, 1195 insertions(+) create mode 100644 pms_long_stay/__init__.py create mode 100644 pms_long_stay/__manifest__.py create mode 100644 pms_long_stay/models/__init__.py create mode 100644 pms_long_stay/models/pms_folio.py create mode 100644 pms_long_stay/models/pms_property.py create mode 100644 pms_long_stay/models/pms_reservation.py create mode 100644 pms_long_stay/models/pms_reservation_long_stay_group.py create mode 100644 pms_long_stay/models/pms_room_type.py create mode 100644 pms_long_stay/models/product_template.py create mode 100644 pms_long_stay/readme/CONTRIBUTORS.rst create mode 100644 pms_long_stay/readme/DESCRIPTION.rst create mode 100644 pms_long_stay/readme/USAGE.rst create mode 100644 pms_long_stay/security/ir.model.access.csv create mode 100644 pms_long_stay/static/description/index.html create mode 100644 pms_long_stay/views/pms_property_views.xml create mode 100644 pms_long_stay/views/pms_room_type_views.xml 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..c13f0da8 --- /dev/null +++ b/pms_long_stay/models/pms_reservation.py @@ -0,0 +1,371 @@ +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_price(self, product_tmpl): + """ + Computes the unit price for the long stay service based on the + reservation's or folio's pricelist. Falls back to the product + list price if no pricelist is found. + """ + self.ensure_one() + + product = product_tmpl.product_variant_id + if not product: + return product_tmpl.list_price + + pricelist = getattr(self, "pricelist_id", False) or getattr( + self.folio_id, "pricelist_id", False + ) + if pricelist: + return pricelist.get_product_price(product, 1.0, self.partner_id) + + # Fallback: use product's list price + return product.lst_price + + def _get_long_stay_service_description(self): + """ + Builds the description for the long stay service line based on + the reservation period type (monthly/weekly) and room type name. + + Examples: + - Monthly: "October 2025 - Double Room" + - Weekly: "S1 October 2025 - Double Room" + """ + self.ensure_one() + + room_type = self.room_type_id + checkin_date = self._to_date(self.checkin) + period = room_type.long_stay_period or "monthly" + + # Month label localized using Odoo's format_date + month_label = format_date(self.env, checkin_date, date_format="MMMM yyyy") + room_name = room_type.display_name or "" + + if period == "monthly": + # Example: "October 2025 - Double Room" + return f"{month_label} - {room_name}" + + # Weekly case: determine week index within the month + # Day 1-7 -> week 1 + # Day 8-14 -> week 2 + # ... + week_index = ((checkin_date.day - 1) // 7) + 1 + # Example: "S1 October 2025 - Double Room" + 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..921d2f2f --- /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..10d02ced --- /dev/null +++ b/pms_long_stay/readme/USAGE.rst @@ -0,0 +1,50 @@ +Creating a long-stay reservation +-------------------------------- + +To create a long-stay reservation: + +1. Open a PMS reservation. +2. Set the ``reservation_type`` to ``long_stay``. +3. Save the record. + +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, +* is operationally independent, +* is linked to the other segments through a long-stay group. + +Service lines +------------- + +Each segment automatically receives a long-stay service line: + +* based on the long-stay product configured in the room type, +* with a quantity of ``1`` per segment, +* priced using the standard PMS service pricing computation + (``_get_price_unit_line``), +* using the **last night** of the segment as ``consumption_date``. + +Service line date +----------------- + +The service line date depends on the PMS Property setting +``long_stay_billing_timing``: + +* ``start`` → service is dated on the **segment check-in**. +* ``end`` → service is dated on the **last night** (checkout - 1 day). + +Room Type configuration +----------------------- + +Enable long-stay support directly on the room type: + +* Select the period type (weekly or monthly). +* Set the base price for the long-stay period. +* Assign taxes to the long-stay product. + +A dedicated long-stay product is created or updated automatically. + 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..f9d9106d --- /dev/null +++ b/pms_long_stay/static/description/index.html @@ -0,0 +1,428 @@ + + + + + +OCR Klippa + + + +
+

OCR Klippa

+ + +

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

+

Module to connect the OCR Klippa with the pms

+

Table of contents

+ +
+

Usage

+

Set api key klippa and url parameters of the OCR service and select klippa provider ocr in pms_property

+
+
+

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

+
    +
  • Commit [Sun]
  • +
+
+ +
+

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 + + + + + + + + + + + + + + + + + + + + + + + + + From 451d412743aa4c32e692ce67eac5cedc0c7d36a6 Mon Sep 17 00:00:00 2001 From: DarioLodeiros Date: Sun, 30 Nov 2025 11:32:14 +0100 Subject: [PATCH 2/3] [ADD]pms:long_stay: add new module --- pms_long_stay/README.rst | 133 ++++++++++++++++++ pms_long_stay/readme/DESCRIPTION.rst | 14 +- pms_long_stay/readme/USAGE.rst | 61 +++----- pms_long_stay/static/description/index.html | 83 ++++++++--- setup/pms_long_stay/odoo/addons/pms_long_stay | 1 + setup/pms_long_stay/setup.py | 6 + 6 files changed, 229 insertions(+), 69 deletions(-) create mode 100644 pms_long_stay/README.rst create mode 120000 setup/pms_long_stay/odoo/addons/pms_long_stay create mode 100644 setup/pms_long_stay/setup.py 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/readme/DESCRIPTION.rst b/pms_long_stay/readme/DESCRIPTION.rst index 921d2f2f..44e840f0 100644 --- a/pms_long_stay/readme/DESCRIPTION.rst +++ b/pms_long_stay/readme/DESCRIPTION.rst @@ -1,16 +1,16 @@ -This module adds support for *long-stay reservations* in the PMS. +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. +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 +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 +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 diff --git a/pms_long_stay/readme/USAGE.rst b/pms_long_stay/readme/USAGE.rst index 10d02ced..83427fc3 100644 --- a/pms_long_stay/readme/USAGE.rst +++ b/pms_long_stay/readme/USAGE.rst @@ -1,50 +1,21 @@ -Creating a long-stay reservation --------------------------------- +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. -To create a long-stay reservation: +Each generated segment has its own check-in and check-out dates and is +linked to the others through a long-stay group. -1. Open a PMS reservation. -2. Set the ``reservation_type`` to ``long_stay``. -3. Save the record. +For each segment, a long-stay service is created automatically: -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. +* 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. -Each generated segment: - -* has its own check-in and check-out dates, -* is operationally independent, -* is linked to the other segments through a long-stay group. - -Service lines -------------- - -Each segment automatically receives a long-stay service line: - -* based on the long-stay product configured in the room type, -* with a quantity of ``1`` per segment, -* priced using the standard PMS service pricing computation - (``_get_price_unit_line``), -* using the **last night** of the segment as ``consumption_date``. - -Service line date ------------------ - -The service line date depends on the PMS Property setting -``long_stay_billing_timing``: - -* ``start`` → service is dated on the **segment check-in**. -* ``end`` → service is dated on the **last night** (checkout - 1 day). - -Room Type configuration ------------------------ - -Enable long-stay support directly on the room type: - -* Select the period type (weekly or monthly). -* Set the base price for the long-stay period. -* Assign taxes to the long-stay product. - -A dedicated long-stay product is created or updated automatically. +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/static/description/index.html b/pms_long_stay/static/description/index.html index f9d9106d..8344b885 100644 --- a/pms_long_stay/static/description/index.html +++ b/pms_long_stay/static/description/index.html @@ -3,7 +3,7 @@ -OCR Klippa +README.rst -
-

OCR Klippa

+
+ + +Odoo Community Association + +
+

PMS Long Stay Reservations

-

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

-

Module to connect the OCR Klippa with the pms

+

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

    @@ -385,33 +414,52 @@

    OCR Klippa

-

Usage

-

Set api key klippa and url parameters of the OCR service and select klippa provider ocr in pms_property

+

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

+

Bug Tracker

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

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

    -
  • Commit [Sun]
  • +
  • Roomdoo
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -419,10 +467,11 @@

Maintainers

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

-

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

+

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/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, +) From 27e3faad15a0ba8c7ea1203da4c4f70f67916714 Mon Sep 17 00:00:00 2001 From: DarioLodeiros Date: Wed, 17 Dec 2025 10:44:59 +0100 Subject: [PATCH 3/3] [IMP]pms_long_stay: service description in reservation lang format --- pms_long_stay/models/pms_reservation.py | 40 ++----------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/pms_long_stay/models/pms_reservation.py b/pms_long_stay/models/pms_reservation.py index c13f0da8..7814623d 100644 --- a/pms_long_stay/models/pms_reservation.py +++ b/pms_long_stay/models/pms_reservation.py @@ -232,56 +232,22 @@ def _prepare_long_stay_child_vals(self, checkin, checkout, group): # --------------------------------------------------------- # SERVICE LONG STAY # -------------------------------------------------------- - def _get_long_stay_service_price(self, product_tmpl): - """ - Computes the unit price for the long stay service based on the - reservation's or folio's pricelist. Falls back to the product - list price if no pricelist is found. - """ - self.ensure_one() - - product = product_tmpl.product_variant_id - if not product: - return product_tmpl.list_price - - pricelist = getattr(self, "pricelist_id", False) or getattr( - self.folio_id, "pricelist_id", False - ) - if pricelist: - return pricelist.get_product_price(product, 1.0, self.partner_id) - - # Fallback: use product's list price - return product.lst_price - def _get_long_stay_service_description(self): - """ - Builds the description for the long stay service line based on - the reservation period type (monthly/weekly) and room type name. - - Examples: - - Monthly: "October 2025 - Double Room" - - Weekly: "S1 October 2025 - Double Room" - """ self.ensure_one() room_type = self.room_type_id checkin_date = self._to_date(self.checkin) period = room_type.long_stay_period or "monthly" - # Month label localized using Odoo's format_date - month_label = format_date(self.env, checkin_date, date_format="MMMM yyyy") + 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": - # Example: "October 2025 - Double Room" return f"{month_label} - {room_name}" - # Weekly case: determine week index within the month - # Day 1-7 -> week 1 - # Day 8-14 -> week 2 - # ... week_index = ((checkin_date.day - 1) // 7) + 1 - # Example: "S1 October 2025 - Double Room" return f"S{week_index} {month_label} - {room_name}" def _create_long_stay_service_for_segment(self):