16.0 pms long stay#13
Draft
DarioLodeiros wants to merge 3 commits into
Draft
Conversation
27e3faa to
5c1b511
Compare
Diff CoverageDiff: origin/16.0...HEAD, staged and unstaged changes
Summary
pms_long_stay/models/pms_folio.pyLines 15-23 15 """
16 Extend base service pricing types to include 'long_stay' so that
17 long stay reservations also use standard service pricing logic.
18 """
! 19 types = list(super()._get_reservation_types_with_service_pricing())
! 20 if "long_stay" not in types:
! 21 types.append("long_stay")
! 22 return tuple(types)pms_long_stay/models/pms_reservation.pyLines 40-71 40 """
41 if vals.get("reservation_type") != "long_stay":
42 return super().create(vals)
43
! 44 if not vals.get("checkin") or not vals.get("checkout"):
! 45 raise ValidationError(
46 _("Check-in and Check-out are required for long stay reservations.")
47 )
! 48 if not vals.get("room_type_id"):
! 49 raise ValidationError(
50 _("Room type is required for long stay reservations.")
51 )
52
53 # Create the initial reservation (will become the first segment)
! 54 master_reservation = super().create(vals)
55
! 56 room_type = master_reservation.room_type_id
! 57 if not room_type.long_stay_period:
! 58 raise ValidationError(
59 _("This room type has no long stay period configured.")
60 )
61
! 62 period = room_type.long_stay_period
! 63 start = master_reservation.checkin
! 64 end = master_reservation.checkout
65
66 # Create the group representing the whole original stay
! 67 group = self.env["pms.reservation.long.stay.group"].create(
68 {
69 "name": "Long Stay %s"
70 % (master_reservation.name or master_reservation.id),
71 "period": period,Lines 74-82 74 }
75 )
76
77 # Link master to the group and mark as master
! 78 master_reservation.write(
79 {
80 "long_stay_group_id": group.id,
81 "is_long_stay_master": True,
82 }Lines 82-90 82 }
83 )
84
85 # Split the reservation: reuse master as first block, create the rest
! 86 master_reservation._split_long_stay_into_periods(
87 period=period,
88 start=start,
89 end=end,
90 group=group,Lines 90-98 90 group=group,
91 )
92
93 # The caller keeps working with the first segment (the reused master)
! 94 return master_reservation
95
96 # ---------------------------------------------------------
97 # HELPERS FOR PERIOD BOUNDARIES
98 # ---------------------------------------------------------Lines 100-112 100 def _to_date(self, value):
101 """
102 Ensure we always work with date objects (not datetime).
103 """
! 104 if isinstance(value, datetime):
! 105 return value.date()
! 106 if isinstance(value, date):
! 107 return value
! 108 raise ValueError("Unsupported type for date conversion: %s" % type(value))
109
110 def _get_next_week_boundary_date(self, start_date):
111 """
112 Returns the end date of the weekly block based on hotel's configurationLines 116-129 116 - week_start = monday -> week ends on Sunday (6)
117 - week_start = sunday -> week ends on Saturday (5)
118 - week_start = saturday -> week ends on Friday (4)
119 """
! 120 self.ensure_one()
121
! 122 week_start = self.pms_property_id.week_start_day or "monday"
123
124 # Python weekday(): Monday=0 ... Sunday=6
! 125 target_end_day = {
126 "monday": 6, # ends Sunday
127 "sunday": 5, # ends Saturday
128 "saturday": 4, # ends Friday
129 }[week_start]Lines 127-140 127 "sunday": 5, # ends Saturday
128 "saturday": 4, # ends Friday
129 }[week_start]
130
! 131 weekday = start_date.weekday()
! 132 days_to_boundary = (target_end_day - weekday) % 7
! 133 if days_to_boundary == 0:
! 134 days_to_boundary = 7 # avoid zero-length interval
135
! 136 return start_date + timedelta(days=days_to_boundary)
137
138 def _get_next_month_boundary_date(self, start_date):
139 """
140 Returns the end date of the monthly block.Lines 142-154 142 Example:
143 - start 23 Jan -> boundary 1 Feb
144 - start 5 Mar -> boundary 1 Apr
145 """
! 146 self.ensure_one()
147
! 148 base = start_date.replace(day=1)
! 149 next_month_first = base + relativedelta(months=1)
! 150 return next_month_first
151
152 # ---------------------------------------------------------
153 # SPLIT LOGIC
154 # ---------------------------------------------------------Lines 160-196 160 All calculations are date-based (no time, no timezone).
161 Additionally, each segment gets an automatic long stay service
162 line using the room type's long stay product.
163 """
! 164 self.ensure_one()
165
! 166 start_date = self._to_date(start)
! 167 end_date = self._to_date(end)
168
! 169 current_start = start_date
! 170 segment_index = 0
171
! 172 while current_start < end_date:
173 # Compute candidate boundary based on period type
! 174 if period == "weekly":
! 175 current_end_candidate = self._get_next_week_boundary_date(current_start)
! 176 elif period == "monthly":
! 177 current_end_candidate = self._get_next_month_boundary_date(
178 current_start
179 )
180 else:
! 181 current_end_candidate = end_date
182
183 # Clip to final checkout
! 184 current_end = min(current_end_candidate, end_date)
185
186 # Safety guard to avoid zero-length loops
! 187 if current_end <= current_start:
! 188 break
189
! 190 if segment_index == 0:
191 # First segment: reuse current reservation
! 192 self.write(
193 {
194 "checkin": current_start,
195 "checkout": current_end,
196 }Lines 195-216 195 "checkout": current_end,
196 }
197 )
198 # Create long stay service for this segment
! 199 self._create_long_stay_service_for_segment()
200 else:
201 # Subsequent segments: create new reservations
! 202 child_vals = self._prepare_long_stay_child_vals(
203 checkin=current_start,
204 checkout=current_end,
205 group=group,
206 )
! 207 child_res = super().create(child_vals)
208 # Create long stay service for the new segment
! 209 child_res._create_long_stay_service_for_segment()
210
! 211 current_start = current_end
! 212 segment_index += 1
213
214 def _prepare_long_stay_child_vals(self, checkin, checkout, group):
215 """
216 Prepare values for child reservations based on the master reservation.Lines 214-224 214 def _prepare_long_stay_child_vals(self, checkin, checkout, group):
215 """
216 Prepare values for child reservations based on the master reservation.
217 """
! 218 self.ensure_one()
219
! 220 return {
221 "reservation_type": "long_stay",
222 "long_stay_group_id": group.id,
223 "room_type_id": self.room_type_id.id,
224 "folio_id": self.folio_id.id,Lines 232-255 232 # ---------------------------------------------------------
233 # SERVICE LONG STAY
234 # --------------------------------------------------------
235 def _get_long_stay_service_description(self):
! 236 self.ensure_one()
237
! 238 room_type = self.room_type_id
! 239 checkin_date = self._to_date(self.checkin)
! 240 period = room_type.long_stay_period or "monthly"
241
! 242 env_lang = self.env(context=dict(self.env.context, lang=self.lang))
243
! 244 month_label = format_date(env_lang, checkin_date)
! 245 room_name = room_type.display_name or ""
246
! 247 if period == "monthly":
! 248 return f"{month_label} - {room_name}"
249
! 250 week_index = ((checkin_date.day - 1) // 7) + 1
! 251 return f"S{week_index} {month_label} - {room_name}"
252
253 def _create_long_stay_service_for_segment(self):
254 """
255 Creates the long stay service for this reservation segment.Lines 261-301 261 * 'end' -> last night of the segment (checkout - 1 day)
262 - Price is computed using pms.service._get_price_unit_line()
263 with the consumption_date set to the last night.
264 """
! 265 self.ensure_one()
266
! 267 room_type = self.room_type_id
! 268 product_tmpl = room_type.long_stay_product_id
! 269 if not product_tmpl:
270 # No long stay product configured for this room type
! 271 return
272
! 273 product = product_tmpl.product_variant_id
! 274 if not product:
! 275 return
276
! 277 property_rec = self.pms_property_id
278
! 279 checkin_date = self._to_date(self.checkin)
! 280 checkout_date = self._to_date(self.checkout)
! 281 last_night_date = checkout_date - timedelta(days=1)
282
283 # Date used in the service line depends on billing timing configuration
! 284 billing_timing = property_rec.long_stay_billing_timing or "end"
! 285 if billing_timing == "start":
! 286 line_date = checkin_date
287 else:
288 # 'end' -> use the last night of the interval
! 289 line_date = last_night_date
290
291 # Consumption date is always the last night of the stay
! 292 consumption_date = last_night_date
293
! 294 description = self._get_long_stay_service_description()
295
296 # Try to keep sale channel consistent with the reservation/folio
! 297 sale_channel_id = (
298 (
299 getattr(self, "sale_channel_origin_id", False)
300 and self.sale_channel_origin_id.id
301 )Lines 307-315 307 or False
308 )
309
310 # Create the service with a single service line
! 311 service = self.env["pms.service"].create(
312 {
313 "product_id": product.id,
314 "folio_id": self.folio_id.id,
315 "reservation_id": self.id,Lines 330-338 330 }
331 )
332
333 # Compute price using existing pricing method, passing the consumption_date
! 334 price = service._get_price_unit_line(date=consumption_date)
335
336 # Update line price with computed value
! 337 service.service_line_ids.write({"price_unit": price})pms_long_stay/models/pms_room_type.pyLines 37-47 37 """
38 Ensure that both long_stay_period and long_stay_price
39 are set together. Partial configuration is not allowed.
40 """
! 41 for room_type in self:
! 42 if room_type.long_stay_period and not room_type.long_stay_price:
! 43 raise ValidationError(
44 _(
45 "You must set a Long Stay Base Price when a Long Stay "
46 "Period is defined for room type '%s'."
47 )Lines 46-55 46 "Period is defined for room type '%s'."
47 )
48 % room_type.display_name
49 )
! 50 if room_type.long_stay_price and not room_type.long_stay_period:
! 51 raise ValidationError(
52 _(
53 "You must set a Long Stay Period when a Long Stay "
54 "Base Price is defined for room type '%s'."
55 )Lines 63-75 63
64 Example:
65 "Double Room long stay monthly"
66 """
! 67 self.ensure_one()
! 68 period_label = dict(self._fields["long_stay_period"].selection).get(
69 self.long_stay_period, ""
70 )
! 71 return f"{self.display_name} long stay {period_label.lower()}"
72
73 def _create_or_update_long_stay_product(self):
74 """
75 Automatically create or update the product.template used forLines 84-96 84 for room_type in self:
85 # If long stay configuration is incomplete, deactivate product
86 if not room_type.long_stay_period or not room_type.long_stay_price:
87 if room_type.long_stay_product_id:
! 88 room_type.long_stay_product_id.active = False
89 continue
90
91 # Build product values
! 92 vals = {
93 "name": room_type._get_long_stay_product_name(),
94 "is_long_stay_product": True,
95 "sale_ok": False, # Not visible as a sellable service
96 "list_price": room_type.long_stay_price,Lines 100-114 100 "categ_id": self.env.ref("pms.product_category_service").id,
101 }
102
103 # Update existing product
! 104 if room_type.long_stay_product_id:
! 105 room_type.long_stay_product_id.write(vals)
106
107 # Create new product
108 else:
! 109 product = ProductTemplate.create(vals)
! 110 room_type.long_stay_product_id = product.id
111
112 @api.model
113 def create(self, vals):
114 """Lines 123-134 123 """
124 Extend write() to update or create the long stay product
125 whenever the relevant configuration changes.
126 """
! 127 res = super().write(vals)
128
! 129 tracked_fields = {"long_stay_period", "long_stay_price", "long_stay_tax_ids"}
! 130 if tracked_fields.intersection(vals.keys()):
! 131 self._create_or_update_long_stay_product()
132
! 133 return res |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces the new "PMS Long Stay Reservations" module, which adds comprehensive support for managing long-stay bookings in the PMS system. The module enables reservations to be automatically split into weekly or monthly segments, each managed as an independent reservation but linked together for cohesive management and billing. It extends core models, introduces new configuration options, and seamlessly integrates with existing PMS pricing and service logic.
CAUTION:
This PR need some changes in PMS base: OCA/pms#364
Key features and changes include:
Long Stay Reservation Functionality:
long_stay) topms.reservationandpms.folio, enabling the creation and management of long-stay bookings. When such a reservation is created, it is automatically split into weekly or monthly segments, each represented as a separate reservation and linked via a new long stay group. Each segment also receives an automatically generated service line for billing, with the timing configurable on the property. [1] [2]Configuration and Model Extensions:
pms.propertywith new configuration fields:week_start_day(to define the start of the week for splitting) andlong_stay_billing_timing(to control when the service is invoiced for each segment).pms.room_typeandproduct_template(via imports) to support long-stay specific configuration, such as period type and dedicated products.New Data Models:
pms.reservation.long.stay.groupto group and reference all reservation segments belonging to the same long-stay booking. This model stores period type, original check-in/out, and links to all related reservations.Module Metadata and Documentation:
__manifest__.py) detailing dependencies, authorship, and data files, and a comprehensive README explaining the purpose, usage, and configuration of the module. [1] [2]Module Initialization:
__init__.pyfiles to ensure all new models and extensions are properly loaded when the module is installed. [1] [2]