Merge pull request #2010 from sumitwebkul/gli-3353

Updated: Room bookings terminal status improvements
This commit is contained in:
Rohit Jain
2026-11-06 12:33:56 +05:30
committed by GitHub
7 changed files with 186 additions and 30 deletions

View File

@@ -147,7 +147,7 @@
{if ($can_edit)}
<td class="product_action center">
<div class="btn-group pull-right">
{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}
<button href="#" class="btn btn-default delete_room_line">
<i class="icon-trash"></i>
{l s='Delete'}

View File

@@ -30,7 +30,7 @@
{/foreach}
</select>
</div>
<div class="form-group" {if $current_room_status == $ROOM_STATUS_ASSIGNED}style="display:none;"{/if}>
<div class="form-group" {if $current_room_status != $ROOM_STATUS_CHECKED_IN && $current_room_status != $ROOM_STATUS_CHECKED_OUT}style="display:none;"{/if}>
<label class="control-label">{l s='Date'}</label>
<div class="input-group">
<input type="text" name="status_date" class="room_status_date wk-input-date" value="" />

View File

@@ -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 changeOrderStatusOnRoomBookingStatusChange($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(

View File

@@ -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->changeOrderStatusOnRoomBookingStatusChange();
}
$success = $statusChanged;
}

View File

@@ -66,6 +66,15 @@ 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,
// booking cancelled after being marked no-show
HotelBookingDetail::STATUS_CANCELLED,
),
);
return isset($transitions[$currentStatus]) ? $transitions[$currentStatus] : array();

View File

@@ -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',

View File

@@ -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 '<span class="badge badge-success">'.$this->l('Yes').'</span>';
}
return '<span class="badge badge-danger">'.$this->l('No').'</span>';
}
/**
* 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' => '<p class="form-control-static">'.$this->getTerminalStatusLabel($this->object->is_terminal).'</p>',
);
}
$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();
}
}