Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/vikbooking/admin/controller.php
+++ b/vikbooking/admin/controller.php
@@ -13957,8 +13957,15 @@
*/
public function upload_customer_document()
{
- $input = JFactory::getApplication()->input;
+ $app = JFactory::getApplication();
+
+ if (!JSession::checkToken()) {
+ // missing CSRF-proof token
+ VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
+ }
+
$dbo = JFactory::getDbo();
+ $input = $app->input;
$customer_id = $input->getUint('customer', 0);
@@ -13979,15 +13986,13 @@
->where($dbo->qn('id') . ' = ' . $customer_id);
$dbo->setQuery($q, 0, 1);
- $dbo->execute();
+ $customer = $dbo->loadObject();
- if (!$dbo->getNumRows())
+ if (!$customer)
{
throw new Exception(sprintf('Customer [%d] not found', $customer_id), 404);
}
- $customer = $dbo->loadObject();
-
// fetch documents folder path
$dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
@@ -14044,8 +14049,7 @@
$result->code = $e->getCode();
}
- echo json_encode($result);
- exit;
+ VBOHttpDocument::getInstance($app)->json($result);
}
/**
@@ -14057,8 +14061,19 @@
*/
public function delete_customer_document()
{
- $input = JFactory::getApplication()->input;
+ $app = JFactory::getApplication();
+
+ if (!JSession::checkToken()) {
+ // missing CSRF-proof token
+ VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
+ }
+
+ if (!JFactory::getUser()->authorise('core.delete', 'com_vikbooking')) {
+ VBOHttpDocument::getInstance($app)->close(403, JText::translate('JERROR_ALERTNOAUTHOR'));
+ }
+
$dbo = JFactory::getDbo();
+ $input = $app->input;
$customer_id = $input->getUint('customer', 0);
@@ -14075,36 +14090,45 @@
if (!$dbo->getNumRows())
{
- throw new Exception(sprintf('Customer [%d] not found', $customer_id), 404);
+ VBOHttpDocument::getInstance($app)->close(404, sprintf('Customer [%d] not found', $customer_id));
}
$folder = $dbo->loadResult();
if (!$folder)
{
- throw new Exception('The customer does not have any documents', 500);
+ VBOHttpDocument::getInstance($app)->close(500, 'The customer does not have any documents');
}
$file = $input->getString('file');
if (!$file)
{
- throw new Exception('File to remove not specified', 400);
+ VBOHttpDocument::getInstance($app)->close(400, 'File to remove not specified');
}
$path = implode(DIRECTORY_SEPARATOR, array(VBO_CUSTOMERS_PATH, $folder, $file));
if (!is_file($path))
{
- throw new Exception(sprintf('File [%s] not found', $path), 404);
+ VBOHttpDocument::getInstance($app)->close(404, sprintf('File [%s] not found', $path));
}
- jimport('joomla.filesystem.file');
+ /**
+ * Accept only non-traversal paths under the customer docs folder.
+ *
+ * @since 1.18.13 (J) - 1.8.13 (WP)
+ */
+ $path = realpath($path);
+
+ if (!$path || strpos($path, VBO_CUSTOMERS_PATH) !== 0)
+ {
+ VBOHttpDocument::getInstance($app)->close(403, 'Path not allowed for file deletion.');
+ }
$removed = JFile::delete($path);
- echo json_encode(array('status' => (int) $removed));
- exit;
+ VBOHttpDocument::getInstance($app)->json(array('status' => (int) $removed));
}
/**
--- a/vikbooking/admin/helpers/einvoicing/drivers/mydata_aade.php
+++ b/vikbooking/admin/helpers/einvoicing/drivers/mydata_aade.php
@@ -1954,6 +1954,9 @@
$inv_tot_paid = empty($data[0]['totpaid']) ? $data[0]['total'] : $data[0]['totpaid'];
if ($correlated) {
$inv_tot_paid = $this->environmental_fee_details['fee_cost'];
+ } elseif (!$correlated && $inv_tot_paid > $data[0]['total']) {
+ // use the calculated booking total amount minus the environmental fees
+ $inv_tot_paid = $data[0]['total'];
}
// invoice payment method
--- a/vikbooking/admin/helpers/report/bookings.php
+++ b/vikbooking/admin/helpers/report/bookings.php
@@ -0,0 +1,818 @@
+<?php
+/**
+ * @package VikBooking
+ * @subpackage com_vikbooking
+ * @author Alessio Gaggii - E4J srl
+ * @copyright Copyright (C) 2026 E4J srl. All rights reserved.
+ * @license GNU General Public License version 2 or later; see LICENSE
+ * @link https://vikwp.com
+ */
+
+defined('ABSPATH') or die('No script kiddies please!');
+
+/**
+ * Bookings report.
+ *
+ * @since 1.18.13 (J) - 1.8.13 (WP)
+ */
+class VikBookingReportBookings extends VikBookingReport
+{
+ /**
+ * Property 'defaultKeySort' is used by the View that renders the report.
+ */
+ public $defaultKeySort = 'day';
+
+ /**
+ * Property 'defaultKeyOrder' is used by the View that renders the report.
+ */
+ public $defaultKeyOrder = 'ASC';
+
+ /**
+ * Property 'exportAllowed' is used by the View to display the export button.
+ */
+ public $exportAllowed = 1;
+
+ /**
+ * Debug mode is activated by passing the value 'e4j_debug' > 0
+ */
+ private $debug;
+
+ /**
+ * Class constructor should define the name of the report and
+ * other vars. Call the parent constructor to define the DB object.
+ */
+ public function __construct()
+ {
+ $this->reportFile = basename(__FILE__, '.php');
+ $this->reportName = JText::translate('VBMENUTHREE');
+ $this->reportFilters = [];
+
+ $this->cols = [];
+ $this->rows = [];
+ $this->footerRow = [];
+
+ $this->debug = JFactory::getApplication()->input->getBool('e4j_debug', false);
+
+ $this->registerExportCSVFileName();
+
+ parent::__construct();
+ }
+
+ /**
+ * Returns the name of this report.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->reportName;
+ }
+
+ /**
+ * Returns the name of this file without .php.
+ *
+ * @return string
+ */
+ public function getFileName()
+ {
+ return $this->reportFile;
+ }
+
+ /**
+ * Returns the filters of this report.
+ *
+ * @return array
+ */
+ public function getFilters()
+ {
+ if ($this->reportFilters) {
+ // do not run this method twice, as it could load JS and CSS files.
+ return $this->reportFilters;
+ }
+
+ $app = JFactory::getApplication();
+
+ // get VBO Application Object
+ $vbo_app = VikBooking::getVboApplication();
+
+ // load the jQuery UI Datepicker
+ $this->loadDatePicker();
+
+ // From Date Filter
+ $filter_opt = array(
+ 'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
+ 'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-report-datepicker vbo-report-datepicker-from" />',
+ 'type' => 'calendar',
+ 'name' => 'fromdate'
+ );
+ array_push($this->reportFilters, $filter_opt);
+
+ // To Date Filter
+ $filter_opt = array(
+ 'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
+ 'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-report-datepicker vbo-report-datepicker-to" />',
+ 'type' => 'calendar',
+ 'name' => 'todate'
+ );
+ array_push($this->reportFilters, $filter_opt);
+
+ // Dates Type filter
+ $pdatetype = $app->input->getString('dates_type', 'checkin');
+ $filter_opt = array(
+ 'label' => '<label for="dates_type">'.JText::translate('VBPSHOWSEASONSTHREE').'</label>',
+ 'html' => '<select id="dates_type" name="dates_type">' .
+ '<option value="checkin"' . ($pdatetype === 'checkin' ? ' selected="selected"' : '') . '>' . JText::translate('VBPICKUPAT') . '</option>' .
+ '<option value="booking"' . ($pdatetype === 'booking' ? ' selected="selected"' : '') . '>' . JText::translate('VBPEDITBUSYTWO') . '</option>' .
+ '<option value="stay"' . ($pdatetype === 'stay' ? ' selected="selected"' : '') . '>' . JText::translate('VBO_CONDTEXT_RULE_STAYDATES') . '</option>' .
+ '</select>',
+ 'type' => 'select',
+ 'name' => 'dates_type'
+ );
+ array_push($this->reportFilters, $filter_opt);
+
+ // Listings Filter
+ $filter_opt = array(
+ 'label' => '<label for="listingsfilt">' . JText::translate('VBO_LISTINGS') . '</label>',
+ 'html' => '<span class="vbo-toolbar-multiselect-wrap">' . $vbo_app->renderElementsDropDown([
+ 'id' => 'listingsfilt',
+ 'elements' => 'listings',
+ 'placeholder' => JText::translate('VBO_LISTINGS'),
+ 'allow_clear' => 1,
+ 'attributes' => [
+ 'name' => 'listings[]',
+ 'multiple' => 'multiple',
+ ],
+ 'selected_values' => (array) JFactory::getApplication()->input->get('listings', [], 'array'),
+ ]) . '</span>',
+ 'type' => 'select',
+ 'multiple' => true,
+ 'name' => 'listings',
+ );
+ array_push($this->reportFilters, $filter_opt);
+
+ // get minimum check-in and maximum check-out for dates filters
+ $df = $this->getDateFormat();
+ $mincheckin = 0;
+ $maxcheckout = 0;
+ $q = "SELECT MIN(`checkin`) AS `mincheckin`, MAX(`checkout`) AS `maxcheckout` FROM `#__vikbooking_orders` WHERE `status`='confirmed' AND `closure`=0;";
+ $this->dbo->setQuery($q);
+ $data = $this->dbo->loadAssoc();
+ if (!empty($data['mincheckin']) && !empty($data['maxcheckout'])) {
+ $mincheckin = $data['mincheckin'];
+ $maxcheckout = $data['maxcheckout'];
+ }
+
+ // calendars setup
+ $pfromdate = $app->input->getString('fromdate', '');
+ $ptodate = $app->input->getString('todate', '');
+ $js = 'jQuery(function() {
+ jQuery(".vbo-report-datepicker:input").datepicker({
+ '.(!empty($mincheckin) ? 'minDate: "'.date($df, $mincheckin).'", ' : '').'
+ '.(!empty($maxcheckout) ? 'maxDate: "'.date($df, $maxcheckout).'", ' : '').'
+ '.(!empty($mincheckin) && !empty($maxcheckout) ? 'yearRange: "'.(date('Y', $mincheckin)).':'.date('Y', $maxcheckout).'", changeMonth: true, changeYear: true, ' : '').'
+ dateFormat: "'.$this->getDateFormat('jui').'",
+ onSelect: vboReportCheckDates
+ });
+ '.(!empty($pfromdate) ? 'jQuery(".vbo-report-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
+ '.(!empty($ptodate) ? 'jQuery(".vbo-report-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
+ });
+ function vboReportCheckDates(selectedDate, inst) {
+ if (selectedDate === null || inst === null) {
+ return;
+ }
+ var cur_from_date = jQuery(this).val();
+ if (jQuery(this).hasClass("vbo-report-datepicker-from") && cur_from_date.length) {
+ var nowstart = jQuery(this).datepicker("getDate");
+ var nowstartdate = new Date(nowstart.getTime());
+ jQuery(".vbo-report-datepicker-to").datepicker("option", {minDate: nowstartdate});
+ }
+ }';
+ $this->setScript($js);
+
+ return $this->reportFilters;
+ }
+
+ /**
+ * Loads the report data from the DB.
+ * Returns true in case of success, false otherwise.
+ * Sets the columns and rows for the report to be displayed.
+ *
+ * @return bool
+ */
+ public function getReportData()
+ {
+ if ($this->getError()) {
+ // export functions may set errors rather than exiting the process, and the View may continue the execution to attempt to render the report.
+ return false;
+ }
+
+ if ($this->rows) {
+ // method must have run already
+ return true;
+ }
+
+ $app = JFactory::getApplication();
+
+ // get the possibly injected report options
+ $options = $this->getReportOptions();
+
+ // injected options will replace request variables, if any
+ $opt_fromdate = $options->get('fromdate', '');
+ $opt_todate = $options->get('todate', '');
+ $opt_dt_type = $options->get('dates_type', '');
+
+ // input fields and other vars
+ $pfromdate = $opt_fromdate ?: $app->input->getString('fromdate', '');
+ $ptodate = $opt_todate ?: $app->input->getString('todate', '');
+ $pdatetype = $opt_dt_type ?: $app->input->getString('dates_type', '') ?: 'checkin';
+
+ $pkrsort = $app->input->getString('krsort', $this->defaultKeySort);
+ $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
+ $pkrorder = $app->input->getString('krorder', $this->defaultKeyOrder);
+ $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
+ $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
+ $plistings = ((array) $app->input->get('listings', [], 'array')) ?: ((array) $options->get('listings', []));
+ $plistings = array_filter(array_map('intval', $plistings));
+
+ $currency_symb = VikBooking::getCurrencySymb();
+ $df = $this->getDateFormat();
+ $datesep = VikBooking::getDateSeparator();
+ if (empty($ptodate)) {
+ $ptodate = $pfromdate;
+ }
+
+ // get date timestamps
+ $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
+ $to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
+ if (empty($pfromdate) || empty($from_ts) || empty($to_ts) || $from_ts > $to_ts) {
+ $this->setError(JText::translate('VBOREPORTSERRNODATES'));
+ return false;
+ }
+
+ // preferred date format
+ $df = $this->getDateFormat();
+ $datesep = VikBooking::getDateSeparator();
+
+ // determine stats calculation type
+ $calc_type = 'checkin';
+ if ($pdatetype === 'booking') {
+ $calc_type = 'booking_dates';
+ } elseif ($pdatetype === 'stay') {
+ $calc_type = 'stay_dates';
+ }
+
+ // access the finance helper object
+ $finance = VBOTaxonomyFinance::getInstance();
+
+ // force calculation options
+ $finance->setOptions([
+ 'booking_level' => true,
+ ]);
+
+ // get the financial stats for the requested dates
+ try {
+ $stats = $finance->getStats(date('Y-m-d', $from_ts), date('Y-m-d', $to_ts), $plistings, $calc_type);
+ } catch (Exception $e) {
+ $this->setError($e->getMessage());
+ return false;
+ }
+
+ // define the columns of the report
+ $this->cols = [
+ // booking ID
+ [
+ 'key' => 'bid',
+ 'sortable' => 1,
+ 'label' => JText::translate('VBDASHUPRESONE'),
+ ],
+ // date
+ [
+ 'key' => 'day',
+ 'sortable' => 1,
+ 'label' => JText::translate('VBOREPORTREVENUEDAY'),
+ ],
+ // guest
+ [
+ 'key' => 'guest',
+ 'label' => JText::translate('VBO_GUEST'),
+ ],
+ // channel
+ [
+ 'key' => 'channel',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'label' => JText::translate('VBOCHANNEL'),
+ ],
+ // rooms booked
+ [
+ 'key' => 'roomsnum',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBLIBTEN'),
+ ],
+ // nights booked
+ [
+ 'key' => 'nights',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBDAYS'),
+ ],
+ // taxes
+ [
+ 'key' => 'taxes',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBCALCRATESTAX'),
+ ],
+ // city taxes
+ [
+ 'key' => 'city_taxes',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBCALCRATESCITYTAX'),
+ ],
+ // fees
+ [
+ 'key' => 'fees',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBCALCRATESFEES'),
+ ],
+ // commissions
+ [
+ 'key' => 'cmms',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBTOTALCOMMISSIONS'),
+ ],
+ // damage deposit
+ [
+ 'key' => 'damage_dep',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBO_DAMAGE_DEPOSIT'),
+ ],
+ // options/extras
+ [
+ 'key' => 'extras',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBOREPORTOPTIONSEXTRAS'),
+ ],
+ // room revenue
+ [
+ 'key' => 'revenue',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBO_ROOM_REVENUE'),
+ ],
+ // grand total
+ [
+ 'key' => 'total',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBOREPORTSTOTALROW'),
+ ],
+ // amount paid
+ [
+ 'key' => 'totalpaid',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'sortable' => 1,
+ 'label' => JText::translate('VBPEDITBUSYTOTPAID'),
+ ],
+ ];
+
+ // options/extras counter
+ $extrasCounter = 0;
+
+ // grand total counter
+ $grandTotalCounter = 0;
+
+ // total paid counter
+ $totalPaidCounter = 0;
+
+ // iterate all bookings involved to build the report rows
+ foreach (($stats['bids'] ?? []) as $bookingId) {
+ try {
+ $registry = VBOBookingRegistry::getInstance(['id' => $bookingId]);
+ } catch (Exception $e) {
+ // ignore broken reservation and go next
+ continue;
+ }
+
+ // obtain booking-customer data
+ list($customer_nominative, $booking_avatar_src, $booking_avatar_alt) = $registry->getBookingCustomerData();
+
+ // extract booking details
+ $bookingChannel = $registry->getProperty('channel');
+ $bookingChannel = $registry->getProperty('idorderota') ? $bookingChannel : '';
+
+ // get booking revenue
+ $bookingRevenue = (float) $stats['_bid_stats'][$bookingId]['revenue'] ?? 0;
+
+ // get options/extras revenue
+ $extrasRevenue = max(0,
+ (float) $registry->getProperty('total', 0) -
+ $bookingRevenue -
+ ((float) $stats['_bid_stats'][$bookingId]['taxes'] ?? 0) -
+ ((float) $stats['_bid_stats'][$bookingId]['damage_deposits'] ?? 0) -
+ ((float) $stats['_bid_stats'][$bookingId]['cmms'] ?? 0)
+ );
+
+ // push report row for the current reservation
+ $this->rows[] = [
+ // booking ID
+ [
+ 'key' => 'bid',
+ 'callback' => function ($val) {
+ return '<a href="index.php?option=com_vikbooking&task=editorder&cid[]='.$val.'" target="_blank"><i class="'.VikBookingIcons::i('external-link').'"></i> '.$val.'</a>';
+ },
+ 'no_export_callback' => 1,
+ 'value' => $registry->getID(),
+ ],
+ // date
+ [
+ 'key' => 'day',
+ 'callback' => function ($val) use ($df, $datesep) {
+ return date(str_replace("/", $datesep, $df), $val);
+ },
+ 'value' => $registry->getProperty('ts', 0),
+ ],
+ // guest
+ [
+ 'key' => 'guest',
+ 'value' => $customer_nominative,
+ ],
+ // channel
+ [
+ 'key' => 'channel',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($booking_avatar_src) {
+ if ($val && $booking_avatar_src) {
+ $channelparts = explode('_', $val);
+ return ($channelparts[1] ?? '') ?: $val;
+ }
+ return JText::translate('VBORDFROMSITE');
+ },
+ 'value' => $bookingChannel,
+ ],
+ // rooms booked
+ [
+ 'key' => 'roomsnum',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'value' => $registry->getProperty('roomsnum', 1),
+ ],
+ // nights booked
+ [
+ 'key' => 'nights',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'value' => $registry->getProperty('days', 1),
+ ],
+ // taxes
+ [
+ 'key' => 'taxes',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['_bid_stats'][$bookingId]['tot_vat'] ?? 0,
+ ],
+ // city taxes
+ [
+ 'key' => 'city_taxes',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['_bid_stats'][$bookingId]['city_taxes'] ?? 0,
+ ],
+ // fees
+ [
+ 'key' => 'fees',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $registry->getProperty('tot_fees', 0),
+ ],
+ // commissions
+ [
+ 'key' => 'cmms',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['_bid_stats'][$bookingId]['cmms'] ?? 0,
+ ],
+ // damage deposit
+ [
+ 'key' => 'damage_dep',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['_bid_stats'][$bookingId]['damage_deposits'] ?? 0,
+ ],
+ // options/extras
+ [
+ 'key' => 'extras',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $extrasRevenue,
+ ],
+ // revenue
+ [
+ 'key' => 'revenue',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $bookingRevenue,
+ ],
+ // grand total
+ [
+ 'key' => 'total',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $registry->getProperty('total', 0),
+ ],
+ // amount paid
+ [
+ 'key' => 'totalpaid',
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $registry->getProperty('totpaid', 0),
+ ],
+ ];
+
+ // increase options/extras counter
+ $extrasCounter += $extrasRevenue;
+
+ // increase grand total counter
+ $grandTotalCounter += $registry->getProperty('total', 0);
+
+ // increase total paid counter
+ $totalPaidCounter += $registry->getProperty('totpaid', 0);
+ }
+
+ // sort rows
+ $this->sortRows($pkrsort, $pkrorder);
+
+ // push footer row with the calculated financial stats
+ $this->footerRow[] = [
+ // booking ID
+ [
+ 'attr' => [
+ 'class="vbo-report-total"',
+ ],
+ 'value' => '<h3>' . JText::translate('VBOREPORTSTOTALROW') . '</h3>',
+ ],
+ // date
+ [
+ 'value' => '',
+ ],
+ // guest
+ [
+ 'value' => '',
+ ],
+ // channel
+ [
+ 'value' => '',
+ ],
+ // rooms booked
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'value' => $stats['rooms_booked'] ?? 0,
+ ],
+ // nights booked
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'value' => $stats['nights_booked'] ?? 0,
+ ],
+ // taxes
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['tot_vat'] ?? 0,
+ ],
+ // city taxes
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['city_taxes'] ?? 0,
+ ],
+ // fees
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => 0,
+ ],
+ // commissions
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['ota_cmms'] ?? 0,
+ ],
+ // damage deposit
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['damage_deposits'] ?? 0,
+ ],
+ // options/extras
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $extrasCounter,
+ ],
+ // room revenue
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $stats['revenue'] ?? 0,
+ ],
+ // grand total
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $grandTotalCounter,
+ ],
+ // amount paid
+ [
+ 'attr' => [
+ 'class="center"',
+ ],
+ 'callback' => function ($val) use ($currency_symb) {
+ return VikBooking::formatCurrencyNumber(
+ VikBooking::numberFormat($val),
+ $currency_symb
+ );
+ },
+ 'value' => $totalPaidCounter,
+ ],
+ ];
+
+ // Debug
+ if ($this->debug) {
+ $this->setWarning('path to report file = '.urlencode(dirname(__FILE__)).'<br/>');
+ $this->setWarning('$stats:<pre>'.print_r($stats, true).'</pre><br/>');
+ }
+
+ return true;
+ }
+
+ /**
+ * Registers the name to give to the CSV file being exported.
+ *
+ * @return void
+ */
+ private function registerExportCSVFileName()
+ {
+ $app = JFactory::getApplication();
+
+ $pfromdate = $app->input->getString('fromdate', '');
+ $ptodate = $app->input->getString('todate', '');
+
+ $this->setExportCSVFileName($this->reportName . '-' . str_replace('/', '_', $pfromdate) . '-' . str_replace('/', '_', $ptodate) . '.csv');
+ }
+}
--- a/vikbooking/admin/helpers/report/rms_occupancy_pace.php
+++ b/vikbooking/admin/helpers/report/rms_occupancy_pace.php
@@ -842,6 +842,13 @@
'label' => JText::translate('VBO_VARIATION'),
'center' => 1,
],
+ // Room Revenue
+ [
+ 'key' => 'roomrev',
+ 'label' => JText::translate('VBO_ROOM_REVENUE'),
+ 'sortable' => $sortingAllowed,
+ 'center' => 1,
+ ],
// Gross Booking Revenue
[
'key' => 'grossrev',
@@ -1099,6 +1106,15 @@
],
'center' => 1,
],
+ // Room Revenue
+ [
+ 'key' => 'roomrev',
+ 'value' => array_sum((array) ($paceData['pace'][$targetIndex][$parseIndex]['roomrev'] ?? [])),
+ 'center' => 1,
+ 'callback' => function($metric) {
+ return VikBooking::formatCurrencyNumber($metric);
+ },
+ ],
// Gross Booking Revenue
[
'key' => 'grossrev',
--- a/vikbooking/admin/helpers/src/booking/registry.php
+++ b/vikbooking/admin/helpers/src/booking/registry.php
@@ -1247,4 +1247,56 @@
return array_values(array_unique($addresses));
}
+
+ /**
+ * Returns the booking routed URI, either in its regular or shorten forms.
+ *
+ * @return string
+ *
+ * @since 1.18.13 (J) - 1.8.13 (WP)
+ */
+ public function getBookingLink(bool $shorten = false)
+ {
+ // construct booking link
+ $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
+ $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
+ $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
+
+ $bookingLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
+
+ if (!$shorten) {
+ return $bookingLink;
+ }
+
+ // access the model for shortening URLs
+ $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
+
+ return $model->getShortUrl($bookingLink);
+ }
+
+ /**
+ * Returns the booking pre-check-in routed URI, either in its regular or shorten forms.
+ *
+ * @return string
+ *
+ * @since 1.18.13 (J) - 1.8.13 (WP)
+ */
+ public function getPrecheckinLink(bool $shorten = false)
+ {
+ // obtain booking link
+ $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
+ $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
+ $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
+
+ $precheckinLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=precheckin&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
+
+ if (!$shorten) {
+ return $precheckinLink;
+ }
+
+ // access the model for shortening URLs
+ $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
+
+ return $model->getShortUrl($precheckinLink);
+ }
}
--- a/vikbooking/admin/helpers/src/taxonomy/finance.php
+++ b/vikbooking/admin/helpers/src/taxonomy/finance.php
@@ -14,795 +14,871 @@
/**
* Taxonomy finance helper class.
*
- * @since 1.16.0 (J) - 1.6.0 (WP)
+ * @since 1.16.0 (J) - 1.6.0 (WP)
*/
class VBOTaxonomyFinance
{
- /**
- * The singleton instance of the class.
- *
- * @var VBOTaxonomyFinance
- */
- protected static $instance = null;
-
- /**
- * Class constructor is protected.
- *
- * @see getInstance()
- */
- protected function __construct()
- {
- // do nothing
- }
-
- /**
- * Returns the global object, either
- * a new instance or the existing instance
- * if the class was already instantiated.
- *
- * @return self A new instance of the class.
- */
- public static function getInstance()
- {
- if (is_null(static::$instance)) {
- static::$instance = new static();
- }
-
- return static::$instance;
- }
-
- /**
- * Returns the number of total units for all rooms, or for a specific room.
- * By default, the rooms unpublished are skipped, and all rooms are used.
- *
- * @param mixed $idroom int or array.
- * @param bool $published true or false.
- *
- * @return int
- */
- public function countRooms($idroom = 0, $published = true)
- {
- $dbo = JFactory::getDbo();
-
- $totrooms = 0;
- $clauses = [];
-
- if (is_int($idroom) && $idroom > 0) {
- $clauses[] = "`id`=" . (int)$idroom;
- } elseif (is_array($idroom) && count($idroom)) {
- $idroom = array_map('intval', $idroom);
- $clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
- }
-
- if ($published) {
- $clauses[] = "`avail`=1";
- }
-
- $q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`" . (count($clauses) ? " WHERE " . implode(' AND ', $clauses) : "");
- $dbo->setQuery($q);
- $totrooms = (int)$dbo->loadResult();
-
- return $totrooms;
- }
-
- /**
- * Counts the number of nights of difference between two timestamps.
- *
- * @param int $to_ts the target end date timestamp.
- * @param int $from_ts the starting date timestamp.
- *
- * @return int the nights of difference between from and to timestamps.
- */
- public function countNightsTo($to_ts, $from_ts = 0)
- {
- if (empty($from_ts)) {
- $from_ts = time();
- }
-
- $from_ymd = date('Y-m-d', $from_ts);
- $to_ymd = date('Y-m-d', $to_ts);
-
- if ($from_ymd == $to_ymd) {
- return 1;
- }
-
- $from_date = new DateTime($from_ymd);
- $to_date = new DateTime($to_ymd);
- $daysdiff = (int)$from_date->diff($to_date)->format('%a');
-
- if ($to_ts < $from_ts) {
- // we need a negative integer number in this case
- $daysdiff = $daysdiff - ($daysdiff * 2);
- }
-
- if ($from_ymd != $to_ymd && $daysdiff > 0) {
- // the to date is actually another night of stay
- $daysdiff += 1;
- }
-
- return $daysdiff;
- }
-
- /**
- * Counts the number of days of difference between two timestamps.
- *
- * @param int $from_ts the starting date timestamp.
- * @param int $to_ts the target end date timestamp.
- *
- * @return int the days of difference from the given dates.
- *
- * @since 1.16.10 (J) - 1.6.10 (WP)
- */
- public function countDaysDiff($from_ts, $to_ts)
- {
- if (empty($from_ts) || empty($to_ts)) {
- return 0;
- }
-
- $from_ymd = date('Y-m-d', $from_ts);
- $to_ymd = date('Y-m-d', $to_ts);
-
- if ($from_ymd == $to_ymd) {
- return 0;
- }
-
- $from_date = new DateTime($from_ymd);
- $to_date = new DateTime($to_ymd);
- $daysdiff = (int)$from_date->diff($to_date)->format('%a');
-
- if ($to_ts < $from_ts) {
- // we need a negative integer number in this case
- $daysdiff = $daysdiff - ($daysdiff * 2);
- }
-
- return $daysdiff;
- }
-
-
- /**
- * Helper method to format long currency values into short numbers.
- * I.e. 2.600.000 = 2.6M (empty decimals are always removed to keep the string short).
- *
- * @param int|float $num the amount to format.
- * @param int $decimals the precision to use.
- *
- * @return string the formatted amount string.
- */
- public function numberFormatShort($num, $decimals = 1)
- {
- // get global formatting values
- $formatvals = VikBooking::getNumberFormatData();
- $formatparts = explode(':', $formatvals);
-
- if ($num < 951) {
- // 0 - 950
- $short_amount = number_format($num, $decimals, $formatparts[1], $formatparts[2]);
- $type_amount = '';
- } elseif ($num < 900000) {
- // 1k - 950k
- $short_amount = number_format($num / 1000, $decimals, $formatparts[1], $formatparts[2]);
- $type_amount = 'k';
- } elseif ($num < 900000000) {
- // 0.9m - 950m
- $short_amount = number_format($num / 1000000, $decimals, $formatparts[1], $formatparts[2]);
- $type_amount = 'm';
- } elseif ($num < 900000000000) {
- // 0.9b - 950b
- $short_amount = number_format($num / 1000000000, $decimals, $formatparts[1], $formatparts[2]);
- $type_amount = 'b';
- } else {
- // >= 0.9t
- $short_amount = number_format($num / 1000000000000, $decimals, $formatparts[1], $formatparts[2]);
- $type_amount = 't';
- }
-
- // unpad zeroes from the right
- if ($decimals > 0) {
- while (substr($short_amount, -1, 1) == '0') {
- $short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
- }
-
- if (substr($short_amount, -1, 1) == $formatparts[1]) {
- // remove also the decimals separator if no more decimals
- $short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
- }
- }
-
- // return the formatted amount string
- return $short_amount . $type_amount;
- }
-
- /**
- * Calculate the absolute percent amount between the current and previous financial stat.
- * This method will use the following calculation: ((A1 - A2) / A2) * 100.
- *
- * @param float $stat the current (previously calculated) financial amount.
- * @param float $compare the previous period financial amount.
- * @param int $precision the precision for apply rounding.
- *
- * @return int|float the absolute percent amount calculated.
- */
- public function calcAbsPercent($current, $compare, $precision = 1)
- {
- if ($current == $compare) {
- return 0;
- }
-
- if ($current > $compare && $compare < 1) {
- return 100;
- }
-
- if ($current < $compare && $current < 1) {
- return 100;
- }
-
- return round(abs(($current - $compare) / $compare * 100), $precision);
- }
-
- /**
- * Obtain booking statistics from a range of dates and an
- * optional list of room types. The data obtained will be
- * based on the effective nights of stay within the range,
- * unless type "booking_dates" to obtain different data.
- *
- * @param string $from the Y-m-d (or website) date from.
- * @param string $to the Y-m-d (or website) date to.
- * @param array $rooms list of room IDs to filter.
- * @param string $type either "stay_dates" or "booking_dates".
- *
- * @return array associative list of information.
- *
- * @throws Exception
- */
- public function getStats($from, $to, array $rooms = [], $type = 'stay_dates')
- {
- $dbo = JFactory::getDbo();
-
- // access the availability helper
- $av_helper = VikBooking::getAvailabilityInstance();
-
- $from_ts = VikBooking::getDateTimestamp($from, 0, 0);
- $to_ts = VikBooking::getDateTimestamp($to, 23, 59, 59);
- if (empty($from) || empty($from_ts) || empty($to_ts) || $to_ts < $from_ts) {
- throw new Exception('Invalid dates provided', 500);
- }
-
- // total number of days (nights) in the range
- $total_range_nights = $this->countNightsTo($to_ts, $from_ts);
- if ($total_range_nights < 1) {
- throw new Exception('Invalid number of days provided', 500);
- }
-
- // total number of room units
- $total_room_units = $this->countRooms($rooms);
-
- if ($total_room_units < 1) {
- throw new Exception('Having no rooms published may lead to divisions by zero, hence errors.', 500);
- }
-
- // the associative array of statistics to collect and return
- $stats = [
- // booking ids involved
- 'bids' => [],
- // total number of room units counted for stats
- 'room_units' => $total_room_units,
- // total number of room units times the number of nights in the range
- 'tot_inventory' => ($total_room_units * $total_range_nights),
- // number of rooms booked
- 'rooms_booked' => 0,
- // number of bookings found
- 'tot_bookings' => 0,
- // total number of nights booked (proportionally adjusted according to affected dates)
- 'nights_booked' => 0,
- // percent value
- 'occupancy' => 0,
- // average length of stay
- 'avg_los' => 0,
- // points of sale revenue (revenue divided by each individual ota and ibe)
- 'pos_revenue' => [],
- // list of countries with ranking
- 'country_ranks' => [],
- // ibe revenue net
- 'ibe_revenue' => 0,
- // otas revenue net (no matter if commissions were applied)
- 'ota_revenue' => 0,
- // total refunded amounts (never deducted)
- 'tot_refunds' => 0,
- // average daily rate
- 'adr' => 0,
- // revenue per available room
- 'revpar' => 0,
- // average booking window
- 'abw' => 0,
- // amount of all taxes
- 'taxes' => 0,
- // amount of VAT
- 'tot_vat' => 0,
- // amount of city taxes
- 'city_taxes' => 0,
- // amount of damage deposits
- 'damage_deposits' => 0,
- // commissions (amount)
- 'cmms' => 0,
- // net revenue before tax (otas + ibe)
- 'revenue' => 0,
- // gross revenue after tax
- 'gross_revenue' => 0,
- // otas total before tax (same as ota_revenue, but only if ota commissions were applied)
- 'ota_tot_net' => 0,
- // otas commissions
- 'ota_cmms' => 0,
- // percent value for average ota commissions amount
- 'ota_avg_cmms' => 0,
- // commission savings amount
- 'cmm_savings' => 0,
- // total cancelled reservations
- 'tot_cancellations' => 0,
- // cancelled reservation IDs
- 'cancellation_ids' => [],
- // total amount of cancelled bookings
- 'cancellations_amt' => 0,
- ];
-
- // get all (real/completed) bookings in the given range of dates
- $q = $dbo->getQuery(true);
- $q->select($dbo->qn([
- 'o.id',
- 'o.ts',
- 'o.status',
- 'o.days',
- 'o.checkin',
- 'o.checkout',
- 'o.totpaid',
- 'o.coupon',
- 'o.roomsnum',
- 'o.total',
- 'o.idorderota',
- 'o.channel',
- 'o.country',
- 'o.tot_taxes',
- 'o.tot_city_taxes',
- 'o.tot_fees',
- 'o.tot_damage_dep',
- 'o.cmms',
- 'o.refund',
- 'or.idorder',
- 'or.idroom',
- 'or.optionals',
- 'or.cust_cost',
- 'or.cust_idiva',
- 'or.extracosts',
- 'or.room_cost',
- 'c.country_name',
- ]));
- $q->from($dbo->qn('#__vikbooking_orders', 'o'));
- $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
- $q->leftjoin($dbo->qn('#__vikbooking_countries', 'c') . ' ON ' . $dbo->qn('c.country_3_code') . ' = ' . $dbo->qn('o.country'));
- $q->where($dbo->qn('o.total') . ' > 0');
- $q->where($dbo->qn('o.closure') . ' = 0');
- // use the "andWhere" method after having set some "where" clauses
- $q->andWhere([
- $dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'),
- $dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'),
- ], 'OR');
- if ($type == 'stay_dates') {
- // regular calculation based on stay dates
- $q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
- $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
- } else {
- // calculation based on booked dates
- $q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
- $q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
- }
-
- /**
- * Do not filter the room booking records by room ID, but always join them, to improve accuracy with calculations.
- *
- * @since 1.18.0 (J) - 1.8.0 (WP)
- */
- if ($rooms) {
- // $q->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $rooms)) . ')');
- }
-
- $q->order($dbo->qn('o.checkin') . ' ASC');
- $q->order($dbo->qn('o.id') . ' ASC');
- $q->order($dbo->qn('or.id') . ' ASC');
-
- $dbo->setQuery($q);
- $records = $dbo->loadAssocList();
-
- if (!$records) {
- // no bookings found, do not proceed
- return $stats;
- }
-
- /**
- * Join the busy records afterwards to support accurate calculations for split stays or early departures/late arrivals.
- * This is also needed to support multi-room reservations without joining the busy records together with the room records.
- *
- * @since 1.18.0 (J) - 1.8.0 (WP)
- */
- $busy_joined = [];
- foreach ($records as &$booking) {
- if (strcasecmp($booking['status'], 'confirmed')) {
- // we only want to target confirmed bookings
- continue;
- }
-
- // build cache signature for busy records joined
- $booking_room_signature = $booking['id'] . '-' . $booking['idroom'];
-
- // build the db query
- $dbo->setQuery(
- $dbo->getQuery(true)
- ->select($dbo->qn('ob.idbusy'))
- ->select($dbo->qn('b.checkin', 'room_checkin'))
- ->select($dbo->qn('b.checkout', 'room_checkout'))
- ->from($dbo->qn('#__vikbooking_ordersbusy', 'ob'))
- ->leftjoin($dbo->qn('#__vikbooking_busy', 'b') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
- ->where($dbo->qn('ob.idorder') . ' = ' . (int) $booking['id'])
- ->where($dbo->qn('b.idroom') . ' = ' . (int) $booking['idroom'])
- ->order($dbo->qn('b.id') . ' ASC')
- );
-
- // either fetch the records or use the previously cached ones to support multi-room bookings with equal room IDs
- $busy_joined[$booking_room_signature] = $busy_joined[$booking_room_signature] ?? $dbo->loadAssocList();
-
- // get the busy record to process for the current booking-room by shifting the list
- $process_busy = array_shift($busy_joined[$booking_room_signature]);
-
- if (!$process_busy) {
- continue;
- }
-
- // inject busy details
- $booking['idbusy'] = $process_busy['idbusy'];
- $booking['room_checkin'] = $process_busy['room_checkin'];
- $booking['room_checkout'] = $process_busy['room_checkout'];
- }
-
- // unset last reference and cached values
- unset($booking, $busy_joined);
-
- /**
- * Immediately count cancellations and unset the records found.
- *
- * @since 1.16.10 (J) - 1.6.10 (WP)
- */
- foreach ($records as $k => $b) {
- if (!strcasecmp($b['status'], 'cancelled')) {
- if (!in_array($b['id'], $stats['cancellation_ids'])) {
- // add statistics for the cancelled booking only once
- $stats['tot_cancellations']++;
- $stats['cancellations_amt'] += (float) $b['total'];
- $stats['cancellation_ids'][] = $b['id'];
- }
-
- // get rid of this record to not alter the regular statistics
- unset($records[$k]);
- }
- }
-
- if ($stats['cancellation_ids']) {
- // reset key values
- $records = array_values($records);
- }
-
- // nest records with multiple rooms booked inside sub-arrays
- $bookings = [];
- foreach ($records as $b) {
- if (!isset($bookings[$b['id']])) {
- $bookings[$b['id']] = [];
- }
-
- // calculate the effective from and to stay timestamps for this room (by supporting split stays or early departures/late arrivals)
- $room_checkin = !empty($b['room_checkin']) && $b['room_checkin'] != $b['checkin'] ? $b['room_checkin'] : $b['checkin'];
- $room_checkout = !empty($b['room_checkout']) && $b['room_checkout'] != $b['checkout'] ? $b['room_checkout'] : $b['checkout'];
- $in_info = getdate($room_checkin);
- $out_info = getdate($room_checkout);
- $b['stay_from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
- $b['stay_to_ts'] = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
- $b['stay_nights'] = $room_checkin != $b['checkin'] || $room_checkout != $b['checkout'] ? $av_helper->countNightsOfStay($room_checkin, $room_checkout) : $b['days'];
- $b['stay_nights'] = $b['stay_nights'] < 1 ? 1 : $b['stay_nights'];
-
- // push room-booking
- $bookings[$b['id']][] = $b;
- }
-
- // free memory up
- unset($records);
-
- // counters and pools
- $los_counter = 0;
- $pos_pool = [];
- $pos_counter = [];
- $countries = [];
- $country_map = [];
-
- // sum of booking window
- $sum_booking_window = 0;
-
- // parse all bookings
- foreach ($bookings as $bid => $booking) {
- // push booking ID
- $stats['bids'][] = $bid;
-
- // increase tot bookings and los counter
- if (!$rooms || ($rooms && array_intersect(array_column($booking, 'idroom'), $rooms))) {
- // make sure to increase counters only if filtered listing(s) involved or if no filters set
- $stats['tot_bookings']++;
- $los_counter += $booking[0]['days'];
- }
-
- // count rooms booked within the reservation
- $booking_rooms = count($booking);
-
- // define the total booking amount for multi-room reservations
- $multi_room_total = 0;
-
- if ($rooms && $booking[0]['roomsnum'] > 1) {
- // when filters applied to multi-room bookings, count the effective number of rooms involved
- $booking_rooms = count(array_intersect(array_column($booking, 'idroom'), $rooms));
-
- // overwrite room total amount when filtering by listing(s)
- foreach ($booking as $room_booking) {
- if (!in_array($room_booking['idroom'], $rooms)) {
- continue;
- }
- if ($room_booking['cust_cost'] > 0) {
- $multi_room_total += $room_booking['cust_cost'];
- } elseif ($room_booking['room_cost'] > 0) {
- $multi_room_total += $room_booking['room_cost'];
- }
- }
- }
-
- // point of sale name
- $pos_name = null;
-
- // parse all rooms booked
- foreach ($booking as $room_booking) {
- if ($rooms && !in_array($room_booking['idroom'], $rooms)) {
- // room booked is excluded from filters
- continue;
- }
-
- // increase rooms booked
- $stats['rooms_booked']++;
-
- // number of nights affected
- $los_affected = $room_booking['stay_nights'];
-
- // use default total values
- $room_total = $multi_room_total ?: $room_booking['total'];
- $room_cmms = $room_booking['cmms'];
- $room_refund = $room_booking['refund'];
- $room_tot_taxes = $room_booking['tot_taxes'];
- $room_tot_city_taxes = $room_booking['tot_city_taxes'];
- $room_tot_damage_dep = $room_booking['tot_damage_dep'];
- $room_tot_fees = $room_booking['tot_fees'];
-
- // check if amounts must be calculated proportionally for the range of dates requested
- if ($type == 'stay_dates' && ($room_booking['stay_from_ts'] < $from_ts || $room_booking['stay_to_ts'] > $to_ts)) {
- // calculate number of nights of stay affected
- $los_affected = $this->countNightsAffected($from_ts, $to_ts, $room_booking['stay_from_ts'], $room_booking['stay_to_ts']);
-
- // adjust the amounts proportionally
- $room_total = $room_total * $los_affected / $room_booking['stay_nights'];
- $room_cmms = $room_cmms * $los_affected / $room_booking['stay_nights'];
- $room_refund = $room_refund * $los_affected / $room_booking['stay_nights'];
- $room_tot_taxes = $room_tot_taxes * $los_affected / $room_booking['stay_nights'];
- $room_tot_city_taxes = $room_tot_city_taxes * $los_affected / $room_booking['stay_nights'];
- $room_tot_damage_dep = $room_tot_damage_dep * $los_affected / $room_booking['stay_nights'];
- $room_tot_fees = $room_tot_fees * $los_affected / $room_booking['stay_nights'];
- }
-
- // apply average values per room booked (with filters or booked in total)
- $room_total /= $booking_rooms;
- $room_cmms /= $booking_rooms;
- $room_refund /= $booking_rooms;
- $room_tot_taxes /= $booking_rooms;
- $room_tot_city_taxes /= $booking_rooms;
- $room_tot_damage_dep /= $room_booking['roomsnum'];
- $room_tot_fees /= $booking_rooms;
-
- // calculate and sum average values per room booked
- $tot_net = $multi_room_total ?: ($room_total - (float) $room_tot_taxes - (float) $room_tot_city_taxes - (float) $room_tot_fees - (float) $room_tot_damage_dep - (float) $room_cmms);
- $tot_revenue = $multi_room_total ? ($tot_net / $booking_rooms) : $tot_net;
- $stats['revenue'] += $tot_revenue;
- $stats['gross_revenue'] += $room_total;
- $stats['nights_booked'] += $los_affected;
-
- // increase booking window for this room-booking
- $booking_window = $this->countDaysDiff($room_booking['ts'], $room_booking['stay_from_ts']);
- $sum_booking_window += $booking_window >= 0 ? $booking_window : 0;
-
- // increase country stats
- $country_code = !empty($room_booking['country']) ? $room_booking['country'] : 'unknown';
- if (!isset($countries[$country_code])) {
- $countries[$country_code] = 0;
- if (!empty($room_booking['country_name'])) {
- $country_map[$country_code] = $room_booking['country_name'];
- }
- }
- $countries[$country_code] += $tot_revenue;
-
- if (!empty($room_booking['idorderota']) && !empty($room_booking['channel'])) {
- $stats['ota_revenue'] += $tot_revenue;
- if ($room_cmms > 0) {
- $stats['ota_tot_net'] += $tot_revenue;
- $stats['ota_cmms'] += $room_cmms;
- }
- // set pos name
- $channel_parts = explode('_', $room_booking['channel']);
- $pos_name = trim($channel_parts[0]);
- } else {
- $stats['ibe_revenue'] += $tot_revenue;
- // set pos name
- $pos_name = 'website';
- }
-
- // set pos net revenue
- if (!isset($pos_pool[$pos_name])) {
- $pos_pool[$pos_name] = 0;
- }
- $pos_pool[$pos_name] += $tot_revenue;
-
- $stats['taxes'] += (float) $room_tot_taxes + (float) $room_tot_city_taxes + (float) $room_tot_fees;
- $stats['tot_vat'] += (float) $room_tot_taxes;
- $stats['city_taxes'] += (float) $room_tot_city_taxes;
- $stats['damage_deposits'] += (float) $room_tot_damage_dep;
- $stats['cmms'] += (float) $room_cmms;
- $stats['tot_refunds'] += $room_refund;
- }
-
- if ($pos_name) {
- // increase number of bookings for this pos
- if (!isset($po