PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / report / istat_ispat.php

istat_ispat.php in VikBooking Hotel Booking Engine & PMS 1.8.16, at admin/helpers/report/istat_ispat.php

2,315 lines 72.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - E4J srl
6 * @copyright Copyright (C) 2025 E4J srl. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * ISPAT PMS Report implementation.
15 * The report can generate and transmit data to Istituto Statistico Provincia Autonoma Trento (ISPAT).
16 *
17 * @see Documentazione WS C59 - V4.pdf
18 * @see Flusso dati in formato C-59 ISTAT.
19 * @see Trentino Digitale SpA.
20 * @see It is suggested to run the report every day for the data transmission with the presences of the day before.
21 *
22 * @since 1.18.2 (J) - 1.8.2 (WP)
23 */
24 class VikBookingReportIstatIspat extends VikBookingReport
25 {
26 /**
27 * Property 'defaultKeySort' is used by the View that renders the report.
28 *
29 * @var string
30 */
31 public $defaultKeySort = 'idbooking';
32
33 /**
34 * Property 'defaultKeyOrder' is used by the View that renders the report.
35 *
36 * @var string
37 */
38 public $defaultKeyOrder = 'ASC';
39
40 /**
41 * Property 'customExport' is used by the View to display custom export buttons.
42 *
43 * @var string
44 */
45 public $customExport = '';
46
47 /**
48 * List of foreign country names and codes.
49 *
50 * @var array
51 */
52 protected $foreignCountries = [];
53
54 /**
55 * List of foreign country 3-char values and codes.
56 *
57 * @var array
58 */
59 protected $foreignCountryCodes = [];
60
61 /**
62 * List of Italian province codes.
63 *
64 * @var array
65 */
66 protected $italianProvinces = [];
67
68 /**
69 * List of Italian Police countries.
70 *
71 * @var array
72 */
73 protected $italianPoliceCountries = [];
74
75 /**
76 * The path to the temporary directory used by this report.
77 *
78 * @var string
79 */
80 protected $report_tmp_path = '';
81
82 /**
83 * List of booking IDs affected by the export.
84 *
85 * @var array
86 */
87 protected $export_booking_ids = [];
88
89 /**
90 * List of exported check-in dates (range).
91 *
92 * @var array
93 */
94 protected $exported_checkin_dates = [];
95
96 /**
97 * The URL to the WSDL for the SOAP operations.
98 *
99 * @var string
100 */
101 protected $wsdl_url = 'https://dtu.provincia.tn.it/c59service?wsdl';
102
103 /**
104 * The URL to the WSDL for the SOAP operations (test mode).
105 *
106 * @var string
107 */
108 protected $wsdl_url_test = 'https://dtu-test.infotn.it/c59service?wsdl';
109
110 /**
111 * String representation of the PHP constant for the SOAP
112 * protocol version (either SOAP 1.1 or SOAP 1.2).
113 *
114 * @var string
115 */
116 protected $soap_version = 'SOAP_1_2';
117
118 /**
119 * Class constructor should define the name of the report and
120 * other vars. Call the parent constructor to define the DB object.
121 */
122 public function __construct()
123 {
124 $this->reportFile = basename(__FILE__, '.php');
125 $this->reportName = 'ISPAT (Trentino Digitale SpA)';
126 $this->reportFilters = [];
127
128 $this->cols = [];
129 $this->rows = [];
130 $this->footerRow = [];
131
132 $this->foreignCountries = $this->loadIstatForeignCountries();
133 $this->foreignCountryCodes = $this->loadIstatForeignCountries('3');
134 $this->italianProvinces = $this->loadCountryStates('ITA');
135 $this->italianPoliceCountries = $this->loadItalianPoliceCountries();
136
137 // set the temporary report directory path
138 $this->report_tmp_path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'ispat_tmp';
139
140 $this->registerExportFileName();
141
142 parent::__construct();
143 }
144
145 /**
146 * Returns the name of this report.
147 *
148 * @return string
149 */
150 public function getName()
151 {
152 return $this->reportName;
153 }
154
155 /**
156 * Returns the name of this file without .php.
157 *
158 * @return string
159 */
160 public function getFileName()
161 {
162 return $this->reportFile;
163 }
164
165 /**
166 * @inheritDoc
167 */
168 public function allowsProfileSettings()
169 {
170 // allow multiple report profile settings
171 return true;
172 }
173
174 /**
175 * @inheritDoc
176 */
177 public function getSettingFields()
178 {
179 return [
180 'title' => [
181 'type' => 'custom',
182 'label' => '',
183 'html' => '<p class="info">Configura le impostazioni per la trasmissione dei flussi turistici verso il sistema amministrativo del turismo ISPAT.<br/>Le informazioni su ID Struttura, Utente, Password sono le medesime credenziali della struttura ricettiva nel sistema amministrativo STU.</p>',
184 ],
185 'test_mode' => [
186 'type' => 'checkbox',
187 'label' => 'Ambiente di Test',
188 'help' => 'Abilitando questa impostazione, la comunicazione dei dati avverrà con l\'ambiente di test. Abilitarla soltanto se si è in possesso di un\'utenza di test.',
189 'default' => 0,
190 ],
191 'propertyid' => [
192 'type' => 'text',
193 'label' => 'ID Struttura',
194 'help' => 'ID della struttura ricettiva a cui i dati si riferiscono. Ogni file trasmesso può contenere i dati di una singola struttura ricettiva. Utilizza l\'azione &quot;Lista Strutture&quot; per leggere gli ID assegnati al tuo account.',
195 ],
196 'user' => [
197 'type' => 'text',
198 'label' => 'Utente',
199 'help' => 'Username dell\'utente, utilizzato per l\'autenticazione al sistema amministrativo STU.',
200 ],
201 'pwd' => [
202 'type' => 'password',
203 'label' => 'Password',
204 'help' => 'Password dell\'utente, utilizzata per l\'autenticazione al sistema amministrativo STU.',
205 ],
206 ];
207 }
208
209 /**
210 * @inheritDoc
211 */
212 public function getScopedActions($scope = null, $visible = true)
213 {
214 // count the total number of rooms
215 $tot_room_units = $this->countRooms();
216
217 // list of custom actions for this report
218 $actions = [
219 [
220 'id' => 'listaStrutture',
221 'name' => 'Lista Strutture',
222 'help' => 'Lettura dell\'elenco strutture ricettive e relativi ID associati all\'utente configurato dalle impostazioni.',
223 'icon' => VikBookingIcons::i('hotel'),
224 'scopes' => [
225 'web',
226 ],
227 ],
228 [
229 'id' => 'ultimoC59',
230 'name' => 'Ultima trasmissione C59',
231 'help' => 'La funzione ritorna l\'ultima trasmissione dati eseguita secondo il formato C59.',
232 'icon' => VikBookingIcons::i('calendar-check'),
233 'scopes' => [
234 'web',
235 ],
236 ],
237 [
238 'id' => 'uploadC59Full',
239 'name' => 'Carica presenze turistiche',
240 'help' => 'Carica il file sul web-service per le presenze turistiche.',
241 'icon' => VikBookingIcons::i('cloud-upload-alt'),
242 // flag to indicate that it requires the report data (lines)
243 'export_data' => true,
244 'scopes' => [
245 'web',
246 'cron',
247 ],
248 ],
249 ];
250
251 // filter actions by scope
252 if ($scope && (!strcasecmp($scope, 'cron') || !strcasecmp($scope, 'web'))) {
253 $actions = array_filter($actions, function($action) use ($scope) {
254 if (!($action['scopes'] ?? [])) {
255 return true;
256 }
257
258 return in_array(strtolower($scope), $action['scopes']);
259 });
260 }
261
262 // filter by visibility
263 if ($visible) {
264 $actions = array_filter($actions, function($action) {
265 return !($action['hidden'] ?? false);
266 });
267 }
268
269 return array_values($actions);
270 }
271
272 /**
273 * Returns the filters of this report.
274 *
275 * @return array
276 */
277 public function getFilters()
278 {
279 if ($this->reportFilters) {
280 // do not run this method twice, as it could load JS and CSS files.
281 return $this->reportFilters;
282 }
283
284 $app = JFactory::getApplication();
285
286 // get VBO Application Object
287 $vbo_app = VikBooking::getVboApplication();
288
289 // get the possibly injected report options
290 $options = $this->getReportOptions();
291
292 // load the jQuery UI Datepicker
293 $this->loadDatePicker();
294
295 // custom export button
296 $this->customExport = '<a href="JavaScript: void(0);" onclick="vboDownloadRecordIspat();" class="vbcsvexport"><i class="'.VikBookingIcons::i('download').'"></i> <span>Download File</span></a>';
297
298 // load report settings
299 $settings = $this->loadSettings();
300
301 // build the hidden values for the selection of Comuni & Province and much more.
302 $hidden_vals = '<div id="vbo-report-ispat-hidden" style="display: none;">';
303
304 // build params container HTML structure
305 $hidden_vals .= '<div class="vbo-admin-container vbo-admin-container-full vbo-admin-container-compact">';
306 $hidden_vals .= ' <div class="vbo-params-wrap">';
307 $hidden_vals .= ' <div class="vbo-params-container">';
308 $hidden_vals .= ' <div class="vbo-params-block vbo-params-block-noborder">';
309
310 // provenienza
311 $hidden_vals .= ' <div id="vbo-report-ispat-provenience" class="vbo-report-ispat-selcont vbo-param-container" style="display: none;">';
312 $hidden_vals .= ' <div class="vbo-param-label">Provincia Italiana o Stato Estero</div>';
313 $hidden_vals .= ' <div class="vbo-param-setting">';
314 $hidden_vals .= ' <select id="choose-provenience" onchange="vboReportChosenProvenience(this);"><option value=""></option>';
315 $hidden_vals .= ' <optgroup label="Province Italiane">';
316 foreach ($this->italianProvinces as $code => $province) {
317 $hidden_vals .= ' <option value="' . $code . '">' . $province . '</option>'."\n";
318 }
319 $hidden_vals .= ' </optgroup>';
320 $hidden_vals .= ' <optgroup label="Stati Esteri">';
321 foreach ($this->foreignCountries as $code => $fcountry) {
322 $hidden_vals .= ' <option value="' . $code . '">' . $fcountry . '</option>'."\n";
323 }
324 $hidden_vals .= ' </optgroup>';
325 $hidden_vals .= ' </select>';
326 $hidden_vals .= ' </div>';
327 $hidden_vals .= ' </div>';
328
329 // close params container HTML structure
330 $hidden_vals .= ' </div>';
331 $hidden_vals .= ' </div>';
332 $hidden_vals .= ' </div>';
333 $hidden_vals .= '</div>';
334
335 // close hidden values container
336 $hidden_vals .= '</div>';
337
338 // From Date Filter (with hidden values for the dropdown menus of Comuni, Province, Stati etc..)
339 $filter_opt = array(
340 'label' => '<label for="fromdate">Data movimento</label>',
341 'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-report-datepicker vbo-report-datepicker-from" />' . $hidden_vals,
342 'type' => 'calendar',
343 'name' => 'fromdate'
344 );
345 array_push($this->reportFilters, $filter_opt);
346
347 // Listings Filter
348 $filter_opt = array(
349 'label' => '<label for="listingsfilt">' . JText::translate('VBO_LISTINGS') . '</label>',
350 'html' => '<span class="vbo-toolbar-multiselect-wrap">' . $vbo_app->renderElementsDropDown([
351 'id' => 'listingsfilt',
352 'elements' => 'listings',
353 'placeholder' => JText::translate('VBO_LISTINGS'),
354 'allow_clear' => 1,
355 'attributes' => [
356 'name' => 'listings[]',
357 'multiple' => 'multiple',
358 ],
359 'selected_values' => (array) ($app->input->get('listings', [], 'array') ?: $options->get('listings', [])),
360 ]) . '</span>',
361 'type' => 'select',
362 'multiple' => true,
363 'name' => 'listings',
364 );
365 array_push($this->reportFilters, $filter_opt);
366
367 // append button to save the data when creating manual values
368 $filter_opt = array(
369 'label' => '<label class="vbo-report-ispat-manualsave" style="display: none;">Dati inseriti</label>',
370 'html' => '<button type="button" class="btn vbo-config-btn vbo-report-ispat-manualsave" style="display: none;" onclick="vboIspatSaveData();"><i class="' . VikBookingIcons::i('save') . '"></i> ' . JText::translate('VBSAVE') . '</button>',
371 );
372 array_push($this->reportFilters, $filter_opt);
373
374 //jQuery code for the datepicker calendars, select2 and triggers for the dropdown menus
375 $pfromdate = VikRequest::getString('fromdate', '', 'request');
376
377 $js = 'var reportActiveCell = null, reportObj = {};
378 var vbo_ispat_ajax_uri = "' . VikBooking::ajaxUrl('index.php?option=com_vikbooking&task=invoke_report&report=' . $this->reportFile) . '";
379 var vbo_ispat_save_icn = "' . VikBookingIcons::i('save') . '";
380 var vbo_ispat_saving_icn = "' . VikBookingIcons::i('circle-notch', 'fa-spin fa-fw') . '";
381 var vbo_ispat_saved_icn = "' . VikBookingIcons::i('check-circle') . '";
382 jQuery(function() {
383 //prepare main filters
384 jQuery(".vbo-report-datepicker:input").datepicker({
385 // data massima di generazione movimenti = oggi
386 maxDate: "0d",
387 dateFormat: "'.$this->getDateFormat('jui').'",
388 });
389 '.(!empty($pfromdate) ? 'jQuery(".vbo-report-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
390 //prepare filler helpers
391 jQuery("#vbo-report-ispat-hidden").children().detach().appendTo(".vbo-info-overlay-report");
392 jQuery("#choose-provenience").select2({placeholder: "- Provincia o Stato Estero -", width: "200px"});
393 //click events
394 jQuery(".vbo-report-load-provenience").click(function() {
395 reportActiveCell = this;
396 jQuery(".vbo-report-ispat-selcont").hide();
397 jQuery("#vbo-report-ispat-provenience").show();
398 vboShowOverlay({
399 title: "Compila informazioni",
400 extra_class: "vbo-modal-rounded vbo-modal-dialog vbo-modal-nofooter",
401 });
402 });
403 });
404 function vboReportChosenProvenience(naz) {
405 var c_code = naz.value;
406 var c_val = naz.options[naz.selectedIndex].text;
407 if (reportActiveCell !== null) {
408 var nowindex = jQuery(".vbo-reports-output table tbody tr").index(jQuery(reportActiveCell).closest("tr"));
409 if (isNaN(nowindex) || parseInt(nowindex) < 0) {
410 alert("Error, cannot find element to update.");
411 } else {
412 var rep_act_cell = jQuery(reportActiveCell);
413 rep_act_cell.addClass("vbo-report-load-elem-filled").find("span").text(c_val);
414 var rep_guest_bid = rep_act_cell.closest("tr").find("a[data-bid]").attr("data-bid");
415 if (!reportObj.hasOwnProperty(nowindex)) {
416 reportObj[nowindex] = {
417 bid: rep_guest_bid,
418 bid_index: jQuery(".vbo-reports-output table tbody tr").index(jQuery("a[data-bid=\"" + rep_guest_bid + "\"]").first().closest("tr"))
419 };
420 }
421 if (jQuery(reportActiveCell).hasClass("vbo-report-load-provenience")) {
422 reportObj[nowindex]["state"] = c_code;
423 }
424 }
425 }
426 reportActiveCell = null;
427 vboHideOverlay();
428 jQuery("#choose-provenience").val("").select2("data", null, false);
429 jQuery(".vbo-report-ispat-manualsave").show();
430 }
431 //download function
432 function vboDownloadRecordIspat(type) {
433 if (!confirm("Sei sicuro di aver compilato tutte le informazioni necessarie?")) {
434 return false;
435 }
436
437 let use_blank = true;
438 if (typeof type === "undefined") {
439 type = 1;
440 } else {
441 use_blank = false;
442 }
443
444 if (use_blank) {
445 document.adminForm.target = "_blank";
446 document.adminForm.action += "&tmpl=component";
447 }
448
449 vboSetFilters({exportreport: type, filler: JSON.stringify(reportObj)}, true);
450
451 setTimeout(function() {
452 document.adminForm.target = "";
453 document.adminForm.action = document.adminForm.action.replace("&tmpl=component", "");
454 vboSetFilters({exportreport: "0", filler: ""}, false);
455 }, 1000);
456 }
457 // save data function
458 function vboIspatSaveData() {
459 jQuery("button.vbo-report-ispat-manualsave").find("i").attr("class", vbo_ispat_saving_icn);
460 VBOCore.doAjax(
461 vbo_ispat_ajax_uri,
462 {
463 call: "updatePaxData",
464 params: reportObj,
465 tmpl: "component"
466 },
467 function(response) {
468 if (!response || !response[0]) {
469 alert("An error occurred.");
470 return false;
471 }
472 jQuery("button.vbo-report-ispat-manualsave").addClass("btn-success").find("i").attr("class", vbo_ispat_saved_icn);
473 },
474 function(error) {
475 alert(error.responseText);
476 jQuery("button.vbo-report-ispat-manualsave").removeClass("btn-success").find("i").attr("class", vbo_ispat_save_icn);
477 }
478 );
479 }
480 ';
481 $this->setScript($js);
482
483 return $this->reportFilters;
484 }
485
486 /**
487 * Loads the report data from the DB.
488 * Returns true in case of success, false otherwise.
489 * Sets the columns and rows for the report to be displayed.
490 *
491 * @return boolean
492 */
493 public function getReportData()
494 {
495 if ($this->getError()) {
496 // export functions may set errors rather than exiting the process, and
497 // the View may continue the execution to attempt to render the report.
498 return false;
499 }
500
501 if ($this->rows) {
502 // method must have run already
503 return true;
504 }
505
506 $dbo = JFactory::getDbo();
507
508 // get the possibly injected report options
509 $options = $this->getReportOptions();
510
511 // injected options will replace request variables, if any
512 $opt_fromdate = $options->get('fromdate', '');
513
514 // input fields and other vars
515 $pfromdate = $opt_fromdate ?: VikRequest::getString('fromdate', '', 'request');
516
517 $pkrsort = VikRequest::getString('krsort', $this->defaultKeySort, 'request');
518 $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
519 $pkrorder = VikRequest::getString('krorder', $this->defaultKeyOrder, 'request');
520 $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
521 $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
522 $plistings = ((array) VikRequest::getVar('listings', array())) ?: ((array) $options->get('listings', []));
523 $plistings = array_filter(array_map('intval', $plistings));
524
525 $currency_symb = VikBooking::getCurrencySymb();
526 $df = $this->getDateFormat();
527 $datesep = VikBooking::getDateSeparator();
528
529 // get translator
530 $vbo_tn = VikBooking::getTranslator();
531
532 // load all countries
533 $all_countries = VikBooking::getCountriesArray();
534
535 // load all rooms
536 $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms();
537
538 // get date timestamps
539 $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
540 $to_ts = VikBooking::getDateTimestamp($pfromdate, 23, 59, 59);
541 if (empty($pfromdate) || empty($from_ts) || empty($to_ts)) {
542 $this->setError(JText::translate('VBOREPORTSERRNODATES'));
543 return false;
544 }
545
546 // set the dates being exported
547 $this->exported_checkin_dates = [
548 date('Y-m-d', $from_ts),
549 date('Y-m-d', $to_ts),
550 ];
551
552 // query to obtain the records (all reservations or stays within the dates filter)
553 $q = $dbo->getQuery(true)
554 ->select([
555 $dbo->qn('o.id'),
556 $dbo->qn('o.custdata'),
557 $dbo->qn('o.ts'),
558 $dbo->qn('o.days'),
559 $dbo->qn('o.status'),
560 $dbo->qn('o.checkin'),
561 $dbo->qn('o.checkout'),
562 $dbo->qn('o.totpaid'),
563 $dbo->qn('o.roomsnum'),
564 $dbo->qn('o.total'),
565 $dbo->qn('o.idorderota'),
566 $dbo->qn('o.channel'),
567 $dbo->qn('o.country'),
568 $dbo->qn('or.idorder'),
569 $dbo->qn('or.idroom'),
570 $dbo->qn('or.adults'),
571 $dbo->qn('or.children'),
572 $dbo->qn('or.t_first_name'),
573 $dbo->qn('or.t_last_name'),
574 $dbo->qn('or.cust_cost'),
575 $dbo->qn('or.cust_idiva'),
576 $dbo->qn('or.extracosts'),
577 $dbo->qn('or.room_cost'),
578 $dbo->qn('co.idcustomer'),
579 $dbo->qn('co.pax_data'),
580 $dbo->qn('c.first_name'),
581 $dbo->qn('c.last_name'),
582 $dbo->qn('c.country', 'customer_country'),
583 $dbo->qn('c.address'),
584 $dbo->qn('c.city'),
585 $dbo->qn('c.state'),
586 $dbo->qn('c.doctype'),
587 $dbo->qn('c.docnum'),
588 $dbo->qn('c.gender'),
589 $dbo->qn('c.bdate'),
590 $dbo->qn('c.pbirth'),
591 ])
592 ->from($dbo->qn('#__vikbooking_orders', 'o'))
593 ->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'))
594 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
595 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
596 ->where($dbo->qn('o.closure') . ' = 0')
597 ->where($dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'))
598 ->order($dbo->qn('o.checkin') . ' ASC')
599 ->order($dbo->qn('o.id') . ' ASC')
600 ->order($dbo->qn('or.id') . ' ASC');
601
602 if ($plistings) {
603 $q->where($this->dbo->qn('or.idroom') . ' IN (' . implode(', ', $plistings) . ')');
604 }
605
606 // fetch all bookings with a stay date between the interval of dates requested
607 $q->andWhere([
608 '(' . $dbo->qn('o.checkin') . ' BETWEEN ' . $from_ts . ' AND ' . $to_ts . ')',
609 '(' . $dbo->qn('o.checkout') . ' BETWEEN ' . $from_ts . ' AND ' . $to_ts . ')',
610 '(' . $dbo->qn('o.checkin') . ' < ' . $from_ts . ' AND ' . $dbo->qn('o.checkout') . ' > ' . $to_ts . ')',
611 ], 'OR');
612
613 $dbo->setQuery($q);
614 $records = $dbo->loadAssocList();
615
616 if (!$records) {
617 $this->setError(JText::translate('VBOREPORTSERRNORESERV'));
618 $this->setError('Nessuna prenotazione trovata nelle date selezionate.');
619 return false;
620 }
621
622 // nest records with multiple rooms booked inside sub-array
623 $bookings = [];
624 foreach ($records as $v) {
625 if (!isset($bookings[$v['id']])) {
626 $bookings[$v['id']] = [];
627 }
628 array_push($bookings[$v['id']], $v);
629 }
630
631 // free some memory up
632 unset($records);
633
634 // define the columns of the report
635 $this->cols = [
636 // id booking
637 [
638 'key' => 'idbooking',
639 'label' => 'ID'
640 ],
641 // first name
642 [
643 'key' => 'first_name',
644 'label' => JText::translate('VBTRAVELERNAME'),
645 ],
646 // last name
647 [
648 'key' => 'last_name',
649 'label' => JText::translate('VBTRAVELERLNAME'),
650 ],
651 // camera
652 [
653 'key' => 'idroom',
654 'attr' => [
655 'class="center"',
656 ],
657 'label' => JText::translate('VBEDITORDERTHREE'),
658 ],
659 // check-in
660 [
661 'key' => 'checkin',
662 'attr' => [
663 'class="center"',
664 ],
665 'label' => JText::translate('VBPICKUPAT'),
666 ],
667 // check-out
668 [
669 'key' => 'checkout',
670 'attr' => [
671 'class="center"',
672 ],
673 'label' => JText::translate('VBRELEASEAT'),
674 ],
675 // is check-in
676 [
677 'key' => 'is_checkin',
678 'label' => 'is check-in',
679 // hide this field in the View
680 'ignore_view' => 1,
681 ],
682 // is check-out
683 [
684 'key' => 'is_checkout',
685 'label' => 'is check-out',
686 // hide this field in the View
687 'ignore_view' => 1,
688 ],
689 // provincia (ITA)
690 [
691 'key' => 'provincia',
692 'label' => 'Provincia',
693 // hide this field in the View
694 'ignore_view' => 1,
695 ],
696 // country
697 [
698 'key' => 'country',
699 'label' => 'Nazione',
700 // hide this field in the View
701 'ignore_view' => 1,
702 ],
703 // country full name
704 [
705 'key' => 'country_full_name',
706 'label' => 'Nazione completa',
707 // hide this field in the View
708 'ignore_view' => 1,
709 ],
710 // provenienza
711 [
712 'key' => 'provenience',
713 'attr' => [
714 'class="center"',
715 ],
716 'label' => 'Provenienza',
717 ],
718 ];
719
720 // line number (to facilitate identifying a specific guest in case of errors with the file submission)
721 $line_number = 0;
722
723 // loop over the bookings to build the rows of the report
724 $from_info = getdate($from_ts);
725 $today_ymd = date('Y-m-d', $from_ts);
726 foreach ($bookings as $gbook) {
727 // count the total number of guests and adults for all rooms of this booking
728 $tot_booking_guests = 0;
729 $tot_booking_adults = 0;
730 $room_guests = [];
731 foreach ($gbook as $rbook) {
732 $tot_booking_guests += ($rbook['adults'] + $rbook['children']);
733 $tot_booking_adults += $rbook['adults'];
734 $room_guests[] = ($rbook['adults'] + $rbook['children']);
735 }
736
737 // make sure to decode the current pax data
738 if (!empty($gbook[0]['pax_data'])) {
739 $gbook[0]['pax_data'] = (array) json_decode($gbook[0]['pax_data'], true);
740 }
741
742 // whether today is the check-in day, the check-out day (otherwise a stay)
743 $is_checkin = $today_ymd == date('Y-m-d', $gbook[0]['checkin']);
744 $is_checkout = $today_ymd == date('Y-m-d', $gbook[0]['checkout']);
745
746 // push a copy of the booking for each guest
747 $guests_rows = [];
748 for ($i = 1; $i <= $tot_booking_guests; $i++) {
749 array_push($guests_rows, $gbook[0]);
750 }
751
752 // create one row for each guest
753 $guest_ind = 1;
754 foreach ($guests_rows as $ind => $guests) {
755 // prepare row record for this room-guest
756 $insert_row = [];
757
758 // find the actual guest-room-index
759 $guest_room_ind = $this->calcGuestRoomIndex($room_guests, $guest_ind);
760
761 // id booking
762 array_push($insert_row, [
763 'key' => 'idbooking',
764 'callback' => function($val) {
765 // make sure to keep the data-bid attribute as it's used by JS to identify the booking ID
766 return '<a data-bid="' . $val . '" href="index.php?option=com_vikbooking&task=editorder&cid[]=' . $val . '" target="_blank"><i class="' . VikBookingIcons::i('external-link') . '"></i> ' . $val . '</a>';
767 },
768 'no_export_callback' => 1,
769 'value' => $guests['id'],
770 ]);
771
772 // nome
773 $nome = !empty($guests['t_first_name']) ? $guests['t_first_name'] : $guests['first_name'];
774 $pax_nome = $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'first_name');
775 $nome = !empty($pax_nome) ? $pax_nome : $nome;
776 array_push($insert_row, [
777 'key' => 'first_name',
778 'value' => $nome,
779 ]);
780
781 // cognome
782 $cognome = !empty($guests['t_last_name']) ? $guests['t_last_name'] : $guests['last_name'];
783 $pax_cognome = $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'last_name');
784 $cognome = !empty($pax_cognome) ? $pax_cognome : $cognome;
785 array_push($insert_row, [
786 'key' => 'last_name',
787 'value' => $cognome,
788 ]);
789
790 // camera
791 array_push($insert_row, [
792 'key' => 'idroom',
793 'attr' => [
794 'class="center"',
795 ],
796 'callback' => function($val) use ($all_rooms) {
797 return $all_rooms[$val]['name'] ?? $val;
798 },
799 'no_export_callback' => 1,
800 'value' => $guests['idroom'],
801 ]);
802
803 // checkin
804 array_push($insert_row, [
805 'key' => 'checkin',
806 'attr' => [
807 'class="center"',
808 ],
809 'callback' => function($val) {
810 return date('Y-m-d', $val);
811 },
812 'value' => $guests['checkin'],
813 ]);
814
815 // checkout
816 array_push($insert_row, [
817 'key' => 'checkout',
818 'attr' => [
819 'class="center"',
820 ],
821 'callback' => function($val) {
822 return date('Y-m-d', $val);
823 },
824 'value' => $guests['checkout'],
825 ]);
826
827 // is checkin
828 array_push($insert_row, [
829 'key' => 'is_checkin',
830 'value' => $is_checkin,
831 'ignore_view' => 1,
832 ]);
833
834 // is checkout
835 array_push($insert_row, [
836 'key' => 'is_checkout',
837 'value' => $is_checkout,
838 'ignore_view' => 1,
839 ]);
840
841 // provenience (Provenienza)
842 $provenience = $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'state') ?: $guests['state'] ?: '';
843 $provenience = $provenience ?: $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'province_s');
844 $provenience = $provenience ?: $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'province_b');
845 $provenience = (string) ($provenience == 'ES' ? '' : $provenience);
846
847 // access any possible country value
848 $pax_country_list = [
849 $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'country'),
850 $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'country_c'),
851 $this->getGuestPaxDataValue($guests['pax_data'], $room_guests, $guest_ind, 'country_b'),
852 $guests['customer_country'],
853 $guests['country'],
854 ];
855
856 // filter empty countries
857 $pax_country_list = array_values(array_filter($pax_country_list));
858
859 // map italian police country codes to 3-char codes
860 $pax_country_list = array_map(function($country_val) {
861 if ($this->italianPoliceCountries[$country_val]['three_code'] ?? null) {
862 return $this->italianPoliceCountries[$country_val]['three_code'];
863 }
864 return $country_val;
865 }, $pax_country_list);
866
867 // provincia (ITA)
868 array_push($insert_row, [
869 'key' => 'provincia',
870 'value' => (!strcasecmp(($pax_country_list[0] ?? ''), 'ITA') && $provenience && isset($this->italianProvinces[$provenience]) ? $provenience : null),
871 'ignore_view' => 1,
872 ]);
873
874 // country
875 array_push($insert_row, [
876 'key' => 'country',
877 'value' => strtoupper($pax_country_list[0] ?? ''),
878 'ignore_view' => 1,
879 ]);
880
881 if (!$provenience && $pax_country_list && stripos($pax_country_list[0], 'ITA') === false) {
882 // for non-italian guests we can try to guess the ISTAT foreign country code
883 $foreign_matches = [];
884
885 // determine the value to match
886 $match_name = $pax_country_list[0];
887 $country_data = VBOStateHelper::getCountryData((int) VBOStateHelper::getCountryId($pax_country_list[0]));
888 if ($country_data) {
889 // try to forse the record conversion into Italian for a more accurate matching of the foreign country
890 $country_data = $vbo_tn->translateRecord($country_data, '#__vikbooking_countries', 'it-IT');
891
892 // assign the country name to match
893 $match_name = $country_data['country_name'];
894 }
895
896 // calculate the similarity for each country ISTAT code
897 foreach ($this->foreignCountries as $fc_code => $fc_name) {
898 // calculate similarity
899 similar_text(strtolower($fc_name), strtolower($match_name), $similarity);
900
901 // assign similarity to country ISTAT code
902 $foreign_matches[$fc_code] = $similarity;
903 }
904
905 // sort similarity in descending order
906 arsort($foreign_matches);
907
908 // assign the first match found, the most similar one
909 foreach ($foreign_matches as $fc_code => $similarity_score) {
910 // we trust the first match to be valid
911 $provenience = $fc_code;
912 break;
913 }
914
915 // at last, check if we can match an exact 3-char known country code
916 if (strlen($pax_country_list[0]) === 3 && $matched_code = array_search($pax_country_list[0], $this->foreignCountryCodes)) {
917 // do not use similarity, but rather the exact country code
918 $provenience = $matched_code;
919 }
920 }
921
922 // country full name
923 array_push($insert_row, [
924 'key' => 'country_full_name',
925 'callback_export' => function($val) {
926 return $val ? strtoupper($val) : $val;
927 },
928 'value' => (($pax_country_list[0] ?? '') && $provenience && strcasecmp($pax_country_list[0], 'ITA') ? ($this->foreignCountries[$provenience] ?? $pax_country_list[0]) : null),
929 'ignore_view' => 1,
930 ]);
931
932 $provenience_elem_class = '';
933 if (empty($provenience)) {
934 // optional selection style
935 $provenience_elem_class = ' vbo-report-load-provenience vbo-report-load-field vbo-report-load-field-optional';
936 } else {
937 // rectify selection style
938 $provenience_elem_class = ' vbo-report-load-provenience vbo-report-load-field vbo-report-load-elem-filled';
939 }
940
941 array_push($insert_row, [
942 'key' => 'provenience',
943 'attr' => [
944 'class="center' . $provenience_elem_class . '"',
945 ],
946 'callback' => function($val) {
947 return $this->italianProvinces[$val] ?? $this->foreignCountries[$val] ?? $val ?: '?';
948 },
949 'no_export_callback' => 1,
950 'value' => $provenience,
951 ]);
952
953 // push fields in the rows array as a new row
954 array_push($this->rows, $insert_row);
955
956 // increment guest index
957 $guest_ind++;
958
959 // increment line number
960 $line_number++;
961 }
962 }
963
964 // do not sort the rows for this report because the lines of the guests of the same booking must be consecutive
965 // $this->sortRows($pkrsort, $pkrorder);
966
967 // the footer row will just print the amount of records to export
968 array_push($this->footerRow, [
969 [
970 'attr' => [
971 'class="vbo-report-total"',
972 ],
973 'value' => '<h3>' . JText::translate('VBOREPORTSTOTALROW') . '</h3>',
974 ],
975 [
976 'attr' => [
977 'colspan="' . (count($this->cols) - 1) . '"',
978 ],
979 'value' => count($this->rows),
980 ],
981 ]);
982
983 return true;
984 }
985
986 /**
987 * Builds the report data for export or transmission.
988 *
989 * @return array Empty array in case of errors, or list of rows.
990 */
991 protected function buildRecordData()
992 {
993 if (!$this->getReportData()) {
994 return [];
995 }
996
997 $app = JFactory::getApplication();
998
999 // get the possibly injected report options
1000 $options = $this->getReportOptions();
1001
1002 // from date filter
1003 $pfromdate = $app->input->getString('fromdate', '') ?: $options->get('fromdate', '');
1004 $pfromdate_ts = VikBooking::getDateTimestamp($pfromdate);
1005
1006 // optional listings filtered
1007 $plistings = (array) ($app->input->get('listings', [], 'array') ?: $options->get('listings', []));
1008
1009 // custom data manually filled before saving and reloading
1010 $pfiller = $app->input->get('filler', '', 'raw');
1011 $pfiller = !empty($pfiller) ? (array) json_decode($pfiller, true) : [];
1012
1013 // pool of booking IDs to update their history
1014 $this->export_booking_ids = [];
1015
1016 // records data pool
1017 $data = [
1018 'bookings' => [],
1019 'booking_groups' => [],
1020 'tot_rooms' => $this->countRooms($plistings),
1021 'tot_rooms_occupied' => $this->countDayRoomsOccupied($pfromdate_ts, $plistings),
1022 'tot_arrivals' => 0,
1023 'tot_departures' => 0,
1024 'tot_stays' => 0,
1025 'xml_c59_full' => '',
1026 ];
1027
1028 // record the main guest information for each booking
1029 $bookings_main_guest = [];
1030
1031 // push the bookings data
1032 foreach ($this->rows as $ind => $row) {
1033 // build row data
1034 $row_data = [];
1035
1036 foreach ($row as $field) {
1037 if ($field['key'] == 'idbooking' && !in_array($field['value'], $this->export_booking_ids)) {
1038 array_push($this->export_booking_ids, $field['value']);
1039 }
1040
1041 if (isset($field['ignore_export'])) {
1042 continue;
1043 }
1044
1045 // report value
1046 if (is_array($pfiller) && isset($pfiller[$ind]) && isset($pfiller[$ind][$field['key']])) {
1047 if (strlen($pfiller[$ind][$field['key']])) {
1048 $field['value'] = $pfiller[$ind][$field['key']];
1049 }
1050 }
1051
1052 if (isset($field['callback_export'])) {
1053 $field['callback'] = $field['callback_export'];
1054 }
1055
1056 // get field value
1057 $value = !isset($field['no_export_callback']) && isset($field['callback']) && is_callable($field['callback']) ? $field['callback']($field['value']) : $field['value'];
1058
1059 // set row data
1060 $row_data[$field['key']] = $value;
1061 }
1062
1063 // check main guest booking
1064 if (!isset($bookings_main_guest[$row_data['idbooking']])) {
1065 // push the main guest data for this booking
1066 $bookings_main_guest[$row_data['idbooking']] = $row_data;
1067 } else {
1068 // when parsing the Nth guest of the same booking, ensure we've got no missing values
1069 $needed_props = [
1070 'provincia',
1071 'country',
1072 'country_full_name',
1073 'provenience',
1074 ];
1075
1076 foreach ($needed_props as $needed_prop) {
1077 if (!empty($bookings_main_guest[$row_data['idbooking']][$needed_prop]) && empty($row_data[$needed_prop])) {
1078 // copy value from the main guest
1079 $row_data[$needed_prop] = $bookings_main_guest[$row_data['idbooking']][$needed_prop];
1080 }
1081 }
1082 }
1083
1084 // push the booking row data to the pool
1085 $data['bookings'][] = $row_data;
1086 }
1087
1088 // group the bookings by italian province or foreign countries
1089 $foreign_countries_map = [];
1090 foreach ($data['bookings'] as $booking_row) {
1091 // determine the group key (default to the empty key "0")
1092 $group_key = 0;
1093
1094 if (!empty($booking_row['provenience']) && is_numeric($booking_row['provenience']) && isset($this->foreignCountryCodes[$booking_row['provenience']])) {
1095 // must be a custom ISTAT country code
1096 $group_key = $this->foreignCountryCodes[$booking_row['provenience']];
1097 } elseif (!empty($booking_row['provenience']) && is_numeric($booking_row['provenience']) && isset($this->foreignCountries[$booking_row['provenience']])) {
1098 // must be a custom ISPAT country code
1099 $group_key = $this->foreignCountries[$booking_row['provenience']];
1100 } elseif (!empty($booking_row['country'])) {
1101 if (!strcasecmp($booking_row['country'], 'ITA')) {
1102 // for italian guests we need to use the province, if available
1103 $group_key = 'ITA_0';
1104 if (!empty($booking_row['provincia'])) {
1105 // italian customer will be grouped by province
1106 $group_key = 'ITA_' . $booking_row['provincia'];
1107 }
1108 } else {
1109 // foreign country
1110 $group_key = $booking_row['country'];
1111 $foreign_countries_map[$group_key] = $booking_row['country_full_name'] ?: $group_key;
1112 }
1113 }
1114
1115 // start group key container
1116 $data['booking_groups'][$group_key] = $data['booking_groups'][$group_key] ?? [];
1117
1118 // push booking to proper group container
1119 $data['booking_groups'][$group_key][] = $booking_row;
1120 }
1121
1122 // build the XML node for C59 "full"
1123 $data['xml_c59_full'] .= '<c59>' . "\n";
1124
1125 // whether the property is open today
1126 $is_open = (bool) (!VikBooking::validateClosingDates($pfromdate_ts, $pfromdate_ts));
1127
1128 // add nodes
1129 // $data['xml_c59_full'] .= '<dataMovimentazione>' . date('Y-m-d', $pfromdate_ts) . '</dataMovimentazione>' . "\n";
1130 $data['xml_c59_full'] .= '<dataMovimentazione>' . date('Y-m-d', $pfromdate_ts) . 'T00:00:00+02:00</dataMovimentazione>' . "\n";
1131 $data['xml_c59_full'] .= '<esercizioAperto>' . intval($is_open) . '</esercizioAperto>' . "\n";
1132
1133 if ($is_open) {
1134 // start building the "movimenti nodes"
1135 foreach ($data['booking_groups'] as $group_key => $bookings_group) {
1136 // tell whether we have an italian group
1137 $is_italian = (bool) preg_match('/^ITA_/', $group_key);
1138 if ($is_italian) {
1139 // adjust the group key to only contain the province 2-char code (unless empty, hence "0")
1140 $group_key = preg_replace('/^ITA_/', '', $group_key);
1141 }
1142
1143 // count arrivals, departures and stays for the current group
1144 $group_tot_arrivals = 0;
1145 $group_tot_departures = 0;
1146 $group_tot_stays = 0;
1147
1148 foreach ($bookings_group as $booking_row) {
1149 if ($booking_row['is_checkin']) {
1150 $group_tot_arrivals++;
1151 }
1152 if ($booking_row['is_checkout']) {
1153 $group_tot_departures++;
1154 }
1155 if (!$booking_row['is_checkin'] && !$booking_row['is_checkout']) {
1156 $group_tot_stays++;
1157 }
1158 }
1159
1160 // increase overall counters
1161 $data['tot_arrivals'] += $group_tot_arrivals;
1162 $data['tot_departures'] += $group_tot_departures;
1163 $data['tot_stays'] += $group_tot_stays;
1164
1165 if (!$group_tot_arrivals && !$group_tot_departures) {
1166 // the node "movimenti" for each "targa" does not accept empty arrivals and empty departures
1167 continue;
1168 }
1169
1170 // build "movimento" node for the current group ("targa")
1171 $data['xml_c59_full'] .= '<movimenti>' . "\n";
1172
1173 // build "targa" node
1174 $targaNode = '';
1175 if ($is_italian && !empty($group_key)) {
1176 // 2-char province expected
1177 $targaNode = '<targa>' . htmlspecialchars(strtoupper($group_key)) . '</targa>' . "\n";
1178 } elseif ($is_italian && empty($group_key)) {
1179 /**
1180 * ISPAT said the "targa" may be mandatory and so we default to TN for Italian customers.
1181 *
1182 * @since 1.18.8 (J) - 1.8.8 (WP)
1183 */
1184 $targaNode = '<targa>TN</targa>' . "\n";
1185 } elseif (!$is_italian && !empty($group_key) && isset($foreign_countries_map[$group_key])) {
1186 // foreign country full name expected
1187 $targaNode = '<targa>' . htmlspecialchars(strtoupper($foreign_countries_map[$group_key])) . '</targa>' . "\n";
1188 } elseif (!$is_italian && !empty($group_key) && array_search($group_key, $this->foreignCountryCodes) !== false) {
1189 // 3-char country name expected from ISTAT country code
1190 $istat_ccode = array_search($group_key, $this->foreignCountryCodes);
1191 $targaNode = '<targa>' . htmlspecialchars(strtoupper($this->foreignCountries[$istat_ccode])) . '</targa>' . "\n";
1192 } elseif (!$is_italian && !empty($group_key) && array_search($group_key, $this->foreignCountries) !== false) {
1193 // full country name expected from ISPAT country code
1194 $istat_ccode = array_search($group_key, $this->foreignCountries);
1195 $targaNode = '<targa>' . htmlspecialchars(strtoupper($this->foreignCountries[$istat_ccode])) . '</targa>' . "\n";
1196 }
1197
1198 // set "movimento" child nodes
1199 $data['xml_c59_full'] .= '<arrivi>' . $group_tot_arrivals . '</arrivi>' . "\n";
1200 $data['xml_c59_full'] .= '<italia>' . intval($is_italian) . '</italia>' . "\n";
1201 $data['xml_c59_full'] .= '<partenze>' . $group_tot_departures . '</partenze>' . "\n";
1202 if ($targaNode) {
1203 // the "targa" node is optional
1204 $data['xml_c59_full'] .= $targaNode;
1205 }
1206
1207 // finalize movimento node
1208 $data['xml_c59_full'] .= '</movimenti>' . "\n";
1209 }
1210 }
1211
1212 // set overall arrivals
1213 $data['xml_c59_full'] .= '<totaleArrivi>' . $data['tot_arrivals'] . '</totaleArrivi>' . "\n";
1214 // set overall departures
1215 $data['xml_c59_full'] .= '<totalePartenze>' . $data['tot_departures'] . '</totalePartenze>' . "\n";
1216 // set overall stays
1217 $data['xml_c59_full'] .= '<totalePresenti>' . $data['tot_stays'] . '</totalePresenti>' . "\n";
1218 // set tot rooms available
1219 $data['xml_c59_full'] .= '<unitaAbitativeDisponibili>' . $data['tot_rooms'] . '</unitaAbitativeDisponibili>' . "\n";
1220 // set tot rooms booked
1221 $data['xml_c59_full'] .= '<unitaAbitativeOccupate>' . $data['tot_rooms_occupied'] . '</unitaAbitativeOccupate>' . "\n";
1222
1223 // finalize C59 node
1224 $data['xml_c59_full'] .= '</c59>' . "\n";
1225
1226 return $data;
1227 }
1228
1229 /**
1230 * Generates the text file for ISTAT Valle d'Aosta, then sends it to output for download.
1231 * In case of errors, the process is not terminated (exit) to let the View display the
1232 * error message(s). The export type argument can eventually determine an action to run.
1233 *
1234 * @param string $export_type Differentiates the type of export requested.
1235 *
1236 * @return void|bool Void in case of script termination, boolean otherwise.
1237 */
1238 public function customExport($export_type = null)
1239 {
1240 // build the record data
1241 $data = $this->buildRecordData();
1242
1243 // build report action data, if needed
1244 $action_data = array_merge($this->getActionData($registry = false), $data);
1245
1246 /**
1247 * Custom export method can run a custom action.
1248 */
1249 if ($export_type && !is_numeric($export_type)) {
1250 try {
1251 // ensure the type of export is a callable scoped action, hidden or visible
1252 $actions = $this->getScopedActions($this->getScope(), $visible = false);
1253 $action_ids = array_column($actions, 'id');
1254 $action_names = array_column($actions, 'name');
1255 if (!in_array($export_type, $action_ids)) {
1256 throw new Exception(sprintf('Cannot invoke the requested type of export [%s].', $export_type), 403);
1257 }
1258
1259 // get the requested action readable name
1260 $action_name = $action_names[array_search($export_type, $action_ids)];
1261
1262 if ($this->getScope() === 'web') {
1263 // run the action and output the HTML response string
1264 $html_result = $this->_callActionReturn($export_type, 'html', $this->getScope(), $action_data);
1265
1266 // build the action result data object
1267 $js_result = json_encode([
1268 'actionName' => $action_name,
1269 'actionHtml' => $html_result,
1270 ]);
1271
1272 // render modal script with the action result
1273 JFactory::getDocument()->addScriptDeclaration(
1274 <<<JS
1275 ;(function($) {
1276 $(function() {
1277 let result = $js_result;
1278 VBOCore.displayModal({
1279 suffix: 'report-custom-scopedaction-result',
1280 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1281 title: result.actionName,
1282 body: result.actionHtml,
1283 });
1284 });
1285 })(jQuery);
1286 JS
1287 );
1288
1289 // abort and let the View render the result within a modal
1290 return;
1291 }
1292
1293 // let the report custom action run and return a boolean value if invoked by a cron
1294 return (bool) $this->_callActionReturn($export_type, 'success', $this->getScope(), $action_data);
1295
1296 } catch (Exception $e) {
1297 // silently catch the error and set it
1298 $this->setError($e->getMessage());
1299
1300 // abort
1301 return false;
1302 }
1303 }
1304
1305 // proceed with the regular export function (write on file through cron or download file through web)
1306
1307 if (!$data || empty($data['xml_c59_full'])) {
1308 // abort
1309 return false;
1310 }
1311
1312 // update history for all bookings affected before exporting
1313 foreach ($this->export_booking_ids as $bid) {
1314 VikBooking::getBookingHistoryInstance($bid)->store('RP', $this->reportName . ' - Export');
1315 }
1316
1317 /**
1318 * Attempt to format the XML string for the C59 node to have a better result.
1319 */
1320 $formattedC59 = $this->loadXmlSoap($data['xml_c59_full'])->formatXml() ?: $data['xml_c59_full'];
1321
1322 /**
1323 * Custom export method supports a custom export handler, if previously set.
1324 */
1325 if ($this->hasExportHandler()) {
1326 // write data onto the custom file handler
1327 $fp = $this->getExportCSVHandler();
1328 fwrite($fp, $formattedC59);
1329 fclose($fp);
1330
1331 // return true as data was written
1332 return true;
1333 }
1334
1335 // force text file download in case of regular export
1336 header("Content-type: text/xml");
1337 header("Cache-Control: no-store, no-cache");
1338 header('Content-Disposition: attachment; filename="' . $this->getExportCSVFileName() . '"');
1339 echo $formattedC59;
1340
1341 exit;
1342 }
1343
1344 /**
1345 * Helper method invoked via AJAX by the controller.
1346 * Needed to save the manual entries for the pax data.
1347 *
1348 * @param array $manual_data the object representation of the manual entries.
1349 *
1350 * @return array one boolean value array with the operation result.
1351 */
1352 public function updatePaxData($manual_data = [])
1353 {
1354 if (!is_array($manual_data) || !$manual_data) {
1355 VBOHttpDocument::getInstance()->close(400, 'Nothing to save!');
1356 }
1357
1358 // re-build manual entries object representation
1359 $bids_guests = [];
1360 foreach ($manual_data as $guest_ind => $guest_data) {
1361 if (!is_numeric($guest_ind) || !is_array($guest_data) || empty($guest_data['bid']) || !isset($guest_data['bid_index']) || count($guest_data) < 2) {
1362 // empty or invalid manual entries array
1363 continue;
1364 }
1365 // the guest index in the reportObj starts from 0
1366 $use_guest_ind = ($guest_ind + 1 - (int)$guest_data['bid_index']);
1367 if (!isset($bids_guests[$guest_data['bid']])) {
1368 $bids_guests[$guest_data['bid']] = [];
1369 }
1370 // set manual entries for this guest number
1371 $bids_guests[$guest_data['bid']][$use_guest_ind] = $guest_data;
1372 // remove the "bid" and "bid_index" keys
1373 unset($bids_guests[$guest_data['bid']][$use_guest_ind]['bid'], $bids_guests[$guest_data['bid']][$use_guest_ind]['bid_index']);
1374 }
1375
1376 if (!$bids_guests) {
1377 VBOHttpDocument::getInstance()->close(400, 'No manual entries to save found');
1378 }
1379
1380 // loop through all bookings to update the data for the various rooms and guests
1381 $bids_updated = 0;
1382 foreach ($bids_guests as $bid => $entries) {
1383 $b_rooms = VikBooking::loadOrdersRoomsData($bid);
1384 if (empty($b_rooms)) {
1385 continue;
1386 }
1387 // count guests per room
1388 $room_guests = [];
1389 foreach ($b_rooms as $b_room) {
1390 $room_guests[] = $b_room['adults'] + $b_room['children'];
1391 }
1392 // get current booking pax data
1393 $pax_data = VBOCheckinPax::getBookingPaxData($bid);
1394 $pax_data = empty($pax_data) ? [] : $pax_data;
1395 foreach ($entries as $guest_ind => $guest_data) {
1396 // find room index for this guest
1397 $room_num = 0;
1398 $use_guest_ind = $guest_ind;
1399 foreach ($room_guests as $room_index => $tot_guests) {
1400 // find the proper guest index for the room to which this belongs
1401 if ($use_guest_ind <= $tot_guests) {
1402 // proper room index found for this guest
1403 $room_num = $room_index;
1404 break;
1405 } else {
1406 // it's probably in a next room
1407 $use_guest_ind -= $tot_guests;
1408 }
1409 }
1410 // push new pax data for this room and guest
1411 if (!isset($pax_data[$room_num])) {
1412 $pax_data[$room_num] = [];
1413 }
1414 if (!isset($pax_data[$room_num][$use_guest_ind])) {
1415 $pax_data[$room_num][$use_guest_ind] = $guest_data;
1416 } else {
1417 $pax_data[$room_num][$use_guest_ind] = array_merge($pax_data[$room_num][$use_guest_ind], $guest_data);
1418 }
1419 }
1420 // update booking pax data
1421 if (VBOCheckinPax::setBookingPaxData($bid, $pax_data)) {
1422 $bids_updated++;
1423 }
1424 }
1425
1426 return $bids_updated ? [true] : [false];
1427 }
1428
1429 /**
1430 * Returns an associative list of states/provinces for the given country.
1431 * Visibility should be public in case other ISTAT reports for Italy may
1432 * require to load the same provinces.
1433 *
1434 * @return array
1435 */
1436 public function loadCountryStates($country)
1437 {
1438 $country_states = [];
1439
1440 foreach (VBOStateHelper::getCountryStates((int) VBOStateHelper::getCountryId($country)) as $state_record) {
1441 $country_states[$state_record['state_2_code']] = $state_record['state_name'];
1442 }
1443
1444 return $country_states;
1445 }
1446
1447 /**
1448 * Returns an associative list of ISTAT foreign country codes. The full list includes the country
1449 * codes compatible with the Alloggiati Web integration (ISTAT codes), as well as a merged list
1450 * of country "false" codes (negative keys) that are supported and required by ISPAT.
1451 *
1452 * @param string $type The string "3" can be passed to get the known 3-char country codes.
1453 *
1454 * @return array Associative list of countries, according to type param.
1455 */
1456 public function loadIstatForeignCountries($type = '')
1457 {
1458 if ($type === '3') {
1459 // return the list for only the known country ISO code (3-char)
1460 return [
1461 '528' => "ARG",
1462 '800' => "AUS",
1463 '038' => "AUT",
1464 '017' => "BEL",
1465 '508' => "BRA",
1466 '068' => "BGR",
1467 '404' => "CAN",
1468 '720' => "CHN",
1469 '600' => "CYP",
1470 '728' => "KOR",
1471 '092' => "HRV",
1472 '008' => "DNK",
1473 '220' => "EGY",
1474 '053' => "EST",
1475 '032' => "FIN",
1476 '001' => "FRA",
1477 '004' => "DEU",
1478 '732' => "JPN",
1479 '009' => "GRC",
1480 '664' => "IND",
1481 '007' => "IRL",
1482 '024' => "ISL",
1483 '624' => "ISR",
1484 '999' => "ITA",
1485 '054' => "LVA",
1486 '055' => "LTU",
1487 '018' => "LUX",
1488 '046' => "MLT",
1489 '412' => "MEX",
1490 '028' => "NOR",
1491 '804' => "NZL",
1492 '230' => "MED",
1493 '003' => "NLD",
1494 '060' => "POL",
1495 '010' => "PRT",
1496 '006' => "GBR",
1497 '061' => "CZE",
1498 '066' => "ROU",
1499 '075' => "RUS",
1500 '063' => "SVK",
1501 '091' => "SVN",
1502 '011' => "ESP",
1503 '400' => "USA",
1504 '388' => "ZAF",
1505 '030' => "SWE",
1506 '036' => "CHE",
1507 '052' => "TUR",
1508 '072' => "UKR",
1509 '064' => "HUN",
1510 '484' => "VEN",
1511 ];
1512 }
1513
1514 // return the whole ISTAT + ISPAT (negative keys) list
1515 return [
1516 '-990' => "AFGHANISTAN",
1517 '-991' => "ALBANIA",
1518 '-992' => "ALGERIA",
1519 '-993' => "ANDORRA",
1520 '-994' => "ANGOLA",
1521 '-995' => "ANTIGUA E BARBUDA",
1522 '-996' => "ARABIA SAUDITA",
1523 '528' => "ARGENTINA",
1524 '-997' => "ARMENIA",
1525 '800' => "AUSTRALIA",
1526 '038' => "AUSTRIA",
1527 '-998' => "AZERBAIGIAN",
1528 '-999' => "BAHAMAS",
1529 '-9910' => "BAHREIN",
1530 '-9911' => "BANGLADESH",
1531 '-9912' => "BARBADOS",
1532 '017' => "BELGIO",
1533 '-9913' => "BELIZE",
1534 '-9914' => "BENIN",
1535 '-9915' => "BHUTAN",
1536 '-9916' => "BIELORUSSIA",
1537 '-9917' => "BOLIVIA",
1538 '-9918' => "BOSNIA-ERZEGOVINA",
1539 '-9919' => "BOTSWANA",
1540 '508' => "BRASILE",
1541 '-9920' => "BRUNEI",
1542 '068' => "BULGARIA",
1543 '-9921' => "BURKINA-FASO",
1544 '-9922' => "BURUNDI",
1545 '-9923' => "CAMBOGIA",
1546 '-9924' => "CAMERUN",
1547 '404' => "CANADA",
1548 '-9925' => "CAPO VERDE",
1549 '-9926' => "CIAD",
1550 '-9927' => "CILE",
1551 '600' => "CIPRO",
1552 '-9928' => "COLOMBIA",
1553 '-9929' => "COMORE",
1554 '-9930' => "CONGO",
1555 '-9931' => "COSTA D'AVORIO",
1556 '-9932' => "COSTARICA",
1557 '092' => "CROAZIA",
1558 '-9933' => "CUBA",
1559 '008' => "DANIMARCA",
1560 '-9934' => "DOMINICA",
1561 '-9935' => "ECUADOR",
1562 '220' => "EGITTO",
1563 '-9936' => "EL SALVADOR",
1564 '-9937' => "EMIRATI ARABI UNITI",
1565 '-9938' => "ERITREA",
1566 '053' => "ESTONIA",
1567 '-9939' => "ETIOPIA",
1568 '075' => "FEDERAZIONE RUSSA",
1569 '-9940' => "FIGI",
1570 '-9941' => "FILIPPINE",
1571 '032' => "FINLANDIA",
1572 '001' => "FRANCIA",
1573 '-9942' => "GABON",
1574 '-9943' => "GAMBIA",
1575 '-9944' => "GEORGIA",
1576 '004' => "GERMANIA",
1577 '-9945' => "GHANA",
1578 '-9946' => "GIAMAICA",
1579 '732' => "GIAPPONE",
1580 '-9947' => "GIBUTI",
1581 '-9948' => "GIORDANIA",
1582 '009' => "GRECIA",
1583 '-9949' => "GRENADA",
1584 '-9950' => "GUATEMALA",
1585 '-9951' => "GUINEA",
1586 '-9952' => "GUINEA BISSAU",
1587 '-9953' => "GUINEA EQUATORIALE",
1588 '-9954' => "GUYANA",
1589 '-9955' => "HAITI",
1590 '-9956' => "HONDURAS",
1591 '664' => "INDIA",
1592 '-9957' => "INDONESIA",
1593 '-9958' => "IRAN, REPUBBLICA ISLAMICA DEL",
1594 '-9959' => "IRAQ",
1595 '007' => "IRLANDA",
1596 '024' => "ISLANDA",
1597 '624' => "ISRAELE",
1598 '999' => "ITALIA",
1599 '-9960' => "JERSEY, ISOLE",
1600 '-9961' => "KAZAKISTAN",
1601 '-9962' => "KENIA",
1602 '-9963' => "KIRGHIZISTAN",
1603 '-9964' => "KIRIBATI",
1604 '-9965' => "KOSOVO",
1605 '-9966' => "KUWAIT",
1606 '-9967' => "LAOS",
1607 '-9968' => "LESOTHO",
1608 '054' => "LETTONIA",
1609 '-9969' => "LIBANO",
1610 '-9970' => "LIBERIA",
1611 '-9971' => "LIBIA",
1612 '-9972' => "LIECHTENSTEIN",
1613 '055' => "LITUANIA",
1614 '018' => "LUSSEMBURGO",
1615 '-9973' => "MACEDONIA (EX REPUBBLICA JUGOSLAVA)",
1616 '-9974' => "MADAGASCAR",
1617 '-9975' => "MALAWI",
1618 '-9976' => "MALAYSIA",
1619 '-9977' => "MALDIVE",
1620 '-9978' => "MALI",
1621 '046' => "MALTA",
1622 '-9979' => "MAROCCO",
1623 '-9980' => "MARSHALL",
1624 '-9981' => "MAURITANIA",
1625 '-9982' => "MAURITIUS",
1626 '412' => "MESSICO",
1627 '-9983' => "MICRONESIA",
1628 '-9984' => "MOLDOVA",
1629 '-9985' => "MONACO",
1630 '-9986' => "MONGOLIA",
1631 '-9987' => "MONTENEGRO",
1632 '-9988' => "MOZAMBICO",
1633 '-9989' => "MYANMAR",
1634 '-9990' => "NAMIBIA",
1635 '-9991' => "NAURU",
1636 '-9992' => "NEPAL",
1637 '-9993' => "NICARAGUA",
1638 '-9994' => "NIGER",
1639 '-9995' => "NIGERIA",
1640 '028' => "NORVEGIA",
1641 '804' => "NUOVA ZELANDA",
1642 '-9996' => "OMAN",
1643 '230' => "PAESI AFRICA MEDITERRANEA",
1644 '003' => "PAESI BASSI",
1645 '-9997' => "PAKISTAN",
1646 '-9998' => "PALAU",
1647 '-9999' => "PANAMA",
1648 '-99100' => "PAPUA NUOVA GUINEA",
1649 '-99101' => "PARAGUAY",
1650 '-99102' => "PERU'",
1651 '060' => "POLONIA",
1652 '010' => "PORTOGALLO",
1653 '-99103' => "QATAR",
1654 '006' => "REGNO UNITO",
1655 '-99104' => "REP.POPOLARE DEMOCRATICA DI COREA (COREA DEL NORD)",
1656 '061' => "REPUBBLICA CECA",
1657 '-99105' => "REPUBBLICA CENTRAFRICANA",
1658 '-99106' => "REPUBBLICA DEMOCRATICA DEL CONGO (EX ZAIRE)",
1659 '728' => "REPUBBLICA DI COREA (COREA DEL SUD)",
1660 '-99107' => "REPUBBLICA DOMINICANA",
1661 '720' => "REPUBBLICA POPOLARE CINESE",
1662 '063' => "REPUBBLICA SLOVACCA",
1663 '-99108' => "RICONOSCIUTI NON CITTADINI (LETTONI)",
1664 '066' => "ROMANIA",
1665 '-99110' => "S. VINCENT E GRENADINE",
1666 '-99111' => "SAINT KITTS E NEVIS",
1667 '-99112' => "SAINT LUCIA",
1668 '-99113' => "SALOMONE",
1669 '-99114' => "SAMOA",
1670 '-99115' => "SAN MARINO",
1671 '-99116' => "SANTA SEDE",
1672 '-99117' => "SAO TOME' E PRINCIPE",
1673 '-99118' => "SENEGAL",
1674 '-99119' => "SERBIA REPUBBLICA DI",
1675 '-99120' => "SEYCHELLES",
1676 '-99121' => "SIERRA LEONE",
1677 '-99122' => "SINGAPORE",
1678 '-99123' => "SIRIA",
1679 '091' => "SLOVENIA",
1680 '-99124' => "SOMALIA",
1681 '011' => "SPAGNA",
1682 '-99125' => "SRI LANKA (CEYLON)",
1683 '400' => "STATI UNITI D'AMERICA",
1684 '388' => "SUD AFRICA",
1685 '-99126' => "SUD SUDAN, REPUBBLICA DEL",
1686 '-99127' => "SUDAN",
1687 '-99128' => "SURINAME",
1688 '030' => "SVEZIA",
1689 '036' => "SVIZZERA",
1690 '-99129' => "SWAZILAND",
1691 '-99130' => "TAGIKISTAN",
1692 '-99131' => "TAIWAN",
1693 '-99132' => "TANZANIA",
1694 '-99133' => "TERRITORI AUTONOMIA PALESTINESE",
1695 '-99134' => "THAILANDIA",
1696 '-99135' => "TIMOR ORIENTALE",
1697 '-99136' => "TOGO",
1698 '-99137' => "TONGA",
1699 '-99138' => "TRINIDAD E TOBAGO",
1700 '-99139' => "TUNISIA",
1701 '052' => "TURCHIA",
1702 '-99140' => "TURKMENISTAN",
1703 '-99141' => "TUVALU",
1704 '-99109' => "UANDA",
1705 '072' => "UCRAINA",
1706 '-99142' => "UGANDA",
1707 '064' => "UNGHERIA",
1708 '-99143' => "URUGUAY",
1709 '-99144' => "UZBEKISTAN",
1710 '-99145' => "VANUATU",
1711 '484' => "VENEZUELA",
1712 '-99146' => "VIETNAM",
1713 '-99147' => "YEMEN",
1714 '-99148' => "ZAMBIA",
1715 '-99149' => "ZIMBABWE",
1716 '777' => "ALTRI PAESI",
1717 '300' => "ALTRI PAESI AFRICA",
1718 '530' => "ALTRI PAESI AMERICA LATINA",
1719 '760' => "ALTRI PAESI DELL'ASIA",
1720 '100' => "ALTRI PAESI EUROPEI",
1721 '750' => "ALTRI PAESI MEDIO ORIENTE",
1722 '410' => "ALTRI PAESI NORD AMERICA",
1723 '810' => "ALTRI PAESI OCEANIA",
1724 ];
1725 }
1726
1727 /**
1728 * Custom scoped action to upload the guest presence records.
1729 * Accepted scopes are "web" and "cron", so the "success" property must be returned.
1730 *
1731 * @param string $scope Optional scope identifier (cron, web, etc..).
1732 * @param array $data Optional associative list of data to process.
1733 *
1734 * @return array The execution result properties.
1735 *
1736 * @throws Exception
1737 */
1738 protected function uploadC59Full($scope = null, array $data = [])
1739 {
1740 if (!($data['xml_c59_full'] ?? null) && $scope === 'web') {
1741 // start the process through the interface by submitting the current data
1742 return [
1743 'html' => '<script type="text/javascript">vboDownloadRecordIspat(\'uploadC59Full\');</script>',
1744 ];
1745 }
1746
1747 if (!($data['xml_c59_full'] ?? null)) {
1748 // attempt to build the report data if not set
1749 $data = array_merge($data, $this->buildRecordData());
1750 }
1751
1752 if (empty($data['xml_c59_full'])) {
1753 throw new Exception('Nessun dato presente per la trasmissione dei record delle presenze turistiche.', 500);
1754 }
1755
1756 $settings = $this->loadSettings();
1757
1758 if (!$settings || empty($settings['propertyid']) || empty($settings['user']) || empty($settings['pwd'])) {
1759 throw new Exception(sprintf('[%s] error: missing settings.', __METHOD__), 500);
1760 }
1761
1762 // determine the WS url to use
1763 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
1764
1765 // build the Soap request message
1766 $request = '<?xml version="1.0" encoding="utf-8"?>
1767 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mov="http://movimentazione.manager.web.module.dtu.infotn.it/">
1768 <soapenv:Header/>
1769 <soapenv:Body>
1770 <mov:inviaC59Full>
1771 <username>' . $settings['user'] . '</username>
1772 <password>' . $settings['pwd'] . '</password>
1773 <struttura>' . $settings['propertyid'] . '</struttura>
1774 ' . $data['xml_c59_full'] . '
1775 </mov:inviaC59Full>
1776 </soapenv:Body>
1777 </soapenv:Envelope>';
1778
1779 try {
1780 // get the SoapClient object from WSDL
1781 $ws_client = $this->getWebServiceClient($settings);
1782
1783 // do the request and get the Soap XML response message
1784 $response = $ws_client->__doRequest($request, preg_replace('/\?wsdl$/i', '', $use_ws_url), 'inviaC59Full', SOAP_1_2);
1785
1786 // wrap the response into an object
1787 $xmlResponse = $this->loadXmlSoap($response);
1788
1789 // check for response errors
1790 $responseFault = $xmlResponse->getSoapRecursiveNsElement([
1791 ['soap', true, 'Body'],
1792 ['soap', true, 'Fault'],
1793 ]);
1794
1795 if (is_object($responseFault) && in_array($responseFault->getName(), ['faultcode', 'faultstring'])) {
1796 // errors returned
1797 $fault_code = $responseFault->faultcode ?? 0;
1798 $fault_mess = $responseFault->faultstring ?? '';
1799 throw new Exception(sprintf("Errore (%s):\n%s.", $fault_code, $fault_mess), 500);
1800 }
1801
1802 // build HTML response string
1803 $html = '';
1804 $html .= '<p class="successmade">Totale registrazioni analizzate: ' . count($data['bookings']) . '</p>';
1805 } catch (Exception $e) {
1806 // propagate the error caught
1807 throw new Exception(sprintf('[%s] error: %s', __METHOD__, $e->getMessage()), $e->getCode() ?: 500);
1808 }
1809
1810 // update history for all bookings affected
1811 foreach ($this->export_booking_ids as $bid) {
1812 // build extra data payload for the history event
1813 $hdata = [
1814 'transmitted' => 1,
1815 'method' => 'uploadC59Full',
1816 'report' => $this->getFileName(),
1817 ];
1818 // store booking history event
1819 VikBooking::getBookingHistoryInstance($bid)
1820 ->setExtraData($hdata)
1821 ->store('RP', $this->reportName . ' - Trasmissione presenze turistiche');
1822 }
1823
1824 // get the currently active profile ID, if any
1825 $activeProfileId = $this->getActiveProfile();
1826
1827 /**
1828 * When the report is executed through a cron-job, the transmission of the guest details
1829 * will also dump the data transmitted onto a resource file, useful for sending it via email.
1830 */
1831 if ($scope === 'cron') {
1832 // prepare the XML resource file information
1833 $xml_name = implode('_', array_filter([$this->getFileName(), 'trasmissione', 'flussi', time(), $activeProfileId, rand(100, 999)])) . '.xml';
1834 $xml_dest = $this->getDataMediaPath() . DIRECTORY_SEPARATOR . $xml_name;
1835 $xml_url = $this->getDataMediaUrl() . $xml_name;
1836
1837 // store the XML bytes into a local file on disk
1838 $stored = JFile::write($xml_dest, $soap_xml);
1839
1840 if ($stored && VBOPlatformDetection::isWordPress()) {
1841 /**
1842 * Trigger files mirroring operation
1843 */
1844 VikBookingLoader::import('update.manager');
1845 VikBookingUpdateManager::triggerUploadBackup($xml_dest);
1846 }
1847
1848 if ($stored) {
1849 // define a new report resource for the generated file
1850 $this->defineResourceFile([
1851 'summary' => sprintf(
1852 '%sFlussi turistici generati e trasmessi con successo.',
1853 ($activeProfileId ? '(' . ucwords($activeProfileId) . ') ' : '')
1854 ),
1855 'url' => $xml_url,
1856 'path' => $xml_dest,
1857 ]);
1858 }
1859 }
1860
1861 // when executed through a cron, store an event in the Notifications Center
1862 if ($scope === 'cron') {
1863 // build the notification record
1864 $notification = [
1865 'sender' => 'reports',
1866 'type' => 'pmsreport.transmit.ok',
1867 'title' => $this->reportName . ' - Trasmissione presenze turistiche',
1868 'summary' => sprintf(
1869 '%sSono stati trasmessi i dati degli ospiti per la data %s. Prenotazioni analizzate: %d.',
1870 ($activeProfileId ? '(' . ucwords($activeProfileId) . ') ' : ''),
1871 $this->exported_checkin_dates[0] ?? '',
1872 count($data['bookings'])
1873 ),
1874 ];
1875
1876 try {
1877 // store the notification record
1878 VBOFactory::getNotificationCenter()->store([$notification]);
1879 } catch (Exception $e) {
1880 // silently catch the error without doing anything
1881 }
1882 }
1883
1884 return [
1885 'html' => $html,
1886 'success' => true,
1887 'bookings' => $data['bookings'],
1888 ];
1889 }
1890
1891 /**
1892 * Custom scoped action to get a list of properties ("listaStrutture").
1893 *
1894 * @param string $scope Optional scope identifier (cron, web, etc..).
1895 * @param array $data Optional associative list of data to process.
1896 *
1897 * @return array The execution result properties.
1898 *
1899 * @throws Exception
1900 */
1901 protected function listaStrutture($scope = null, array $data = [])
1902 {
1903 $settings = $this->loadSettings();
1904
1905 if (!$settings || empty($settings['user']) || empty($settings['pwd'])) {
1906 throw new Exception(sprintf('[%s] error: missing settings.', __METHOD__), 500);
1907 }
1908
1909 // determine the WS url to use
1910 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
1911
1912 // build the Soap request message
1913 $request = '<?xml version="1.0" encoding="utf-8"?>
1914 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mov="http://movimentazione.manager.web.module.dtu.infotn.it/">
1915 <soapenv:Header/>
1916 <soapenv:Body>
1917 <mov:listaStrutture>
1918 <username>' . $settings['user'] . '</username>
1919 <password>' . $settings['pwd'] . '</password>
1920 </mov:listaStrutture>
1921 </soapenv:Body>
1922 </soapenv:Envelope>';
1923
1924 try {
1925 // get the SoapClient object from WSDL
1926 $ws_client = $this->getWebServiceClient($settings);
1927
1928 // do the request and get the Soap XML response message
1929 $response = $ws_client->__doRequest($request, preg_replace('/\?wsdl$/i', '', $use_ws_url), 'listaStrutture', SOAP_1_2);
1930
1931 // wrap the response into an object
1932 $xmlResponse = $this->loadXmlSoap($response);
1933
1934 // check for response errors
1935 $responseFault = $xmlResponse->getSoapRecursiveNsElement([
1936 ['soap', true, 'Body'],
1937 ['soap', true, 'Fault'],
1938 ]);
1939
1940 if (isset($responseFault->faultstring) && strlen((string) $responseFault->faultstring)) {
1941 // errors returned
1942 $fault_code = $responseFault->faultcode ?? 0;
1943 $fault_mess = $responseFault->faultstring ?? '';
1944 throw new Exception(sprintf("Errore (%s):\n%s.", $fault_code, $fault_mess), 500);
1945 }
1946
1947 // access the response body
1948 $xmlBody = $xmlResponse->getSoapRecursiveNsElement([
1949 ['soap', true, 'Body'],
1950 ['ns2', true, 'listaStruttureResponse'],
1951 ]);
1952
1953 if (!isset($xmlBody->return)) {
1954 // no properties returned
1955 throw new UnexpectedValueException(sprintf('Nessuna struttura trovata [%s]', $xmlBody->formatXml() ?: $response), 500);
1956 }
1957
1958 // list of properties found
1959 $properties = [];
1960
1961 // build HTML response string
1962 $html = '';
1963 $html .= '<p class="successmade">Lista strutture assegnate:</p>';
1964 $html .= '<ol>';
1965
1966 // scan all properties returned
1967 foreach ($xmlBody->return as $returnNode) {
1968 // get the property name and id
1969 $property_id = (string) ($returnNode->struttura ?? 0);
1970 $property_name = (string) ($returnNode->name ?? '');
1971
1972 // push property found
1973 $properties[$property_id] = $property_name;
1974
1975 // get the readable properties name and id
1976 $property_id_read = (string) ($returnNode->struttura ?? '??');
1977 $property_name_read = (string) ($returnNode->name ?? '----');
1978
1979 // build HTML property string
1980 $html .= '<li>' . sprintf('ID <strong>%s</strong> - <strong>%s</strong>', $property_id_read, $property_name_read) . '</li>';
1981 }
1982
1983 // finalize HTML
1984 $html .= '</ol>';
1985
1986 } catch (Exception $e) {
1987 // propagate the error caught
1988 throw new Exception(sprintf('[%s] error: %s', __METHOD__, $e->getMessage()), $e->getCode() ?: 500);
1989 }
1990
1991 return [
1992 'html' => $html,
1993 'properties' => $properties,
1994 ];
1995 }
1996
1997 /**
1998 * Custom scoped action to get the last C59 transmission uploaded ("ultimoC59").
1999 *
2000 * @param string $scope Optional scope identifier (cron, web, etc..).
2001 * @param array $data Optional associative list of data to process.
2002 *
2003 * @return array The execution result properties.
2004 *
2005 * @throws Exception
2006 */
2007 protected function ultimoC59($scope = null, array $data = [])
2008 {
2009 $settings = $this->loadSettings();
2010
2011 if (!$settings || empty($settings['user']) || empty($settings['pwd'])) {
2012 throw new Exception(sprintf('[%s] error: missing settings.', __METHOD__), 500);
2013 }
2014
2015 if (empty($settings['propertyid'])) {
2016 throw new Exception('ID struttura mancante nelle impostazioni. Utilizza la funzione Lista Strutture per leggere gli ID associati al tuo account ed inserisci un ID nelle impostazioni.', 500);
2017 }
2018
2019 // determine the WS url to use
2020 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
2021
2022 // build the Soap request message
2023 $request = '<?xml version="1.0" encoding="utf-8"?>
2024 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mov="http://movimentazione.manager.web.module.dtu.infotn.it/">
2025 <soapenv:Header/>
2026 <soapenv:Body>
2027 <mov:ultimoC59>
2028 <username>' . $settings['user'] . '</username>
2029 <password>' . $settings['pwd'] . '</password>
2030 <struttura>' . $settings['propertyid'] . '</struttura>
2031 </mov:ultimoC59>
2032 </soapenv:Body>
2033 </soapenv:Envelope>';
2034
2035 try {
2036 // get the SoapClient object from WSDL
2037 $ws_client = $this->getWebServiceClient($settings);
2038
2039 // do the request and get the Soap XML response message
2040 $response = $ws_client->__doRequest($request, preg_replace('/\?wsdl$/i', '', $use_ws_url), 'ultimoC59', SOAP_1_2);
2041
2042 // wrap the response into an object
2043 $xmlResponse = $this->loadXmlSoap($response);
2044
2045 // check for response errors
2046 $responseFault = $xmlResponse->getSoapRecursiveNsElement([
2047 ['soap', true, 'Body'],
2048 ['soap', true, 'Fault'],
2049 ]);
2050
2051 if (isset($responseFault->faultstring) && strlen((string) $responseFault->faultstring)) {
2052 // errors returned
2053 $fault_code = $responseFault->faultcode ?? 0;
2054 $fault_mess = $responseFault->faultstring ?? '';
2055 throw new Exception(sprintf("Errore (%s):\n%s.", $fault_code, $fault_mess), 500);
2056 }
2057
2058 // access the response body
2059 $xmlBody = $xmlResponse->getSoapRecursiveNsElement([
2060 ['soap', true, 'Body'],
2061 ['ns2', true, 'ultimoC59Response'],
2062 ]);
2063
2064 if (!isset($xmlBody->return)) {
2065 // no operations returned
2066 throw new UnexpectedValueException(sprintf('Nessun movimento C59 trovato [%s]', $xmlBody->formatXml() ?: $response), 500);
2067 }
2068
2069 // build HTML response string
2070 $html = '';
2071 $html .= '<p class="successmade">Ultimo C59 disponibile nella base dati:</p>';
2072 $html .= '<pre>' . htmlspecialchars($xmlBody->formatXml() ?: $response ?: '') . '</pre>';
2073
2074 } catch (Exception $e) {
2075 // propagate the error caught
2076 throw new Exception(sprintf('[%s] error: %s', __METHOD__, $e->getMessage()), $e->getCode() ?: 500);
2077 }
2078
2079 return [
2080 'html' => $html,
2081 'ultimoC59Return' => $xmlBody->return,
2082 ];
2083 }
2084
2085 /**
2086 * Establishes a SOAP connection with the remote WSDL and returns the client.
2087 * Updates the internal properties according to settings related to the WS.
2088 *
2089 * @param array $settings List of report settings for production or test mode.
2090 *
2091 * @return SoapClient
2092 *
2093 * @throws Exception
2094 */
2095 protected function getWebServiceClient(array $settings = [])
2096 {
2097 // determine the WS url to use
2098 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
2099
2100 try {
2101 return new SoapClient($use_ws_url, [
2102 'soap_version' => constant($this->soap_version),
2103 ]);
2104 } catch (Throwable $e) {
2105 // prevent PHP fatal errors by catching and propagating them as Exceptions
2106 throw new Exception(sprintf('PHP Fatal Error: %s', $e->getMessage()), $e->getCode() ?: 500);
2107 }
2108 }
2109
2110 /**
2111 * Parses a SOAP XML message into a VBOXmlSoap object.
2112 *
2113 * @param string $xmlMessage The SOAP XML message to load.
2114 *
2115 * @return VBOXmlSoap
2116 *
2117 * @throws Exception
2118 */
2119 protected function loadXmlSoap($xmlMessage)
2120 {
2121 if (empty($xmlMessage)) {
2122 throw new Exception('Empty Soap XML message.', 500);
2123 }
2124
2125 // suppress warning messages
2126 libxml_use_internal_errors(true);
2127
2128 // parse the Soap XML message
2129 return simplexml_load_string($xmlMessage, VBOXmlSoap::class);
2130 }
2131
2132 /**
2133 * Registers the name to give to the file being exported.
2134 *
2135 * @return void
2136 */
2137 protected function registerExportFileName()
2138 {
2139 $pfromdate = VikRequest::getString('fromdate', '', 'request');
2140
2141 $this->setExportCSVFileName($this->reportName . '-' . str_replace('/', '_', $pfromdate) . '.xml');
2142 }
2143
2144 /**
2145 * Helper method to quickly get a pax_data property for the guest.
2146 *
2147 * @param array $pax_data the current pax_data stored.
2148 * @param array $guests list of total guests per room.
2149 * @param int $guest_ind the guest index.
2150 * @param string $key the pax_data key to look for.
2151 *
2152 * @return mixed null on failure or value fetched.
2153 */
2154 protected function getGuestPaxDataValue($pax_data, $guests, $guest_ind, $key)
2155 {
2156 if (!is_array($pax_data) || !$pax_data || empty($key)) {
2157 return null;
2158 }
2159
2160 // find room index for this guest number
2161 $room_num = 0;
2162 $use_guest_ind = $guest_ind;
2163 foreach ($guests as $room_index => $room_tot_guests) {
2164 // find the proper guest index for the room to which this belongs
2165 if ($use_guest_ind <= $room_tot_guests) {
2166 // proper room index found for this guest
2167 $room_num = $room_index;
2168 break;
2169 } else {
2170 // it's probably in a next room
2171 $use_guest_ind -= $room_tot_guests;
2172 }
2173 }
2174
2175 // check if a value exists for the requested key in the found room and guest indexes
2176 if (isset($pax_data[$room_num]) && isset($pax_data[$room_num][$use_guest_ind])) {
2177 if (isset($pax_data[$room_num][$use_guest_ind][$key])) {
2178 // we've got a value previously stored
2179 return $pax_data[$room_num][$use_guest_ind][$key];
2180 }
2181 }
2182
2183 // nothing was found
2184 return null;
2185 }
2186
2187 /**
2188 * Helper method to determine the exact number for this guest in the room booked.
2189 *
2190 * @param array $guests list of total guests per room.
2191 * @param int $guest_ind the guest index.
2192 *
2193 * @return int the actual guest room index starting from 1.
2194 */
2195 protected function calcGuestRoomIndex($guests, $guest_ind)
2196 {
2197 // find room index for this guest number
2198 $room_num = 0;
2199 $use_guest_ind = $guest_ind;
2200 foreach ($guests as $room_index => $room_tot_guests) {
2201 // find the proper guest index for the room to which this belongs
2202 if ($use_guest_ind <= $room_tot_guests) {
2203 // proper room index found for this guest
2204 $room_num = $room_index;
2205 break;
2206 } else {
2207 // it's probably in a next room
2208 $use_guest_ind -= $room_tot_guests;
2209 }
2210 }
2211
2212 return $use_guest_ind;
2213 }
2214
2215 /**
2216 * Parses the file Nazioni.csv and returns an associative
2217 * array with the code and name of the Nazione.
2218 * Every line of the CSV is composed of: Codice, Nazione.
2219 *
2220 * @return array
2221 */
2222 protected function loadItalianPoliceCountries()
2223 {
2224 $nazioni = [];
2225
2226 $csv = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Nazioni.csv';
2227 $rows = file($csv);
2228 foreach ($rows as $row) {
2229 if (empty($row)) {
2230 continue;
2231 }
2232
2233 $v = explode(';', $row);
2234 if (count($v) != 3) {
2235 continue;
2236 }
2237
2238 $country_id = trim($v[0]);
2239 $country_name = trim($v[1]);
2240 $country_code = trim($v[2]);
2241
2242 $nazioni[$country_id]['name'] = $country_name;
2243 $nazioni[$country_id]['three_code'] = $country_code;
2244 }
2245
2246 return $nazioni;
2247 }
2248
2249 /**
2250 * Counts the number of occupied rooms on the given day.
2251 *
2252 * @param int $day_ts The day timestamp (at midnight).
2253 * @param array $listings Optional list of filtered listing IDs.
2254 *
2255 * @return int Number of occupied rooms found.
2256 */
2257 protected function countDayRoomsOccupied($day_ts, array $listings = [])
2258 {
2259 // get the timestamp at the last second of the day
2260 $day_ts = strtotime('23:59:59', $day_ts);
2261
2262 $dbo = JFactory::getDbo();
2263
2264 if ($listings) {
2265 // perform a more accurate query over the listings specified
2266 $dbo->setQuery(
2267 $dbo->getQuery(true)
2268 ->select('COUNT(*)')
2269 ->from($dbo->qn('#__vikbooking_orders', 'o'))
2270 ->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('or.idorder'))
2271 ->where($dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'))
2272 ->where($dbo->qn('o.closure') . ' = 0')
2273 ->where($dbo->qn('o.checkin') . ' < ' . (int) $day_ts)
2274 ->where($dbo->qn('o.checkout') . ' > ' . (int) $day_ts)
2275 ->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $listings)) . ')')
2276 );
2277 } else {
2278 // quick query to fetch the total number of occupied rooms
2279 $dbo->setQuery(
2280 $dbo->getQuery(true)
2281 ->select('SUM(' . $dbo->qn('roomsnum') . ')')
2282 ->from($dbo->qn('#__vikbooking_orders'))
2283 ->where($dbo->qn('status') . ' = ' . $dbo->q('confirmed'))
2284 ->where($dbo->qn('closure') . ' = 0')
2285 ->where($dbo->qn('checkin') . ' < ' . (int) $day_ts)
2286 ->where($dbo->qn('checkout') . ' > ' . (int) $day_ts)
2287 );
2288 }
2289
2290 return (int) $dbo->loadResult();
2291 }
2292
2293 /**
2294 * Parses a report row into an associative list of key-value pairs.
2295 *
2296 * @param array $row The report row to parse.
2297 *
2298 * @return array Associative list of row keys and values.
2299 */
2300 protected function getAssocRowData(array $row)
2301 {
2302 $row_data = [];
2303
2304 foreach ($row as $field) {
2305 $field_val = $field['value'];
2306 if (!($field['no_export_callback'] ?? 0) && is_callable($field['callback_export'] ?? null)) {
2307 $field_val = $field['callback_export']($field_val);
2308 }
2309 $row_data[$field['key']] = $field_val;
2310 }
2311
2312 return $row_data;
2313 }
2314 }
2315