Merge branch 'develop' into gli-2855

This commit is contained in:
Amit sharma
2026-03-30 12:37:30 +05:30
14 changed files with 348 additions and 88 deletions

225
AGENTS.md Normal file
View File

@@ -0,0 +1,225 @@
# AGENTS.md
## Project Overview
QloApps is an open-source hotel reservation and property management platform. It enables hotels to manage rooms, bookings, guests, and payments through a web-based system.
## Purpose of this File
This document provides guidance for AI coding agents contributing to the QloApps project. It defines conventions, safety rules, and workflows to ensure consistent and secure code generation.
## Technology Stack
- **Language:** PHP 8.1–8.4 (backend), Smarty 3.x (templates), JavaScript/jQuery (frontend)
- **Database:** MySQL 5.7, 8.0+; MariaDB 10.5, 10.6, 10.11, 11.0, 11.2, 11.4
- **Architecture:** MVC with hook-based module system
- **License:** OSL-3.0 (core), AFL-3.0 (modules)
- **Required PHP Extensions:** PDO_MySQL, cURL, OpenSSL, SOAP, GD, SimpleXML, DOM, Zip, Phar
## Environment Setup
Install dependencies:
```bash
composer install
```
Clear caches:
```bash
rm -rf cache/smarty/compile/* cache/smarty/cache/*
rm -f cache/class_index.php
```
Clear class cache after adding or modifying overrides.
## Project Structure
```
.
├── classes/ # Core models extending ObjectModel
├── controllers/ # Front and admin controllers
│ ├── admin/
│ └── front/
├── modules/ # Feature modules with isolated functionality
├── override/ # Core class and controller overrides
├── themes/ # Smarty templates (.tpl files)
├── config/ # Configuration files (settings.inc.php contains secrets)
├── cache/ # Generated cache files
├── tests/ # PHPUnit test suite
```
**QloApps Agent Skills:** Install reusable development skills using:
```bash
npx skills add Qloapps/agent-skills
```
Available skills: module-development, payment-module-development, stats-module-development. Check installed skills before implementing new functionality.
## Architecture Overview
**MVC Pattern**
- Models: classes/ extending ObjectModel
- Controllers: FrontController or AdminController
- Views: Smarty templates (.tpl)
**Modules**
- Located in modules/<modulename>/
- Provide isolated functionality
- Integrate using hooks
**Overrides**
- Located in override/
- Extend core classes using the Core suffix
- Clear class cache after creating overrides
**Context**
- Access runtime objects using Context::getContext()
## Core vs Module Development Rules
**When working on a module:**
1. Never modify core files directly
2. Use **hooks** to integrate module functionality into core features
3. If no suitable hook exists, create a **custom hook** — but only if the hook placement is generic and useful for other modules too
4. Use **overrides** only as a last resort — overrides can conflict with other module override files and require manual resolution
**When working on a core feature:**
- Make changes directly in core files
- Do not use hooks or overrides for core-to-core changes
## Coding Conventions
**Classes:** PascalCase — `HotelBookingData`
**Methods:** camelCase — `getBookingDetails()`
**Variables:** camelCase — `$hotelId`
**Constants:** UPPER_SNAKE_CASE — `BOOKING_STATUS_CONFIRMED`
**Database Tables:** _DB_PREFIX_ + lowercase_snake — `qlo_hotel_booking`
**Config Keys:** MODULENAME_SETTING — `HOTELRESERVATION_ENABLED`
**Files:** One class per file, filename matches class name
**Templates:** lowercase-hyphens.tpl — `booking-form.tpl`
Add PHPDoc blocks to all classes and methods.
## Translation
Never hardcode user-facing English strings — always wrap them in the appropriate translation method.
| Context | Method |
|---------|--------|
| Module main file | `$this->l('string')` |
| Module admin controller | `$this->l('string')` |
| Module front controller | `$this->module->l('string', 'controllerName')` |
| Module classes | `$objModule->l('string', 'ClassName')` |
| Core admin controller | `$this->l('string')` |
| Core front controller | `Tools::displayError('string')` |
| Smarty template (core) | `{l s='string'}` |
| Smarty template (module) | `{l s='string' mod='modulename'}` |
## Multi-language
- Always include `id_lang` in queries that return translatable content
- Use `Context::getContext()->language->id` for the current language
## Module Development Guidelines
**Module Location:** modules/<modulename>/
**Required Files:**
- <modulename>.php — Main class extending Module
- config.xml — Module metadata
**Optional Directories:**
- classes/ — Module-specific models
- controllers/ — Module controllers
- views/templates/ — Smarty templates
- upgrade/ — Version migration scripts
**Hook Integration:**
- Register hooks in install() method
- Unregister hooks in uninstall() method
- Keep hook handlers lightweight
**Configuration:**
- Store settings using Configuration::updateValue()
- Retrieve settings using Configuration::get()
- Prefix config keys with module name
## Database Rules
**Table Prefix:** Always use _DB_PREFIX_ constant instead of hardcoding the table prefix
**Escaping:**
- Strings: pSQL($value)
- Integers: (int)$value
- Table/column names: bqSQL($name)
**Preferred Access:** Use ObjectModel for CRUD operations instead of raw SQL queries
**Prohibited:** Never concatenate raw user input into SQL queries.
## Security Guidelines
**Input Handling**
- Use Tools::getValue() for request parameters
- Cast numeric values to (int)
- Escape strings using pSQL()
- Validate input using Validate class methods
**Output Escaping**
- Use Tools::safeOutput() in PHP
- Use Smarty escape modifiers in templates
**Sensitive Data**
- Never expose config/settings.inc.php
- Never commit API keys, passwords, or tokens
**Authorization**
- Verify permissions before performing admin operations
## Testing
Testing infrastructure is being configured. Check tests/ directory for available tests before running.
## AI Agent Workflow
1. Check installed agent skills before implementing new functionality
2. Search codebase for similar implementations before creating new code
3. Extend existing classes rather than duplicating functionality
4. Follow patterns established in surrounding code
5. Reuse existing utilities: Tools, Validate, Db, Configuration classes
6. Prioritize consistency with existing codebase over new approaches
After making changes:
- Clear caches if modifying templates or overrides
- Add PHPDoc to new methods
## Safety Rules
**Agents must not:**
- Delete files unless explicitly instructed
- Run git commands automatically
- Modify composer.json without approval
- Modify config/settings.inc.php
- Execute DROP, TRUNCATE, or destructive SQL
- Modify core files directly when working on a module (use hooks or override system)
**Agents must:**
- Use pSQL() for strings and (int) for IDs in SQL
- Use Tools::getValue() for request parameters
- Escape all output
- Check installed agent skills before implementing new functionality
- Follow project naming conventions
- Validate inputs before processing
**Agents should ask before:**
- Deleting any files
- Running git commands
- Changing dependencies
- Modifying database schema
- Altering payment or booking logic
---
**Resources:**
- Documentation: https://docs.qloapps.com
- Forum: https://forums.qloapps.com
- GitHub: https://github.com/Qloapps/QloApps
- Security Issues: support@qloapps.com

View File

@@ -104,7 +104,7 @@
<div class="col-lg-1"><span class="pull-right">{include file="controllers/products/multishop/checkbox.tpl" field="name" type="default" multilang="true"}</span></div>
<label class="control-label col-lg-2 required" id="name" for="name_{$id_lang}">
<span class="label-tooltip" data-toggle="tooltip" title="{l s='Write the name of the Room Type for ex. Delux, Executive etc.'} {l s='Invalid characters:'} &lt;&gt;;=#{}">
{l s='Room Type'}
{l s='Name'}
</span>
</label>
<div class="col-lg-5">

View File

@@ -123,7 +123,7 @@
function startTree(idElem) {
if (typeof $.fn.tree === 'undefined') {
setTimeout(startTree, 100);
setTimeout(function() { startTree(idElem); }, 100);
return;
}

View File

@@ -332,6 +332,14 @@ class CookieCore
$this->logout();
}
if (isset($this->_content['id_customer']) && isset($this->_content['passwd']) && (int)$this->_content['id_customer']) {
if (Validate::isLoadedObject($objCustomer = new Customer((int)$this->_content['id_customer']))) {
if ($objCustomer->passwd != $this->_content['passwd']) {
$this->logout();
}
}
}
if (!isset($this->_content['date_add'])) {
$this->_content['date_add'] = date('Y-m-d H:i:s');
}

View File

@@ -2397,7 +2397,7 @@ class AdminControllerCore extends Controller
$must_have_module_list = file_get_contents(_PS_ROOT_DIR_.Module::CACHE_FILE_MUST_HAVE_MODULES_LIST);
if (!empty($must_have_module_list) && $must_have_module_list_xml = @simplexml_load_string($must_have_module_list)) {
$must_have_module_list_array = array();
if (is_object($country_module_list_xml->module)) {
if (is_object($must_have_module_list_xml->module)) {
foreach ($must_have_module_list_xml->module as $l => $mo) {
$all_module_list[] = (string)$mo->name;
}

View File

@@ -368,6 +368,54 @@ class AdminOrdersControllerCore extends AdminController
'icon' => 'process-icon-new'
);
}
if ($this->display == 'view') {
/** @var Order $order */
if (Validate::isLoadedObject($order = $this->loadObject())) {
// hotel link in header
if ($idHotel = HotelBookingDetail::getIdHotelByIdOrder($order->id)) {
$this->page_header_toolbar_btn['hotel'] = array(
'href' => $this->context->link->getAdminLink('AdminAddHotel').'&id='.$idHotel.'&updatehtl_branch_info',
'desc' => $this->l('View Hotel'),
'class' => 'icon-building',
'target' => true,
);
}
if (Configuration::get('PS_INVOICE') && $order->hasInvoice() && !$this->lite_display) {
$this->page_header_toolbar_btn['file'] = array(
'short' => $this->l('Invoice'),
'href' => $this->context->link->getAdminLink('AdminPdf').'&submitAction=generateInvoicePDF&id_order='.$order->id,
'desc' => $this->l('View invoice'),
'class' => 'icon-file-text',
'target' => true,
);
}
$this->page_header_toolbar_btn['print'] = array(
'short' => $this->l('Print'),
'href' => 'javascript:window.print()',
'desc' => $this->l('Print order'),
'class' => 'icon-print',
);
if ($this->tabAccess['edit'] === 1) {
if (((int) $order->isReturnable())
&& !$order->hasCompletelyRefunded(Order::ORDER_COMPLETE_CANCELLATION_OR_REFUND_REQUEST_FLAG, 0, 0)
) {
$orderTotalPaid = $order->getTotalPaid();
$orderDiscounts = $order->getCartRules();
$hasOrderDiscountOrPayment = ((float)$orderTotalPaid > 0 || $orderDiscounts) ? true : false;
$this->page_header_toolbar_btn['cancel'] = array(
'short' => ($hasOrderDiscountOrPayment) ? $this->l('Refund') : $this->l('Cancel'),
'id' => 'desc-order-standard_refund',
'desc' => ($hasOrderDiscountOrPayment) ? $this->l('Initiate refund') : $this->l('Cancel bookings'),
'class' => 'icon-exchange',
'target' => true,
);
}
}
}
}
parent::initPageHeaderToolbar();
}
@@ -476,55 +524,6 @@ class AdminOrdersControllerCore extends AdminController
public function initToolbar()
{
if ($this->display == 'view') {
/** @var Order $order */
if (Validate::isLoadedObject($order = $this->loadObject())) {
// hotel link in header
if ($idHotel = HotelBookingDetail::getIdHotelByIdOrder($order->id)) {
$this->toolbar_btn['hotel'] = array(
'href' => $this->context->link->getAdminLink('AdminAddHotel').'&id='.$idHotel.'&updatehtl_branch_info',
'desc' => $this->l('View Hotel'),
'class' => 'icon-building',
'target' => true,
);
}
if (Configuration::get('PS_INVOICE') && $order->hasInvoice() && !$this->lite_display) {
$this->toolbar_btn['file'] = array(
'short' => $this->l('Invoice'),
'href' => $this->context->link->getAdminLink('AdminPdf').'&submitAction=generateInvoicePDF&id_order='.$order->id,
'desc' => $this->l('View invoice'),
'class' => 'icon-file-text',
'target' => true,
);
}
$this->toolbar_btn['print'] = array(
'short' => $this->l('Print'),
'href' => 'javascript:window.print()',
'desc' => $this->l('Print order'),
'class' => 'icon-print',
);
if ($this->tabAccess['edit'] === 1) {
if (((int) $order->isReturnable())
&& !$order->hasCompletelyRefunded(Order::ORDER_COMPLETE_CANCELLATION_OR_REFUND_REQUEST_FLAG, 0, 0)
) {
$orderTotalPaid = $order->getTotalPaid();
$orderDiscounts = $order->getCartRules();
$hasOrderDiscountOrPayment = ((float)$orderTotalPaid > 0 || $orderDiscounts) ? true : false;
$this->toolbar_btn['cancel'] = array(
'short' => ($hasOrderDiscountOrPayment) ? $this->l('Refund') : $this->l('Cancel'),
'id' => 'desc-order-standard_refund',
'desc' => ($hasOrderDiscountOrPayment) ? $this->l('Initiate refund') : $this->l('Cancel bookings'),
'class' => 'icon-exchange',
'target' => true,
);
}
}
}
}
$res = parent::initToolbar();
if (Context::getContext()->shop->getContext() != Shop::CONTEXT_SHOP && isset($this->toolbar_btn['new']) && Shop::isFeatureActive()) {
unset($this->toolbar_btn['new']);
@@ -3710,6 +3709,7 @@ class AdminOrdersControllerCore extends AdminController
'ROOM_STATUS_CHECKED_OUT' => HotelBookingDetail::STATUS_CHECKED_OUT,
'ALLOTMENT_MANUAL' => HotelBookingDetail::ALLOTMENT_MANUAL,
'order_convenience_fee_services' => $orderConvenienceFeeServices,
'page_header_toolbar_btn' => $this->page_header_toolbar_btn,
);
return parent::renderView();

View File

@@ -714,8 +714,11 @@ class AdminThemesControllerCore extends AdminController
}
$ids_themes = Tools::unSerialize(Configuration::get('PS_ADDONS_THEMES_IDS'));
if (array_key_exists($obj->directory, $ids_themes)) {
unset($ids_themes[$obj->directory]);
if(is_array($ids_themes)) {
if (array_key_exists($obj->directory, $ids_themes)) {
unset($ids_themes[$obj->directory]);
}
}
$obj->removeMetas();

View File

@@ -261,7 +261,7 @@
{/if}
<td>{HotelRoomInformation::getRoomStatusTitle($unavail_dtl_v['id_status'])|escape:'htmlall':'UTF-8'}</td>
<td>
{if $unavail_dtl_v['date_from'] && $unavail_dtl_v['date_to']}
{if isset($unavail_dtl_v['date_from']) && isset($unavail_dtl_v['date_to'])}
{dateFormat date=$unavail_dtl_v['date_from']} - {dateFormat date=$unavail_dtl_v['date_to']}
{else}
--

View File

@@ -137,39 +137,46 @@ class AdminPaypalCommerceTransactionController extends ModuleAdminController
$smartyVars = array();
$refundData = array();
$totalRefunded = 0;
$transactionData = WKPayPalCommerceOrder::getTransactionDetails((int)$idTrans);
$orderCurrency = new Currency((int)$transactionData['id_currency']);
$refundData = WkPaypalCommerceRefund::getRefundListByTransID((int)$idTrans);
$totalRefundedFormatted = WkPaypalCommerceRefund::getTotalRefundedAmount((int)$idTrans, true);
if ($transactionData = WKPayPalCommerceOrder::getTransactionDetails((int)$idTrans)) {
$orderCurrency = new Currency((int)$transactionData['id_currency']);
$refundData = WkPaypalCommerceRefund::getRefundListByTransID((int)$idTrans);
$totalRefundedFormatted = WkPaypalCommerceRefund::getTotalRefundedAmount((int)$idTrans, true);
$totalRefunded = WkPaypalCommerceRefund::getTotalRefundedAmount((int)$idTrans, false);
$remainingRefund = (float)($transactionData['pp_paid_total'] - $totalRefunded);
$response = json_decode($transactionData['response'], true);
$totalRefunded = WkPaypalCommerceRefund::getTotalRefundedAmount((int)$idTrans, false);
$remainingRefund = (float)($transactionData['pp_paid_total'] - $totalRefunded);
$response = Tools::jsonDecode($transactionData['response'], true);
// Buyer making a payment in a different currency (ex: EUR) which is different from the default currency of merchant (Ex: USD), In all those cross currency cases, After Capture, transaction will fall into Pending state and will require merchant to manually go to his PayPal account and accept the payment.
$objPPOrder = new WKPayPalCommerceOrder();
if (isset($response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason'])
&& ($transactionData['pp_payment_status'] == 'PENDING' || $transactionData['pp_payment_status'] == 'DENIED')
&& isset($response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason'])
) {
if (isset($objPPOrder->ppStatusDetail[$response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason']])) {
$smartyVars['ppstatusDetailMsg'] = $objPPOrder->ppStatusDetail[$response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason']];
// Buyer making a payment in a different currency (ex: EUR) which is different from the default currency of merchant (Ex: USD), In all those cross currency cases, After Capture, transaction will fall into Pending state and will require merchant to manually go to his PayPal account and accept the payment.
$objPPOrder = new WKPayPalCommerceOrder();
if (
isset($response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason'])
&& ($transactionData['pp_payment_status'] == 'PENDING' || $transactionData['pp_payment_status'] == 'DENIED')
&& isset($response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason'])
) {
if (isset($objPPOrder->ppStatusDetail[$response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason']])) {
$smartyVars['ppstatusDetailMsg'] = $objPPOrder->ppStatusDetail[$response['data']['purchase_units'][0]['payments']['captures'][0]['status_details']['reason']];
}
}
}
$smartyVars['transaction_url'] = $this->context->link->getAdminLink('AdminPaypalCommerceTransaction',Tools::getAdminTokenLite('AdminPaypalCommerceTransaction')).'&viewwk_paypal_commerce_order&id_paypal_commerce_order=' . (int)$idTrans;
$smartyVars['transaction_data'] = $transactionData;
$smartyVars['refund_data'] = $refundData;
$smartyVars['refunded_amount'] = $totalRefundedFormatted;
$smartyVars['remaining_refund'] = $remainingRefund;
$smartyVars['remaining_refund_format'] = Tools::displayPrice($remainingRefund, $orderCurrency);
$smartyVars['currency'] = $orderCurrency;
$smartyVars['WK_PAYPAL_COMMERCE_REFUND_TYPE_FULL'] = WkPaypalCommerceRefund::WK_PAYPAL_COMMERCE_REFUND_TYPE_FULL;
$smartyVars['WK_PAYPAL_COMMERCE_REFUND_TYPE_PARTIAL'] = WkPaypalCommerceRefund::WK_PAYPAL_COMMERCE_REFUND_TYPE_PARTIAL;
$smartyVars['transaction_url'] = $this->context->link->getAdminLink('AdminPaypalCommerceTransaction',Tools::getAdminTokenLite('AdminPaypalCommerceTransaction')).'&viewwk_paypal_commerce_order&id_paypal_commerce_order=' . (int)$idTrans;
$smartyVars['transaction_data'] = $transactionData;
$smartyVars['refund_data'] = $refundData;
$smartyVars['refunded_amount'] = $totalRefundedFormatted;
$smartyVars['remaining_refund'] = $remainingRefund;
$smartyVars['remaining_refund_format'] = Tools::displayPrice($remainingRefund, $orderCurrency);
$smartyVars['currency'] = $orderCurrency;
$smartyVars['WK_PAYPAL_COMMERCE_REFUND_TYPE_FULL'] = WkPaypalCommerceRefund::WK_PAYPAL_COMMERCE_REFUND_TYPE_FULL;
$smartyVars['WK_PAYPAL_COMMERCE_REFUND_TYPE_PARTIAL'] = WkPaypalCommerceRefund::WK_PAYPAL_COMMERCE_REFUND_TYPE_PARTIAL;
$this->context->smarty->assign($smartyVars);
$this->context->smarty->assign($smartyVars);
$this->base_tpl_view = 'view.tpl';
return parent::renderView();
} else {
Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token);
}
} else {
Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token);
}
$this->base_tpl_view = 'view.tpl';
return parent::renderView();
}
public function postProcess()

View File

@@ -176,6 +176,7 @@
<th style="text-align: center">{l s='Amount' mod='qlopaypalcommerce'}</th>
<th style="text-align: center">{l s='Type' mod='qlopaypalcommerce'}</th>
<th style="text-align: center">{l s='Status' mod='qlopaypalcommerce'}</th>
<th style="text-align: center">{l s='Remark' mod='qlopaypalcommerce'}</th>
<th style="text-align: center">{l s='Date' mod='qlopaypalcommerce'}</th>
</tr>
{if $refund_data}
@@ -197,6 +198,7 @@
<label class="label label-danger">{$refund.refund_status|escape:'html':'UTF-8'}</label>
{/if}
</td>
<td style="text-align: center">{$refund.refund_reason|escape:'html':'UTF-8'}</td>
<td style="text-align: center">{$refund.date_add|escape:'html':'UTF-8'}</td>
</tr>
{/foreach}

View File

@@ -310,10 +310,14 @@ class StatsSales extends ModuleGraph
{
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query.$this->getDate().$this->query_group_by.$this->query_having_filter);
foreach ($result as $row) {
$days = (int)substr($row['invoice_date'], 8, 2);
if (!isset($this->_values[$days])) {
$this->_values[$days] = 0;
}
if ($this->option == 1) {
$this->_values[(int)substr($row['invoice_date'], 8, 2)] += 1;
$this->_values[$days] += 1;
} else {
$this->_values[(int)substr($row['invoice_date'], 8, 2)] += $row['total_revenue'];
$this->_values[$days] += $row['total_revenue'];
}
}
}

View File

@@ -28,11 +28,19 @@
<th class="header small" valign="middle">{l s='Invoice Date' pdf='true'}</th>
<th class="header small" valign="middle">{l s='Order Reference' pdf='true'}</th>
<th class="header small" valign="middle">{l s='Order date' pdf='true'}</th>
{if $addresses.invoice->vat_number}
<th class="header small" valign="middle">{l s='VAT Number' pdf='true'}</th>
{/if}
</tr>
<tr>
<td class="center small white">{$title|escape:'html':'UTF-8'}</td>
<td class="center small white">{dateFormat date=$order->invoice_date full=0}</td>
<td class="center small white">{$order->getUniqReference()}</td>
<td class="center small white">{dateFormat date=$order->date_add full=0}</td>
{if $addresses.invoice->vat_number}
<td class="center small white">
{$addresses.invoice->vat_number|escape:'html':'UTF-8'}
</td>
{/if}
</tr>
</table>

View File

@@ -5776,10 +5776,13 @@ textarea {
background: #fff; }
#columns {
padding: 0;
}
#columns:has(#center_column > *) {
position: relative;
padding-bottom: 50px;
padding-top: 15px; }
padding-top: 15px;
}
#index .tab-content {
margin-top: 35px;
margin-bottom: 34px; }

View File

@@ -74,7 +74,7 @@
<span class="htl-address-icon htl-email-icon"></span>{$hotel['email']}
</p>
<div class="hotel-branch-info-actions">
<a href="{$hotel['view_rooms_link']}" target="_blank" class="btn btn-primary view_rooms_btn col-sm-6 col-xs-12">{l s='View Rooms'}</a>
<a href="{$hotel['view_rooms_link']}" target="_blank" class="btn btn-primary view_rooms_btn col-sm-6 col-xs-12">{l s='View Property'}</a>
{if ($hotel['latitude'] != 0 || $hotel['longitude'] != 0) && $viewOnMap}
<a class="btn htl-map-direction-btn col-sm-6 col-xs-12" href="http://maps.google.com/maps?daddr=({$hotel['latitude']},{$hotel['longitude']})" target="_blank">{l s='View on map'}</a>
{/if}