PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
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.7, at admin/helpers/report/istat_ispat.php

2,308 lines 72.5 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 = ieri
386 maxDate: "-1d",
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) && isset($foreign_countries_map[$group_key])) {
1179 // foreign country full name expected
1180 $targaNode = '<targa>' . htmlspecialchars(strtoupper($foreign_countries_map[$group_key])) . '</targa>' . "\n";
1181 } elseif (!$is_italian && !empty($group_key) && array_search($group_key, $this->foreignCountryCodes) !== false) {
1182 // 3-char country name expected from ISTAT country code
1183 $istat_ccode = array_search($group_key, $this->foreignCountryCodes);
1184 $targaNode = '<targa>' . htmlspecialchars(strtoupper($this->foreignCountries[$istat_ccode])) . '</targa>' . "\n";
1185 } elseif (!$is_italian && !empty($group_key) && array_search($group_key, $this->foreignCountries) !== false) {
1186 // full country name expected from ISPAT country code
1187 $istat_ccode = array_search($group_key, $this->foreignCountries);
1188 $targaNode = '<targa>' . htmlspecialchars(strtoupper($this->foreignCountries[$istat_ccode])) . '</targa>' . "\n";
1189 }
1190
1191 // set "movimento" child nodes
1192 $data['xml_c59_full'] .= '<arrivi>' . $group_tot_arrivals . '</arrivi>' . "\n";
1193 $data['xml_c59_full'] .= '<italia>' . intval($is_italian) . '</italia>' . "\n";
1194 $data['xml_c59_full'] .= '<partenze>' . $group_tot_departures . '</partenze>' . "\n";
1195 if ($targaNode) {
1196 // the "targa" node is optional
1197 $data['xml_c59_full'] .= $targaNode;
1198 }
1199
1200 // finalize movimento node
1201 $data['xml_c59_full'] .= '</movimenti>' . "\n";
1202 }
1203 }
1204
1205 // set overall arrivals
1206 $data['xml_c59_full'] .= '<totaleArrivi>' . $data['tot_arrivals'] . '</totaleArrivi>' . "\n";
1207 // set overall departures
1208 $data['xml_c59_full'] .= '<totalePartenze>' . $data['tot_departures'] . '</totalePartenze>' . "\n";
1209 // set overall stays
1210 $data['xml_c59_full'] .= '<totalePresenti>' . $data['tot_stays'] . '</totalePresenti>' . "\n";
1211 // set tot rooms available
1212 $data['xml_c59_full'] .= '<unitaAbitativeDisponibili>' . $data['tot_rooms'] . '</unitaAbitativeDisponibili>' . "\n";
1213 // set tot rooms booked
1214 $data['xml_c59_full'] .= '<unitaAbitativeOccupate>' . $data['tot_rooms_occupied'] . '</unitaAbitativeOccupate>' . "\n";
1215
1216 // finalize C59 node
1217 $data['xml_c59_full'] .= '</c59>' . "\n";
1218
1219 return $data;
1220 }
1221
1222 /**
1223 * Generates the text file for ISTAT Valle d'Aosta, then sends it to output for download.
1224 * In case of errors, the process is not terminated (exit) to let the View display the
1225 * error message(s). The export type argument can eventually determine an action to run.
1226 *
1227 * @param string $export_type Differentiates the type of export requested.
1228 *
1229 * @return void|bool Void in case of script termination, boolean otherwise.
1230 */
1231 public function customExport($export_type = null)
1232 {
1233 // build the record data
1234 $data = $this->buildRecordData();
1235
1236 // build report action data, if needed
1237 $action_data = array_merge($this->getActionData($registry = false), $data);
1238
1239 /**
1240 * Custom export method can run a custom action.
1241 */
1242 if ($export_type && !is_numeric($export_type)) {
1243 try {
1244 // ensure the type of export is a callable scoped action, hidden or visible
1245 $actions = $this->getScopedActions($this->getScope(), $visible = false);
1246 $action_ids = array_column($actions, 'id');
1247 $action_names = array_column($actions, 'name');
1248 if (!in_array($export_type, $action_ids)) {
1249 throw new Exception(sprintf('Cannot invoke the requested type of export [%s].', $export_type), 403);
1250 }
1251
1252 // get the requested action readable name
1253 $action_name = $action_names[array_search($export_type, $action_ids)];
1254
1255 if ($this->getScope() === 'web') {
1256 // run the action and output the HTML response string
1257 $html_result = $this->_callActionReturn($export_type, 'html', $this->getScope(), $action_data);
1258
1259 // build the action result data object
1260 $js_result = json_encode([
1261 'actionName' => $action_name,
1262 'actionHtml' => $html_result,
1263 ]);
1264
1265 // render modal script with the action result
1266 JFactory::getDocument()->addScriptDeclaration(
1267 <<<JS
1268 ;(function($) {
1269 $(function() {
1270 let result = $js_result;
1271 VBOCore.displayModal({
1272 suffix: 'report-custom-scopedaction-result',
1273 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1274 title: result.actionName,
1275 body: result.actionHtml,
1276 });
1277 });
1278 })(jQuery);
1279 JS
1280 );
1281
1282 // abort and let the View render the result within a modal
1283 return;
1284 }
1285
1286 // let the report custom action run and return a boolean value if invoked by a cron
1287 return (bool) $this->_callActionReturn($export_type, 'success', $this->getScope(), $action_data);
1288
1289 } catch (Exception $e) {
1290 // silently catch the error and set it
1291 $this->setError($e->getMessage());
1292
1293 // abort
1294 return false;
1295 }
1296 }
1297
1298 // proceed with the regular export function (write on file through cron or download file through web)
1299
1300 if (!$data || empty($data['xml_c59_full'])) {
1301 // abort
1302 return false;
1303 }
1304
1305 // update history for all bookings affected before exporting
1306 foreach ($this->export_booking_ids as $bid) {
1307 VikBooking::getBookingHistoryInstance($bid)->store('RP', $this->reportName . ' - Export');
1308 }
1309
1310 /**
1311 * Attempt to format the XML string for the C59 node to have a better result.
1312 */
1313 $formattedC59 = $this->loadXmlSoap($data['xml_c59_full'])->formatXml() ?: $data['xml_c59_full'];
1314
1315 /**
1316 * Custom export method supports a custom export handler, if previously set.
1317 */
1318 if ($this->hasExportHandler()) {
1319 // write data onto the custom file handler
1320 $fp = $this->getExportCSVHandler();
1321 fwrite($fp, $formattedC59);
1322 fclose($fp);
1323
1324 // return true as data was written
1325 return true;
1326 }
1327
1328 // force text file download in case of regular export
1329 header("Content-type: text/xml");
1330 header("Cache-Control: no-store, no-cache");
1331 header('Content-Disposition: attachment; filename="' . $this->getExportCSVFileName() . '"');
1332 echo $formattedC59;
1333
1334 exit;
1335 }
1336
1337 /**
1338 * Helper method invoked via AJAX by the controller.
1339 * Needed to save the manual entries for the pax data.
1340 *
1341 * @param array $manual_data the object representation of the manual entries.
1342 *
1343 * @return array one boolean value array with the operation result.
1344 */
1345 public function updatePaxData($manual_data = [])
1346 {
1347 if (!is_array($manual_data) || !$manual_data) {
1348 VBOHttpDocument::getInstance()->close(400, 'Nothing to save!');
1349 }
1350
1351 // re-build manual entries object representation
1352 $bids_guests = [];
1353 foreach ($manual_data as $guest_ind => $guest_data) {
1354 if (!is_numeric($guest_ind) || !is_array($guest_data) || empty($guest_data['bid']) || !isset($guest_data['bid_index']) || count($guest_data) < 2) {
1355 // empty or invalid manual entries array
1356 continue;
1357 }
1358 // the guest index in the reportObj starts from 0
1359 $use_guest_ind = ($guest_ind + 1 - (int)$guest_data['bid_index']);
1360 if (!isset($bids_guests[$guest_data['bid']])) {
1361 $bids_guests[$guest_data['bid']] = [];
1362 }
1363 // set manual entries for this guest number
1364 $bids_guests[$guest_data['bid']][$use_guest_ind] = $guest_data;
1365 // remove the "bid" and "bid_index" keys
1366 unset($bids_guests[$guest_data['bid']][$use_guest_ind]['bid'], $bids_guests[$guest_data['bid']][$use_guest_ind]['bid_index']);
1367 }
1368
1369 if (!$bids_guests) {
1370 VBOHttpDocument::getInstance()->close(400, 'No manual entries to save found');
1371 }
1372
1373 // loop through all bookings to update the data for the various rooms and guests
1374 $bids_updated = 0;
1375 foreach ($bids_guests as $bid => $entries) {
1376 $b_rooms = VikBooking::loadOrdersRoomsData($bid);
1377 if (empty($b_rooms)) {
1378 continue;
1379 }
1380 // count guests per room
1381 $room_guests = [];
1382 foreach ($b_rooms as $b_room) {
1383 $room_guests[] = $b_room['adults'] + $b_room['children'];
1384 }
1385 // get current booking pax data
1386 $pax_data = VBOCheckinPax::getBookingPaxData($bid);
1387 $pax_data = empty($pax_data) ? [] : $pax_data;
1388 foreach ($entries as $guest_ind => $guest_data) {
1389 // find room index for this guest
1390 $room_num = 0;
1391 $use_guest_ind = $guest_ind;
1392 foreach ($room_guests as $room_index => $tot_guests) {
1393 // find the proper guest index for the room to which this belongs
1394 if ($use_guest_ind <= $tot_guests) {
1395 // proper room index found for this guest
1396 $room_num = $room_index;
1397 break;
1398 } else {
1399 // it's probably in a next room
1400 $use_guest_ind -= $tot_guests;
1401 }
1402 }
1403 // push new pax data for this room and guest
1404 if (!isset($pax_data[$room_num])) {
1405 $pax_data[$room_num] = [];
1406 }
1407 if (!isset($pax_data[$room_num][$use_guest_ind])) {
1408 $pax_data[$room_num][$use_guest_ind] = $guest_data;
1409 } else {
1410 $pax_data[$room_num][$use_guest_ind] = array_merge($pax_data[$room_num][$use_guest_ind], $guest_data);
1411 }
1412 }
1413 // update booking pax data
1414 if (VBOCheckinPax::setBookingPaxData($bid, $pax_data)) {
1415 $bids_updated++;
1416 }
1417 }
1418
1419 return $bids_updated ? [true] : [false];
1420 }
1421
1422 /**
1423 * Returns an associative list of states/provinces for the given country.
1424 * Visibility should be public in case other ISTAT reports for Italy may
1425 * require to load the same provinces.
1426 *
1427 * @return array
1428 */
1429 public function loadCountryStates($country)
1430 {
1431 $country_states = [];
1432
1433 foreach (VBOStateHelper::getCountryStates((int) VBOStateHelper::getCountryId($country)) as $state_record) {
1434 $country_states[$state_record['state_2_code']] = $state_record['state_name'];
1435 }
1436
1437 return $country_states;
1438 }
1439
1440 /**
1441 * Returns an associative list of ISTAT foreign country codes. The full list includes the country
1442 * codes compatible with the Alloggiati Web integration (ISTAT codes), as well as a merged list
1443 * of country "false" codes (negative keys) that are supported and required by ISPAT.
1444 *
1445 * @param string $type The string "3" can be passed to get the known 3-char country codes.
1446 *
1447 * @return array Associative list of countries, according to type param.
1448 */
1449 public function loadIstatForeignCountries($type = '')
1450 {
1451 if ($type === '3') {
1452 // return the list for only the known country ISO code (3-char)
1453 return [
1454 '528' => "ARG",
1455 '800' => "AUS",
1456 '038' => "AUT",
1457 '017' => "BEL",
1458 '508' => "BRA",
1459 '068' => "BGR",
1460 '404' => "CAN",
1461 '720' => "CHN",
1462 '600' => "CYP",
1463 '728' => "KOR",
1464 '092' => "HRV",
1465 '008' => "DNK",
1466 '220' => "EGY",
1467 '053' => "EST",
1468 '032' => "FIN",
1469 '001' => "FRA",
1470 '004' => "DEU",
1471 '732' => "JPN",
1472 '009' => "GRC",
1473 '664' => "IND",
1474 '007' => "IRL",
1475 '024' => "ISL",
1476 '624' => "ISR",
1477 '999' => "ITA",
1478 '054' => "LVA",
1479 '055' => "LTU",
1480 '018' => "LUX",
1481 '046' => "MLT",
1482 '412' => "MEX",
1483 '028' => "NOR",
1484 '804' => "NZL",
1485 '230' => "MED",
1486 '003' => "NLD",
1487 '060' => "POL",
1488 '010' => "PRT",
1489 '006' => "GBR",
1490 '061' => "CZE",
1491 '066' => "ROU",
1492 '075' => "RUS",
1493 '063' => "SVK",
1494 '091' => "SVN",
1495 '011' => "ESP",
1496 '400' => "USA",
1497 '388' => "ZAF",
1498 '030' => "SWE",
1499 '036' => "CHE",
1500 '052' => "TUR",
1501 '072' => "UKR",
1502 '064' => "HUN",
1503 '484' => "VEN",
1504 ];
1505 }
1506
1507 // return the whole ISTAT + ISPAT (negative keys) list
1508 return [
1509 '-990' => "AFGHANISTAN",
1510 '-991' => "ALBANIA",
1511 '-992' => "ALGERIA",
1512 '-993' => "ANDORRA",
1513 '-994' => "ANGOLA",
1514 '-995' => "ANTIGUA E BARBUDA",
1515 '-996' => "ARABIA SAUDITA",
1516 '528' => "ARGENTINA",
1517 '-997' => "ARMENIA",
1518 '800' => "AUSTRALIA",
1519 '038' => "AUSTRIA",
1520 '-998' => "AZERBAIGIAN",
1521 '-999' => "BAHAMAS",
1522 '-9910' => "BAHREIN",
1523 '-9911' => "BANGLADESH",
1524 '-9912' => "BARBADOS",
1525 '017' => "BELGIO",
1526 '-9913' => "BELIZE",
1527 '-9914' => "BENIN",
1528 '-9915' => "BHUTAN",
1529 '-9916' => "BIELORUSSIA",
1530 '-9917' => "BOLIVIA",
1531 '-9918' => "BOSNIA-ERZEGOVINA",
1532 '-9919' => "BOTSWANA",
1533 '508' => "BRASILE",
1534 '-9920' => "BRUNEI",
1535 '068' => "BULGARIA",
1536 '-9921' => "BURKINA-FASO",
1537 '-9922' => "BURUNDI",
1538 '-9923' => "CAMBOGIA",
1539 '-9924' => "CAMERUN",
1540 '404' => "CANADA",
1541 '-9925' => "CAPO VERDE",
1542 '-9926' => "CIAD",
1543 '-9927' => "CILE",
1544 '600' => "CIPRO",
1545 '-9928' => "COLOMBIA",
1546 '-9929' => "COMORE",
1547 '-9930' => "CONGO",
1548 '-9931' => "COSTA D'AVORIO",
1549 '-9932' => "COSTARICA",
1550 '092' => "CROAZIA",
1551 '-9933' => "CUBA",
1552 '008' => "DANIMARCA",
1553 '-9934' => "DOMINICA",
1554 '-9935' => "ECUADOR",
1555 '220' => "EGITTO",
1556 '-9936' => "EL SALVADOR",
1557 '-9937' => "EMIRATI ARABI UNITI",
1558 '-9938' => "ERITREA",
1559 '053' => "ESTONIA",
1560 '-9939' => "ETIOPIA",
1561 '075' => "FEDERAZIONE RUSSA",
1562 '-9940' => "FIGI",
1563 '-9941' => "FILIPPINE",
1564 '032' => "FINLANDIA",
1565 '001' => "FRANCIA",
1566 '-9942' => "GABON",
1567 '-9943' => "GAMBIA",
1568 '-9944' => "GEORGIA",
1569 '004' => "GERMANIA",
1570 '-9945' => "GHANA",
1571 '-9946' => "GIAMAICA",
1572 '732' => "GIAPPONE",
1573 '-9947' => "GIBUTI",
1574 '-9948' => "GIORDANIA",
1575 '009' => "GRECIA",
1576 '-9949' => "GRENADA",
1577 '-9950' => "GUATEMALA",
1578 '-9951' => "GUINEA",
1579 '-9952' => "GUINEA BISSAU",
1580 '-9953' => "GUINEA EQUATORIALE",
1581 '-9954' => "GUYANA",
1582 '-9955' => "HAITI",
1583 '-9956' => "HONDURAS",
1584 '664' => "INDIA",
1585 '-9957' => "INDONESIA",
1586 '-9958' => "IRAN, REPUBBLICA ISLAMICA DEL",
1587 '-9959' => "IRAQ",
1588 '007' => "IRLANDA",
1589 '024' => "ISLANDA",
1590 '624' => "ISRAELE",
1591 '999' => "ITALIA",
1592 '-9960' => "JERSEY, ISOLE",
1593 '-9961' => "KAZAKISTAN",
1594 '-9962' => "KENIA",
1595 '-9963' => "KIRGHIZISTAN",
1596 '-9964' => "KIRIBATI",
1597 '-9965' => "KOSOVO",
1598 '-9966' => "KUWAIT",
1599 '-9967' => "LAOS",
1600 '-9968' => "LESOTHO",
1601 '054' => "LETTONIA",
1602 '-9969' => "LIBANO",
1603 '-9970' => "LIBERIA",
1604 '-9971' => "LIBIA",
1605 '-9972' => "LIECHTENSTEIN",
1606 '055' => "LITUANIA",
1607 '018' => "LUSSEMBURGO",
1608 '-9973' => "MACEDONIA (EX REPUBBLICA JUGOSLAVA)",
1609 '-9974' => "MADAGASCAR",
1610 '-9975' => "MALAWI",
1611 '-9976' => "MALAYSIA",
1612 '-9977' => "MALDIVE",
1613 '-9978' => "MALI",
1614 '046' => "MALTA",
1615 '-9979' => "MAROCCO",
1616 '-9980' => "MARSHALL",
1617 '-9981' => "MAURITANIA",
1618 '-9982' => "MAURITIUS",
1619 '412' => "MESSICO",
1620 '-9983' => "MICRONESIA",
1621 '-9984' => "MOLDOVA",
1622 '-9985' => "MONACO",
1623 '-9986' => "MONGOLIA",
1624 '-9987' => "MONTENEGRO",
1625 '-9988' => "MOZAMBICO",
1626 '-9989' => "MYANMAR",
1627 '-9990' => "NAMIBIA",
1628 '-9991' => "NAURU",
1629 '-9992' => "NEPAL",
1630 '-9993' => "NICARAGUA",
1631 '-9994' => "NIGER",
1632 '-9995' => "NIGERIA",
1633 '028' => "NORVEGIA",
1634 '804' => "NUOVA ZELANDA",
1635 '-9996' => "OMAN",
1636 '230' => "PAESI AFRICA MEDITERRANEA",
1637 '003' => "PAESI BASSI",
1638 '-9997' => "PAKISTAN",
1639 '-9998' => "PALAU",
1640 '-9999' => "PANAMA",
1641 '-99100' => "PAPUA NUOVA GUINEA",
1642 '-99101' => "PARAGUAY",
1643 '-99102' => "PERU'",
1644 '060' => "POLONIA",
1645 '010' => "PORTOGALLO",
1646 '-99103' => "QATAR",
1647 '006' => "REGNO UNITO",
1648 '-99104' => "REP.POPOLARE DEMOCRATICA DI COREA (COREA DEL NORD)",
1649 '061' => "REPUBBLICA CECA",
1650 '-99105' => "REPUBBLICA CENTRAFRICANA",
1651 '-99106' => "REPUBBLICA DEMOCRATICA DEL CONGO (EX ZAIRE)",
1652 '728' => "REPUBBLICA DI COREA (COREA DEL SUD)",
1653 '-99107' => "REPUBBLICA DOMINICANA",
1654 '720' => "REPUBBLICA POPOLARE CINESE",
1655 '063' => "REPUBBLICA SLOVACCA",
1656 '-99108' => "RICONOSCIUTI NON CITTADINI (LETTONI)",
1657 '066' => "ROMANIA",
1658 '-99110' => "S. VINCENT E GRENADINE",
1659 '-99111' => "SAINT KITTS E NEVIS",
1660 '-99112' => "SAINT LUCIA",
1661 '-99113' => "SALOMONE",
1662 '-99114' => "SAMOA",
1663 '-99115' => "SAN MARINO",
1664 '-99116' => "SANTA SEDE",
1665 '-99117' => "SAO TOME' E PRINCIPE",
1666 '-99118' => "SENEGAL",
1667 '-99119' => "SERBIA REPUBBLICA DI",
1668 '-99120' => "SEYCHELLES",
1669 '-99121' => "SIERRA LEONE",
1670 '-99122' => "SINGAPORE",
1671 '-99123' => "SIRIA",
1672 '091' => "SLOVENIA",
1673 '-99124' => "SOMALIA",
1674 '011' => "SPAGNA",
1675 '-99125' => "SRI LANKA (CEYLON)",
1676 '400' => "STATI UNITI D'AMERICA",
1677 '388' => "SUD AFRICA",
1678 '-99126' => "SUD SUDAN, REPUBBLICA DEL",
1679 '-99127' => "SUDAN",
1680 '-99128' => "SURINAME",
1681 '030' => "SVEZIA",
1682 '036' => "SVIZZERA",
1683 '-99129' => "SWAZILAND",
1684 '-99130' => "TAGIKISTAN",
1685 '-99131' => "TAIWAN",
1686 '-99132' => "TANZANIA",
1687 '-99133' => "TERRITORI AUTONOMIA PALESTINESE",
1688 '-99134' => "THAILANDIA",
1689 '-99135' => "TIMOR ORIENTALE",
1690 '-99136' => "TOGO",
1691 '-99137' => "TONGA",
1692 '-99138' => "TRINIDAD E TOBAGO",
1693 '-99139' => "TUNISIA",
1694 '052' => "TURCHIA",
1695 '-99140' => "TURKMENISTAN",
1696 '-99141' => "TUVALU",
1697 '-99109' => "UANDA",
1698 '072' => "UCRAINA",
1699 '-99142' => "UGANDA",
1700 '064' => "UNGHERIA",
1701 '-99143' => "URUGUAY",
1702 '-99144' => "UZBEKISTAN",
1703 '-99145' => "VANUATU",
1704 '484' => "VENEZUELA",
1705 '-99146' => "VIETNAM",
1706 '-99147' => "YEMEN",
1707 '-99148' => "ZAMBIA",
1708 '-99149' => "ZIMBABWE",
1709 '777' => "ALTRI PAESI",
1710 '300' => "ALTRI PAESI AFRICA",
1711 '530' => "ALTRI PAESI AMERICA LATINA",
1712 '760' => "ALTRI PAESI DELL'ASIA",
1713 '100' => "ALTRI PAESI EUROPEI",
1714 '750' => "ALTRI PAESI MEDIO ORIENTE",
1715 '410' => "ALTRI PAESI NORD AMERICA",
1716 '810' => "ALTRI PAESI OCEANIA",
1717 ];
1718 }
1719
1720 /**
1721 * Custom scoped action to upload the guest presence records.
1722 * Accepted scopes are "web" and "cron", so the "success" property must be returned.
1723 *
1724 * @param string $scope Optional scope identifier (cron, web, etc..).
1725 * @param array $data Optional associative list of data to process.
1726 *
1727 * @return array The execution result properties.
1728 *
1729 * @throws Exception
1730 */
1731 protected function uploadC59Full($scope = null, array $data = [])
1732 {
1733 if (!($data['xml_c59_full'] ?? null) && $scope === 'web') {
1734 // start the process through the interface by submitting the current data
1735 return [
1736 'html' => '<script type="text/javascript">vboDownloadRecordIspat(\'uploadC59Full\');</script>',
1737 ];
1738 }
1739
1740 if (!($data['xml_c59_full'] ?? null)) {
1741 // attempt to build the report data if not set
1742 $data = array_merge($data, $this->buildRecordData());
1743 }
1744
1745 if (empty($data['xml_c59_full'])) {
1746 throw new Exception('Nessun dato presente per la trasmissione dei record delle presenze turistiche.', 500);
1747 }
1748
1749 $settings = $this->loadSettings();
1750
1751 if (!$settings || empty($settings['propertyid']) || empty($settings['user']) || empty($settings['pwd'])) {
1752 throw new Exception(sprintf('[%s] error: missing settings.', __METHOD__), 500);
1753 }
1754
1755 // determine the WS url to use
1756 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
1757
1758 // build the Soap request message
1759 $request = '<?xml version="1.0" encoding="utf-8"?>
1760 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mov="http://movimentazione.manager.web.module.dtu.infotn.it/">
1761 <soapenv:Header/>
1762 <soapenv:Body>
1763 <mov:inviaC59Full>
1764 <username>' . $settings['user'] . '</username>
1765 <password>' . $settings['pwd'] . '</password>
1766 <struttura>' . $settings['propertyid'] . '</struttura>
1767 ' . $data['xml_c59_full'] . '
1768 </mov:inviaC59Full>
1769 </soapenv:Body>
1770 </soapenv:Envelope>';
1771
1772 try {
1773 // get the SoapClient object from WSDL
1774 $ws_client = $this->getWebServiceClient($settings);
1775
1776 // do the request and get the Soap XML response message
1777 $response = $ws_client->__doRequest($request, preg_replace('/\?wsdl$/i', '', $use_ws_url), 'inviaC59Full', SOAP_1_2);
1778
1779 // wrap the response into an object
1780 $xmlResponse = $this->loadXmlSoap($response);
1781
1782 // check for response errors
1783 $responseFault = $xmlResponse->getSoapRecursiveNsElement([
1784 ['soap', true, 'Body'],
1785 ['soap', true, 'Fault'],
1786 ]);
1787
1788 if (in_array(@$responseFault->getName(), ['faultcode', 'faultstring'])) {
1789 // errors returned
1790 $fault_code = $responseFault->faultcode ?? 0;
1791 $fault_mess = $responseFault->faultstring ?? '';
1792 throw new Exception(sprintf("Errore (%s):\n%s.", $fault_code, $fault_mess), 500);
1793 }
1794
1795 // build HTML response string
1796 $html = '';
1797 $html .= '<p class="successmade">Totale registrazioni analizzate: ' . count($data['bookings']) . '</p>';
1798 } catch (Exception $e) {
1799 // propagate the error caught
1800 throw new Exception(sprintf('[%s] error: %s', __METHOD__, $e->getMessage()), $e->getCode() ?: 500);
1801 }
1802
1803 // update history for all bookings affected
1804 foreach ($this->export_booking_ids as $bid) {
1805 // build extra data payload for the history event
1806 $hdata = [
1807 'transmitted' => 1,
1808 'method' => 'uploadC59Full',
1809 'report' => $this->getFileName(),
1810 ];
1811 // store booking history event
1812 VikBooking::getBookingHistoryInstance($bid)
1813 ->setExtraData($hdata)
1814 ->store('RP', $this->reportName . ' - Trasmissione presenze turistiche');
1815 }
1816
1817 // get the currently active profile ID, if any
1818 $activeProfileId = $this->getActiveProfile();
1819
1820 /**
1821 * When the report is executed through a cron-job, the transmission of the guest details
1822 * will also dump the data transmitted onto a resource file, useful for sending it via email.
1823 */
1824 if ($scope === 'cron') {
1825 // prepare the XML resource file information
1826 $xml_name = implode('_', array_filter([$this->getFileName(), 'trasmissione', 'flussi', time(), $activeProfileId, rand(100, 999)])) . '.xml';
1827 $xml_dest = $this->getDataMediaPath() . DIRECTORY_SEPARATOR . $xml_name;
1828 $xml_url = $this->getDataMediaUrl() . $xml_name;
1829
1830 // store the XML bytes into a local file on disk
1831 $stored = JFile::write($xml_dest, $soap_xml);
1832
1833 if ($stored && VBOPlatformDetection::isWordPress()) {
1834 /**
1835 * Trigger files mirroring operation
1836 */
1837 VikBookingLoader::import('update.manager');
1838 VikBookingUpdateManager::triggerUploadBackup($xml_dest);
1839 }
1840
1841 if ($stored) {
1842 // define a new report resource for the generated file
1843 $this->defineResourceFile([
1844 'summary' => sprintf(
1845 '%sFlussi turistici generati e trasmessi con successo.',
1846 ($activeProfileId ? '(' . ucwords($activeProfileId) . ') ' : '')
1847 ),
1848 'url' => $xml_url,
1849 'path' => $xml_dest,
1850 ]);
1851 }
1852 }
1853
1854 // when executed through a cron, store an event in the Notifications Center
1855 if ($scope === 'cron') {
1856 // build the notification record
1857 $notification = [
1858 'sender' => 'reports',
1859 'type' => 'pmsreport.transmit.ok',
1860 'title' => $this->reportName . ' - Trasmissione presenze turistiche',
1861 'summary' => sprintf(
1862 '%sSono stati trasmessi i dati degli ospiti per la data %s. Prenotazioni analizzate: %d.',
1863 ($activeProfileId ? '(' . ucwords($activeProfileId) . ') ' : ''),
1864 $this->exported_checkin_dates[0] ?? '',
1865 count($data['bookings'])
1866 ),
1867 ];
1868
1869 try {
1870 // store the notification record
1871 VBOFactory::getNotificationCenter()->store([$notification]);
1872 } catch (Exception $e) {
1873 // silently catch the error without doing anything
1874 }
1875 }
1876
1877 return [
1878 'html' => $html,
1879 'success' => true,
1880 'bookings' => $data['bookings'],
1881 ];
1882 }
1883
1884 /**
1885 * Custom scoped action to get a list of properties ("listaStrutture").
1886 *
1887 * @param string $scope Optional scope identifier (cron, web, etc..).
1888 * @param array $data Optional associative list of data to process.
1889 *
1890 * @return array The execution result properties.
1891 *
1892 * @throws Exception
1893 */
1894 protected function listaStrutture($scope = null, array $data = [])
1895 {
1896 $settings = $this->loadSettings();
1897
1898 if (!$settings || empty($settings['user']) || empty($settings['pwd'])) {
1899 throw new Exception(sprintf('[%s] error: missing settings.', __METHOD__), 500);
1900 }
1901
1902 // determine the WS url to use
1903 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
1904
1905 // build the Soap request message
1906 $request = '<?xml version="1.0" encoding="utf-8"?>
1907 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mov="http://movimentazione.manager.web.module.dtu.infotn.it/">
1908 <soapenv:Header/>
1909 <soapenv:Body>
1910 <mov:listaStrutture>
1911 <username>' . $settings['user'] . '</username>
1912 <password>' . $settings['pwd'] . '</password>
1913 </mov:listaStrutture>
1914 </soapenv:Body>
1915 </soapenv:Envelope>';
1916
1917 try {
1918 // get the SoapClient object from WSDL
1919 $ws_client = $this->getWebServiceClient($settings);
1920
1921 // do the request and get the Soap XML response message
1922 $response = $ws_client->__doRequest($request, preg_replace('/\?wsdl$/i', '', $use_ws_url), 'listaStrutture', SOAP_1_2);
1923
1924 // wrap the response into an object
1925 $xmlResponse = $this->loadXmlSoap($response);
1926
1927 // check for response errors
1928 $responseFault = $xmlResponse->getSoapRecursiveNsElement([
1929 ['soap', true, 'Body'],
1930 ['soap', true, 'Fault'],
1931 ]);
1932
1933 if (isset($responseFault->faultstring) && strlen((string) $responseFault->faultstring)) {
1934 // errors returned
1935 $fault_code = $responseFault->faultcode ?? 0;
1936 $fault_mess = $responseFault->faultstring ?? '';
1937 throw new Exception(sprintf("Errore (%s):\n%s.", $fault_code, $fault_mess), 500);
1938 }
1939
1940 // access the response body
1941 $xmlBody = $xmlResponse->getSoapRecursiveNsElement([
1942 ['soap', true, 'Body'],
1943 ['ns2', true, 'listaStruttureResponse'],
1944 ]);
1945
1946 if (!isset($xmlBody->return)) {
1947 // no properties returned
1948 throw new UnexpectedValueException(sprintf('Nessuna struttura trovata [%s]', $xmlBody->formatXml() ?: $response), 500);
1949 }
1950
1951 // list of properties found
1952 $properties = [];
1953
1954 // build HTML response string
1955 $html = '';
1956 $html .= '<p class="successmade">Lista strutture assegnate:</p>';
1957 $html .= '<ol>';
1958
1959 // scan all properties returned
1960 foreach ($xmlBody->return as $returnNode) {
1961 // get the property name and id
1962 $property_id = (string) ($returnNode->struttura ?? 0);
1963 $property_name = (string) ($returnNode->name ?? '');
1964
1965 // push property found
1966 $properties[$property_id] = $property_name;
1967
1968 // get the readable properties name and id
1969 $property_id_read = (string) ($returnNode->struttura ?? '??');
1970 $property_name_read = (string) ($returnNode->name ?? '----');
1971
1972 // build HTML property string
1973 $html .= '<li>' . sprintf('ID <strong>%s</strong> - <strong>%s</strong>', $property_id_read, $property_name_read) . '</li>';
1974 }
1975
1976 // finalize HTML
1977 $html .= '</ol>';
1978
1979 } catch (Exception $e) {
1980 // propagate the error caught
1981 throw new Exception(sprintf('[%s] error: %s', __METHOD__, $e->getMessage()), $e->getCode() ?: 500);
1982 }
1983
1984 return [
1985 'html' => $html,
1986 'properties' => $properties,
1987 ];
1988 }
1989
1990 /**
1991 * Custom scoped action to get the last C59 transmission uploaded ("ultimoC59").
1992 *
1993 * @param string $scope Optional scope identifier (cron, web, etc..).
1994 * @param array $data Optional associative list of data to process.
1995 *
1996 * @return array The execution result properties.
1997 *
1998 * @throws Exception
1999 */
2000 protected function ultimoC59($scope = null, array $data = [])
2001 {
2002 $settings = $this->loadSettings();
2003
2004 if (!$settings || empty($settings['user']) || empty($settings['pwd'])) {
2005 throw new Exception(sprintf('[%s] error: missing settings.', __METHOD__), 500);
2006 }
2007
2008 if (empty($settings['propertyid'])) {
2009 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);
2010 }
2011
2012 // determine the WS url to use
2013 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
2014
2015 // build the Soap request message
2016 $request = '<?xml version="1.0" encoding="utf-8"?>
2017 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mov="http://movimentazione.manager.web.module.dtu.infotn.it/">
2018 <soapenv:Header/>
2019 <soapenv:Body>
2020 <mov:ultimoC59>
2021 <username>' . $settings['user'] . '</username>
2022 <password>' . $settings['pwd'] . '</password>
2023 <struttura>' . $settings['propertyid'] . '</struttura>
2024 </mov:ultimoC59>
2025 </soapenv:Body>
2026 </soapenv:Envelope>';
2027
2028 try {
2029 // get the SoapClient object from WSDL
2030 $ws_client = $this->getWebServiceClient($settings);
2031
2032 // do the request and get the Soap XML response message
2033 $response = $ws_client->__doRequest($request, preg_replace('/\?wsdl$/i', '', $use_ws_url), 'ultimoC59', SOAP_1_2);
2034
2035 // wrap the response into an object
2036 $xmlResponse = $this->loadXmlSoap($response);
2037
2038 // check for response errors
2039 $responseFault = $xmlResponse->getSoapRecursiveNsElement([
2040 ['soap', true, 'Body'],
2041 ['soap', true, 'Fault'],
2042 ]);
2043
2044 if (isset($responseFault->faultstring) && strlen((string) $responseFault->faultstring)) {
2045 // errors returned
2046 $fault_code = $responseFault->faultcode ?? 0;
2047 $fault_mess = $responseFault->faultstring ?? '';
2048 throw new Exception(sprintf("Errore (%s):\n%s.", $fault_code, $fault_mess), 500);
2049 }
2050
2051 // access the response body
2052 $xmlBody = $xmlResponse->getSoapRecursiveNsElement([
2053 ['soap', true, 'Body'],
2054 ['ns2', true, 'ultimoC59Response'],
2055 ]);
2056
2057 if (!isset($xmlBody->return)) {
2058 // no operations returned
2059 throw new UnexpectedValueException(sprintf('Nessun movimento C59 trovato [%s]', $xmlBody->formatXml() ?: $response), 500);
2060 }
2061
2062 // build HTML response string
2063 $html = '';
2064 $html .= '<p class="successmade">Ultimo C59 disponibile nella base dati:</p>';
2065 $html .= '<pre>' . htmlspecialchars($xmlBody->formatXml() ?: $response ?: '') . '</pre>';
2066
2067 } catch (Exception $e) {
2068 // propagate the error caught
2069 throw new Exception(sprintf('[%s] error: %s', __METHOD__, $e->getMessage()), $e->getCode() ?: 500);
2070 }
2071
2072 return [
2073 'html' => $html,
2074 'ultimoC59Return' => $xmlBody->return,
2075 ];
2076 }
2077
2078 /**
2079 * Establishes a SOAP connection with the remote WSDL and returns the client.
2080 * Updates the internal properties according to settings related to the WS.
2081 *
2082 * @param array $settings List of report settings for production or test mode.
2083 *
2084 * @return SoapClient
2085 *
2086 * @throws Exception
2087 */
2088 protected function getWebServiceClient(array $settings = [])
2089 {
2090 // determine the WS url to use
2091 $use_ws_url = !empty($settings['test_mode']) ? $this->wsdl_url_test : $this->wsdl_url;
2092
2093 try {
2094 return new SoapClient($use_ws_url, [
2095 'soap_version' => constant($this->soap_version),
2096 ]);
2097 } catch (Throwable $e) {
2098 // prevent PHP fatal errors by catching and propagating them as Exceptions
2099 throw new Exception(sprintf('PHP Fatal Error: %s', $e->getMessage()), $e->getCode() ?: 500);
2100 }
2101 }
2102
2103 /**
2104 * Parses a SOAP XML message into a VBOXmlSoap object.
2105 *
2106 * @param string $xmlMessage The SOAP XML message to load.
2107 *
2108 * @return VBOXmlSoap
2109 *
2110 * @throws Exception
2111 */
2112 protected function loadXmlSoap($xmlMessage)
2113 {
2114 if (empty($xmlMessage)) {
2115 throw new Exception('Empty Soap XML message.', 500);
2116 }
2117
2118 // suppress warning messages
2119 libxml_use_internal_errors(true);
2120
2121 // parse the Soap XML message
2122 return simplexml_load_string($xmlMessage, VBOXmlSoap::class);
2123 }
2124
2125 /**
2126 * Registers the name to give to the file being exported.
2127 *
2128 * @return void
2129 */
2130 protected function registerExportFileName()
2131 {
2132 $pfromdate = VikRequest::getString('fromdate', '', 'request');
2133
2134 $this->setExportCSVFileName($this->reportName . '-' . str_replace('/', '_', $pfromdate) . '.xml');
2135 }
2136
2137 /**
2138 * Helper method to quickly get a pax_data property for the guest.
2139 *
2140 * @param array $pax_data the current pax_data stored.
2141 * @param array $guests list of total guests per room.
2142 * @param int $guest_ind the guest index.
2143 * @param string $key the pax_data key to look for.
2144 *
2145 * @return mixed null on failure or value fetched.
2146 */
2147 protected function getGuestPaxDataValue($pax_data, $guests, $guest_ind, $key)
2148 {
2149 if (!is_array($pax_data) || !$pax_data || empty($key)) {
2150 return null;
2151 }
2152
2153 // find room index for this guest number
2154 $room_num = 0;
2155 $use_guest_ind = $guest_ind;
2156 foreach ($guests as $room_index => $room_tot_guests) {
2157 // find the proper guest index for the room to which this belongs
2158 if ($use_guest_ind <= $room_tot_guests) {
2159 // proper room index found for this guest
2160 $room_num = $room_index;
2161 break;
2162 } else {
2163 // it's probably in a next room
2164 $use_guest_ind -= $room_tot_guests;
2165 }
2166 }
2167
2168 // check if a value exists for the requested key in the found room and guest indexes
2169 if (isset($pax_data[$room_num]) && isset($pax_data[$room_num][$use_guest_ind])) {
2170 if (isset($pax_data[$room_num][$use_guest_ind][$key])) {
2171 // we've got a value previously stored
2172 return $pax_data[$room_num][$use_guest_ind][$key];
2173 }
2174 }
2175
2176 // nothing was found
2177 return null;
2178 }
2179
2180 /**
2181 * Helper method to determine the exact number for this guest in the room booked.
2182 *
2183 * @param array $guests list of total guests per room.
2184 * @param int $guest_ind the guest index.
2185 *
2186 * @return int the actual guest room index starting from 1.
2187 */
2188 protected function calcGuestRoomIndex($guests, $guest_ind)
2189 {
2190 // find room index for this guest number
2191 $room_num = 0;
2192 $use_guest_ind = $guest_ind;
2193 foreach ($guests as $room_index => $room_tot_guests) {
2194 // find the proper guest index for the room to which this belongs
2195 if ($use_guest_ind <= $room_tot_guests) {
2196 // proper room index found for this guest
2197 $room_num = $room_index;
2198 break;
2199 } else {
2200 // it's probably in a next room
2201 $use_guest_ind -= $room_tot_guests;
2202 }
2203 }
2204
2205 return $use_guest_ind;
2206 }
2207
2208 /**
2209 * Parses the file Nazioni.csv and returns an associative
2210 * array with the code and name of the Nazione.
2211 * Every line of the CSV is composed of: Codice, Nazione.
2212 *
2213 * @return array
2214 */
2215 protected function loadItalianPoliceCountries()
2216 {
2217 $nazioni = [];
2218
2219 $csv = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Nazioni.csv';
2220 $rows = file($csv);
2221 foreach ($rows as $row) {
2222 if (empty($row)) {
2223 continue;
2224 }
2225
2226 $v = explode(';', $row);
2227 if (count($v) != 3) {
2228 continue;
2229 }
2230
2231 $country_id = trim($v[0]);
2232 $country_name = trim($v[1]);
2233 $country_code = trim($v[2]);
2234
2235 $nazioni[$country_id]['name'] = $country_name;
2236 $nazioni[$country_id]['three_code'] = $country_code;
2237 }
2238
2239 return $nazioni;
2240 }
2241
2242 /**
2243 * Counts the number of occupied rooms on the given day.
2244 *
2245 * @param int $day_ts The day timestamp (at midnight).
2246 * @param array $listings Optional list of filtered listing IDs.
2247 *
2248 * @return int Number of occupied rooms found.
2249 */
2250 protected function countDayRoomsOccupied($day_ts, array $listings = [])
2251 {
2252 // get the timestamp at the last second of the day
2253 $day_ts = strtotime('23:59:59', $day_ts);
2254
2255 $dbo = JFactory::getDbo();
2256
2257 if ($listings) {
2258 // perform a more accurate query over the listings specified
2259 $dbo->setQuery(
2260 $dbo->getQuery(true)
2261 ->select('COUNT(*)')
2262 ->from($dbo->qn('#__vikbooking_orders', 'o'))
2263 ->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('or.idorder'))
2264 ->where($dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'))
2265 ->where($dbo->qn('o.closure') . ' = 0')
2266 ->where($dbo->qn('o.checkin') . ' < ' . (int) $day_ts)
2267 ->where($dbo->qn('o.checkout') . ' > ' . (int) $day_ts)
2268 ->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $listings)) . ')')
2269 );
2270 } else {
2271 // quick query to fetch the total number of occupied rooms
2272 $dbo->setQuery(
2273 $dbo->getQuery(true)
2274 ->select('SUM(' . $dbo->qn('roomsnum') . ')')
2275 ->from($dbo->qn('#__vikbooking_orders'))
2276 ->where($dbo->qn('status') . ' = ' . $dbo->q('confirmed'))
2277 ->where($dbo->qn('closure') . ' = 0')
2278 ->where($dbo->qn('checkin') . ' < ' . (int) $day_ts)
2279 ->where($dbo->qn('checkout') . ' > ' . (int) $day_ts)
2280 );
2281 }
2282
2283 return (int) $dbo->loadResult();
2284 }
2285
2286 /**
2287 * Parses a report row into an associative list of key-value pairs.
2288 *
2289 * @param array $row The report row to parse.
2290 *
2291 * @return array Associative list of row keys and values.
2292 */
2293 protected function getAssocRowData(array $row)
2294 {
2295 $row_data = [];
2296
2297 foreach ($row as $field) {
2298 $field_val = $field['value'];
2299 if (!($field['no_export_callback'] ?? 0) && is_callable($field['callback_export'] ?? null)) {
2300 $field_val = $field['callback_export']($field_val);
2301 }
2302 $row_data[$field['key']] = $field_val;
2303 }
2304
2305 return $row_data;
2306 }
2307 }
2308