From 4a7750bb53d37fb78c5e1b3540e9033f21e80020 Mon Sep 17 00:00:00 2001 From: Sumit Panwar Date: Thu, 5 Nov 2026 19:46:39 +0530 Subject: [PATCH 1/4] Fixed terminal status issues withNo show room status --- .../controllers/orders/_product_line.tpl | 2 +- .../orders/modals/_room_status_form.tpl | 2 +- classes/order/Order.php | 43 ++++++++ .../classes/HotelBookingStatus.php | 7 ++ .../admin/AdminRoomStatusesController.php | 98 ++++++++++++++----- 5 files changed, 124 insertions(+), 28 deletions(-) diff --git a/admin-dev/themes/default/template/controllers/orders/_product_line.tpl b/admin-dev/themes/default/template/controllers/orders/_product_line.tpl index 493123c86..3a9e87aa8 100644 --- a/admin-dev/themes/default/template/controllers/orders/_product_line.tpl +++ b/admin-dev/themes/default/template/controllers/orders/_product_line.tpl @@ -147,7 +147,7 @@ {if ($can_edit)}
- {if $data.id_status == $ROOM_STATUS_CANCELLED || $data.id_status == $ROOM_STATUS_NO_SHOW} + {if isset($htl_booking_statuses[$data.id_status]) && $htl_booking_statuses[$data.id_status].is_terminal}
-
+
diff --git a/classes/order/Order.php b/classes/order/Order.php index 969bab3fb..1ebfa6392 100644 --- a/classes/order/Order.php +++ b/classes/order/Order.php @@ -3095,6 +3095,49 @@ class OrderCore extends ObjectModel return true; } + /** + * Restore the order status from the paid amount once the order is no longer + * completely No-show/Cancelled, e.g. when the admin reverts a No-show room back + * to Assigned or Checked-in. + * + * Paid amount 0 => Awaiting payment, less than order total => Partial payment + * accepted, otherwise => Payment accepted. + * + * @param bool $useEmail whether to email the customer about the change + * @return bool whether the order status was actually changed + */ + public function syncPaymentStatusAfterBookingRevert($useEmail = true) + { + if (!in_array($this->current_state, array( + Configuration::get('PS_OS_NO_SHOW'), + Configuration::get('PS_OS_CANCELED'), + ))) { + return false; + } + + // every room is still No-show/Cancelled, keep the order status as it is + if ($this->getOrderCompleteRefundStatus()) { + return false; + } + + $totalPaid = $this->getTotalPaid(); + if ($totalPaid <= 0) { + $idOrderState = Configuration::get('PS_OS_AWAITING_PAYMENT'); + } elseif ($totalPaid < $this->getOrderTotal()) { + $idOrderState = Configuration::get('PS_OS_PARTIAL_PAYMENT_ACCEPTED'); + } else { + $idOrderState = Configuration::get('PS_OS_PAYMENT_ACCEPTED'); + } + + $objOrderHistory = new OrderHistory(); + $objOrderHistory->id_order = (int) $this->id; + $objOrderHistory->id_employee = isset(Context::getContext()->employee->id) ? (int) Context::getContext()->employee->id : 0; + $objOrderHistory->changeIdOrderState($idOrderState, $this, !$this->hasInvoice()); + $useEmail ? $objOrderHistory->addWithemail() : $objOrderHistory->add(); + + return true; + } + public function getWsBookings() { return Db::getInstance()->executeS( diff --git a/modules/hotelreservationsystem/classes/HotelBookingStatus.php b/modules/hotelreservationsystem/classes/HotelBookingStatus.php index 1694e5476..3b72984fe 100644 --- a/modules/hotelreservationsystem/classes/HotelBookingStatus.php +++ b/modules/hotelreservationsystem/classes/HotelBookingStatus.php @@ -66,6 +66,13 @@ class HotelBookingStatus extends ObjectModel HotelBookingDetail::STATUS_ASSIGNED, HotelBookingDetail::STATUS_CHECKED_IN, ), + // only reachable when No-show is not marked as terminal (is_terminal = 0) + HotelBookingDetail::STATUS_NO_SHOW => array( + // revert: undo an accidental no-show + HotelBookingDetail::STATUS_ASSIGNED, + // late arrival: guest checks in after being marked no-show + HotelBookingDetail::STATUS_CHECKED_IN, + ), ); return isset($transitions[$currentStatus]) ? $transitions[$currentStatus] : array(); diff --git a/modules/hotelreservationsystem/controllers/admin/AdminRoomStatusesController.php b/modules/hotelreservationsystem/controllers/admin/AdminRoomStatusesController.php index 540efa9c0..d98381cf2 100644 --- a/modules/hotelreservationsystem/controllers/admin/AdminRoomStatusesController.php +++ b/modules/hotelreservationsystem/controllers/admin/AdminRoomStatusesController.php @@ -61,9 +61,31 @@ class AdminRoomStatusesController extends ModuleAdminController ); } + /** + * List column callback: display the terminal (final) flag as a Yes/No badge. + * + * @param mixed $value 1 or 0 + * @return string + */ public function getTerminalStatusLabel($value) { - return $value ? $this->l('Yes') : $this->l('No'); + if ($value) { + return ''.$this->l('Yes').''; + } + + return ''.$this->l('No').''; + } + + /** + * Whether the terminal (final) flag of the given status can be changed by the admin. + * Only No-show is configurable for now; every other status keeps its fixed value. + * + * @param int $idBookingStatus + * @return bool + */ + protected function isTerminalEditable($idBookingStatus) + { + return (int) $idBookingStatus == HotelBookingDetail::STATUS_NO_SHOW; } public function initToolbar() @@ -92,12 +114,38 @@ class AdminRoomStatusesController extends ModuleAdminController return; } - // decoupled from the real 'is_terminal' field name on purpose: copyFromPost() - // writes any POST key matching a real object property, and 'disabled' on the - // switch only blocks submission client-side (removable via devtools). Using - // a name with no matching property makes server-side tampering impossible, - // not just hidden. - $this->fields_value['is_terminal_display'] = $this->object->is_terminal; + // Only No-show gets an editable switch posting the real 'is_terminal' field. + // Every other status shows a read-only Yes/No badge and posts nothing for it; + // processSave() also rejects a tampered 'is_terminal' for those statuses. + if ($this->isTerminalEditable($this->object->id)) { + $this->fields_value['is_terminal'] = $this->object->is_terminal; + $terminalInput = array( + 'type' => 'switch', + 'label' => $this->l('Terminal (final) status'), + 'name' => 'is_terminal', + 'hint' => $this->l('If set to Yes, a room with this status cannot be edited, only deleted. If No, the room status can still be changed, e.g. to undo an accidental no-show or for a late check-in.'), + 'values' => array( + array( + 'id' => 'is_terminal_on', + 'value' => 1, + 'label' => $this->l('Yes'), + ), + array( + 'id' => 'is_terminal_off', + 'value' => 0, + 'label' => $this->l('No'), + ), + ), + ); + } else { + $terminalInput = array( + 'type' => 'html', + 'label' => $this->l('Terminal (final) status'), + 'name' => 'is_terminal_display', + 'hint' => $this->l('If set to Yes, a room with this status cannot be edited, only deleted. If No, the room can still be edited. This setting is fixed for this status and cannot be changed.'), + 'html_content' => '

'.$this->getTerminalStatusLabel($this->object->is_terminal).'

', + ); + } $this->fields_form = array( 'legend' => array( @@ -119,25 +167,7 @@ class AdminRoomStatusesController extends ModuleAdminController 'name' => 'color', 'required' => true, ), - array( - 'type' => 'switch', - 'label' => $this->l('Terminal (final) status'), - 'name' => 'is_terminal_display', - 'hint' => $this->l('If set to Yes, a room with this status cannot be edited, only deleted. If No, the room can still be edited. This setting is fixed for each status, so it is disabled here.'), - 'disabled' => true, - 'values' => array( - array( - 'id' => 'is_terminal_on', - 'value' => 1, - 'label' => $this->l('Yes'), - ), - array( - 'id' => 'is_terminal_off', - 'value' => 0, - 'label' => $this->l('No'), - ), - ), - ), + $terminalInput, ), 'submit' => array( 'title' => $this->l('Save'), @@ -159,6 +189,22 @@ class AdminRoomStatusesController extends ModuleAdminController $this->errors[] = $this->l('Status name is required at least in ').$objDefaultLanguage['name']; } + if (Tools::getIsset('is_terminal')) { + $isTerminal = Tools::getValue('is_terminal'); + if (!$this->isTerminalEditable($this->object->id)) { + if ((int) $isTerminal != (int) $this->object->is_terminal) { + $this->errors[] = $this->l('Terminal (final) status can not be changed for this room status.'); + } + } elseif (!Validate::isBool($isTerminal)) { + $this->errors[] = $this->l('Invalid value for terminal (final) status.'); + } + } + + if (!empty($this->errors)) { + $this->display = 'edit'; + return false; + } + return parent::processSave(); } } From ec61f743387a7ff00302ac0455e54d7328bc30f1 Mon Sep 17 00:00:00 2001 From: Sumit Panwar Date: Thu, 5 Nov 2026 19:54:52 +0530 Subject: [PATCH 2/4] Fixed terminal status issues withNo show room status --- controllers/admin/AdminOrdersController.php | 60 ++++++++++++++++++- .../classes/HotelBookingStatus.php | 2 + 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index 3d1cf5b7c..a8bfffba2 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -925,6 +925,11 @@ class AdminOrdersControllerCore extends AdminController $currentRoomStatus = (int) Tools::getValue('current_room_status'); $htlOrderStatus = $objHotelBookingStatus->getAllStatuses($this->context->language->id); $allowedStatuses = HotelBookingStatus::getAllowedTransitions($currentRoomStatus); + // no transition is allowed out of a terminal (final) status + $objCurrentStatus = new HotelBookingStatus($currentRoomStatus); + if (Validate::isLoadedObject($objCurrentStatus) && $objCurrentStatus->is_terminal) { + $allowedStatuses = array(); + } foreach ($htlOrderStatus as &$status) { $status['disabled'] = !in_array((int) $status['id_status'], $allowedStatuses); } @@ -935,6 +940,8 @@ class AdminOrdersControllerCore extends AdminController 'current_index' => self::$currentIndex, 'hotel_order_status' => $htlOrderStatus, 'ROOM_STATUS_ASSIGNED' => HotelBookingDetail::STATUS_ASSIGNED, + 'ROOM_STATUS_CHECKED_IN' => HotelBookingDetail::STATUS_CHECKED_IN, + 'ROOM_STATUS_CHECKED_OUT' => HotelBookingDetail::STATUS_CHECKED_OUT, 'current_room_status' => $currentRoomStatus ) ); @@ -9060,6 +9067,11 @@ class AdminOrdersControllerCore extends AdminController $this->errors[] = Tools::displayError('Invalid booking status found.'); } elseif ($newStatus == $objHotelBookingDetail->id_status) { $this->errors[] = Tools::displayError('This room is already set to the selected status.'); + } elseif (($objCurrentStatus = new HotelBookingStatus((int) $objHotelBookingDetail->id_status)) + && Validate::isLoadedObject($objCurrentStatus) + && $objCurrentStatus->is_terminal + ) { + $this->errors[] = Tools::displayError('Room status cannot be changed as the current status of this room is final.'); } elseif (($newStatus == HotelBookingDetail::STATUS_NO_SHOW || $newStatus == HotelBookingDetail::STATUS_CANCELLED) && ($objHotelBookingDetail->id_status == HotelBookingDetail::STATUS_CHECKED_IN || $objHotelBookingDetail->id_status == HotelBookingDetail::STATUS_CHECKED_OUT) @@ -9083,6 +9095,24 @@ class AdminOrdersControllerCore extends AdminController $this->errors[] = Tools::displayError('Room cannot be marked as No-show before the booking\'s check-in date.'); } + // reverting a No-show/Cancelled room makes it occupy the inventory again + if (empty($this->errors) + && ($objHotelBookingDetail->id_status == HotelBookingDetail::STATUS_NO_SHOW + || $objHotelBookingDetail->id_status == HotelBookingDetail::STATUS_CANCELLED) + && $newStatus != HotelBookingDetail::STATUS_NO_SHOW + && $newStatus != HotelBookingDetail::STATUS_CANCELLED + ) { + if ((new OrderReturn())->hasCompletelyRefundedBooking($objHotelBookingDetail->id)) { + $this->errors[] = Tools::displayError('Room status cannot be changed as this room booking has already been refunded.'); + } elseif ($objHotelBookingDetail->chechRoomBooked( + $objHotelBookingDetail->id_room, + $objHotelBookingDetail->date_from, + $objHotelBookingDetail->date_to + )) { + $this->errors[] = Tools::displayError('Room status cannot be changed as this room is already booked in another booking for the same dates. Please reallocate the room first.'); + } + } + if ($objHotelBookingDetail->id_status == HotelBookingDetail::STATUS_CHECKED_OUT && (date('Y-m-d', strtotime($objHotelBookingDetail->check_out)) != date('Y-m-d', strtotime($objHotelBookingDetail->date_to))) && ($booking = $objHotelBookingDetail->chechRoomBooked($objHotelBookingDetail->id_room, $objHotelBookingDetail->check_out, $objHotelBookingDetail->date_to)) @@ -9169,8 +9199,29 @@ class AdminOrdersControllerCore extends AdminController ? OrderReturn::EVENT_TYPE_CANCELLATION : OrderReturn::EVENT_TYPE_NO_SHOW; + // a No-show room moved to Cancelled usually already has a refund + // request from the No-show: reuse it instead of creating a duplicate + $existingRefundRequest = false; + if ($objHotelBookingDetail->id_status == HotelBookingDetail::STATUS_NO_SHOW + && ($refundRequests = OrderReturn::getOrdersReturnDetail($order->id, 0, $objHotelBookingDetail->id)) + ) { + // latest request first + $latestRequest = reset($refundRequests); + if (!$latestRequest['denied']) { + $existingRefundRequest = true; + if (!$latestRequest['refunded']) { + // still pending: it now stands for the cancellation + $objOrderReturn = new OrderReturn((int) $latestRequest['id_order_return']); + if (Validate::isLoadedObject($objOrderReturn)) { + $objOrderReturn->event_type = $eventType; + $objOrderReturn->save(); + } + } + } + } + // no refund request is created if refund is disabled for the hotel - if ($order->isReturnable()) { + if (!$existingRefundRequest && $order->isReturnable()) { $objOrderReturn = new OrderReturn(); $objOrderReturn->id_customer = $order->id_customer; $objOrderReturn->id_order = $order->id; @@ -9211,6 +9262,13 @@ class AdminOrdersControllerCore extends AdminController $success = $statusChanged; } else { $statusChanged = $objHotelBookingDetail->changeStatus($newStatus, $changeParams); + + if ($statusChanged) { + // a No-show room reverted back: the order is no longer completely + // No-show/Cancelled, so restore its status from the paid amount + $order = new Order($order->id); + $order->syncPaymentStatusAfterBookingRevert(); + } $success = $statusChanged; } diff --git a/modules/hotelreservationsystem/classes/HotelBookingStatus.php b/modules/hotelreservationsystem/classes/HotelBookingStatus.php index 3b72984fe..3623dfa69 100644 --- a/modules/hotelreservationsystem/classes/HotelBookingStatus.php +++ b/modules/hotelreservationsystem/classes/HotelBookingStatus.php @@ -72,6 +72,8 @@ class HotelBookingStatus extends ObjectModel HotelBookingDetail::STATUS_ASSIGNED, // late arrival: guest checks in after being marked no-show HotelBookingDetail::STATUS_CHECKED_IN, + // booking cancelled after being marked no-show + HotelBookingDetail::STATUS_CANCELLED, ), ); From 4c4a0106cd4b662e93c0b57e07747c349ab3b5a1 Mon Sep 17 00:00:00 2001 From: Sumit Panwar Date: Fri, 6 Nov 2026 10:37:00 +0530 Subject: [PATCH 3/4] while installation noshow is_terminal will be set as 0 --- modules/hotelreservationsystem/classes/HotelHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/hotelreservationsystem/classes/HotelHelper.php b/modules/hotelreservationsystem/classes/HotelHelper.php index baca47ba8..c08f0fcfc 100644 --- a/modules/hotelreservationsystem/classes/HotelHelper.php +++ b/modules/hotelreservationsystem/classes/HotelHelper.php @@ -1582,7 +1582,7 @@ class HotelHelper array( 'id' => HotelBookingDetail::STATUS_NO_SHOW, 'color' => '#FF8C00', - 'is_terminal' => 1, + 'is_terminal' => 0, 'name' => array( 'en' => 'No-show', 'nl' => 'Niet verschenen', From 48ba57ade829c08382d1068c9b273a11003df91b Mon Sep 17 00:00:00 2001 From: Sumit Panwar Date: Fri, 6 Nov 2026 10:46:38 +0530 Subject: [PATCH 4/4] Updated: minor code improvements --- classes/order/Order.php | 2 +- controllers/admin/AdminOrdersController.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/order/Order.php b/classes/order/Order.php index 1ebfa6392..52d2b9afc 100644 --- a/classes/order/Order.php +++ b/classes/order/Order.php @@ -3106,7 +3106,7 @@ class OrderCore extends ObjectModel * @param bool $useEmail whether to email the customer about the change * @return bool whether the order status was actually changed */ - public function syncPaymentStatusAfterBookingRevert($useEmail = true) + public function changeOrderStatusOnRoomBookingStatusChange($useEmail = true) { if (!in_array($this->current_state, array( Configuration::get('PS_OS_NO_SHOW'), diff --git a/controllers/admin/AdminOrdersController.php b/controllers/admin/AdminOrdersController.php index a8bfffba2..6b7184614 100644 --- a/controllers/admin/AdminOrdersController.php +++ b/controllers/admin/AdminOrdersController.php @@ -9267,7 +9267,7 @@ class AdminOrdersControllerCore extends AdminController // a No-show room reverted back: the order is no longer completely // No-show/Cancelled, so restore its status from the paid amount $order = new Order($order->id); - $order->syncPaymentStatusAfterBookingRevert(); + $order->changeOrderStatusOnRoomBookingStatusChange(); } $success = $statusChanged; }