PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.9
VikBooking Hotel Booking Engine & PMS v1.8.9
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 / einvoicing / drivers / mydata_aade.php

mydata_aade.php in VikBooking Hotel Booking Engine & PMS 1.8.9, at admin/helpers/einvoicing/drivers/mydata_aade.php

4,243 lines 158.1 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) 2024 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 * MydataAade child Class of VikBookingEInvoicing
15 *
16 * @since 1.15.0 (J) - 1.5.0 (WP)
17 */
18 class VikBookingEInvoicingMydataAade extends VikBookingEInvoicing
19 {
20 /**
21 * Property 'defaultKeySort' is used by the View that renders the driver.
22 *
23 * @var string
24 */
25 public $defaultKeySort = 'ts';
26
27 /**
28 * Property 'defaultKeyOrder' is used by the View that renders the driver.
29 *
30 * @var string
31 */
32 public $defaultKeyOrder = 'ASC';
33
34 /**
35 * The path to this driver helper directory. Used only by this driver.
36 *
37 * @var string
38 */
39 protected $driverHelperPath = '';
40
41 /**
42 * An array of session filters.
43 *
44 * @var array
45 */
46 protected $sessionFilters;
47
48 /**
49 * An array of bookings.
50 *
51 * @var array
52 */
53 protected $bookings;
54
55 /**
56 * @var array
57 *
58 * @since 1.16.7 (J) - 1.6.7 (WP)
59 */
60 protected $environmental_fee_details = [];
61
62 /**
63 * Class constructor should define the name of the driver and
64 * other vars. Call the parent constructor to define the DB object.
65 */
66 public function __construct()
67 {
68 $this->driverFile = basename(__FILE__, '.php');
69 $this->driverName = "myDATA - ΑΑΔΕ Greece";
70 $this->driverFilters = [];
71 $this->driverButtons = [];
72
73 // driver helper dir path
74 $this->driverHelperPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . str_replace(' ', '', ucwords(str_replace('_', ' ', $this->driverFile))) . DIRECTORY_SEPARATOR;
75
76 // this driver has settings
77 $this->hasSettings = true;
78
79 // reset session filters
80 $this->sessionFilters = [];
81
82 // reset bookings array
83 $this->bookings = [];
84
85 $this->cols = [];
86 $this->rows = [];
87 $this->footerRow = [];
88
89 // require class constants
90 $this->importHelper($this->driverHelperPath . 'constants.php');
91
92 parent::__construct();
93 }
94
95 /**
96 * Returns the name of this file without .php.
97 *
98 * @return string
99 */
100 public function getFileName()
101 {
102 return $this->driverFile;
103 }
104
105 /**
106 * Returns the name of this driver.
107 *
108 * @return string
109 */
110 public function getName()
111 {
112 return $this->driverName;
113 }
114
115 /**
116 * Returns the filters of this driver.
117 *
118 * @return array
119 */
120 public function getFilters()
121 {
122 if (count($this->driverFilters)) {
123 // do not run this method twice, as it could load JS and CSS files.
124 return $this->driverFilters;
125 }
126
127 // session filters
128 $sessfilters = $this->loadSessionFilters();
129
130 // get VBO Application Object
131 $vbo_app = VikBooking::getVboApplication();
132
133 // load the jQuery UI Datepicker
134 $this->loadDatePicker();
135
136 // date format
137 $df = $this->getDateFormat();
138
139 // request variables
140 $pfromdate = VikRequest::getString('fromdate', '', 'request');
141 $ptodate = VikRequest::getString('todate', '', 'request');
142 $peinvtype = VikRequest::getInt('einvtype', 0, 'request');
143 $peinvkword = VikRequest::getString('einvkword', '', 'request');
144 $pdatetype = VikRequest::getString('datetype', $this->getSessionFilter('datetype', ''), 'request');
145
146 // js lang vars
147 JText::script('VBDELCONFIRM');
148
149 // From Date Filter
150 $filter_opt = array(
151 'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
152 'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-einvoicing-datepicker vbo-einvoicing-datepicker-from" size="12" autocomplete="off" />',
153 'type' => 'calendar',
154 'name' => 'fromdate'
155 );
156 array_push($this->driverFilters, $filter_opt);
157
158 // To Date Filter
159 $filter_opt = array(
160 'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
161 'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-einvoicing-datepicker vbo-einvoicing-datepicker-to" size="12" autocomplete="off" />',
162 'type' => 'calendar',
163 'name' => 'todate'
164 );
165 array_push($this->driverFilters, $filter_opt);
166
167 // jQuery code for the datepicker calendars and other events
168 if (empty($pfromdate) && empty($ptodate)) {
169 // if both request values are empty, take them from the session
170 $pfromdate = $this->getSessionFilter('fromdate');
171 $ptodate = $this->getSessionFilter('todate');
172 }
173 $js = '
174 jQuery(function() {
175 jQuery(".vbo-einvoicing-datepicker:input").datepicker({
176 maxDate: "+1y",
177 dateFormat: "'.$this->getDateFormat('jui').'",
178 onSelect: vboEInvoicingCheckDates
179 });
180 '.(!empty($pfromdate) && empty($peinvkword) ? 'jQuery(".vbo-einvoicing-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
181 '.(!empty($ptodate) && empty($peinvkword) ? 'jQuery(".vbo-einvoicing-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
182 jQuery("#monyear").change(function() {
183 var monopt = jQuery(this).find("option:selected");
184 if (monopt && monopt.length && monopt.val().length) {
185 var from = monopt.attr("data-from");
186 var to = monopt.attr("data-to");
187 jQuery(".vbo-einvoicing-datepicker-from").datepicker("setDate", from);
188 jQuery(".vbo-einvoicing-datepicker-to").datepicker("setDate", to);
189 jQuery("#einvkword").val("");
190 }
191 });
192 jQuery(".vbo-einvoicing-selaction").change(function() {
193 var prop = "excludebid"+jQuery(this).attr("data-bid");
194 var pobj = {};
195 var actval = parseInt(jQuery(this).val());
196 pobj[prop] = actval;
197 vboSetFilters(pobj, false);
198 if (actval > 0) {
199 // update cell data attribute for CSS to not-generate
200 jQuery(this).closest("td").attr("data-einvaction", 0);
201 } else {
202 // update cell data attribute for CSS to generate
203 jQuery(this).closest("td").attr("data-einvaction", 1);
204 }
205 });
206 jQuery(".vbo-einvoicing-existaction").change(function() {
207 var prop = "regeneratebid"+jQuery(this).attr("data-bid");
208 var propexcl = "excludesendbid"+jQuery(this).attr("data-bid");
209 var einvid = parseInt(jQuery(this).val());
210 var pobj = {};
211 if (einvid > 0) {
212 // update cell data attribute for CSS to generate
213 jQuery(this).closest("td").attr("data-einvaction", 1);
214 // set re-generate and exclude send
215 pobj[prop] = einvid;
216 pobj[propexcl] = 1;
217 } else {
218 if (einvid < 0) {
219 // update cell data attribute for CSS to not-transmit
220 jQuery(this).closest("td").attr("data-einvaction", 0);
221 // set exclude send and not re-generate
222 pobj[prop] = 0;
223 pobj[propexcl] = 1;
224 } else {
225 // update cell data attribute for CSS to transmit (value = 0)
226 jQuery(this).closest("td").attr("data-einvaction", -2);
227 // set send and not re-generate
228 pobj[prop] = 0;
229 pobj[propexcl] = 0;
230 }
231 }
232 vboSetFilters(pobj, false);
233 });
234 jQuery(".vbo-einvoicing-sentaction").change(function() {
235 var propregen = "regeneratebid"+jQuery(this).attr("data-bid");
236 var propresend = "resendbid"+jQuery(this).attr("data-bid");
237 var propresendecofee = "resendecofeebid"+jQuery(this).attr("data-bid");
238 var curval = jQuery(this).val();
239 var splitval = curval.split("-");
240 var einvid = parseInt(splitval[0]);
241 var pobj = {};
242 if (einvid === 0) {
243 // update cell data attribute for CSS to transmitted
244 jQuery(this).closest("td").attr("data-einvaction", -1);
245 pobj[propregen] = einvid;
246 pobj[propresend] = einvid;
247 pobj[propresendecofee] = einvid;
248 } else {
249 if (splitval[1] == "regen") {
250 // update cell data attribute for CSS to generate
251 jQuery(this).closest("td").attr("data-einvaction", 1);
252 pobj[propregen] = einvid;
253 pobj[propresend] = 0;
254 pobj[propresendecofee] = 0;
255 } else if (splitval[1] == "resend") {
256 // update cell data attribute for CSS to transmitted
257 jQuery(this).closest("td").attr("data-einvaction", -1);
258 pobj[propregen] = 0;
259 pobj[propresend] = einvid;
260 pobj[propresendecofee] = 0;
261 } else if (splitval[1] == "resendecofee") {
262 // update cell data attribute for CSS to transmitted
263 jQuery(this).closest("td").attr("data-einvaction", -1);
264 pobj[propregen] = 0;
265 pobj[propresend] = 0;
266 pobj[propresendecofee] = einvid;
267 }
268 }
269 vboSetFilters(pobj, false);
270 });
271 jQuery(".vbo-driver-output-vieweinv").click(function() {
272 var id = jQuery(this).attr("data-einvid");
273 vboSetFilters({einvid: id}, false);
274 vboDriverDoAction("viewEInvoice", true);
275 });
276 jQuery(".vbo-driver-output-editeinv").click(function() {
277 var id = jQuery(this).attr("data-einvid");
278 var bid = jQuery(this).attr("data-envfeebid");
279 vboSetFilters({drivercontent: "editEInvoice", einvid: id, envfeebid: (bid || null)}, true);
280 });
281 jQuery(".vbo-driver-output-rmeinv").click(function() {
282 var id = jQuery(this).attr("data-einvid");
283 if (confirm(Joomla.JText._("VBDELCONFIRM"))) {
284 vboSetFilters({einvid: id}, false);
285 vboDriverDoAction("removeEInvoice", false);
286 }
287 });
288 });
289 function vboEInvoicingCheckDates(selectedDate, inst) {
290 if (selectedDate === null || inst === null) {
291 return;
292 }
293 jQuery("#monyear").val("");
294 jQuery("#einvkword").val("");
295 var cur_from_date = jQuery(this).val();
296 if (jQuery(this).hasClass("vbo-einvoicing-datepicker-from") && cur_from_date.length) {
297 var nowstart = jQuery(this).datepicker("getDate");
298 var nowstartdate = new Date(nowstart.getTime());
299 jQuery(".vbo-einvoicing-datepicker-to").datepicker("option", {minDate: nowstartdate});
300 }
301 }';
302 $this->setScript($js);
303
304 // month-year filter
305 $q = "SELECT MIN(`for_date`) AS `mindate`, MAX(`for_date`) AS `maxdate` FROM `#__vikbooking_einvoicing_data`;";
306 $this->dbo->setQuery($q);
307 $minmax = $this->dbo->loadAssoc();
308 if ($minmax) {
309 if (!empty($minmax['mindate']) && !empty($minmax['maxdate'])) {
310 $infomin = getdate(strtotime($minmax['mindate']));
311 $infomax = getdate(strtotime($minmax['maxdate']));
312 $startts = mktime(0, 0, 0, $infomin['mon'], 1, $infomin['year']);
313 $lastts = mktime(23, 59, 59, $infomax['mon'], date('t', $infomax[0]), $infomax['year']);
314 $monthys = [];
315 while ($startts < $lastts) {
316 array_push($monthys, array(
317 'mon' => $infomin['mon'],
318 'year' => $infomin['year'],
319 'from' => $startts,
320 'to' => mktime(0, 0, 0, $infomin['mon'], date('t', $infomin[0]), $infomin['year'])
321 ));
322 $startts = mktime(0, 0, 0, ($infomin['mon'] + 1), 1, $infomin['year']);
323 $infomin = getdate($startts);
324 }
325 $opts = '';
326 foreach ($monthys as $my) {
327 $dfrom = date($df, $my['from']);
328 $dto = date($df, $my['to']);
329 $selectedstat = $pfromdate == $dfrom && $ptodate == $dto ? ' selected="selected"' : '';
330 $opts .= '<option value="'.$my['from'].'" data-from="'.$dfrom.'" data-to="'.$dto.'"'.$selectedstat.'>'.$this->getMonthString($my['mon']).' '.$my['year'].'</option>';
331 }
332 $filter_opt = array(
333 'label' => '<label for="monyear">' . JText::translate('VBPVIEWRESTRICTIONSTWO') . '</label>',
334 'html' => '<select name="monyear" id="monyear"><option value=""></option>'.$opts.'</select>',
335 'type' => 'select',
336 'name' => 'monyear'
337 );
338 array_push($this->driverFilters, $filter_opt);
339 }
340 }
341
342 // date type filter
343 $filter_opt = array(
344 'label' => '<label for="datetype">' . JText::translate('VBPVIEWORDERSONE') . '</label>',
345 'html' => '<select name="datetype" id="datetype">
346 <option value="ts"'.($pdatetype == 'ts' ? ' selected="selected"' : '').'>' . JText::translate('VBRENTALORD') . '</option>
347 <option value="checkin"'.($pdatetype == 'checkin' ? ' selected="selected"' : '').'>' . JText::translate('VBPICKUPAT') . '</option>
348 <option value="checkout"'.($pdatetype == 'checkout' ? ' selected="selected"' : '').'>' . JText::translate('VBRELEASEAT') . '</option>
349 </select>',
350 'type' => 'select',
351 'name' => 'datetype'
352 );
353 array_push($this->driverFilters, $filter_opt);
354
355 // invoice type filter
356 $filter_opt = array(
357 'label' => '<label for="einvtype">Show</label>',
358 'html' => '<select name="einvtype" id="einvtype">
359 <option value="0">All reservations</option>
360 <option value="1"'.($peinvtype == 1 ? ' selected="selected"' : '').'>- To be invoiced</option>
361 <option value="-1"'.($peinvtype == -1 ? ' selected="selected"' : '').'>- To be transmitted</option>
362 <option value="-2"'.($peinvtype == -2 ? ' selected="selected"' : '').'>- Trasmitted</option>
363 </select>',
364 'type' => 'select',
365 'name' => 'einvtype'
366 );
367 array_push($this->driverFilters, $filter_opt);
368
369 // search invoice filter
370 $filter_opt = array(
371 'label' => '<label for="einvkword">' . JText::translate('VBODASHSEARCHKEYS') . '</label>',
372 'html' => '<div class="input-append"><input type="text" id="einvkword" name="einvkword" value="'.htmlspecialchars($peinvkword).'" size="15" /><button type="button" class="btn btn-secondary" onclick="document.getElementById(\'einvkword\').value = \'\';"><i class="icon-remove"></i></button></div>',
373 'type' => 'text',
374 'name' => 'einvkword'
375 );
376 array_push($this->driverFilters, $filter_opt);
377
378 return $this->driverFilters;
379 }
380
381 /**
382 * Whether there are enough filters in the session to render data when the page loads.
383 *
384 * @return boolean
385 */
386 public function hasFiltersSet()
387 {
388 return (bool)(count($this->loadSessionFilters()) > 0);
389 }
390
391 /**
392 * Returns the current filters saved in the session.
393 * This protected method is only used by this class.
394 *
395 * @return array
396 */
397 protected function loadSessionFilters()
398 {
399 if ($this->sessionFilters) {
400 return $this->sessionFilters;
401 }
402
403 $session = JFactory::getSession();
404 $sessfilters = $session->get($this->getFileName().'Filt', '');
405 $sessfilters = empty($sessfilters) || !is_array($sessfilters) ? array() : $sessfilters;
406
407 $this->sessionFilters = $sessfilters;
408
409 return $this->sessionFilters;
410 }
411
412 /**
413 * Returns the current session filter for the given name.
414 * This protected method is only used by this class.
415 *
416 * @param string the name of the filter to fetch
417 * @param mixed the default filter value if empty
418 *
419 * @return mixed the current session filter requested, or a default empty value
420 */
421 protected function getSessionFilter($name, $def = '')
422 {
423 if (isset($this->sessionFilters[$name])) {
424 return $this->sessionFilters[$name];
425 }
426
427 return $def;
428 }
429
430 /**
431 * Sets and updates the session filters.
432 *
433 * @param string the name of the filter to set
434 * @param mixed the value to set for the filter
435 *
436 * @return void
437 */
438 protected function setSessionFilter($name, $val)
439 {
440 $this->sessionFilters[$name] = $val;
441
442 // update session
443 $session = JFactory::getSession();
444 $sessfilters = $session->set($this->getFileName().'Filt', $this->sessionFilters);
445
446 return;
447 }
448
449 /**
450 * Returns the buttons for the driver actions.
451 *
452 * @return array
453 */
454 public function getButtons()
455 {
456 // generate invoices button
457 array_push($this->driverButtons, '
458 <a href="JavaScript: void(0);" onclick="vboDriverDoAction(\'generateEInvoices\', false);" class="vbcsvexport"><i class="vboicn-file-text2 icn-nomargin"></i> <span>'.JText::translate('VBODRIVERGENERATEINVS').'</span></a>
459 ');
460
461 // transmit invoices button
462 array_push($this->driverButtons, '
463 <a href="JavaScript: void(0);" onclick="vboDriverDoAction(\'transmitEInvoices\', false);" class="vbo-perms-operators"><i class="vboicn-truck icn-nomargin"></i> <span>Transmit to myDATA</span></a>
464 ');
465
466 // download invoices button
467 array_push($this->driverButtons, '
468 <a href="JavaScript: void(0);" onclick="vboDriverDoAction(\'downloadEInvoices\', true);" class="vbo-perms-operators"><i class="vboicn-download icn-nomargin"></i> <span>Download XML files</span></a>
469 ');
470
471 return $this->driverButtons;
472 }
473
474 /**
475 * Prepares the data for saving the driver settings.
476 * Validate post vars to make sure they are correct.
477 *
478 * @return stdClass
479 */
480 protected function prepareSavingSettings()
481 {
482 $data = new stdClass;
483 $params = new stdClass;
484
485 // settings vars
486 $automatic = VikRequest::getInt('automatic', 0, 'request');
487 $progcount = VikRequest::getInt('progcount', 1, 'request');
488 $invoiceinum = VikRequest::getInt('invoiceinum', 1, 'request');
489 $invoiceinum = $invoiceinum < 1 ? 1 : $invoiceinum;
490 // we lower the next invoice num because VikBooking::getNextInvoiceNumber() returns increased by 1
491 $invoiceinum--;
492
493 $einvdttype = VikRequest::getString('einvdttype', 'today', 'request');
494 $einvexnumdt = VikRequest::getString('einvexnumdt', 'new', 'request');
495 $einvtypecode = VikRequest::getString('einvtypecode', '1.1', 'request');
496 $vat_exempt_cat = VikRequest::getString('vat_exempt_cat', '1', 'request');
497 $einv_paymethod = VikRequest::getString('einv_paymethod', '1', 'request');
498 $einv_inc_class_type = VikRequest::getString('einv_inc_class_type', '', 'request');
499 $einv_inc_class_cat = VikRequest::getString('einv_inc_class_cat', '', 'request');
500 $schema_validate = VikRequest::getInt('schema_validate', 0, 'request');
501
502 $aade_user_id = VikRequest::getString('aade_user_id', '', 'request');
503 $aade_subscription_key = VikRequest::getString('aade_subscription_key', '', 'request');
504 $test_mode = VikRequest::getInt('test_mode', 0, 'request');
505 $mydata_endpoint_url = VikRequest::getString('mydata_endpoint_url', '', 'request');
506
507 $companyname = VikRequest::getString('companyname', '', 'request');
508 $vatid = VikRequest::getString('vatid', '', 'request');
509 $country = VikRequest::getString('country', '', 'request');
510 $address = VikRequest::getString('address', '', 'request');
511 $streetnumber = VikRequest::getString('streetnumber', '', 'request');
512 $zip = VikRequest::getString('zip', '', 'request');
513 $city = VikRequest::getString('city', '', 'request');
514
515 // fields validation
516 $mandatory = [
517 $companyname,
518 $vatid,
519 $country,
520 $address,
521 $streetnumber,
522 $zip,
523 $city,
524 $aade_user_id,
525 $aade_subscription_key,
526 ];
527 foreach ($mandatory as $field) {
528 if (empty($field)) {
529 $this->setError(JText::translate('VBO_PLEASE_FILL_FIELDS'));
530 return false;
531 }
532 }
533
534 // update the global configuration setting 'invoiceinum'
535 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$this->dbo->quote((string)$invoiceinum)." WHERE `param`='invoiceinum';";
536 $this->dbo->setQuery($q);
537 $this->dbo->execute();
538
539 // build data for saving
540 $params->einvdttype = $einvdttype;
541 $params->einvexnumdt = $einvexnumdt;
542 $params->einvtypecode = $einvtypecode;
543 $params->vat_exempt_cat = $vat_exempt_cat;
544 $params->einv_paymethod = $einv_paymethod;
545 $params->einv_inc_class_type = $einv_inc_class_type;
546 $params->einv_inc_class_cat = $einv_inc_class_cat;
547 $params->schema_validate = $schema_validate;
548
549 $params->aade_user_id = $aade_user_id;
550 $params->aade_subscription_key = $aade_subscription_key;
551 $params->test_mode = $test_mode;
552 $params->mydata_endpoint_url = $mydata_endpoint_url;
553
554 $params->companyname = $companyname;
555 $params->vatid = $vatid;
556 $params->country = $country;
557 $params->address = $address;
558 $params->streetnumber = $streetnumber;
559 $params->zip = $zip;
560 $params->city = $city;
561
562 /**
563 * Environmental Fee settings
564 *
565 * @since 1.16.7 (J) - 1.6.7 (WP)
566 */
567 $params->environmental_invoice = VikRequest::getInt('environmental_invoice', 0, 'request');
568 $params->envfeeinvoiceinum = VikRequest::getInt('envfeeinvoiceinum', 1, 'request');
569 $params->envfeevboid = VikRequest::getInt('envfeevboid', 0, 'request');
570
571 $data->driver = $this->getFileName();
572 $data->params = json_encode($params);
573 $data->automatic = $automatic;
574 $data->progcount = $progcount;
575
576 return $data;
577 }
578
579 /**
580 * Gets an array with the default settings.
581 *
582 * @return array
583 */
584 protected function getDefaultSettings()
585 {
586 return [
587 'id' => -1,
588 'driver' => $this->getFileName(),
589 'params' => array(),
590 'automatic' => 0
591 ];
592 }
593
594 /**
595 * Echoes the HTML required for the driver settings form.
596 *
597 * @return void
598 */
599 public function printSettings()
600 {
601 // load current driver settings
602 $settings = $this->loadSettings();
603 if ($settings === false) {
604 $settings = $this->getDefaultSettings();
605 /**
606 * it's the first time we run the driver, so we print a warning message
607 * with some instructions for generating the invoices and to transmit them.
608 */
609 $this->displayInstructions();
610 }
611
612 /**
613 * Load and inject all the configured fees within VikBooking to support the environmental fee.
614 *
615 * @since 1.16.7 (J) - 1.6.7 (WP)
616 */
617 $this->dbo->setQuery(
618 $this->dbo->getQuery(true)
619 ->select('*')
620 ->from($this->dbo->qn('#__vikbooking_optionals'))
621 ->where(1)
622 ->andWhere([
623 $this->dbo->qn('forcesel') . ' = 1',
624 $this->dbo->qn('is_fee') . ' = 1',
625 ], 'OR')
626 ->order($this->dbo->qn('name') . ' ASC')
627 );
628 $settings['mandatory_fees'] = $this->dbo->loadAssocList();
629
630 // settings layout file
631 $fpath = $this->driverHelperPath . 'settings.php';
632
633 // load helper file and echo its content
634 echo $this->loadHelperFile($fpath, $settings);
635 }
636
637 /**
638 * Sets some warning messages.
639 *
640 * @return array
641 */
642 protected function displayInstructions()
643 {
644 $this->setWarning('Driver settings not available. Make sure to save your personal myDATA information, or the data transmission will not work.');
645 $this->setWarning('Fill in all the required information related to your company and to your myDATA profile in order to be able to start generating electronic invoices for AADE.');
646 }
647
648 /**
649 * This method converts each booking array into a matrix with one room-booking per index.
650 * It also adds information about the customer and the invoices generated for each booking.
651 *
652 * @param array $records the array containing the bookings before nesting
653 *
654 * @return array
655 */
656 protected function nestBookingsData($records)
657 {
658 // to avoid heavy and extra joins, we load all customers for the returned booking ids
659 $allids = [];
660 foreach ($records as $b) {
661 if (!isset($b['customer']) && !in_array($b['id'], $allids)) {
662 array_push($allids, $b['id']);
663 }
664 }
665 $customers_books = [];
666 if (count($allids)) {
667 $q = "SELECT `c`.*,`co`.`idorder`,`cy`.`country_name`,`cy`.`country_2_code` FROM `#__vikbooking_customers` AS `c`
668 LEFT JOIN `#__vikbooking_customers_orders` `co` ON `c`.`id`=`co`.`idcustomer`
669 LEFT JOIN `#__vikbooking_countries` AS `cy` ON `c`.`country`=`cy`.`country_3_code`
670 WHERE `co`.`idorder`".(count($allids) === 1 ? "=".(int)$allids[0] : " IN (".implode(', ', $allids).")").";";
671 $this->dbo->setQuery($q);
672 $allcustomers = $this->dbo->loadAssocList();
673 if ($allcustomers) {
674 foreach ($allcustomers as $customer) {
675 $customers_books[$customer['idorder']] = $customer;
676 }
677 }
678 }
679
680 // nest records with multiple rooms booked inside sub-array
681 $bookings = [];
682 foreach ($records as $v) {
683 if (!isset($bookings[$v['id']])) {
684 $bookings[$v['id']] = [];
685 }
686 // to avoid heavy joins, we put the customer record onto the first nested room booked
687 if (!isset($v['customer']) && !$bookings[$v['id']]) {
688 $v['customer'] = isset($customers_books[$v['id']]) ? $customers_books[$v['id']] : [];
689 }
690
691 // push room sub-array
692 array_push($bookings[$v['id']], $v);
693 }
694
695 return $bookings;
696 }
697
698 /**
699 * Loads the bookings from the DB according to the filters set.
700 * Gathers the information for the electronic invoices generation.
701 * Sets the columns and rows for the page and commands to be displayed.
702 * Updates the internal bookings array for any custom action.
703 *
704 * @return boolean
705 */
706 public function getBookingsData()
707 {
708 if (strlen($this->getError())) {
709 // other methods may set errors rather than exiting the process, and the View may continue the execution to attempt to render the page.
710 return false;
711 }
712
713 if (count($this->bookings)) {
714 // this method may be called by other generation methods, so it's useless to run it twice
715 return true;
716 }
717
718 $cpin = VikBooking::getCPinIstance();
719 $customsq = '';
720 // input fields and other vars
721 $pdatetype = VikRequest::getString('datetype', $this->getSessionFilter('datetype', 'ts'), 'request');
722 $peinvtype = VikRequest::getInt('einvtype', 0, 'request');
723 $peinvkword = VikRequest::getString('einvkword', '', 'request');
724 $pfromdate = VikRequest::getString('fromdate', '', 'request');
725 $ptodate = VikRequest::getString('todate', '', 'request');
726 if (empty($pfromdate) && empty($ptodate)) {
727 // if both request values are empty, take them from the session
728 $pfromdate = $this->getSessionFilter('fromdate');
729 $ptodate = $this->getSessionFilter('todate');
730 }
731 $pkrsort = VikRequest::getString('krsort', $this->defaultKeySort, 'request');
732 $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
733 $pkrorder = VikRequest::getString('krorder', $this->defaultKeyOrder, 'request');
734 $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
735 $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
736 $currency_symb = VikBooking::getCurrencySymb();
737 $df = $this->getDateFormat();
738 $datesep = VikBooking::getDateSeparator();
739 if (empty($ptodate)) {
740 $ptodate = $pfromdate;
741 }
742 // get dates timestamps
743 $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
744 $to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
745 if (empty($peinvkword) && (empty($pfromdate) || empty($from_ts) || empty($to_ts) || $from_ts > $to_ts)) {
746 $this->setError('Please select the dates to filter invoices and reservations.');
747 return false;
748 }
749
750 // update session filters
751 $this->setSessionFilter('fromdate', $pfromdate);
752 $this->setSessionFilter('todate', $ptodate);
753 $this->setSessionFilter('datetype', $pdatetype);
754
755 // query to obtain the records
756 $records = [];
757 if (!empty($peinvkword)) {
758 // search invoice requires a different query
759 $seekclauses = [];
760 $maybenum = $this->getOnlyNumbers($peinvkword, true);
761 $maybevat = $this->getOnlyNumbers($peinvkword);
762 if (!empty($maybenum)) {
763 // try to seek for this invoice number
764 array_push($seekclauses, '`ei`.`number`='.(int)$maybenum);
765 }
766 if (!empty($maybevat)) {
767 // customer vat number
768 array_push($seekclauses, "`cust`.`vat` LIKE ".$this->dbo->quote("%".$maybevat."%"));
769 }
770 // customer company name
771 array_push($seekclauses, "`cust`.`company` LIKE ".$this->dbo->quote("%".$peinvkword."%"));
772 // customer full name
773 array_push($seekclauses, "CONCAT_WS(' ', `cust`.`first_name`, `cust`.`last_name`) LIKE ".$this->dbo->quote("%".$peinvkword."%"));
774 // customer email
775 if (strpos($peinvkword, '@') !== false) {
776 // customer email
777 array_push($seekclauses, "`cust`.`email`=".$this->dbo->quote($peinvkword));
778 }
779 // customer fiscal code
780 array_push($seekclauses, "`cust`.`fisccode`=".$this->dbo->quote($peinvkword));
781
782 // find first the booking IDs with a specific query given the filters
783 $oidsfound = [];
784 $q = "SELECT `ei`.`id`,`ei`.`idorder` FROM `#__vikbooking_einvoicing_data` AS `ei` ".
785 "LEFT JOIN `#__vikbooking_customers` AS `cust` ON `ei`.`idcustomer` = `cust`.`id` ".
786 "WHERE `ei`.`obliterated`=0 AND (".implode(' OR ', $seekclauses).") ".
787 "GROUP BY `ei`.`driverid`,`ei`.`number`;";
788 $this->dbo->setQuery($q);
789 $results = $this->dbo->loadAssocList();
790 if (!$results) {
791 $this->setError('No invoice found with the specified filters');
792 return false;
793 }
794
795 $mergecustoms = false;
796 $customsids = [];
797 foreach ($results as $res) {
798 if ($res['idorder'] < 0) {
799 $mergecustoms = true;
800 array_push($customsids, $res['id']);
801 }
802 array_push($oidsfound, $res['idorder']);
803 }
804 // we make the same query but by passing the IDs of the bookings found according to the filters
805 $q = "SELECT `o`.`id`,`o`.`ts`,`o`.`days`,`o`.`checkin`,`o`.`checkout`,`o`.`totpaid`,`o`.`idpayment`,`o`.`coupon`,`o`.`roomsnum`,`o`.`total`,`o`.`idorderota`,`o`.`channel`,`o`.`chcurrency`,`o`.`country`,`o`.`tot_taxes`,".
806 "`o`.`tot_city_taxes`,`o`.`tot_fees`,`o`.`cmms`,`o`.`pkg`,`o`.`refund`,`or`.`idorder`,`or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`c`.`country_name`,`c`.`country_2_code`,`r`.`name` AS `room_name`,`r`.`fromadult`,`r`.`toadult`,`ei`.`id` AS `einvid`,`ei`.`driverid` AS `einvdriver`,`ei`.`for_date` AS `einvdate`,`ei`.`number` AS `einvnum`,`ei`.`transmitted` AS `einvsent`,`ei`.`trans_data` AS `einvtndata` ".
807 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
808 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
809 "LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
810 "LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
811 "WHERE `o`.`id` IN (".implode(', ', array_unique($oidsfound)).") ".
812 "ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
813 // check if we need to merge custom (manual) invoices
814 if ($mergecustoms) {
815 $customsq = "SELECT `ei`.`id` AS `einvid`,`ei`.`driverid` AS `einvdriver`,`ei`.`created_on`,`ei`.`for_date` AS `einvdate`,`ei`.`number` AS `einvnum`,`ei`.`transmitted` AS `einvsent`,`ei`.`trans_data` AS `einvtndata`,`ei`.`idorder`,`ei`.`idcustomer`,`inv`.`id` AS `invid`,`inv`.`rawcont`,`inv`.`for_date` AS `inv_fordate_ts` ".
816 "FROM `#__vikbooking_einvoicing_data` AS `ei` ".
817 "LEFT JOIN `#__vikbooking_invoices` AS `inv` ON `ei`.`idorder`=`inv`.`idorder` ".
818 "WHERE `ei`.`idorder` < 0 AND `ei`.`obliterated`=0 AND `ei`.`id` IN (".implode(', ', $customsids).");";
819 }
820 } else {
821 // use date filters for the regular query
822 $mergecustoms = false;
823 $typeclause = '';
824 // filter by type
825 switch ($peinvtype) {
826 case 1:
827 $typeclause = '`ei`.`id` IS NULL AND ';
828 break;
829 case -1:
830 $mergecustoms = true;
831 $typeclause = '`ei`.`id` IS NOT NULL AND `ei`.`transmitted`=0 AND ';
832 break;
833 case -2:
834 $mergecustoms = true;
835 $typeclause = '`ei`.`id` IS NOT NULL AND `ei`.`transmitted`=1 AND ';
836 break;
837 default:
838 // when no e-invoice type filter set, try to merge custom (manual) invoices
839 $mergecustoms = true;
840 break;
841 }
842 $q = "SELECT `o`.`id`,`o`.`ts`,`o`.`days`,`o`.`checkin`,`o`.`checkout`,`o`.`totpaid`,`o`.`idpayment`,`o`.`coupon`,`o`.`roomsnum`,`o`.`total`,`o`.`idorderota`,`o`.`channel`,`o`.`chcurrency`,`o`.`country`,`o`.`tot_taxes`,".
843 "`o`.`tot_city_taxes`,`o`.`tot_fees`,`o`.`cmms`,`o`.`pkg`,`o`.`refund`,`or`.`idorder`,`or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`c`.`country_name`,`c`.`country_2_code`,`r`.`name` AS `room_name`,`r`.`fromadult`,`r`.`toadult`,`ei`.`id` AS `einvid`,`ei`.`driverid` AS `einvdriver`,`ei`.`for_date` AS `einvdate`,`ei`.`number` AS `einvnum`,`ei`.`transmitted` AS `einvsent`,`ei`.`trans_data` AS `einvtndata` ".
844 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
845 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
846 "LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
847 "LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
848 "WHERE ".$typeclause.
849 "(`o`.`status`='confirmed' OR (`o`.`status`='cancelled' AND `o`.`totpaid`>0)) AND `o`.`closure`=0 AND `o`.`{$pdatetype}`>=".$from_ts." AND `o`.`{$pdatetype}`<=".$to_ts." ".
850 "ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
851 // check if we need to merge custom (manual) invoices (they should be searched with the apposite dates filters for matching `created_on` or `for_date`)
852 if ($mergecustoms) {
853 $customsq = "SELECT `ei`.`id` AS `einvid`,`ei`.`driverid` AS `einvdriver`,`ei`.`created_on`,`ei`.`for_date` AS `einvdate`,`ei`.`number` AS `einvnum`,`ei`.`transmitted` AS `einvsent`,`ei`.`trans_data` AS `einvtndata`,`ei`.`idorder`,`ei`.`idcustomer`,`inv`.`id` AS `invid`,`inv`.`rawcont`,`inv`.`for_date` AS `inv_fordate_ts` ".
854 "FROM `#__vikbooking_einvoicing_data` AS `ei` ".
855 "LEFT JOIN `#__vikbooking_invoices` AS `inv` ON `ei`.`idorder`=`inv`.`idorder` ".
856 "WHERE `ei`.`idorder` < 0 AND `ei`.`obliterated`=0 AND {$typeclause}".
857 "( (`ei`.`created_on`>=".$this->dbo->quote(date('Y-m-d H:i:s', $from_ts))." AND `ei`.`created_on`<=".$this->dbo->quote(date('Y-m-d H:i:s', $to_ts)).") OR ".
858 "(`ei`.`for_date`>=".$this->dbo->quote(date('Y-m-d', $from_ts))." AND `ei`.`for_date`<=".$this->dbo->quote(date('Y-m-d', $to_ts)).") ) AND ".
859 /**
860 * We need to add also the following clause in order to not get multiple records with equal invoice numbers for manual bookings.
861 */
862 "( (`inv`.`created_on`>={$from_ts} AND `inv`.`created_on`<={$to_ts}) OR ".
863 "(`inv`.`for_date`>={$from_ts} AND `inv`.`for_date`<={$to_ts}) );";
864 }
865 }
866 $this->dbo->setQuery($q);
867 $records = $this->dbo->loadAssocList();
868
869 if (!empty($customsq)) {
870 // we make a query to fetch the custom (manual) invoices to merge them with the real bookings
871 $this->dbo->setQuery($customsq);
872 $custom_records = $this->dbo->loadAssocList();
873
874 foreach ($custom_records as $customrec) {
875 $custom_data = $this->prepareCustomInvoiceData($customrec, $cpin->getCustomerByID($customrec['idcustomer']));
876 // push the prepared custom invoice array to the global records array
877 array_push($records, $custom_data[0]);
878 }
879 }
880
881 if (!$records) {
882 $this->setError('No reservation or invoice found with the specified filters.');
883 return false;
884 }
885
886 // nest records with multiple rooms booked inside sub-array
887 $bookings = $this->nestBookingsData($records);
888
889 // define the columns of the page
890 $this->cols = array(
891 // id
892 array(
893 'key' => 'id',
894 'sortable' => 1,
895 'label' => 'ID'
896 ),
897 // date
898 array(
899 'key' => 'ts',
900 'attr' => array(
901 'class="center"'
902 ),
903 'sortable' => 1,
904 'label' => JText::translate('VBPVIEWORDERSONE')
905 ),
906 // checkin
907 array(
908 'key' => 'checkin',
909 'sortable' => 1,
910 'label' => JText::translate('VBPICKUPAT')
911 ),
912 // checkout
913 array(
914 'key' => 'checkout',
915 'sortable' => 1,
916 'label' => JText::translate('VBRELEASEAT')
917 ),
918 // customer
919 array(
920 'key' => 'customer',
921 'sortable' => 1,
922 'label' => JText::translate('VBOCUSTOMER')
923 ),
924 // country
925 array(
926 'key' => 'country',
927 'sortable' => 1,
928 'label' => JText::translate('ORDER_STATE')
929 ),
930 // city
931 array(
932 'key' => 'city',
933 'attr' => array(
934 'class="center"'
935 ),
936 'sortable' => 1,
937 'label' => JText::translate('ORDER_CITY')
938 ),
939 // vat
940 array(
941 'key' => 'vat',
942 'attr' => array(
943 'class="center"'
944 ),
945 'sortable' => 1,
946 'label' => JText::translate('VBCUSTOMERCOMPANYVAT')
947 ),
948 // counterpart company name
949 array(
950 'key' => 'company',
951 'sortable' => 1,
952 'label' => JText::translate('VBCUSTOMERCOMPANY')
953 ),
954 // total
955 array(
956 'key' => 'tot',
957 'attr' => array(
958 'class="center"'
959 ),
960 'sortable' => 1,
961 'label' => JText::translate('VBPVIEWORDERSSEVEN')
962 ),
963 // commands
964 array(
965 'key' => 'commands',
966 'attr' => array(
967 'class="center"'
968 ),
969 'label' => ''
970 ),
971 // action
972 array(
973 'key' => 'action',
974 'attr' => array(
975 'class="center"'
976 ),
977 'sortable' => 1,
978 'label' => JText::translate('VBO_BACKUP_ACTION_LABEL')
979 ),
980 );
981
982 // build the rows of the page
983 foreach ($bookings as $bk => $gbook) {
984 $bid = $gbook[0]['id'];
985 $analog_id = isset($gbook[0]['invid']) && !empty($gbook[0]['invid']) ? $gbook[0]['invid'] : null;
986 /**
987 * Manual invoices could have the same number and so negative id order across multiple years.
988 * For this reason, searching for an invoice by number may display invalid links to the manual
989 * invoices, and so we build a list of invoice IDs with related dates to be displayed.
990 */
991 $multi_analog_ids = [];
992 if (!empty($analog_id) && count($gbook) > 1) {
993 $all_analog_ids = [];
994 foreach ($gbook as $subinv) {
995 if (!isset($subinv['invid']) || !isset($subinv['inv_fordate_ts'])) {
996 continue;
997 }
998 $inv_key_identifier = $subinv['invid'] . $subinv['inv_fordate_ts'];
999 if (in_array($inv_key_identifier, $all_analog_ids)) {
1000 continue;
1001 }
1002 array_push($all_analog_ids, $inv_key_identifier);
1003 array_push($multi_analog_ids, array(
1004 'invid' => $subinv['invid'],
1005 'for_date' => date(str_replace("/", $datesep, $df), $subinv['inv_fordate_ts']),
1006 ));
1007 }
1008 }
1009 //
1010 $tsinfo = getdate($gbook[0]['ts']);
1011 $tswday = $this->getWdayString($tsinfo['wday'], 'short');
1012 $ininfo = getdate($gbook[0]['checkin']);
1013 $inwday = $this->getWdayString($ininfo['wday'], 'short');
1014 $outinfo = getdate($gbook[0]['checkout']);
1015 $outwday = $this->getWdayString($outinfo['wday'], 'short');
1016 $customer = $gbook[0]['customer'];
1017 $country3 = $gbook[0]['country'];
1018 $country2 = $gbook[0]['country_2_code'];
1019 $countryfull = $gbook[0]['country_name'];
1020 if (empty($country3) && $customer && !empty($customer['country'])) {
1021 $country3 = $customer['country'];
1022 $gbook[0]['country'] = $country3;
1023 }
1024 if (empty($country2) && $customer && !empty($customer['country_2_code'])) {
1025 $country2 = $customer['country_2_code'];
1026 $gbook[0]['country_2_code'] = $country2;
1027 }
1028 if (empty($countryfull) && $customer && !empty($customer['country_name'])) {
1029 $countryfull = $customer['country_name'];
1030 $gbook[0]['country_name'] = $countryfull;
1031 }
1032 $totguests = 0;
1033 $rooms_map = [];
1034 $rooms_str = [];
1035 foreach ($gbook as $book) {
1036 $totguests += $book['adults'] + $book['children'];
1037 if (!isset($book['room_name'])) {
1038 // custom (manual) invoice records may be missing this property
1039 continue;
1040 }
1041 if (!isset($rooms_map[$book['room_name']])) {
1042 $rooms_map[$book['room_name']] = 0;
1043 }
1044 $rooms_map[$book['room_name']]++;
1045 }
1046 foreach ($rooms_map as $rname => $rcount) {
1047 array_push($rooms_str, $rname . ($rcount > 1 ? ' x'.$rcount : ''));
1048 }
1049 $rooms_str = implode(', ', $rooms_str);
1050
1051 // einvnum (if exists)
1052 $einvnum = !empty($gbook[0]['einvnum']) ? $gbook[0]['einvnum'] : 0;
1053
1054 // attempt to decode the transaction data, if any
1055 if (!empty($gbook[0]['einvtndata']) && is_scalar($gbook[0]['einvtndata'])) {
1056 $gbook[0]['einvtndata'] = json_decode($gbook[0]['einvtndata'], true);
1057 }
1058
1059 // set e-invoice transaction data, if any
1060 $einvtndata = is_array(($gbook[0]['einvtndata'] ?? null)) ? $gbook[0]['einvtndata'] : null;
1061
1062 // always update the main array reference
1063 $bookings[$bk] = $gbook;
1064
1065 // check whether the invoice can be issued
1066 list($canbeinvoiced, $noinvoicereason) = $this->canBookingBeInvoiced($bookings[$bk]);
1067
1068 // push fields in the rows array as a new row
1069 array_push($this->rows, array(
1070 array(
1071 'key' => 'id',
1072 'callback' => function ($val) use ($analog_id, $multi_analog_ids) {
1073 if ($val < 0 && !empty($analog_id)) {
1074 // custom (manual) invoices have a negative idorder (-number)
1075 $returi = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1076 if (count($multi_analog_ids) < 2) {
1077 // just one manual invoice found
1078 return '<a href="index.php?option=com_vikbooking&task=editmaninvoice&cid[]='.$analog_id.'&goto='.$returi.'"><i class="'.VikBookingIcons::i('external-link').'"></i> '.JText::translate('VBOMANUALINVOICE').'</a>';
1079 }
1080 /**
1081 * There can be conflictual manual invoices with the same number and negative order
1082 * across multiple years, so we print a link to display them all with an alert.
1083 * @since 1.13.5
1084 */
1085 $all_links = [];
1086 foreach ($multi_analog_ids as $analog_info) {
1087 array_push($all_links, '<a href="index.php?option=com_vikbooking&task=editmaninvoice&cid[]='.$analog_info['invid'].'&goto='.$returi.'" onclick="alert(\'Use date filters to not list manual invoices with the same number\'); return true;"><i class="'.VikBookingIcons::i('external-link').'"></i> '.JText::translate('VBOMANUALINVOICE').' (' . $analog_info['for_date'] . ')</a>');
1088 }
1089 return implode('<br/>', $all_links);
1090 }
1091 return '<a href="index.php?option=com_vikbooking&task=editorder&cid[]='.$val.'" target="_blank"><i class="'.VikBookingIcons::i('external-link').'"></i> '.$val.'</a>';
1092 },
1093 'value' => $bid
1094 ),
1095 array(
1096 'key' => 'ts',
1097 'attr' => array(
1098 'class="center"'
1099 ),
1100 'callback' => function ($val) use ($df, $datesep, $tswday) {
1101 return $tswday.', '.date(str_replace("/", $datesep, $df), $val);
1102 },
1103 'value' => $gbook[0]['ts']
1104 ),
1105 array(
1106 'key' => 'checkin',
1107 'callback' => function ($val) use ($df, $datesep, $inwday) {
1108 if (empty($val)) {
1109 // custom (manual) invoices have an empty timestamp
1110 return '-----';
1111 }
1112 return $inwday.', '.date(str_replace("/", $datesep, $df), $val);
1113 },
1114 'value' => $gbook[0]['checkin']
1115 ),
1116 array(
1117 'key' => 'checkout',
1118 'callback' => function ($val) use ($df, $datesep, $outwday) {
1119 if (empty($val)) {
1120 // custom (manual) invoices have an empty timestamp
1121 return '-----';
1122 }
1123 return $outwday.', '.date(str_replace("/", $datesep, $df), $val);
1124 },
1125 'value' => $gbook[0]['checkout']
1126 ),
1127 array(
1128 'key' => 'customer',
1129 'callback' => function ($val) use ($customer, $bid) {
1130 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1131 if (!empty($val)) {
1132 $cont = count($customer) ? '<a href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">'.$val.'</a>' : $val;
1133 if (count($customer) && !empty($customer['country'])) {
1134 if (is_file(VBO_ADMIN_PATH.DS.'resources'.DS.'countries'.DS.$customer['country'].'.png')) {
1135 $cont .= '<img src="'.VBO_ADMIN_URI.'resources/countries/'.$customer['country'].'.png'.'" title="'.$customer['country'].'" class="vbo-country-flag vbo-country-flag-left"/>';
1136 }
1137 }
1138 } else {
1139 // if empty customer ($val) print danger button to assign a customer to this booking ID
1140 $cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=newcustomer&bid='.$bid.'&goto='.$goto.'">' . JText::translate('VBOCREATENEWCUST') . '</a>';
1141 }
1142 return $cont;
1143 },
1144 'value' => (count($customer) ? $customer['first_name'].' '.$customer['last_name'] : '')
1145 ),
1146 array(
1147 'key' => 'country',
1148 'callback' => function ($val) {
1149 return !empty($val) ? $val : '-----';
1150 },
1151 'value' => $countryfull
1152 ),
1153 array(
1154 'key' => 'city',
1155 'attr' => array(
1156 'class="center"'
1157 ),
1158 'callback' => function ($val) use ($customer) {
1159 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1160 if (empty($val)) {
1161 if (count($customer) && !empty($customer['id'])) {
1162 // just an empty City, edit the customer
1163 $cont = '<a class="btn" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
1164 } else {
1165 $cont = '-----';
1166 }
1167 return $cont;
1168 }
1169 if (count($customer) && empty($customer['zip'])) {
1170 // postal code is mandatory
1171 return '<a class="btn" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">No Postal Code</a>';
1172 }
1173 if (count($customer) && empty($customer['address'])) {
1174 // address is mandatory
1175 return '<a class="btn btn-secondary" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">No Address</a>';
1176 }
1177 return $val;
1178 },
1179 'value' => (count($customer) && !empty($customer['city']) ? $customer['city'] : '')
1180 ),
1181 array(
1182 'key' => 'vat',
1183 'attr' => array(
1184 'class="center"'
1185 ),
1186 'callback' => function ($val) use ($customer, $bid) {
1187 if (!empty($val)) {
1188 $cont = $val;
1189 } else {
1190 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1191 if (count($customer) && !empty($customer['id'])) {
1192 // empty VAT Number, which may be mandatory for both issuer and counterpart
1193 $cont = '<a class="btn" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
1194 } else {
1195 // if empty customer ($val) print danger button to assign a customer to this booking ID
1196 $cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=newcustomer&bid='.$bid.'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
1197 }
1198 }
1199 return $cont;
1200 },
1201 'value' => (count($customer) && !empty($customer['vat']) ? $customer['vat'] : '')
1202 ),
1203 array(
1204 'key' => 'company',
1205 'callback' => function ($val) use ($customer) {
1206 $cont = !empty($val) ? $val : '-----';
1207 if (count($customer)) {
1208 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1209 $cont = '<a href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">'.$cont.'</a>';
1210 }
1211 return $cont;
1212 },
1213 'value' => (count($customer) && !empty($customer['company']) ? $customer['company'] : '')
1214 ),
1215 array(
1216 'key' => 'tot',
1217 'attr' => array(
1218 'class="center"'
1219 ),
1220 'callback' => function ($val) use ($currency_symb) {
1221 return $currency_symb.' '.VikBooking::numberFormat($val);
1222 },
1223 'value' => $gbook[0]['total']
1224 ),
1225 array(
1226 'key' => 'commands',
1227 'attr' => array(
1228 'class="center"'
1229 ),
1230 'callback' => function ($val) use ($bid, $noinvoicereason) {
1231 if ($val === 0 || $val === 1) {
1232 // invoice cannot be issued or is about to be issued
1233 return '';
1234 }
1235 $buttons = [];
1236 if ($val === -1 || $val === -2) {
1237 // invoice generated or generated and transmitted
1238 array_push($buttons, '<i class="vboicn-eye icn-nomargin vbo-driver-customoutput vbo-driver-output-vieweinv" title="View invoice" data-einvid="' . $noinvoicereason . '"></i>');
1239 array_push($buttons, '<i class="vboicn-pencil2 icn-nomargin vbo-driver-customoutput vbo-driver-output-editeinv" title="Edit invoice" data-einvid="' . $noinvoicereason . '"></i>');
1240 $correlated_inv_numb = $this->getPreviousCorrelatedInvoiceData($noinvoicereason, $bid);
1241 if ($correlated_inv_numb) {
1242 array_push($buttons, '<i class="vboicn-pencil2 icn-nomargin vbo-driver-customoutput vbo-driver-output-editeinv" title="Edit environmental fee invoice" data-einvid="' . $noinvoicereason . '" data-envfeebid="' . $bid . '"></i>');
1243 }
1244 array_push($buttons, '<i class="vboicn-bin icn-nomargin vbo-driver-customoutput vbo-driver-output-rmeinv" title="Delete invoice" data-einvid="' . $noinvoicereason . '"></i>');
1245 }
1246 return implode("\n", $buttons);
1247 },
1248 'value' => $canbeinvoiced
1249 ),
1250 array(
1251 'key' => 'action',
1252 'attr' => array(
1253 'class="center vbo-einvoicing-cellaction"',
1254 'data-einvaction="'.$canbeinvoiced.'"'
1255 ),
1256 'callback' => function ($val) use ($bid, $noinvoicereason, $einvnum, $einvtndata) {
1257 if ($val === 0) {
1258 // invoice cannot be issued
1259 $noinvoicereason = empty($noinvoicereason) ? 'Missing data to generate the invoice' : $noinvoicereason;
1260 return '<button type="button" class="btn btn-secondary" onclick="alert(\''.addslashes($noinvoicereason).'\');"><i class="vboicn-blocked icn-nomargin"></i> Not billable</button>';
1261 }
1262 if ($val === -1) {
1263 // e-invoice already issued and transmitted: print drop down to let the customer regenerate this invoice and obliterate the other or to re-send
1264 return '<select class="vbo-einvoicing-sentaction" data-bid="'.$bid.'"><option value="0-none">Invoice #'.$einvnum.' transmitted</option><option value="'.$noinvoicereason.'-regen">- Regenerate invoice</option><option value="'.$noinvoicereason.'-resend">- Retransmit invoice</option>' . (isset($einvtndata['env_fee_tn_res']) && !$einvtndata['env_fee_tn_res'] ?'<option value="'.$noinvoicereason.'-resendecofee">- Retransmit (only) eco-fee invoice</option>' : '') . '</select>';
1265 }
1266 if ($val === -2) {
1267 // e-invoice already issued but NOT transmitted: print drop down to let the customer regenerate this invoice and obliterate the other
1268 return '<select class="vbo-einvoicing-existaction" data-bid="'.$bid.'"><option value="0">Transmit invoice #'.$einvnum.'</option><option value="-1">- Do NOT transmit invoice</option><option value="'.$noinvoicereason.'">- Regenerate invoice</option></select>';
1269 }
1270 // invoice can be issued: print drop down to let the customer skip this generation
1271 return '<select class="vbo-einvoicing-selaction" data-bid="'.$bid.'"><option value="0">Generate invoice</option><option value="1">- Do NOT generate invoice</option></select>';
1272 },
1273 'value' => $canbeinvoiced
1274 ),
1275 ));
1276 }
1277
1278 // sort rows
1279 $this->sortRows($pkrsort, $pkrorder);
1280
1281 // build footer rows
1282 $totcols = count($this->cols);
1283 $footerstats = [];
1284 foreach ($this->rows as $k => $row) {
1285 foreach ($row as $col) {
1286 if ($col['key'] != 'action') {
1287 continue;
1288 }
1289 if (!isset($footerstats[$col['value']])) {
1290 $footerstats[$col['value']] = 0;
1291 }
1292 $footerstats[$col['value']]++;
1293 }
1294 }
1295 $avgcolspan = floor($totcols / count($footerstats));
1296 $footercells = [];
1297 foreach ($footerstats as $canbeinvoiced => $tot) {
1298 switch ($canbeinvoiced) {
1299 case 1:
1300 $descr = 'To be invoiced';
1301 break;
1302 case -1:
1303 $descr = 'Transmitted invoices';
1304 break;
1305 case -2:
1306 $descr = 'Generated invoices';
1307 break;
1308 default:
1309 $descr = 'Not billable';
1310 break;
1311 }
1312 array_push($footercells, array(
1313 'attr' => array(
1314 'class="vbo-report-total vbo-driver-total"',
1315 'colspan="'.$avgcolspan.'"'
1316 ),
1317 'value' => '<h3>'.$descr.': '.$tot.'</h3>'
1318 ));
1319 }
1320 $this->footerRow[0] = $footercells;
1321 $missingcols = $totcols - ($avgcolspan * count($footerstats));
1322 if ($missingcols > 0) {
1323 array_push($this->footerRow[0], array(
1324 'attr' => array(
1325 'class="vbo-report-total vbo-driver-total"',
1326 'colspan="'.$missingcols.'"'
1327 ),
1328 'value' => ''
1329 ));
1330 }
1331
1332 // update bookings array for the other methods to avoid double executions
1333 $this->bookings = $bookings;
1334
1335 return true;
1336 }
1337
1338 /**
1339 * Checks whether an e-invoice can be issued for this booking.
1340 *
1341 * @param array the booking array with one array-room per array value
1342 *
1343 * @return array to be used with list(): 0 => (int) can be invoiced, 1 => (string) reason message
1344 *
1345 * @see https://www.aade.gr/sites/default/files/2020-04/myDATA%20API%20Documentation%20v0%206b_eng.pdf
1346 */
1347 protected function canBookingBeInvoiced($booking)
1348 {
1349 // load driver settings
1350 $settings = $this->loadSettings();
1351
1352 /**
1353 * For certain types of invoice, the customer (counterpart) node is forbidden,
1354 * hence no customer information is actually required.
1355 *
1356 * @since 1.18.2 (J) - 1.8.2 (WP)
1357 */
1358 $inv_types_forbid_counterpart = [
1359 '11.1',
1360 '11.2',
1361 ];
1362
1363 // access the configured invoice type
1364 $invtype = $settings && !empty($settings['params']['einvtypecode']) ? $settings['params']['einvtypecode'] : VikBookingMydataAadeConstants::DEFAULT_INVOICE_TYPE;
1365
1366 // check whether the counterpart is mandatory
1367 $mandatory_counterpart = !in_array($invtype, $inv_types_forbid_counterpart);
1368
1369 if (empty($booking[0]['customer'])) {
1370 // customer record is mandatory to identify a reservation with complete details
1371 return array(0, 'Missing customer record');
1372 }
1373
1374 // validate booking customer information required
1375 if ($mandatory_counterpart) {
1376 if (empty($booking[0]['customer']['vat'])) {
1377 // the VAT number is a mandatory field for both issuer and counterpart
1378 return array(0, 'Missing VAT Number');
1379 }
1380
1381 if (empty($booking[0]['customer']['country']) || empty($booking[0]['customer']['country_2_code'])) {
1382 return array(0, 'Missing country');
1383 }
1384
1385 if (empty($booking[0]['customer']['city'])) {
1386 return array(0, 'Missing City');
1387 }
1388
1389 if (empty($booking[0]['customer']['zip'])) {
1390 return array(0, 'Missing Postal Code');
1391 }
1392 }
1393
1394 // check if an electronic invoice was already issued for this booking ID by this driver
1395 if (!empty($booking[0]['einvid']) && $booking[0]['einvdriver'] == $this->getDriverId()) {
1396 if ($booking[0]['einvsent'] > 0) {
1397 // in this case we return -1 because an e-invoice was already issued and transmitted. We use the second key for the ID of the e-invoice
1398 return array(-1, $booking[0]['einvid']);
1399 }
1400 // in this case we return -2 because an e-invoice was already issued but NOT transmitted. We use the second key for the ID of the e-invoice
1401 return array(-2, $booking[0]['einvid']);
1402 }
1403
1404 return array(1, '');
1405 }
1406
1407 /**
1408 * @inheritDoc
1409 *
1410 * @since 1.16.7 (J) - 1.6.7 (WP)
1411 */
1412 public function elaborateBookingDetails(array &$booking, array &$rooms = [])
1413 {
1414 // load driver settings
1415 $settings = $this->loadSettings();
1416 if (!$settings || !$settings['params']) {
1417 return;
1418 }
1419
1420 // make sure the environmental fee invoice generation setting is enabled
1421 if (empty($settings['params']['environmental_invoice']) || empty($settings['params']['envfeevboid'])) {
1422 return;
1423 }
1424
1425 // look for the environmental fee details
1426 $this->environmental_fee_details = [];
1427
1428 // scan the list of room reservation options to find the environmental fee
1429 foreach ($rooms as $k => $or) {
1430 if (empty($or['optionals'])) {
1431 continue;
1432 }
1433 $stepo = explode(";", $or['optionals']);
1434 foreach ($stepo as $roptkey => $oo) {
1435 if (empty($oo)) {
1436 continue;
1437 }
1438 $stept = explode(":", $oo);
1439 if ((int)$stept[0] != (int)$settings['params']['envfeevboid']) {
1440 continue;
1441 }
1442
1443 $this->dbo->setQuery(
1444 $this->dbo->getQuery(true)
1445 ->select('*')
1446 ->from($this->dbo->qn('#__vikbooking_optionals'))
1447 ->where($this->dbo->qn('id') . ' = ' . (int)$settings['params']['envfeevboid'])
1448 , 0, 1);
1449 $environmental_fee = $this->dbo->loadAssoc();
1450
1451 if ($environmental_fee && !$environmental_fee['pcentroom'] && $environmental_fee['cost']) {
1452 // we've found what we needed, calculate the fee cost
1453 $fee_cost = $environmental_fee['perday'] ? ($environmental_fee['cost'] * $booking['days']) : $environmental_fee['cost'];
1454 if ($environmental_fee['perperson']) {
1455 $fee_cost = $fee_cost * $or['adults'];
1456 }
1457
1458 if (!$this->environmental_fee_details) {
1459 $this->environmental_fee_details = $environmental_fee;
1460 $this->environmental_fee_details['fee_cost'] = 0;
1461 }
1462
1463 // set final cost
1464 $this->environmental_fee_details['fee_cost'] += $fee_cost;
1465
1466 // unset the extra service from the room reservation record
1467 unset($stepo[$roptkey]);
1468 $rooms[$k]['optionals'] = implode(';', $stepo);
1469
1470 // just one fee per room is supported
1471 break;
1472 }
1473 }
1474 }
1475
1476 if (!$this->environmental_fee_details && !empty($booking['idorderota']) && !empty($booking['channel'])) {
1477 // in case of OTA reservation, scan the list of custom extra services and their type
1478 foreach ($rooms as $k => $or) {
1479 if (empty($or['extracosts'])) {
1480 continue;
1481 }
1482 $extra_costs = json_decode($or['extracosts'], true);
1483 if (!is_array($extra_costs) || !$extra_costs) {
1484 continue;
1485 }
1486 foreach ($extra_costs as $ec_key => $ec_data) {
1487 if (!$ec_data || empty($ec_data['type']) || empty($ec_data['name']) || empty($ec_data['cost'])) {
1488 continue;
1489 }
1490 if (!strcasecmp($ec_data['type'], 'env_fee')) {
1491 // environmental fee found from OTA reservation
1492 if (!$this->environmental_fee_details) {
1493 $this->environmental_fee_details = $ec_data;
1494 $this->environmental_fee_details['fee_cost'] = 0;
1495 }
1496
1497 // set final cost
1498 $this->environmental_fee_details['fee_cost'] += (float)$ec_data['cost'];
1499
1500 // unset the custom extra service from the room reservation record
1501 unset($extra_costs[$ec_key]);
1502 if (!$extra_costs) {
1503 $rooms[$k]['extracosts'] = null;
1504 } else {
1505 $rooms[$k]['extracosts'] = json_encode($extra_costs);
1506 }
1507
1508 // just one fee per room is supported
1509 break;
1510 }
1511 }
1512 }
1513 }
1514
1515 if ($this->environmental_fee_details) {
1516 // lower booking total value
1517 $booking['total'] -= $this->environmental_fee_details['fee_cost'];
1518
1519 if (isset($booking['optionals'])) {
1520 $booking['optionals'] = $rooms[0]['optionals'];
1521 }
1522
1523 if (isset($booking['extracosts'])) {
1524 $booking['extracosts'] = $rooms[0]['extracosts'];
1525 }
1526 }
1527
1528 return;
1529 }
1530
1531 /**
1532 * @inheritDoc
1533 *
1534 * @since 1.16.7 (J) - 1.6.7 (WP)
1535 */
1536 public function getBookingExtraInvoices($bid)
1537 {
1538 $this->dbo->setQuery(
1539 $this->dbo->getQuery(true)
1540 ->select('*')
1541 ->from($this->dbo->qn('#__vikbooking_einvoicing_data'))
1542 ->where($this->dbo->qn('driverid') . ' = ' . $this->dbo->q($this->getDriverId()))
1543 ->where($this->dbo->qn('idorder') . ' = ' . (int)$bid)
1544 ->where($this->dbo->qn('transmitted') . ' = 1')
1545 ->where($this->dbo->qn('obliterated') . ' = 0')
1546 ->order($this->dbo->qn('id') . ' DESC')
1547 , 0, 1);
1548
1549 $last_einvoice = $this->dbo->loadAssoc();
1550
1551 if (!$last_einvoice) {
1552 return [];
1553 }
1554
1555 $booking = VikBooking::getBookingInfoFromID($bid);
1556
1557 // check if a correlated invoice was generated
1558 $correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($last_einvoice['id'], $bid), []);
1559
1560 if (!$correlated_inv_raw_data || empty($correlated_inv_raw_data['transmission']) || empty($correlated_inv_raw_data['transmission']['pdf'])) {
1561 return [];
1562 }
1563
1564 // build the PDF locations
1565 $pdffname = implode('_', ['envfee', $booking['id'], ($booking['sid'] ?: $booking['ts'])]) . '.pdf';
1566 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
1567 $urlpdf = VBO_SITE_URI . 'helpers/invoices/generated/' . $pdffname;
1568
1569 return [
1570 [
1571 'label' => 'Correlated Invoice',
1572 'path' => $pathpdf,
1573 'uri' => $urlpdf,
1574 ]
1575 ];
1576 }
1577
1578 /**
1579 * Generates the electronic invoices according to the input parameters.
1580 * This is a 'driver action', and so it's called before getBookingsData()
1581 * in the view. This method will save/update records in the DB so that when
1582 * the view re-calls getBookingsData(), the information will be up to date.
1583 *
1584 * @return boolean True if at least one e-invoice was generated
1585 */
1586 public function generateEInvoices()
1587 {
1588 // call the main method to generate rows, cols and bookings array
1589 $this->getBookingsData();
1590
1591 if (strlen($this->getError()) || !$this->bookings) {
1592 return false;
1593 }
1594
1595 $generated = 0;
1596
1597 foreach ($this->bookings as $gbook) {
1598 // check whether this booking ID was set to be skipped
1599 $exclude = VikRequest::getInt('excludebid'.$gbook[0]['id'], 0, 'request');
1600 if ($exclude > 0) {
1601 // skipping this invoice
1602 continue;
1603 }
1604
1605 // check if an electronic invoice was already issued for this booking ID by this driver
1606 if (!empty($gbook[0]['einvid']) && $gbook[0]['einvdriver'] == $this->getDriverId()) {
1607 $regenerate = VikRequest::getInt('regeneratebid'.$gbook[0]['id'], 0, 'request');
1608 if (!($regenerate > 0)) {
1609 // we do not re-generate an invoice for this booking ID
1610 continue;
1611 }
1612 }
1613
1614 // generate invoice
1615 if ($this->generateEInvoice($gbook)) {
1616 $generated++;
1617 }
1618 }
1619
1620 // we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
1621 $this->bookings = [];
1622 // unset also cols, rows and footer row to not merge data
1623 $this->cols = [];
1624 $this->rows = [];
1625 $this->footerRow = [];
1626
1627 // set info message
1628 $this->setInfo('Invoices generated: '.$generated);
1629
1630 return ($generated > 0);
1631 }
1632
1633 /**
1634 * Given two arguments, the current analogic invoice record and the customer record, this
1635 * method should prepare and return an array that can be later passed onto generateEInvoice().
1636 * This originally abstract method must be implemented for the generation of the custom (manual) invoices
1637 * that are not related to any bookings (idorder = -number), that were manually created for certain customers.
1638 *
1639 * @param array $invoice the analogic invoice record
1640 * @param array $customer the customer record obtained through VikBookingCustomersPin::getCustomerByID()
1641 *
1642 * @return array the data array compatible with generateEInvoice()
1643 *
1644 * @see generateEInvoice()
1645 */
1646 public function prepareCustomInvoiceData($invoice, $customer)
1647 {
1648 if (!isset($invoice['number']) && !empty($invoice['einvnum'])) {
1649 // getBookingsData() may call this method by knowing only the electronic invoice number
1650 $invoice['number'] = $invoice['einvnum'];
1651 }
1652
1653 // make sure to get an integer value from the invoice number, which is a string with a probable suffix
1654 $numnumber = intval(preg_replace("/[^\d]+/", '', $invoice['number']));
1655
1656 // make sure the key rawcont is an array
1657 if (!is_array($invoice['rawcont'])) {
1658 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : [];
1659 $rawcont = is_array($rawcont) ? $rawcont : [];
1660 $invoice['rawcont'] = $rawcont;
1661 }
1662
1663 // build necessary data array compatible with generateEInvoice()
1664 $data = array(
1665 'id' => ($numnumber - ($numnumber * 2)),
1666 'ts' => (isset($invoice['created_on']) ? strtotime($invoice['created_on']) : time()),
1667 'checkin' => 0,
1668 'checkout' => 0,
1669 'adults' => 0,
1670 'children' => 0,
1671 'total' => $invoice['rawcont']['totaltot'],
1672 'country' => $customer['country'],
1673 'country_name' => $customer['country_name'],
1674 'country_2_code' => (isset($customer['country_2_code']) ? $customer['country_2_code'] : null),
1675 'tot_taxes' => $invoice['rawcont']['totaltax'],
1676 'tot_city_taxes' => 0,
1677 'tot_fees' => 0,
1678 'customer' => $customer,
1679 'pkg' => null,
1680 'einvid' => (isset($invoice['einvid']) ? $invoice['einvid'] : null),
1681 'einvdriver' => (isset($invoice['einvdriver']) ? $invoice['einvdriver'] : null),
1682 'einvdate' => (isset($invoice['einvdate']) ? $invoice['einvdate'] : null),
1683 'einvnum' => (isset($invoice['einvnum']) ? $invoice['einvnum'] : null),
1684 'einvsent' => (isset($invoice['einvsent']) ? $invoice['einvsent'] : null),
1685 // this could be the ID of the analogic invoice
1686 'invid' => (isset($invoice['invid']) ? $invoice['invid'] : null),
1687 // this could be the for date timestamp of the analogic invoice
1688 'inv_fordate_ts' => (isset($invoice['inv_fordate_ts']) ? $invoice['inv_fordate_ts'] : null),
1689 );
1690
1691 // make sure to inject the raw content of the custom invoice
1692 $this->externalData['einvrawcont'] = $invoice['rawcont'];
1693
1694 // original data array contains nested rooms booked so we need to return it as the 0th value
1695 return array($data);
1696 }
1697
1698 /**
1699 * Checks whether an active electronic invoice already exists from the given details.
1700 *
1701 * @param mixed $data array or StdClass object with properties to identify the e-invoice
1702 *
1703 * @return mixed False if the invoice does not exist, its ID otherwise.
1704 */
1705 public function eInvoiceExists($data)
1706 {
1707 if (is_object($data)) {
1708 // cast to array
1709 $data = (array)$data;
1710 }
1711
1712 // allowed properties to check
1713 $properties = array(
1714 'id' => 'einvid',
1715 'idorder' => 'idorder',
1716 'number' => 'number',
1717 );
1718
1719 $filters = [];
1720 foreach ($properties as $k => $v) {
1721 if (isset($data[$v]) && !empty($data[$v])) {
1722 $filters[$k] = $data[$v];
1723 } elseif (isset($data[$k]) && !empty($data[$k])) {
1724 $filters[$k] = $data[$k];
1725 }
1726 }
1727
1728 if (empty($filters)) {
1729 return false;
1730 }
1731
1732 $clauses = [];
1733 foreach ($filters as $col => $val) {
1734 array_push($clauses, "`{$col}`=".$this->dbo->quote($val));
1735 }
1736
1737 $q = "SELECT `id` FROM `#__vikbooking_einvoicing_data` WHERE `driverid`=".(int)$this->getDriverId()." AND `obliterated`=0 AND ".implode(' AND ', $clauses)." ORDER BY `id` DESC LIMIT 1;";
1738 $this->dbo->setQuery($q);
1739 $this->dbo->execute();
1740 if (!$this->dbo->getNumRows()) {
1741 return false;
1742 }
1743
1744 return $this->dbo->loadResult();
1745 }
1746
1747 /**
1748 * Attempts to set one e-invoice to obliterated.
1749 *
1750 * @param mixed $data array or StdClass object with properties to identify the e-invoice
1751 *
1752 * @return void
1753 */
1754 public function obliterateEInvoice($data)
1755 {
1756 if (is_object($data)) {
1757 // cast to array
1758 $data = (array)$data;
1759 }
1760
1761 // allowed properties to check
1762 $properties = array(
1763 'id' => 'einvid',
1764 'idorder' => 'idorder',
1765 'number' => 'number',
1766 );
1767
1768 $filters = [];
1769 foreach ($properties as $k => $v) {
1770 if (isset($data[$v]) && !empty($data[$v])) {
1771 $filters[$k] = $data[$v];
1772 } elseif (isset($data[$k]) && !empty($data[$k])) {
1773 $filters[$k] = $data[$k];
1774 }
1775 }
1776
1777 if (empty($filters)) {
1778 return;
1779 }
1780
1781 $clauses = [];
1782 foreach ($filters as $col => $val) {
1783 array_push($clauses, "`{$col}`=".$this->dbo->quote($val));
1784 }
1785
1786 $q = "UPDATE `#__vikbooking_einvoicing_data` SET `obliterated`=1 WHERE `driverid`=".(int)$this->getDriverId()." AND ".implode(' AND ', $clauses).";";
1787 $this->dbo->setQuery($q);
1788 $this->dbo->execute();
1789 }
1790
1791 /**
1792 * Generates one single electronic invoice. If no array data provided, the booking ID should
1793 * be passed as argument. In this case the method would fetch and nest the booking data.
1794 *
1795 * @param mixed $data either the booking ID or the booking array (one room info per index)
1796 * @param bool $correlated true if the invoice should only contain the environmental fee details.
1797 *
1798 * @return string|bool true if the e-invoice was generated, or string if it was the correlated one.
1799 */
1800 public function generateEInvoice($data, $correlated = false)
1801 {
1802 // load driver settings
1803 $settings = $this->loadSettings();
1804 if ($settings === false || !$settings['params']) {
1805 $this->setError('Missing driver settings. Please set up the driver first.');
1806 return false;
1807 }
1808
1809 if ($correlated && !$this->environmental_fee_details) {
1810 $this->setError('Could not generate the correlated invoice');
1811 return false;
1812 }
1813
1814 if (is_int($data)) {
1815 // query to obtain the booking records
1816 $q = "SELECT `o`.`id`,`o`.`ts`,`o`.`days`,`o`.`checkin`,`o`.`checkout`,`o`.`totpaid`,`o`.`idpayment`,`o`.`coupon`,`o`.`roomsnum`,`o`.`total`,`o`.`idorderota`,`o`.`channel`,`o`.`chcurrency`,`o`.`country`,`o`.`tot_taxes`,".
1817 "`o`.`tot_city_taxes`,`o`.`tot_fees`,`o`.`cmms`,`o`.`pkg`,`o`.`refund`,`or`.`idorder`,`or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`c`.`country_name`,`r`.`name` AS `room_name`,`r`.`fromadult`,`r`.`toadult`,`ei`.`id` AS `einvid`,`ei`.`driverid` AS `einvdriver`,`ei`.`for_date` AS `einvdate`,`ei`.`number` AS `einvnum`,`ei`.`transmitted` AS `einvsent`,`ei`.`trans_data` AS `einvtndata` ".
1818 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
1819 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
1820 "LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
1821 "LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
1822 "WHERE ".
1823 "(`o`.`status`='confirmed' OR (`o`.`status`='cancelled' AND `o`.`totpaid`>0)) AND `o`.`closure`=0 AND `o`.`id`=".$this->dbo->quote($data)." ".
1824 "ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
1825 $this->dbo->setQuery($q);
1826 $record = $this->dbo->loadAssocList();
1827 if (!$record) {
1828 $this->setError('Could not find the booking information');
1829 return false;
1830 }
1831
1832 // nest records with multiple rooms booked inside sub-array
1833 $record = $this->nestBookingsData($record);
1834 $data = $record[$data];
1835 }
1836
1837 if (!is_array($data) || empty($data)) {
1838 $this->setError('No bookings found');
1839 return false;
1840 }
1841
1842 /**
1843 * Elaborate the booking details in case of environmental fee available.
1844 *
1845 * @since 1.16.7 (J) - 1.6.7 (WP)
1846 */
1847 if (!$correlated) {
1848 // build the booking record
1849 $elaborate_booking = $data[0];
1850
1851 // build the room reservation records
1852 $elaborate_rooms = $data;
1853
1854 // elaborate data for the environmental fee
1855 $this->elaborateBookingDetails($elaborate_booking, $elaborate_rooms);
1856
1857 // replace values, all room reservation records first, then the main booking
1858 $data = $elaborate_rooms;
1859 $data[0] = $elaborate_booking;
1860 }
1861
1862 // check whether the invoice can be issued
1863 list($canbeinvoiced, $noinvoicereason) = $this->canBookingBeInvoiced($data);
1864 if ($canbeinvoiced === 0) {
1865 /**
1866 * IMPORTANT: if this method is not called by generateEInvoices(), then the script should
1867 * make sure that an e-invoice is not already available for this booking ID because
1868 * here we skip only if $canbeinvoiced=0 and when e-invoices exist, the code is -1 or -2.
1869 */
1870
1871 // do not raise any errors unless called externally, we just skip this booking because it cannot be invoiced
1872 if ($this->externalCall) {
1873 if ($data[0]['id'] < 0) {
1874 $message = "Could not generate electronic invoice from custom invoice: {$noinvoicereason}";
1875 } else {
1876 $message = "Could not generate electronic invoice for booking ID {$data[0]['id']} ({$noinvoicereason})";
1877 }
1878 $this->setError($message);
1879 }
1880
1881 return false;
1882 }
1883
1884 // counterpart branch number
1885 $branch = '0';
1886
1887 // invoice/correlated invoice number
1888 $invnum = '';
1889 $correlated_invnum = '';
1890
1891 // counterpart name must not be submitted if entity is from Greece
1892 $client_name = '';
1893 if ((!empty($data[0]['customer']['first_name']) || !empty($data[0]['customer']['last_name'])) && $data[0]['customer']['country'] != 'GRC') {
1894 $client_name = $data[0]['customer']['first_name'] . ' ' . $data[0]['customer']['last_name'];
1895 }
1896
1897 // invoice date and number (suffix not supported for AA serial number)
1898 if (!empty($data[0]['einvnum']) && $settings['params']['einvexnumdt'] == 'old') {
1899 // if an invoice already exists, we re-use the same number also because the setting said so
1900 $invnum = $data[0]['einvnum'];
1901 $invdate = $data[0]['einvdate'];
1902 if ($correlated) {
1903 // get the previous correlated invoice number
1904 $correlated_invnum = $this->getPreviousCorrelatedInvoiceData($data[0]['einvid'], $data[0]['id']);
1905 }
1906 } else {
1907 // get a new invoice number
1908 if ($correlated) {
1909 $correlated_invnum = (int) ($settings['params']['envfeeinvoiceinum'] ?: 0) + 1;
1910 } else {
1911 $invnum = VikBooking::getNextInvoiceNumber();
1912 }
1913
1914 // get the new invoice date
1915 $invdate = $settings['params']['einvdttype'] == 'ts' ? date('Y-m-d', $data[0]['ts']) : date('Y-m-d');
1916
1917 /**
1918 * Trigger event to allow third party plugins to apply a custom invoice number and date.
1919 *
1920 * @since 1.18.6 (J) - 1.8.6 (WP)
1921 */
1922 $custom_einv_data = VBOFactory::getPlatform()->getDispatcher()->filter('onMydataDetermineNewEinvoiceProperties', [($correlated ? $correlated_invnum : $invnum), $invdate, $correlated, $settings, $data]);
1923 if ($custom_einv_data) {
1924 // invoice number is expected to be returned at index 0
1925 if ($correlated) {
1926 $correlated_invnum = ($custom_einv_data[0] ?? '') ?: $correlated_invnum;
1927 } else {
1928 $invnum = ($custom_einv_data[0] ?? '') ?: $invnum;
1929 }
1930 // invoice date is expected to be returned at index 1
1931 $invdate = ($custom_einv_data[1] ?? '') ?: $invdate;
1932 }
1933 }
1934
1935 if (isset($this->externalData['einvnum']) && intval($this->externalData['einvnum']) > 0) {
1936 // external calls may inject the invoice number to use
1937 $invnum = (int)$this->externalData['einvnum'];
1938 }
1939 if (isset($this->externalData['einvdate']) && !empty($this->externalData['einvdate'])) {
1940 // external calls may inject the invoice date to use
1941 $invdate = is_int($this->externalData['einvdate']) ? date('Y-m-d', $this->externalData['einvdate']) : $this->externalData['einvdate'];
1942 }
1943
1944 // invoice series ("in case of non-issuance of series of an invoice, the series field must have a value of 0")
1945 $series = $correlated ? 'C' : '0';
1946 // invoice serial number "aa" (we use the e-invoice number in VBO with no suffix as it must be a positive number, or it could be just '0')
1947 $aa_serial_number = $correlated && $correlated_invnum ? $correlated_invnum : $invnum;
1948 // invoice type
1949 $invtype = !empty($settings['params']['einvtypecode']) ? $settings['params']['einvtypecode'] : VikBookingMydataAadeConstants::DEFAULT_INVOICE_TYPE;
1950 $orig_invtype = $invtype;
1951 $invtype = $correlated ? '8.2' : $invtype;
1952
1953 // invoice total paid amount
1954 $inv_tot_paid = empty($data[0]['totpaid']) ? $data[0]['total'] : $data[0]['totpaid'];
1955 if ($correlated) {
1956 $inv_tot_paid = $this->environmental_fee_details['fee_cost'];
1957 }
1958
1959 // invoice payment method
1960 $inv_pay_method = '';
1961 if (!empty($data[0]['idpayment'])) {
1962 $pay_info_parts = explode('=', $data[0]['idpayment']);
1963 $inv_pay_method = !empty($pay_info_parts[1]) ? $pay_info_parts[1] : $inv_pay_method;
1964 }
1965
1966 // compose the invoice UID
1967 $invoice_uid_parts = [
1968 $settings['params']['vatid'],
1969 $invdate,
1970 $branch,
1971 $invtype,
1972 $series,
1973 $aa_serial_number,
1974 ];
1975 $invoice_uid = sha1(implode('', $invoice_uid_parts));
1976
1977 // invoice details and summaries
1978 $invoice_details = [];
1979 $summaries = [];
1980 $summariesvat = [];
1981 $rounded_nets = [];
1982
1983 // whether to include "incomeClassification" nodes
1984 $use_income_classf = (!empty($settings['params']['einv_inc_class_type']) && !empty($settings['params']['einv_inc_class_cat']));
1985
1986 $is_package = (!empty($data[0]['pkg']));
1987 $isdue = 0;
1988 $extralinenum = 0;
1989 $discountval = 0;
1990 if ($data[0]['id'] < 0 && isset($this->externalData['einvrawcont'])) {
1991 // custom (manual) invoice, get the raw content of the invoice
1992 foreach ($this->externalData['einvrawcont']['rows'] as $ind => $row) {
1993 if (!isset($summariesvat[$row['aliq']])) {
1994 $summariesvat[$row['aliq']] = array('net' => 0, 'tax' => 0);
1995 $rounded_nets[$row['aliq']] = 0;
1996 }
1997 $summariesvat[$row['aliq']]['net'] += $row['net'];
1998 $summariesvat[$row['aliq']]['tax'] += $row['tax'];
1999 $rounded_nets[$row['aliq']] += (float)number_format($row['net'], 2, '.', '');
2000
2001 // income classification
2002 $inc_classf_nodes = '';
2003 if ($use_income_classf) {
2004 $inc_classf_nodes = '<incomeClassification>
2005 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2006 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2007 <N1:amount>' . number_format($row['net'], 2, '.', '') . '</N1:amount>
2008 </incomeClassification>';
2009 }
2010
2011 // push invoice details node
2012 $vat_category = VikBookingMydataAadeConstants::getVatCategory($row['aliq']);
2013 array_push($invoice_details, '
2014 <invoiceDetails>
2015 <lineNumber>' . ($ind + 1) . '</lineNumber>
2016 <netValue>' . number_format($row['net'], 2, '.', '') . '</netValue>
2017 <vatCategory>' . $vat_category . '</vatCategory>
2018 <vatAmount>' . number_format($row['tax'], 2, '.', '') . '</vatAmount>
2019 ' . ((int)$row['aliq'] === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2020 <lineComments>'.$this->convertSpecials($row['service']).'</lineComments>
2021 ' . $inc_classf_nodes . '
2022 </invoiceDetails>');
2023
2024 }
2025 } else {
2026 // invoice for a regular booking
2027 $tars = $this->getBookingTariffs($data);
2028
2029 // check discount (coupon and/or refund)
2030 $discount_nodes = '';
2031 if (isset($data[0]['coupon']) && strlen($data[0]['coupon']) > 0) {
2032 $expcoupon = explode(";", $data[0]['coupon']);
2033 $discountval += (float)$expcoupon[1];
2034 }
2035 if (isset($data[0]['refund']) && $data[0]['refund'] > 0) {
2036 $discountval += $data[0]['refund'];
2037 }
2038 if ($discountval > 0) {
2039 $discount_nodes = '
2040 <discountOption>true</discountOption>
2041 <deductionsAmount>' . number_format($discountval, 2, '.', '') . '</deductionsAmount>';
2042 }
2043
2044 foreach ($data as $kor => $or) {
2045 $num = $kor + 1;
2046
2047 if ($correlated) {
2048 // push invoice details node
2049 array_push($invoice_details, '
2050 <invoiceDetails>
2051 <lineNumber>1</lineNumber>
2052 <netValue>0.00</netValue>
2053 <vatCategory>8</vatCategory>
2054 <vatAmount>0.00</vatAmount>
2055 <incomeClassification>
2056 <N1:classificationCategory>category1_95</N1:classificationCategory>
2057 <N1:amount>0</N1:amount>
2058 </incomeClassification>
2059 </invoiceDetails>
2060 <taxesTotals>
2061 <taxes>
2062 <taxType>3</taxType>
2063 <taxCategory>10</taxCategory>
2064 <underlyingValue>' . number_format($or['total'], 2, '.', '') . '</underlyingValue>
2065 <taxAmount>' . number_format($this->environmental_fee_details['fee_cost'], 2, '.', '') . '</taxAmount>
2066 </taxes>
2067 </taxesTotals>');
2068 // break the loop for the single environmental fee
2069 break;
2070 }
2071
2072 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2073 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
2074 $descr = $is_package ? sprintf(VikBookingMydataAadeConstants::DESCRPACKAGENIGHTS, $or['days']) : sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
2075 $cost_minus_tax = VikBooking::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
2076 $cost_tax_amount = (VikBooking::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']) - $cost_minus_tax);
2077 $aliq = $this->getAliquoteById($or['cust_idiva']);
2078 if (!isset($summariesvat[$aliq])) {
2079 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2080 $rounded_nets[$aliq] = 0;
2081 }
2082 $summariesvat[$aliq]['net'] += $cost_minus_tax;
2083 $summariesvat[$aliq]['tax'] += $cost_tax_amount;
2084 $rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
2085
2086 // income classification
2087 $inc_classf_nodes = '';
2088 if ($use_income_classf) {
2089 $inc_classf_nodes = '<incomeClassification>
2090 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2091 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2092 <N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
2093 </incomeClassification>';
2094 }
2095
2096 // push invoice details node
2097 array_push($invoice_details, '
2098 <invoiceDetails>
2099 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2100 <netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
2101 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2102 <vatAmount>' . number_format($cost_tax_amount, 2, '.', '') . '</vatAmount>
2103 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2104 ' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
2105 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2106 ' . $inc_classf_nodes . '
2107 </invoiceDetails>');
2108 } elseif (isset($tars[$num]) && is_array($tars[$num])) {
2109 // regular tariff
2110 $descr = sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
2111 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
2112 $calctar = VikBooking::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
2113 $aliq = $this->getAliquoteFromPriceId($tars[$num]['idprice']);
2114 $isdue += $calctar;
2115 if ($calctar == $display_rate) {
2116 $cost_minus_tax = VikBooking::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
2117 $tax = ($display_rate - $cost_minus_tax);
2118 } else {
2119 $cost_minus_tax = $display_rate;
2120 $tax = ($calctar - $display_rate);
2121 }
2122 if (!isset($summariesvat[$aliq])) {
2123 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2124 $rounded_nets[$aliq] = 0;
2125 }
2126 $summariesvat[$aliq]['net'] += $cost_minus_tax;
2127 $summariesvat[$aliq]['tax'] += $tax;
2128 $rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
2129
2130 // income classification
2131 $inc_classf_nodes = '';
2132 if ($use_income_classf) {
2133 $inc_classf_nodes = '<incomeClassification>
2134 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2135 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2136 <N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
2137 </incomeClassification>';
2138 }
2139
2140 // push invoice details node
2141 array_push($invoice_details, '
2142 <invoiceDetails>
2143 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2144 <netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
2145 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2146 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
2147 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2148 ' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
2149 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2150 ' . $inc_classf_nodes . '
2151 </invoiceDetails>');
2152 }
2153
2154 // room options
2155 if (!empty($or['optionals']) && !$correlated) {
2156 $stepo = explode(";", $or['optionals']);
2157 foreach ($stepo as $roptkey => $oo) {
2158 if (empty($oo)) {
2159 continue;
2160 }
2161 $stept = explode(":", $oo);
2162 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $this->dbo->quote($stept[0]) . ";";
2163 $this->dbo->setQuery($q);
2164 $actopt = $this->dbo->loadAssocList();
2165 if (!$actopt) {
2166 continue;
2167 }
2168 $chvar = '';
2169 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
2170 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
2171 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
2172 $optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
2173 $child_num = VikBooking::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
2174 $optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
2175 $agestept = explode('-', $stept[1]);
2176 $stept[1] = $agestept[0];
2177 $chvar = $agestept[1];
2178 $realcost = 0;
2179 if (!empty($chvar)) {
2180 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
2181 // percentage value of the adults tariff
2182 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2183 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
2184 } else {
2185 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
2186 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
2187 }
2188 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
2189 // percentage value of room base cost
2190 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2191 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
2192 } else {
2193 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
2194 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
2195 }
2196 }
2197 $actopt[0]['chageintv'] = $chvar;
2198 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
2199 $actopt[0]['quan'] = $stept[1];
2200 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $or['days'] * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
2201 }
2202 } else {
2203 $actopt[0]['quan'] = $stept[1];
2204 // VBO 1.11 - options percentage cost of the room total fee
2205 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2206 $deftar_basecosts = $or['cust_cost'];
2207 } else {
2208 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
2209 }
2210 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
2211 //
2212 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $or['days'] * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
2213 }
2214 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
2215 $realcost = $actopt[0]['maxprice'];
2216 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
2217 $realcost = $actopt[0]['maxprice'] * $stept[1];
2218 }
2219 }
2220 if ($actopt[0]['perperson'] == 1) {
2221 $realcost = $realcost * $or['adults'];
2222 }
2223
2224 /**
2225 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
2226 *
2227 * @since 1.17.7 (J) - 1.7.7 (WP)
2228 */
2229 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $or, $or]);
2230 if ($custom_calculation) {
2231 $realcost = (float) $custom_calculation[0];
2232 }
2233
2234 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
2235 $isdue += $tmpopr;
2236 // increase line number
2237 $extralinenum++;
2238 //
2239 $aliq = $this->getAliquoteById($actopt[0]['idiva']);
2240 if ($tmpopr == $realcost) {
2241 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
2242 $tax = ($realcost - $opt_minus_tax);
2243 } else {
2244 $opt_minus_tax = $realcost;
2245 $tax = ($tmpopr - $realcost);
2246 }
2247 $descr = $actopt[0]['is_citytax'] == 1 ? VikBookingMydataAadeConstants::DESCRTOURISTTAX : sprintf(VikBookingMydataAadeConstants::DESCRROOMOPTION, strtoupper($actopt[0]['name']));
2248 if (!isset($summariesvat[$aliq])) {
2249 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2250 $rounded_nets[$aliq] = 0;
2251 }
2252
2253 $summariesvat[$aliq]['net'] += $opt_minus_tax;
2254 $summariesvat[$aliq]['tax'] += $tax;
2255 $rounded_nets[$aliq] += (float)number_format($opt_minus_tax, 2, '.', '');
2256
2257 // income classification
2258 $inc_classf_nodes = '';
2259 if ($use_income_classf) {
2260 $inc_classf_nodes = '<incomeClassification>
2261 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2262 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2263 <N1:amount>' . number_format($opt_minus_tax, 2, '.', '') . '</N1:amount>
2264 </incomeClassification>';
2265 }
2266
2267 // push invoice details node
2268 array_push($invoice_details, '
2269 <invoiceDetails>
2270 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2271 <netValue>' . number_format($opt_minus_tax, 2, '.', '') . '</netValue>
2272 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2273 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
2274 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2275 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2276 ' . $inc_classf_nodes . '
2277 </invoiceDetails>');
2278 }
2279 }
2280
2281 // custom extra costs
2282 if (!empty($or['extracosts']) && !$correlated) {
2283 $cur_extra_costs = json_decode($or['extracosts'], true);
2284 foreach ($cur_extra_costs as $eck => $ecv) {
2285 // increase line number
2286 $extralinenum++;
2287 //
2288 $ecplustax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
2289 $isdue += $ecplustax;
2290 $descr = sprintf(VikBookingMydataAadeConstants::DESCRROOMEXTRACOST, strtoupper($ecv['name']));
2291 if ($ecplustax == $ecv['cost']) {
2292 $ec_minus_tax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
2293 $tax = ($ecv['cost'] - $ec_minus_tax);
2294 } else {
2295 $ec_minus_tax = ($ecplustax - $ecv['cost']);
2296 $tax = ($ecplustax - $ecv['cost']);
2297 }
2298 $aliq = $this->getAliquoteById($ecv['idtax']);
2299 if (!isset($summariesvat[$aliq])) {
2300 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2301 $rounded_nets[$aliq] = 0;
2302 }
2303
2304 $summariesvat[$aliq]['net'] += $ec_minus_tax;
2305 $summariesvat[$aliq]['tax'] += $tax;
2306 $rounded_nets[$aliq] += (float)number_format($ec_minus_tax, 2, '.', '');
2307
2308 // income classification
2309 $inc_classf_nodes = '';
2310 if ($use_income_classf) {
2311 $inc_classf_nodes = '<incomeClassification>
2312 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2313 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2314 <N1:amount>' . number_format($ec_minus_tax, 2, '.', '') . '</N1:amount>
2315 </incomeClassification>';
2316 }
2317
2318 /**
2319 * Removed "quantity" and "measurementUnit" nodes from every "invoiceDetails" node.
2320 *
2321 * <quantity>1.00</quantity>
2322 * <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
2323 *
2324 * @since 1.16.7 (J) - 1.6.7 (WP)
2325 */
2326
2327 // push invoice details node
2328 array_push($invoice_details, '
2329 <invoiceDetails>
2330 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2331 <netValue>' . number_format($ec_minus_tax, 2, '.', '') . '</netValue>
2332 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2333 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
2334 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2335 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2336 ' . $inc_classf_nodes . '
2337 </invoiceDetails>');
2338 }
2339 }
2340 }
2341 }
2342
2343 // build riepiloghi IVA
2344 $grand_total_net = 0;
2345 $grand_total_vat = 0;
2346 $grand_total_tax_no_rate = 0;
2347 $environmental_fee_amount = $correlated ? $this->environmental_fee_details['fee_cost'] : 0;
2348 foreach ($summariesvat as $aliq => $vat_summary) {
2349 $totnet = number_format($vat_summary['net'], 2, '.', '');
2350 $tottax = number_format($vat_summary['tax'], 2, '.', '');
2351 if (isset($rounded_nets[$aliq]) && (float)$totnet != $rounded_nets[$aliq]) {
2352 /**
2353 * In case of several rows in the invoice, maybe a lot of Extra Services,
2354 * there can be a discrepancy between the sum of the <netValue> nodes
2355 * in the <invoiceDetails> nodes, and the <taxAmount> in <taxesTotals>.
2356 * We need to prevent the amounts to be different because of number_format and
2357 * adjust the amounts and obtain the same value as the sum of the nets in the lines.
2358 * The issue was reproduced with 9 Extra Services, one Room, one Tourist Tax (Option).
2359 *
2360 * @see sandbox booking ID 1140
2361 */
2362 if ($rounded_nets[$aliq] > (float)$totnet) {
2363 $diff = $rounded_nets[$aliq] - (float)$totnet;
2364 $totnet = number_format($rounded_nets[$aliq], 2, '.', '');
2365 $tottax = number_format(((float)$tottax - $diff), 2, '.', '');
2366 } else {
2367 $diff = (float)$totnet - $rounded_nets[$aliq];
2368 $totnet = number_format($rounded_nets[$aliq], 2, '.', '');
2369 $tottax = number_format(((float)$tottax + $diff), 2, '.', '');
2370 }
2371 }
2372
2373 // sum grand total values
2374 $grand_total_net += $totnet;
2375 if ((int)$aliq > 0) {
2376 $grand_total_vat += $tottax;
2377 } else {
2378 /**
2379 * @todo are we doing good by summing this kind of tax, which is not VAT because
2380 * the tax rate is 0%, to the "total withheld amount"? Or should we use the
2381 * node <totalOtherTaxesAmount> instead?
2382 */
2383 $grand_total_tax_no_rate += $tottax;
2384 }
2385
2386 /**
2387 * For the moment we ingore completely the <taxesTotals> node and sub-nodes. Docs say:
2388 * "Field taxesTotals contains all taxes except VAT. If user users this element,
2389 * taxes will not exist in invoiceDetails".
2390 * However, here we have a sum of tax amounts for any aliquote (tax rate) involved.
2391 *
2392 * @todo check if these nodes should be somehow composed even if they are optional.
2393 */
2394 }
2395
2396 /**
2397 * Address element is forbidden for issuer from Greece.
2398 */
2399 $issuer_address_nodes = '';
2400 if (strcasecmp($settings['params']['country'], 'GR')) {
2401 // issuer not from Greece, compose address
2402 $issuer_address_nodes = '<address>
2403 <street>' . $this->convertSpecials($settings['params']['address']) . '</street>
2404 <number>' . $settings['params']['streetnumber'] . '</number>
2405 <postalCode>' . $settings['params']['zip'] . '</postalCode>
2406 <city>' . $this->convertSpecials($settings['params']['city']) . '</city>
2407 </address>';
2408 }
2409
2410 // total income classification
2411 $inc_classf_nodes = '';
2412 if ($use_income_classf) {
2413 if ($correlated) {
2414 $inc_classf_nodes = '<incomeClassification>
2415 <N1:classificationCategory>category1_95</N1:classificationCategory>
2416 <N1:amount>' . number_format(0, 2, '.', '') . '</N1:amount>
2417 </incomeClassification>';
2418 } else {
2419 $inc_classf_nodes = '<incomeClassification>
2420 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2421 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2422 <N1:amount>' . number_format($grand_total_net, 2, '.', '') . '</N1:amount>
2423 </incomeClassification>';
2424 }
2425 }
2426
2427 /**
2428 * The "Counterpart" node is forbidden for certain invoice types.
2429 *
2430 * @since 1.18.2 (J) - 1.8.2 (WP)
2431 */
2432 $inv_types_forbid_counterpart = [
2433 '11.1',
2434 '11.2',
2435 ];
2436
2437 // build counterpart node with the customer information
2438 $counterpartNode = '';
2439 if (!in_array($orig_invtype, $inv_types_forbid_counterpart)) {
2440 $counterpartNode = '<counterpart>
2441 <vatNumber>' . $this->convertSpecials($data[0]['customer']['vat'] ?: '') . '</vatNumber>
2442 ' . (!empty($data[0]['customer']['country_2_code']) ? '<country>' . $this->convertSpecials($data[0]['customer']['country_2_code']) . '</country>' : '') . '
2443 <branch>' . $branch . '</branch>
2444 ' . (!empty($client_name) ? '<name>' . $this->convertSpecials($client_name) . '</name>' : '') . '
2445 <address>
2446 <street>' . $this->convertSpecials(preg_replace("/[0-9]/", '', $data[0]['customer']['address'])) . '</street>
2447 <number>' . preg_replace("/[^0-9]/", '', $data[0]['customer']['address']) . '</number>
2448 <postalCode>' . $this->convertSpecials($data[0]['customer']['zip']) . '</postalCode>
2449 <city>' . $this->convertSpecials($data[0]['customer']['city']) . '</city>
2450 </address>
2451 </counterpart>';
2452 }
2453
2454 // build XML
2455 $root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
2456 $xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
2457 <InvoicesDoc ' . $root_namespaces . '>
2458 <invoice>
2459 <uid>' . $invoice_uid . '</uid>
2460 <mark>' . ($correlated ? $correlated_invnum : $settings['progcount']) . '</mark>
2461 <issuer>
2462 <vatNumber>' . $settings['params']['vatid'] . '</vatNumber>
2463 <country>' . $settings['params']['country'] . '</country>
2464 <branch>0</branch>
2465 ' . (strcasecmp($settings['params']['country'], 'GR') ? '<name>' . $this->convertSpecials($settings['params']['companyname']) . '</name>' : '') . '
2466 ' . (!empty($issuer_address_nodes) ? $issuer_address_nodes : '') . '
2467 </issuer>
2468 ' . $counterpartNode . '
2469 <invoiceHeader>
2470 <series>' . $series . '</series>
2471 <aa>' . $aa_serial_number . '</aa>
2472 <issueDate>' . $invdate . '</issueDate>
2473 <invoiceType>' . $invtype . '</invoiceType>
2474 <currency>' . VikBooking::getCurrencyName() . '</currency>
2475 ' . ($correlated ? '<correlatedInvoices>{main_invoice_mark}</correlatedInvoices>' : '') . '
2476 </invoiceHeader>
2477 <paymentMethods>
2478 <paymentMethodDetails>
2479 <type>' . $settings['params']['einv_paymethod'] . '</type>
2480 <amount>' . number_format($inv_tot_paid, 2, '.', '') . '</amount>
2481 ' . (!empty($inv_pay_method) ? '<paymentMethodInfo>' . $this->convertSpecials($inv_pay_method) . '</paymentMethodInfo>' : '') . '
2482 </paymentMethodDetails>
2483 </paymentMethods>
2484 ' . implode("\n", $invoice_details) . '
2485 <invoiceSummary>
2486 <totalNetValue>' . number_format($grand_total_net, 2, '.', '') . '</totalNetValue>
2487 <totalVatAmount>' . number_format($grand_total_vat, 2, '.', '') . '</totalVatAmount>
2488 <totalWithheldAmount>' . number_format($grand_total_tax_no_rate, 2, '.', '') . '</totalWithheldAmount>
2489 <totalFeesAmount>0.00</totalFeesAmount>
2490 <totalStampDutyAmount>0.00</totalStampDutyAmount>
2491 <totalOtherTaxesAmount>' . number_format($environmental_fee_amount, 2, '.', '') . '</totalOtherTaxesAmount>
2492 <totalDeductionsAmount>' . number_format(($correlated ? 0 : $discountval), 2, '.', '') . '</totalDeductionsAmount>
2493 <totalGrossValue>' . number_format(($correlated ? $this->environmental_fee_details['fee_cost'] : $data[0]['total']), 2, '.', '') . '</totalGrossValue>
2494 ' . $inc_classf_nodes . '
2495 </invoiceSummary>
2496 </invoice>
2497 </InvoicesDoc>';
2498
2499 // attempt to properly format the XML string
2500 $this->formatXmlString($xml);
2501
2502 if ($correlated) {
2503 /**
2504 * Trigger event to allow third party plugins to apply a custom eco-fee e-invoice auto-increment number.
2505 *
2506 * @since 1.18.6 (J) - 1.8.6 (WP)
2507 */
2508 $custom_einv_data = VBOFactory::getPlatform()->getDispatcher()->filter('onMydataUpdateEcofeeEinvoiceAutoincrementNumber', [$correlated_invnum, $settings, $data]);
2509 if (intval($custom_einv_data[0] ?? '')) {
2510 // overwrite correlated e-invoice auto-increment number
2511 $correlated_invnum = (int) $custom_einv_data[0];
2512 }
2513
2514 // update driver setting
2515 $this->updateDriverSetting('envfeeinvoiceinum', $correlated_invnum);
2516
2517 // return the raw XML for the correlated invoice just built
2518 return $xml;
2519 }
2520
2521 // check if we need to validate the XML against the official schema
2522 if (!empty($settings['params']['schema_validate'])) {
2523 /**
2524 * It may not be possible to validate the XML against the schema, as on
2525 * some environments this process may run out of execution time.
2526 */
2527 try {
2528 $schema_validation = $this->validateXmlAgainstSchema($xml);
2529 if ($schema_validation === null) {
2530 // display warning
2531 $this->setWarning('Missing PHP libraries for DOMDocument to validate the XML invoice against the official schema.');
2532 }
2533 } catch (Exception $e) {
2534 // display warning
2535 $this->setWarning('Could not validate the XML invoice against the official Schema - process failed with no response.');
2536 }
2537 }
2538
2539 if ($this->debugging()) {
2540 $this->setWarning('<pre>'.htmlentities($xml).'</pre><br/>');
2541 // break the process when in debug mode
2542 return false;
2543 }
2544
2545 // we proceed with the generation
2546
2547 // invoice name (transmission date-time string + auto-increment registration value just for our internal purpose)
2548 $einvname = date('YmdHis') . '_' . $settings['progcount'] . '.xml';
2549
2550 // get current datetime object in local format
2551 $date_obj = JFactory::getDate();
2552 $date_obj->setTimezone(new DateTimeZone(date_default_timezone_get()));
2553
2554 // prepare object for storing the invoice
2555 $einvobj = new stdClass;
2556 $einvobj->driverid = $settings['id'];
2557 $einvobj->created_on = $date_obj->toSql($local = true);
2558 $einvobj->for_date = $invdate;
2559 $einvobj->filename = $einvname;
2560 $einvobj->number = $invnum;
2561 $einvobj->idorder = $data[0]['id'];
2562 $einvobj->idcustomer = !empty($data[0]['customer']['id']) ? $data[0]['customer']['id'] : 0;
2563 $einvobj->country = !empty($data[0]['customer']['country']) ? $data[0]['customer']['country'] : null;
2564 // this column is not needed in this driver, but we give it a default value
2565 $einvobj->recipientcode = '';
2566 $einvobj->xml = $xml;
2567 // always reset transmitted and obliterated values for new e-invoices
2568 $einvobj->transmitted = 0;
2569 $einvobj->obliterated = 0;
2570
2571 $newinvid = $this->storeEInvoice($einvobj);
2572 if ($newinvid === false) {
2573 $this->setError('Error storing the electronic invoice for the reservation ID '.$data[0]['id']);
2574 return false;
2575 }
2576
2577 if ($canbeinvoiced < 0) {
2578 // log event history when regenerating an e-invoice
2579 VikBooking::getBookingHistoryInstance()->setBid($data[0]['id'])->store('BI', ($this->getName() . ' #' . $invnum));
2580 }
2581
2582 // update settings before generating the analogic invoice in PDF format to prevent exceptions to be thrown or exit/die calls.
2583 // update configuration setting for VikBooking::getNextInvoiceNumber()
2584 if ($data[0]['id'] > 0) {
2585 // we exclude custom (manual) invoices which would have a booking ID set to -number
2586 $this->updateInvoiceNumber($invnum);
2587 }
2588 // update auto-increment driver setting by increasing it for the next run
2589 $this->updateProgressiveNumber(++$settings['progcount']);
2590
2591 /**
2592 * Check if we should generate another, correlated, invoice.
2593 */
2594 if (!$correlated && $this->environmental_fee_details) {
2595 // re-call the same method to generate the invoice for the environmental fee
2596 $correlated_inv_xml = $this->generateEInvoice($data, true);
2597 if ($correlated_inv_xml) {
2598 // let the method store the raw XML for the correlated invoice
2599 $this->prepareCorrelatedInvoice($einvobj, $data, $correlated_inv_xml);
2600 }
2601 }
2602
2603 if (!$correlated && !$this->hasAnalogicInvoice($data[0]['id'])) {
2604 // no analogic invoice in PDF available, so we create it
2605 if (!$this->generateAnalogicInvoice($data[0]['id'], $invnum, $invdate)) {
2606 // raise warning in case of error
2607 $this->setWarning('It was not possible to generate the courtesy PDF version of the invoice for the reservation ID '.$data[0]['id']);
2608 }
2609 }
2610
2611 return true;
2612 }
2613
2614 /**
2615 * Builds the param name to read the correlated e-invoice data from the db settings.
2616 *
2617 * @param int $einv_id the e-invoice record ID.
2618 * @param int $booking_id the reservation record ID.
2619 *
2620 * @return string
2621 *
2622 * @since 1.16.7 (J) - 1.6.7 (WP)
2623 */
2624 public function getCorrelatedInvoiceParamName($einv_id, $booking_id)
2625 {
2626 $driver_id = $this->getDriverId();
2627
2628 return "envfee_invoice_{$driver_id}_{$einv_id}_{$booking_id}";
2629 }
2630
2631 /**
2632 * Stores a record with the information to create the environmental fee invoice (correlated invoice).
2633 * The actual invoice will be created upon transmitting the main one for the reservation because
2634 * the environmental fee invoice requires the correlated number to be the Mark of the main invoice.
2635 *
2636 * @param object $einvobj the main e-invoice record.
2637 * @param array $data the nested booking record.
2638 * @param string $xml the raw XML generated.
2639 *
2640 * @return bool
2641 *
2642 * @since 1.16.7 (J) - 1.6.7 (WP)
2643 */
2644 public function prepareCorrelatedInvoice($einvobj, array $data, $xml)
2645 {
2646 if (!is_object($einvobj) || empty($einvobj->id) || !$data) {
2647 return false;
2648 }
2649
2650 $booking_id = $data[0]['id'];
2651 $einv_id = $einvobj->id;
2652 $driver_id = $this->getDriverId();
2653
2654 $config_param_name = $this->getCorrelatedInvoiceParamName($einv_id, $booking_id);
2655
2656 $config_param_value = [
2657 'bid' => $booking_id,
2658 'einvid' => $einv_id,
2659 'envfee' => $this->environmental_fee_details,
2660 'xml' => $xml,
2661 ];
2662
2663 VBOFactory::getConfig()->set($config_param_name, $config_param_value);
2664
2665 return true;
2666 }
2667
2668 /**
2669 * Attempts to get the previous correlated invoice number.
2670 * Useful when re-generating an invoice already transmitted.
2671 *
2672 * @param int $einv_id the main invoice ID.
2673 * @param int $bid the VBO booking ID.
2674 * @param string $type the type of data to fetch.
2675 *
2676 * @return string|array
2677 */
2678 public function getPreviousCorrelatedInvoiceData($einv_id, $bid, $type = '')
2679 {
2680 $driver_id = $this->getDriverId();
2681
2682 $correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einv_id, $bid), []);
2683
2684 if (!$correlated_inv_raw_data || empty($correlated_inv_raw_data['xml'])) {
2685 return '';
2686 }
2687
2688 $xml_inv = simplexml_load_string($correlated_inv_raw_data['xml']);
2689
2690 if (!$xml_inv) {
2691 return '';
2692 }
2693
2694 if (!strcasecmp($type, 'date')) {
2695 // previous invoice date
2696 return (string)$xml_inv->invoice->invoiceHeader->issueDate;
2697 }
2698
2699 if (!strcasecmp($type, 'xml')) {
2700 // return the plain XML
2701 return $correlated_inv_raw_data['xml'];
2702 }
2703
2704 if (!strcasecmp($type, 'record')) {
2705 // return the whole array record
2706 return $correlated_inv_raw_data;
2707 }
2708
2709 if (!strcasecmp($type, 'transmission')) {
2710 // return the whole transmission array, if available
2711 return $correlated_inv_raw_data['transmission'] ?? [];
2712 }
2713
2714 // default to previous invoice number
2715 return (string)$xml_inv->invoice->mark;
2716 }
2717
2718 /**
2719 * Manipulates the previously created correlated invoice by adding the proper invoice mark,
2720 * then transmits the second e-invoice to myDATA.
2721 *
2722 * @param object|array $main_invoice_data the main e-invoice transaction information object (or associative array).
2723 * @param array $env_fee_data the prepared data for the correlated invoice and fee.
2724 * @param array $extras associative array to pass extra information.
2725 *
2726 * @return bool
2727 *
2728 * @since 1.16.7 (J) - 1.6.7 (WP)
2729 */
2730 public function transmitCorrelatedInvoice($main_invoice_data, array $env_fee_data, array $extras)
2731 {
2732 // always cast main invoice data to an object
2733 $main_invoice_data = (object) $main_invoice_data;
2734
2735 /**
2736 * Customers have reported an update to myDATA that no longer accepts the XML nodes
2737 * <uid> and <mark> (right under the node <invoice>) otherwise errors with code 273
2738 * will be raised stating that such details will be generated and provided by myDATA.
2739 *
2740 * @since 1.18.6 (J) - 1.8.6 (WP)
2741 */
2742 $xmlObj = simplexml_load_string($env_fee_data['xml']);
2743 if ($xmlObj->invoice->uid ?? null) {
2744 // delete nodes, will be added back in case of successful transmission
2745 unset($xmlObj->invoice->uid, $xmlObj->invoice->mark);
2746
2747 // re-build XML string
2748 $env_fee_data['xml'] = $xmlObj->asXML();
2749 }
2750
2751 // first off, set the proper correlated invoice number by using the mark
2752 // we use a regex that will capture two groups to avoid problems with the number-value for replacement
2753 $correlated_einv_final_xml = preg_replace("/<(correlatedInvoices)>(\{[a-z_]*\})?<\/correlatedInvoices>/i", '<$1>' . $main_invoice_data->invoice_mark . '</$1>', $env_fee_data['xml']);
2754
2755 // transmit the XML correlated e-invoice to myDATA
2756 $response = $this->myDATARequestPOST('SendInvoices', $correlated_einv_final_xml);
2757 if ($response->code != 200) {
2758 // the request was not successful, and the XML invoices were not parsed at all by myDATA
2759 $this->setError(sprintf('Correlated e-invoice - Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
2760 $this->setError('Correlated e-invoice - Could not send the invoice to myDATA.');
2761 return false;
2762 }
2763
2764 // process the transmission response
2765 $res_obj = simplexml_load_string($response->body);
2766 if (!is_object($res_obj) || !isset($res_obj->response)) {
2767 $this->setError('Could not parse XML response');
2768 $this->setError('<pre>' . htmlentities($response->body) . '</pre>');
2769 return false;
2770 }
2771
2772 if (!isset($res_obj->response->statusCode)) {
2773 $this->setError('Unexpected nodes in XML response (missing statusCode)');
2774 $this->setError('<pre>' . htmlentities($response->body) . '</pre>');
2775 return false;
2776 }
2777
2778 // check if we have a successful status code for this invoice
2779 if (!strcasecmp((string)$res_obj->response->statusCode, 'Success')) {
2780 // get the invoice UID
2781 $invoice_uid = isset($res_obj->response->invoiceUid) ? (string)$res_obj->response->invoiceUid : null;
2782
2783 // get the invoice mark (needed for a later cancellation)
2784 $invoice_mark = isset($res_obj->response->invoiceMark) ? (string)$res_obj->response->invoiceMark : null;
2785
2786 // get the invoice QRCode URL
2787 $invoice_qrcode = isset($res_obj->response->qrUrl) ? (string)$res_obj->response->qrUrl : null;
2788 $invoice_qrcode = !isset($res_obj->response->qrUrl) && isset($res_obj->response->qrCodeUrl) ? (string)$res_obj->response->qrCodeUrl : $invoice_qrcode;
2789
2790 /**
2791 * Update the original XML on main invoice to set the content of the nodes UID and Mark, because
2792 * they were removed at runtime before the transmission after the myDATA update (Jan 2026).
2793 *
2794 * @since 1.18.6 (J) - 1.8.6 (WP)
2795 */
2796 if ($invoice_uid && $invoice_mark) {
2797 // load XML e-invoice
2798 $dom = new DOMDocument();
2799 $dom->preserveWhiteSpace = false;
2800 $dom->formatOutput = true;
2801 $dom->loadXML($correlated_einv_final_xml);
2802
2803 // locate the parent <invoice> node
2804 $invoiceNode = $dom->getElementsByTagName('invoice')->item(0);
2805
2806 // locate the <issuer> node
2807 $issuerNode = $invoiceNode->getElementsByTagName('issuer')->item(0);
2808
2809 // create the two element nodes that should be added
2810 $uidNode = $dom->createElement('uid', $invoice_uid);
2811 $markNode = $dom->createElement('mark', $invoice_mark);
2812
2813 // take care of the mark node first, because it will go after uid
2814 $invoiceNode->insertBefore($markNode, $issuerNode);
2815
2816 // take care of the uid node, by placing it before the mark node
2817 $invoiceNode->insertBefore($uidNode, $markNode);
2818
2819 // update the final XML content before it gets saved
2820 $correlated_einv_final_xml = $dom->saveXML();
2821 }
2822
2823 // prepare data transmission values to be updated
2824 $config_param_name = $this->getCorrelatedInvoiceParamName($extras['einvid'], $extras['bid']);
2825 $env_fee_data['xml'] = $correlated_einv_final_xml;
2826 $env_fee_data['transmission'] = [
2827 'ts' => time(),
2828 'uid' => $invoice_uid,
2829 'mark' => $invoice_mark,
2830 'qrurl' => $invoice_qrcode,
2831 'qrcode_img' => '',
2832 'pdf' => '',
2833 ];
2834
2835 if ($invoice_qrcode) {
2836 // generate the QR Code for the environmental fee invoice as well
2837
2838 // the QR Code PNG file name
2839 $filename = "aade_qrcode_env_{$extras['bid']}_{$extras['einvid']}.png";
2840
2841 if ($this->generateQRCodeImage($invoice_qrcode, VikBookingMydataAadeConstants::getQRCodeBase('path', $filename))) {
2842 // set the QR Code image property
2843 $env_fee_data['transmission']['qrcode_img'] = $filename;
2844 }
2845 }
2846
2847 // immediately update data transmission values before generating the PDF invoice
2848 VBOFactory::getConfig()->set($config_param_name, $env_fee_data);
2849
2850 // generate the PDF (courtesy) for the correlated invoice (will set the PDF path in case of success)
2851 if ($this->generateAnalogicEnvFeeInvoice($env_fee_data)) {
2852 // update data transmission values again, as they will contain the path to the PDF file
2853 VBOFactory::getConfig()->set($config_param_name, $env_fee_data);
2854 }
2855
2856 return true;
2857 }
2858
2859 // at this point we expect an error
2860 if (!isset($res_obj->response->errors) || !isset($res_obj->response->errors->error)) {
2861 // errors should be set, but if they aren't, this is unexpected
2862 $this->setError('Unexpected nodes in XML response (missing errors or error)');
2863 $this->setError('<pre>' . htmlentities($response->body) . '</pre>');
2864 return false;
2865 }
2866
2867 // loop through the errors
2868 foreach ($res_obj->response->errors->error as $resp_err) {
2869 $err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
2870 $err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
2871 $this->setError(sprintf('Error (%s): %s', $err_code, $err_mess));
2872 }
2873
2874 return false;
2875 }
2876
2877 /**
2878 * Generates a PDF file for the correlated invoice for the environmental fee.
2879 *
2880 * @param array &$env_fee_data the raw environmental fee information data.
2881 *
2882 * @return bool
2883 *
2884 * @since 1.16.7 (J) - 1.6.7 (WP)
2885 */
2886 public function generateAnalogicEnvFeeInvoice(&$env_fee_data)
2887 {
2888 // get the customer information
2889 $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($env_fee_data['bid']);
2890
2891 // build a dummy invoice associative array with the information required
2892 $invoice = [
2893 'id' => -1,
2894 'number' => $this->getPreviousCorrelatedInvoiceData($env_fee_data['einvid'], $env_fee_data['bid']),
2895 'for_date' => strtotime($this->getPreviousCorrelatedInvoiceData($env_fee_data['einvid'], $env_fee_data['bid'], 'date')),
2896 'rawcont' => [
2897 'totalnet' => 0,
2898 'totaltax' => $env_fee_data['envfee']['fee_cost'],
2899 'totaltot' => $env_fee_data['envfee']['fee_cost'],
2900 'rows' => [
2901 [
2902 'service' => $env_fee_data['envfee']['name'],
2903 'net' => 0,
2904 'tax' => $env_fee_data['envfee']['fee_cost'],
2905 'tot' => $env_fee_data['envfee']['fee_cost'],
2906 ],
2907 ],
2908 ],
2909 'env_fee_data' => $env_fee_data,
2910 'feeseries' => 'C',
2911 ];
2912
2913 // load the custom invoice template file
2914 list($invoice_tmpl, $pdfparams) = VikBooking::loadCustomInvoiceTmpl($invoice, $customer);
2915
2916 // trigger an event to allow third-party plugins to manipulate the content of the custom invoice
2917 VBOFactory::getPlatform()->getDispatcher()->trigger('onMydataBeforeGenerateEnvFeeCourtesyInvoice', [$env_fee_data, $invoice, $invoice_tmpl]);
2918
2919 // parse the content of the template file
2920 $invoice_body = VikBooking::parseCustomInvoiceTemplate($invoice_tmpl, $invoice, $customer);
2921
2922 // reload booking details
2923 $booking_details = VikBooking::getBookingInfoFromID($env_fee_data['bid']);
2924
2925 // force the execution of the conditional text rules
2926 VikBooking::getConditionalRulesInstance()
2927 ->set(
2928 [
2929 'booking',
2930 'rooms',
2931 ],
2932 [
2933 $booking_details,
2934 VikBooking::loadOrdersRoomsData($env_fee_data['bid']),
2935 ]
2936 )
2937 ->parseTokens($invoice_body);
2938
2939 // load dependencies
2940 if (!class_exists('TCPDF')) {
2941 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
2942 }
2943 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
2944
2945 /**
2946 * Trigger event to allow third party plugins to return a specific font name.
2947 */
2948 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
2949 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
2950 $usepdffont = $custom_pdf_font[0];
2951 }
2952
2953 // write the PDF on file
2954 $pdffname = implode('_', ['envfee', $booking_details['id'], ($booking_details['sid'] ?: $booking_details['ts'])]) . '.pdf';
2955 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
2956
2957 if (is_file($pathpdf)) {
2958 @unlink($pathpdf);
2959 }
2960
2961 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
2962
2963 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
2964 $pdf->SetTitle(JText::translate('VBOINVNUM') . ' ' . $invoice['number']);
2965
2966 // header for each page of the pdf
2967 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
2968 $pdf->SetHeaderData($pdfparams['header_data'][0], $pdfparams['header_data'][1], $pdfparams['header_data'][2], $pdfparams['header_data'][3], $pdfparams['header_data'][4], $pdfparams['header_data'][5]);
2969 }
2970
2971 // change some currencies to their unicode (decimal) value
2972 $currencyname = VikBooking::getCurrencyName();
2973 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
2974 if (array_key_exists($currencyname, $unichr_map)) {
2975 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
2976 }
2977
2978 // header and footer fonts
2979 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
2980 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
2981
2982 // margins
2983 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
2984 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
2985 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
2986
2987 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
2988 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
2989 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
2990
2991 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
2992 $pdf->SetPrintHeader(false);
2993 }
2994 if ($pdfparams['show_footer'] == 0) {
2995 $pdf->SetPrintFooter(false);
2996 }
2997
2998 $pdf->AddPage();
2999 $pdf->writeHTML($invoice_body, true, false, true, false, '');
3000 $pdf->lastPage();
3001 $pdf->Output($pathpdf, 'F');
3002
3003 if (!is_file($pathpdf)) {
3004 return false;
3005 }
3006
3007 if (VBOPlatformDetection::isWordPress()) {
3008 /**
3009 * @wponly - trigger files mirroring
3010 */
3011 VikBookingLoader::import('update.manager');
3012 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
3013 }
3014
3015 // set the PDF file name at last
3016 $env_fee_data['transmission']['pdf'] = $pdffname;
3017
3018 return true;
3019 }
3020
3021 /**
3022 * Returns the calculated tariffs given their IDs per room booked.
3023 *
3024 * @param array $booking the booking array with one array-room per array value
3025 *
3026 * @return array associative array of tariffs for each room booked
3027 */
3028 protected function getBookingTariffs($booking)
3029 {
3030 $tars = [];
3031
3032 $is_package = (!empty($booking[0]['pkg']));
3033
3034 foreach ($booking as $kor => $or) {
3035 $num = $kor + 1;
3036 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
3037 // package or custom cost set from the back-end does not need calculation
3038 continue;
3039 }
3040 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=".(int)$or['idtar'].";";
3041 $this->dbo->setQuery($q);
3042 $tar = $this->dbo->loadAssocList();
3043 if ($tar) {
3044 $tar = VikBooking::applySeasonsRoom($tar, $or['checkin'], $or['checkout']);
3045
3046 // apply OBP rules
3047 $tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $or, $or['adults']);
3048
3049 $tars[$num] = $tar[0];
3050 }
3051 }
3052
3053 return $tars;
3054 }
3055
3056 /**
3057 * Transmits the electronic invoices to myDATA according to the input parameters.
3058 * This is a 'driver action', and so it's called before getBookingsData()
3059 * in the view. This method will save/update records in the DB so that when
3060 * the view re-calls getBookingsData(), the information will be up to date.
3061 *
3062 * @return boolean True if at least one e-invoice was transmitted
3063 */
3064 public function transmitEInvoices()
3065 {
3066 // make sure the transmission settings are not empty
3067 $settings = $this->loadSettings();
3068 if ($settings === false || !$settings['params']) {
3069 $this->setError('Missing settings to transmit the invoices. Please set up the driver settings first.');
3070 return false;
3071 }
3072 // make sure the settings we need are not empty
3073 $required = [
3074 $settings['params']['aade_user_id'],
3075 $settings['params']['aade_subscription_key'],
3076 ];
3077 foreach ($required as $reqset) {
3078 if (empty($reqset)) {
3079 $this->setError('Invalid settings to transmit the invoices. Please make sure to provide all the information from the driver settings.');
3080 return false;
3081 }
3082 }
3083
3084 // call the main method to generate rows, cols and bookings array
3085 $this->getBookingsData();
3086
3087 if ($this->getError() || !$this->bookings) {
3088 return false;
3089 }
3090
3091 // get the driver ID
3092 $driver_id = $this->getDriverId();
3093
3094 // pool of e-invoice IDs to transmit
3095 $einvspool = [];
3096 $einvnumbs = [];
3097
3098 // electronic invoices IDs referenced to booking IDs
3099 $einvs_bids_ref = [];
3100
3101 // list of eco-fee-only invoices to be (re-)transmitted
3102 $ecofee_einvs_retn = [];
3103
3104 foreach ($this->bookings as $gbook) {
3105 // check whether this booking ID was set to be skipped from transmission
3106 $exclude = VikRequest::getInt('excludesendbid'.$gbook[0]['id'], 0, 'request');
3107 if ($exclude > 0) {
3108 // skipping this invoice from transmission
3109 continue;
3110 }
3111
3112 // make sure an electronic invoice was already issued for this booking ID by this driver
3113 if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
3114 // no e-invoices available for this booking, skipping
3115 continue;
3116 }
3117
3118 // check if an e-invoice was already sent for this booking
3119 if ($gbook[0]['einvsent'] > 0) {
3120 $resend = VikRequest::getInt('resendbid'.$gbook[0]['id'], 0, 'request');
3121 $resendecofee = VikRequest::getInt('resendecofeebid'.$gbook[0]['id'], 0, 'request');
3122 if ($resendecofee) {
3123 // push record for the eco-fee invoice re-transmit only
3124 $ecofee_einvs_retn[] = $gbook[0];
3125 continue;
3126 }
3127 if (!$resend) {
3128 // we do not re-send the invoice for this booking ID
3129 continue;
3130 }
3131 }
3132
3133 // push e-invoice ID to the pool
3134 array_push($einvspool, $gbook[0]['einvid']);
3135
3136 // push also the corresponding invoice number
3137 array_push($einvnumbs, $gbook[0]['einvnum']);
3138
3139 // set the e-invoice ID/booking ID relation
3140 $einvs_bids_ref[$gbook[0]['einvid']] = $gbook[0]['id'];
3141 }
3142
3143 if ($einvspool && $ecofee_einvs_retn) {
3144 // pre-check: conflict with re-transmission of eco-fee invoice(s) and main invoice first transmission or re-transmission
3145 $this->setWarning('If you choose to re-transmit just eco-fee invoices, then any other main invoice should be excluded from the transmission to avoid conflicts.');
3146 return false;
3147 }
3148
3149 if (!$ecofee_einvs_retn) {
3150 // attempt to transmit or re-transmit the main invoices with their (eventually) related eco-fee invoices
3151 if (!$einvspool) {
3152 // no e-invoices generated or ready to be transmitted
3153 $this->setWarning('No e-invoices generated or ready to be transmitted to myDATA. Please generate first the XML invoices or select some for the re-transmission.');
3154 return false;
3155 }
3156
3157 // build one XML file for all XML e-invoices (if more than one)
3158 $einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
3159 if ($einv_xml_body === false) {
3160 // something went wrong with the creation of the XML file
3161 $this->setError('Error creating the XML file for the request. Unable to proceed.');
3162 return false;
3163 }
3164
3165 if ($this->debugging()) {
3166 // when in debug mode, the raw XML request is sent to output
3167 $this->setWarning('Raw XML request for Debug Mode');
3168 $this->setWarning('<pre> ' . htmlentities($einv_xml_body) . ' </pre>');
3169 }
3170
3171 // transmit e-invoices to myDATA
3172 $response = $this->myDATARequestPOST('SendInvoices', $einv_xml_body, $settings);
3173 if ($response->code != 200) {
3174 // the request was not successful, and the XML invoices were not parsed at all by myDATA
3175 $this->setError(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
3176 $this->setError('Could not send the invoice(s) to myDATA.');
3177 return false;
3178 }
3179
3180 if ($this->debugging()) {
3181 // when in debug mode, the raw XML response is sent to output
3182 $this->setWarning('Raw XML response for Debug Mode');
3183 $this->setWarning('<pre> ' . htmlentities($response->body) . ' </pre>');
3184 }
3185
3186 // check if the XML response contains errors, and adjust the e-invoices that succeeded
3187 list($success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls) = $this->myDATAParseXMLResponse($response->body, $einvspool, $einvnumbs);
3188
3189 if (!$success) {
3190 // some errors occurred
3191 if (!is_array($valid_einv_marks) || !$valid_einv_marks) {
3192 $this->setError('Could not send the invoice(s) to myDATA.');
3193 return false;
3194 } else {
3195 // some e-invoices were transmitted successfully
3196 $einvspool = array_keys($valid_einv_marks);
3197 }
3198 }
3199
3200 // update ProgressivoInvio driver setting by increasing it for the next run
3201 $this->updateProgressiveNumber(++$settings['progcount']);
3202
3203 // set to transmitted=1 all e-invoice IDs that were transmitted with success
3204 foreach ($einvspool as $einvid) {
3205 // find the corresponding booking ID
3206 $einv_bid = $einvs_bids_ref[$einvid] ?? 0;
3207
3208 // flag to check if the PDF invoice should be refreshed
3209 $qrcode_fname = null;
3210
3211 // prepare "transmission data" object
3212 $trans_data = new stdClass;
3213 $trans_data->invoice_uid = (isset($valid_einv_uids[$einvid]) && $einvid != $valid_einv_uids[$einvid] ? $valid_einv_uids[$einvid] : null);
3214 $trans_data->invoice_mark = (isset($valid_einv_marks[$einvid]) && $einvid != $valid_einv_marks[$einvid] ? $valid_einv_marks[$einvid] : null);
3215 $trans_data->invoice_qrcode = (isset($valid_qrcode_urls[$einvid]) && $einvid != $valid_qrcode_urls[$einvid] ? $valid_qrcode_urls[$einvid] : null);
3216 $trans_data->qrcode_img = null;
3217 $trans_data->trans_dtime = date('Y-m-d H:i:s');
3218
3219 if ($trans_data->invoice_qrcode) {
3220 /**
3221 * Attempt to generate the QR Code image file for the current invoice correctly transmitted.
3222 *
3223 * @since 1.16.7 (J) - 1.6.7 (WP)
3224 */
3225 $qrcode_fname = $this->generateInvoiceQRCode($einvid, $einv_bid, $trans_data);
3226 if ($qrcode_fname) {
3227 $trans_data->qrcode_img = $qrcode_fname;
3228 }
3229 }
3230
3231 /**
3232 * Update the original XML on main invoice to change the content of the nodes UID and Mark, even
3233 * if they will be removed at runtime before the transmission after the myDATA update (Jan 2026).
3234 *
3235 * @since 1.18.6 (J) - 1.8.6 (WP)
3236 */
3237 $updatedEinvoiceXML = null;
3238 $prev_einv_data = $this->loadEInvoiceDetails($einvid);
3239 if ($trans_data->invoice_uid && $trans_data->invoice_mark && !empty($prev_einv_data['xml'])) {
3240 // load XML e-invoice
3241 $dom = new DOMDocument();
3242 $dom->preserveWhiteSpace = false;
3243 $dom->formatOutput = true;
3244 $dom->loadXML($prev_einv_data['xml']);
3245
3246 // locate the parent <invoice> node
3247 $invoiceNode = $dom->getElementsByTagName('invoice')->item(0);
3248
3249 // locate the <uid> node
3250 $uidNode = $invoiceNode->getElementsByTagName('uid')->item(0);
3251 if ($uidNode) {
3252 // replace text content
3253 $uidNode->nodeValue = $trans_data->invoice_uid;
3254 }
3255
3256 // locate the <mark> node
3257 $markNode = $invoiceNode->getElementsByTagName('mark')->item(0);
3258 if ($markNode) {
3259 // replace text content
3260 $markNode->nodeValue = $trans_data->invoice_mark;
3261 }
3262
3263 // turn flag on for updating the XML content
3264 $updatedEinvoiceXML = $dom->saveXML();
3265 }
3266
3267 // build e-invoice object for update (with "transmission data")
3268 $data = new stdClass;
3269 $data->id = $einvid;
3270 $data->transmitted = 1;
3271 $data->trans_data = json_encode($trans_data);
3272
3273 if ($updatedEinvoiceXML) {
3274 // e-invoice XML content should be updated
3275 $data->xml = $updatedEinvoiceXML;
3276 }
3277
3278 // update e-invoice record
3279 $this->updateEInvoice($data);
3280
3281 // check if the PDF invoice requires a refresh to let the conditional text rules run after having updated the e-invoice
3282 if ($qrcode_fname) {
3283 // attempt to refresh the PDF invoice, if available, in case it uses the Conditional Text Rules
3284 if ($this->refreshPdfInvoice($einv_bid)) {
3285 $qrcode_url = VikBookingMydataAadeConstants::getQRCodeBase('uri', $qrcode_fname);
3286 // trigger an event to allow third-party plugins to do something, like sending the invoice via email
3287 VBOFactory::getPlatform()->getDispatcher()->trigger('onMydataAfterQrcodeInvoiceSubmitted', [$einv_bid, $einvid, $trans_data, $qrcode_url]);
3288 }
3289 }
3290
3291 /**
3292 * Check if an e-invoice for the environmental fee was prepared to be generated and transmitted as well.
3293 *
3294 * @since 1.16.7 (J) - 1.6.7 (WP)
3295 */
3296 $env_fee_inv_details = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einvid, $einv_bid), []);
3297 if ($env_fee_inv_details && $trans_data->invoice_mark) {
3298 /**
3299 * It is recommended to sleep at least one second after the main invoice has been
3300 * transmitted and before the correlated invoice gets transmitted to ensure the
3301 * main invoice mark is registered on the myDATA platform.
3302 */
3303 sleep(1);
3304
3305 // use the obtained invoice mark to adjust and transmit the environmental fee invoice
3306 $env_fee_tn_result = $this->transmitCorrelatedInvoice(
3307 $trans_data,
3308 $env_fee_inv_details,
3309 [
3310 'einvid' => $einvid,
3311 'bid' => $einv_bid,
3312 ]
3313 );
3314
3315 /**
3316 * Store within the main e-invoice record the transmission result and timestamp for the eco-fee invoice.
3317 * This is useful for eventually allowing to re-transmit just the eco-fee invoice in case of errors, usually
3318 * caused by a premature transmission of the eco-fee invoice before the main invoice mark is registered.
3319 *
3320 * @since 1.18.3 (J) - 1.8.3 (WP)
3321 */
3322 if (is_object($data ?? null) && is_object($trans_data ?? null)) {
3323 // set the eco-fee invoice transmission result within the main invoice transaction data object
3324 $trans_data->env_fee_tn_res = (int) $env_fee_tn_result;
3325 $trans_data->env_fee_tn_ts = time();
3326
3327 // update object property
3328 $data->trans_data = json_encode($trans_data);
3329
3330 // update main e-invoice record
3331 $this->updateEInvoice($data);
3332 }
3333 }
3334 }
3335
3336 // display info message
3337 $this->setInfo('Electronic invoices transmitted: ' . count($einvspool));
3338 } else {
3339 /**
3340 * Retry to (re-)transmit just the selected eco-fee invoice(s).
3341 *
3342 * @since 1.18.3 (J) - 1.8.3 (WP)
3343 */
3344 $eco_fee_invs_retn_attempts = 0;
3345 $eco_fee_invs_retn_success = 0;
3346 foreach ($ecofee_einvs_retn as $ecofee_einv_data) {
3347 // get the eco-fee e-invoice details
3348 $env_fee_inv_details = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($ecofee_einv_data['einvid'], $ecofee_einv_data['id']), []);
3349 // get the main e-invoice transaction data
3350 $trans_data = $ecofee_einv_data['einvtndata'] ?? null;
3351 if ($env_fee_inv_details && is_array($trans_data) && ($trans_data['invoice_mark'] ?? null)) {
3352 // increase global retry counter
3353 $eco_fee_invs_retn_attempts++;
3354
3355 // use the previous invoice mark to adjust and transmit the environmental fee invoice
3356 $env_fee_tn_result = $this->transmitCorrelatedInvoice(
3357 $trans_data,
3358 $env_fee_inv_details,
3359 [
3360 'einvid' => $ecofee_einv_data['einvid'],
3361 'bid' => $ecofee_einv_data['id'],
3362 ]
3363 );
3364
3365 if ($env_fee_tn_result) {
3366 // increase success counter
3367 $eco_fee_invs_retn_success++;
3368 }
3369
3370 // always update the transmission result for the eco-fee invoice within the main invoice transaction data object
3371 $trans_data['env_fee_tn_res'] = (int) $env_fee_tn_result;
3372 $trans_data['env_fee_tn_ts'] = time();
3373
3374 // build the object record for update
3375 $main_record = new stdClass;
3376 $main_record->id = $ecofee_einv_data['einvid'];
3377 $main_record->trans_data = json_encode($trans_data);
3378
3379 // update main e-invoice record
3380 $this->updateEInvoice($main_record);
3381 }
3382 }
3383
3384 // display info message
3385 $this->setInfo('Electronic invoices (eco-fee) re-transmitted: ' . $eco_fee_invs_retn_attempts . ' - Success counter: ' . $eco_fee_invs_retn_success);
3386 }
3387
3388 // we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
3389 $this->bookings = [];
3390
3391 // unset also cols, rows and footer row to not merge data
3392 $this->cols = [];
3393 $this->rows = [];
3394 $this->footerRow = [];
3395
3396 return true;
3397 }
3398
3399 /**
3400 * Generates one XML string for the request body to myDATA. If more
3401 * than one e-invoice ID passed, attempts to parse all XML files for
3402 * the already generated e-invoices in order to compose one single
3403 * XML request body that contains all invoices. Every e-invoice has
3404 * got an XML file compliant for the transmission, but when we need
3405 * to transmit in mass multiple e-invoices, we try to use just one
3406 * HTTP request by merging all e-invoices into one single XML file.
3407 *
3408 * @param array $einvspool an array of e-invoice IDs
3409 * @param array $settings the driver settings
3410 *
3411 * @return bool|string false on failure or XML request body string.
3412 */
3413 protected function buildTransmissionXMLBody($einvspool, $settings)
3414 {
3415 if (!is_array($einvspool) || !$einvspool) {
3416 return false;
3417 }
3418
3419 // the list of XML strings
3420 $xml_strings = [];
3421
3422 // generate XML files for the requested e-invoice IDs
3423 foreach ($einvspool as $einvid) {
3424 // load e-invoice details
3425 $einv_data = $this->loadEInvoiceDetails($einvid);
3426 if (!$einv_data || !is_array($einv_data) || empty($einv_data['xml'])) {
3427 // all e-invoices must exist as they will be set to transmitted=1 so we break the process
3428 $this->setError('Unable to load data for the electronic invoice ID ' . $einvid);
3429 return false;
3430 }
3431
3432 /**
3433 * Customers have reported an update to myDATA that no longer accepts the XML nodes
3434 * <uid> and <mark> (right under the node <invoice>) otherwise errors with code 273
3435 * will be raised stating that such details will be generated and provided by myDATA.
3436 *
3437 * @since 1.18.6 (J) - 1.8.6 (WP)
3438 */
3439 $xmlObj = simplexml_load_string($einv_data['xml']);
3440 if ($xmlObj->invoice->uid ?? null) {
3441 // delete nodes
3442 unset($xmlObj->invoice->uid, $xmlObj->invoice->mark);
3443
3444 // re-build XML string
3445 $einv_data['xml'] = $xmlObj->asXML();
3446 }
3447
3448 // push e-invoice content
3449 $xml_strings[] = $einv_data['xml'];
3450 }
3451
3452 if (!$xml_strings) {
3453 // no XML files created, break the process
3454 return false;
3455 }
3456
3457 if (count($xml_strings) === 1) {
3458 // just one XML file, no need to build an XML container
3459 return $xml_strings[0];
3460 }
3461
3462 // return one whole XML request body for all e-invoices
3463 return $this->mergeXMLInvoices($xml_strings);
3464 }
3465
3466 /**
3467 * Given a list of XML e-invoice strings, attempts to merge them
3468 * into one single XML to avoid making one HTTP request per e-invoice.
3469 *
3470 * @param array $xml_strings list of XML strings for each e-invoice.
3471 *
3472 * @return bool|string false or whole XML string for all e-invoices.
3473 */
3474 protected function mergeXMLInvoices($xml_strings)
3475 {
3476 if (!is_array($xml_strings) || !$xml_strings) {
3477 return false;
3478 }
3479
3480 if (count($xml_strings) === 1) {
3481 return $xml_strings[0];
3482 }
3483
3484 if (!class_exists('SimpleXMLElement')) {
3485 /**
3486 * We cannot afford to do a string manipulation only because SimpleXMLElement
3487 * is missing on the server. It has to be available, it's a native library.
3488 */
3489 $this->setError('SimpleXMLElement is missing on your server.');
3490 $this->setError('This is unusual, and you should contact your hosting company to enable this native PHP library.');
3491 $this->setError('You can only transmit single e-invoices, not more than one because SimpleXMLElement is missing');
3492
3493 return false;
3494 }
3495
3496 /**
3497 * Define the XML root element for the InvoicesDoc message.
3498 * Namespace attributes will affect the incomeClassification sub nodes.
3499 *
3500 * @see VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
3501 * @see https://mydata-dev.portal.azure-api.net/issues/5f3c411ac75730207831ead4
3502 */
3503 $root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
3504 $xml_root = <<<XML
3505 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
3506 <InvoicesDoc $root_namespaces>
3507 </InvoicesDoc>
3508 XML;
3509 // get the SimpleXMLElement object
3510 $xml = new SimpleXMLElement($xml_root);
3511
3512 // define the namespace rules for the children elements of <incomeClassification>
3513 $child_nmspaces = [
3514 'incomeClassification' => VikBookingMydataAadeConstants::getInvoiceChildrenNamespace()
3515 ];
3516
3517 // parse all e-invoices
3518 $parsed = 0;
3519 foreach ($xml_strings as $k => $einvoice) {
3520 $xml_einvoice = simplexml_load_string($einvoice);
3521 if (!is_object($xml_einvoice)) {
3522 $this->setWarning('Unable to parse the XML of the e-invoice index ' . ($k + 1));
3523 $this->setWarning($this->libxml_display_errors());
3524 continue;
3525 }
3526 // append XML tree to a new <invoice> node
3527 $invoice_node = $xml->addChild('invoice');
3528 $this->simpleXmlAppendTree($invoice_node, $xml_einvoice->invoice, $child_nmspaces);
3529 // increase parsed invoices
3530 $parsed++;
3531 }
3532
3533 if (!$parsed) {
3534 $this->setError('No e-invoices could be parsed to merge the XML trees and related nodes into one single XML body.');
3535 return false;
3536 }
3537
3538 // get the whole XML request body just built from all e-invoices
3539 $full_xml = $xml->asXML();
3540
3541 /**
3542 * When appending child nodes with namespaces to "<incomeClassification>", these may be added as
3543 * "<N1:classificationCategory xmlns:N1="N1">category1_3</N1:classificationCategory>" so with both
3544 * the proper namespace in the node name, but also with the attribute 'xmlns:N1="N1"' which is making
3545 * the whole XML failing according to the schema. Therefore, we manipulate the string to remove such attributes.
3546 */
3547 if (!empty($child_nmspaces['incomeClassification'])) {
3548 $seek_pattern = $child_nmspaces['incomeClassification'];
3549 $full_xml = str_replace('xmlns:' . $seek_pattern . '="' . $seek_pattern . '"', '', $full_xml);
3550 }
3551
3552 // attempt to properly format the XML string
3553 $this->formatXmlString($full_xml);
3554
3555 // return the whole XML request body containing all the e-invoices
3556 return $full_xml;
3557 }
3558
3559 /**
3560 * Recursive method to append a SimpleXMLElement tree node, and
3561 * related children nodes, to another SimpleXMLElement. Used to
3562 * dinamically add an entire tree of a single e-invoice XML file
3563 * under a single node <invoice> of the whole XML request body.
3564 *
3565 * @param SimpleXMLElement $xml_to the node where the tree will be appended.
3566 * @param SimpleXMLElement $xml_from the element to append with all its children.
3567 * @param array $child_nmspaces associative list of children namespaces.
3568 *
3569 * @return void
3570 */
3571 protected function simpleXmlAppendTree(&$xml_to, &$xml_from, $child_nmspaces = [])
3572 {
3573 $child_nmspace = null;
3574 $child_isprefix = false;
3575
3576 $node_name = $xml_to->getName();
3577 if (!empty($child_nmspaces[$node_name])) {
3578 $child_nmspace = $child_nmspaces[$node_name];
3579 $child_isprefix = true;
3580 }
3581
3582 foreach ($xml_from->children($child_nmspace, $child_isprefix) as $xml_child) {
3583 $add_node_name = $xml_child->getName();
3584 if (!empty($child_nmspace)) {
3585 $add_node_name = "$child_nmspace:$add_node_name";
3586 }
3587 $xml_temp = $xml_to->addChild($add_node_name, (string)$xml_child, $child_nmspace);
3588 foreach ($xml_child->attributes() as $attr_key => $attr_value) {
3589 $xml_temp->addAttribute($attr_key, $attr_value);
3590 }
3591 $this->simpleXmlAppendTree($xml_temp, $xml_child, $child_nmspaces);
3592 }
3593 }
3594
3595 /**
3596 * Loads the details of the given e-invoice ID. The given ID should not be obliterated.
3597 *
3598 * @param int $einvid the ID of the e-invoice
3599 *
3600 * @return mixed array if the e-invoice exists and is not obliterated, false otherwise.
3601 */
3602 protected function loadEInvoiceDetails($einvid)
3603 {
3604 if (empty($einvid)) {
3605 return false;
3606 }
3607
3608 $q = "SELECT * FROM `#__vikbooking_einvoicing_data` WHERE `id`=" . (int)$einvid . " AND `obliterated`=0;";
3609 $this->dbo->setQuery($q);
3610 $einv = $this->dbo->loadAssoc();
3611
3612 return $einv ? $einv : false;
3613 }
3614
3615 /**
3616 * Performs a POST request to the myDATA infrastructure.
3617 *
3618 * @param string $url_path the path to append to the base endpoint URI.
3619 * @param mixed $body the request body.
3620 * @param array $settings driver settings or any other option to inject.
3621 *
3622 * @return JHttpResponse object with code and body properties
3623 */
3624 protected function myDATARequestPOST($url_path = '', $body = null, $settings = [])
3625 {
3626 if (empty($settings)) {
3627 $settings = $this->loadSettings();
3628 }
3629
3630 $aade_user_id = $settings['params']['aade_user_id'];
3631 $aade_subscription_key = $settings['params']['aade_subscription_key'];
3632 $aade_endp_url = $settings['params']['mydata_endpoint_url'];
3633 if (!empty($settings['params']['test_mode'])) {
3634 $aade_endp_url = VikBookingMydataAadeConstants::getDevEndpointBaseUrl();
3635 }
3636
3637 if (!empty($url_path)) {
3638 $aade_endp_url .= ltrim($url_path, '/');
3639 }
3640
3641 // build request headers
3642 $headers = [
3643 'Content-Type' => 'application/xml',
3644 'aade-user-id' => $aade_user_id,
3645 'Ocp-Apim-Subscription-Key' => $aade_subscription_key,
3646 ];
3647
3648 // invoke CMS native transporter
3649 $transporter = new JHttp;
3650 $response = $transporter->post($aade_endp_url, $body, $headers);
3651
3652 if ($response->code != 200) {
3653 $this->setError('Erroneous response with HTTP code ' . $response->code);
3654 $this->setError(htmlentities($response->body));
3655 }
3656
3657 return $response;
3658 }
3659
3660 /**
3661 * Checks if the XML response string from myDATA contains errors.
3662 * Returns an array with boolean "success" and array with "einv_id => einv_mark".
3663 *
3664 * @param string $body the raw response body from the request.
3665 * @param array $einvspool list of e-invoice ids in VBO just transmitted.
3666 * @param array $einvnumbs list of e-invoice numbers in VBO just transmitted.
3667 *
3668 * @return array to be used with list($success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls).
3669 */
3670 protected function myDATAParseXMLResponse($body, $einvspool = [], $einvnumbs = [])
3671 {
3672 // the default information to return
3673 $success = false;
3674 $valid_einv_marks = [];
3675 $valid_einv_uids = [];
3676 $valid_qrcode_urls = [];
3677
3678 $res_obj = $body;
3679 if (!is_object($res_obj)) {
3680 $res_obj = simplexml_load_string($body);
3681 }
3682
3683 if (!is_object($res_obj) || !isset($res_obj->response)) {
3684 $this->setError('Could not parse XML response');
3685 $this->setError('<pre>' . htmlentities($body) . '</pre>');
3686 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3687 }
3688
3689 // errors counter
3690 $errors_found = 0;
3691
3692 // loop through each response node
3693 foreach ($res_obj->response as $invoice_resp) {
3694 if (!isset($invoice_resp->statusCode)) {
3695 $this->setError('Unexpected nodes in XML response (missing statusCode)');
3696 $this->setError('<pre>' . htmlentities($body) . '</pre>');
3697 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3698 }
3699
3700 /**
3701 * Endpoint POST /SendInvoices only (/CancelInvoice would not return this data).
3702 * Get the index of the current invoice response (line-number starts from 1).
3703 */
3704 $invoice_index = isset($invoice_resp->index) ? (int)$invoice_resp->index : 0;
3705
3706 // check if we have a successful status code for this invoice
3707 if (!strcasecmp((string)$invoice_resp->statusCode, 'Success')) {
3708 // success!
3709 if ($invoice_index > 0 && isset($einvspool[($invoice_index - 1)])) {
3710 // push successful invoice
3711 $einv_id = $einvspool[($invoice_index - 1)];
3712
3713 // get the invoice UID
3714 $invoice_uid = isset($invoice_resp->invoiceUid) ? (string)$invoice_resp->invoiceUid : $einv_id;
3715 $valid_einv_uids[$einv_id] = $invoice_uid;
3716
3717 // get the invoice mark (needed for a later cancellation)
3718 $invoice_mark = isset($invoice_resp->invoiceMark) ? (string)$invoice_resp->invoiceMark : $einv_id;
3719 $valid_einv_marks[$einv_id] = $invoice_mark;
3720
3721 /**
3722 * Check if a QRCode URL is available for the electronic invoice.
3723 * Valid property name should be "qrUrl".
3724 *
3725 * @since 1.16.7 (J) - 1.6.7 (WP)
3726 */
3727 $invoice_qrcode = isset($invoice_resp->qrUrl) ? (string)$invoice_resp->qrUrl : $einv_id;
3728 $invoice_qrcode = !isset($invoice_resp->qrUrl) && isset($invoice_resp->qrCodeUrl) ? (string)$invoice_resp->qrCodeUrl : $invoice_qrcode;
3729 $valid_qrcode_urls[$einv_id] = $invoice_qrcode;
3730 }
3731 continue;
3732 }
3733
3734 // at this point we expect an error
3735 if (!isset($invoice_resp->errors) || !isset($invoice_resp->errors->error)) {
3736 // errors should be set, but if they aren't, this is unexpected
3737 $this->setError('Unexpected nodes in XML response (missing errors or error)');
3738 $this->setError('<pre>' . htmlentities($body) . '</pre>');
3739 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3740 }
3741
3742 // loop through the errors
3743 foreach ($invoice_resp->errors->error as $resp_err) {
3744 $errors_found++;
3745 $err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
3746 $err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
3747 $inv_numb = isset($einvnumbs[($invoice_index - 1)]) ? $einvnumbs[($invoice_index - 1)] : '???';
3748 $this->setError(sprintf('Error (%s) in invoice index %d (#%s): %s', $err_code, $invoice_index, $inv_numb, $err_mess));
3749 }
3750 }
3751
3752 // if we had no errors at all, the response was successful
3753 $success = (!$errors_found);
3754
3755 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3756 }
3757
3758 /**
3759 * Downloads the electronic invoices by storing temporary files.
3760 * This is a 'driver action', and so it's called before getBookingsData()
3761 * in the view. This method will not save/update records in the DB.
3762 *
3763 * @return void
3764 */
3765 public function downloadEInvoices()
3766 {
3767 // make sure the transmission settings are not empty
3768 $settings = $this->loadSettings();
3769 if ($settings === false || !$settings['params']) {
3770 $this->setError('Missing settings. Please set up the driver first.');
3771 return false;
3772 }
3773
3774 // call the main method to generate rows, cols and bookings array
3775 $this->getBookingsData();
3776
3777 if (strlen($this->getError()) || !$this->bookings) {
3778 return false;
3779 }
3780
3781 // pool of e-invoice IDs to download
3782 $einvspool = [];
3783
3784 foreach ($this->bookings as $gbook) {
3785 // make sure an electronic invoice was already issued for this booking ID by this driver
3786 if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
3787 // no e-invoices available for this booking, skipping
3788 continue;
3789 }
3790
3791 // push e-invoice ID to the pool
3792 array_push($einvspool, $gbook[0]['einvid']);
3793 }
3794
3795 if (!$einvspool) {
3796 // no e-invoices generated
3797 $this->setWarning('No electronic invoices can be downloaded. Please generate them first.');
3798 return false;
3799 }
3800
3801 // build one whole XML file
3802 $einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
3803 if ($einv_xml_body === false) {
3804 // something went wrong with the creation of the file to download
3805 $this->setError('Could not generate the XML file containing all the electronic invoices.');
3806 return false;
3807 }
3808
3809 // force the download of the XML string
3810 header('Content-Disposition: attachment; filename="mydata-aade-einvoices' . date('Y-m-d') . '.xml"');
3811 header("Content-Type: text/xml");
3812 header("Content-Length:" . strlen($einv_xml_body));
3813 header('Connection: close');
3814 echo $einv_xml_body;
3815
3816 exit;
3817 }
3818
3819 /**
3820 * Forces the display of an electronic invoice. This is a 'driver action', and so it's called
3821 * before getBookingsData() in the view. This method will not save/update records in the DB.
3822 * This method truncates the execution of the script to read the XML data.
3823 *
3824 * @return void
3825 */
3826 public function viewEInvoice()
3827 {
3828 $einvid = VikRequest::getInt('einvid', 0, 'request');
3829 $einv_data = $this->loadEInvoiceDetails($einvid);
3830 if (!$einv_data) {
3831 die('Missing e-invoice ID');
3832 }
3833
3834 // force the output
3835 header("Content-type:text/xml");
3836 echo $einv_data['xml'];
3837
3838 exit;
3839 }
3840
3841 /**
3842 * Removes an electonic invoice. This is a 'driver action',
3843 * and so it's called before getBookingsData() in the view.
3844 * It also removes the analogic version in PDF of the invoice.
3845 *
3846 * @return void
3847 */
3848 public function removeEInvoice()
3849 {
3850 $einvid = VikRequest::getInt('einvid', '', 'request');
3851 $einv_data = $this->loadEInvoiceDetails($einvid);
3852 if (!$einv_data) {
3853 $this->setError('Missing e-invoice ID. Unable to delete the e-invoice.');
3854 return false;
3855 }
3856
3857 // get "transmission data" (if any)
3858 $trans_data = !empty($einv_data['trans_data']) ? json_decode($einv_data['trans_data']) : null;
3859 if (is_object($trans_data) && !empty($trans_data->invoice_mark)) {
3860 /**
3861 * This invoice was transmitted before, make sure to cancel it also from myDATA.
3862 * However, the endpoint requires a "mark" value for the invoice, which could be the
3863 * invoiceMark property upon a successful submission or the number we pass to compose
3864 * the XML of the electronic invoice (our progressive number). There are two "mark"
3865 * values, but we got errors for both, hence we don't know which one to use. We always
3866 * check if $trans_data->invoice_mark is not empty so that we know the invoice was
3867 * already transmitted before to myDATA and accepted.
3868 *
3869 * @todo what's the right invoice mark? the "number" is inside the XML that we generate
3870 * even before the transmission, while "invoice_mark" is returned in the myDATA response.
3871 */
3872 $mydata_invoice_mark = $einv_data['number'];
3873 $mydata_invoice_mark = $trans_data->invoice_mark;
3874
3875 /**
3876 * Check if a correlated invoice was transmitted, because it should be removed first.
3877 *
3878 * @since 1.16.7 (J) - 1.6.7 (WP)
3879 */
3880 $correlated_inv_data_tn = $this->getPreviousCorrelatedInvoiceData($einv_data['id'], $einv_data['idorder'], 'transmission');
3881 if ($correlated_inv_data_tn && !empty($correlated_inv_data_tn['mark'])) {
3882 // delete the correlated invoice first, by making the POST request
3883 $response = $this->myDATARequestPOST('CancelInvoice?mark=' . $correlated_inv_data_tn['mark']);
3884 if ($response->code != 200) {
3885 // the request was not successful
3886 $this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
3887 $this->setWarning('Could not cancel the correlated invoice from myDATA with mark ' . $correlated_inv_data_tn['mark']);
3888 } else {
3889 // check the XML response
3890 $xml_result = $this->myDATAParseXMLResponse($response->body);
3891 if ($xml_result[0]) {
3892 // success! the correlated invoice was cancelled from myDATA
3893 $this->setInfo(sprintf('Correlated invoice mark %s successfully cancelled from myDATA', $correlated_inv_data_tn['mark']));
3894
3895 /**
3896 * IMPORTANT: sleep for 2 seconds, or deleting immediately the main invoice below may
3897 * result into an error like "Error (255) in invoice index 0 (#???): Invoice with MARK 400001924172498
3898 * cannot be cancelled because it is connected with active invoice with MARK 400001924172499"
3899 */
3900 sleep(2);
3901 }
3902 }
3903
3904 // always delete the record for the correlated invoice
3905 VBOFactory::getConfig()->remove($this->getCorrelatedInvoiceParamName($einv_data['id'], $einv_data['idorder']));
3906
3907 // check if the PDF version of the environmental fee exists
3908 if (!empty($correlated_inv_data_tn['pdf'])) {
3909 $envfee_pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $correlated_inv_data_tn['pdf'];
3910 if (is_file($envfee_pathpdf)) {
3911 @unlink($envfee_pathpdf);
3912 }
3913 }
3914 }
3915
3916 // make the POST request
3917 $response = $this->myDATARequestPOST('CancelInvoice?mark=' . $mydata_invoice_mark);
3918 if ($response->code != 200) {
3919 // the request was not successful, and the XML invoices were not parsed at all by myDATA
3920 $this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
3921 $this->setWarning('Could not cancel the invoice from myDATA.');
3922 } else {
3923 // check the XML response
3924 $xml_result = $this->myDATAParseXMLResponse($response->body);
3925 if ($xml_result[0]) {
3926 // success! the invoice was cancelled from myDATA
3927 $this->setInfo(sprintf('Invoice mark %s (#%s) successfully cancelled from myDATA', $trans_data->invoice_mark, $einv_data['number']));
3928 }
3929 }
3930 }
3931
3932 // remove e-invoice
3933 $q = "DELETE FROM `#__vikbooking_einvoicing_data` WHERE `id`=".$einv_data['id'].";";
3934 $this->dbo->setQuery($q);
3935 $this->dbo->execute();
3936
3937 // remove analogic invoice for this booking
3938 $pdfremoved = false;
3939 if (!empty($einv_data['idorder'])) {
3940 $pdfname = '';
3941 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
3942 $this->dbo->setQuery($q);
3943 $analogic = $this->dbo->loadAssoc();
3944 if ($analogic) {
3945 $pdfname = $analogic['file_name'];
3946 $q = "DELETE FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
3947 $this->dbo->setQuery($q);
3948 $this->dbo->execute();
3949
3950 }
3951 $pdfpath = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . $pdfname;
3952 if (!empty($pdfname) && is_file($pdfpath)) {
3953 $pdfremoved = true;
3954 @unlink($pdfpath);
3955 }
3956 }
3957
3958 $this->setInfo(($pdfremoved ? 'Electronic and PDF invoices deleted' : 'Electronic invoice deleted'));
3959 }
3960
3961 /**
3962 * Attempts to generate a QR Code PNG image file with the e-invoice URL.
3963 *
3964 * @param int $einv_id the generated e-invoice record ID.
3965 * @param int $bid the reservation record ID.
3966 * @param object $data transaction data object with myDATA values.
3967 *
3968 * @return string empty string in case of failure, or generated QR Code file name.
3969 *
3970 * @since 1.16.7 (J) - 1.6.7 (WP)
3971 */
3972 protected function generateInvoiceQRCode($einv_id, $bid, $data)
3973 {
3974 if (!is_object($data) || empty($data->invoice_qrcode)) {
3975 return '';
3976 }
3977
3978 // the QR Code PNG file name
3979 $filename = "aade_qrcode_{$bid}_{$einv_id}.png";
3980
3981 // generate the image
3982 if ($this->generateQRCodeImage($data->invoice_qrcode, VikBookingMydataAadeConstants::getQRCodeBase('path', $filename))) {
3983 // file was written successfully
3984 return $filename;
3985 }
3986
3987 // an error has occurred
3988 return '';
3989 }
3990
3991 /**
3992 * Generates a QR Code image with the given URL in the given path.
3993 *
3994 * @param string $url the URL to be assigned (content) to the QR Code.
3995 * @param string $path the full path where the file should be saved.
3996 *
3997 * @return bool
3998 */
3999 protected function generateQRCodeImage($url, $path)
4000 {
4001 try {
4002 // require the TCPDF 2D Barcode library
4003 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf_barcodes_2d.php';
4004
4005 // set the barcode content and type
4006 $barCode = new TCPDF2DBarcode($url, 'QRCODE,H');
4007
4008 // generate the QR code as PNG image
4009 $qr = $barCode->getBarcodePngData(
4010 VikBookingMydataAadeConstants::QRCODE_PNG_WIDTH,
4011 VikBookingMydataAadeConstants::QRCODE_PNG_HEIGHT,
4012 explode(',', preg_replace("/[^0-9\.\,]/", '', VikBookingMydataAadeConstants::QRCODE_PNG_COLOR_RGB))
4013 );
4014
4015 // write the image on disk
4016 return (bool)JFile::write($path, $qr);
4017 } catch (Throwable $t) {
4018 // do nothing
4019 }
4020
4021 return false;
4022 }
4023
4024 /**
4025 * Right after obtaining a QR Code for an electronic invoice, the driver
4026 * calls this method to refresh the PDF (courtesy) invoice so that any
4027 * conditional text rule used on the invoice template file will run correctly.
4028 *
4029 * @param int $bid the reservation record ID for which the invoice should be refreshed.
4030 *
4031 * @return bool
4032 *
4033 * @since 1.16.7 (J) - 1.6.7 (WP)
4034 */
4035 protected function refreshPdfInvoice($bid)
4036 {
4037 return (bool)VikBooking::generateBookingInvoice(
4038 // load booking details
4039 VikBooking::getBookingInfoFromID($bid),
4040 // do not set an invoice number, because the previous one must be used
4041 $invoice_num = 0,
4042 // do not set an invoice suffix, because the previous one must be used
4043 $invoice_suff = '',
4044 // do not set an invoice date, because the previous one must be used
4045 $invoice_date = '',
4046 // company information will be re-fetched
4047 $company_info = '',
4048 // translation is not needed
4049 $translate = false,
4050 // set the argument to request a re-generation (refresh) of the existing invoice
4051 $refresh_pdf = true
4052 );
4053 }
4054
4055 /**
4056 * Validates the XML against the Schema.
4057 *
4058 * @param string $xml the xml string to validate
4059 *
4060 * @return null|boolean
4061 */
4062 protected function validateXmlAgainstSchema($xml) {
4063 if (!class_exists('DOMDocument')) {
4064 // we cannot validate the XML because DOMDocument is missing
4065 return null;
4066 }
4067
4068 $schema_path = VikBookingMydataAadeConstants::getSchemaPath();
4069
4070 libxml_use_internal_errors(true);
4071
4072 $dom = new DOMDocument();
4073 $dom->load($xml);
4074 if (!$dom->schemaValidate($schema_path)) {
4075 $this->setWarning('The schema validation of the electronic XML invoice returned errors, but they may be related to an unreadable schema.');
4076 $this->setWarning($this->libxml_display_errors());
4077 return false;
4078 }
4079
4080 return true;
4081 }
4082
4083 /**
4084 * Formats the XML errors occurred
4085 *
4086 * @return string the error string
4087 */
4088 protected function libxml_display_errors() {
4089 $errorstr = "";
4090 $errors = libxml_get_errors();
4091 foreach ($errors as $error) {
4092 $errorstr .= $this->libxml_display_error($error);
4093 }
4094 libxml_clear_errors();
4095
4096 return $errorstr;
4097 }
4098
4099 /**
4100 * Explanation of the XML error
4101 *
4102 * @param object $error the libxml error object
4103 *
4104 * @return string the explained error occurred
4105 */
4106 protected function libxml_display_error($error) {
4107 $return = "\n";
4108 switch ($error->level) {
4109 case LIBXML_ERR_WARNING :
4110 $return .= "Warning ".$error->code.": ";
4111 break;
4112 case LIBXML_ERR_ERROR :
4113 $return .= "Error ".$error->code.": ";
4114 break;
4115 case LIBXML_ERR_FATAL :
4116 $return .= "Fatal Error ".$error->code.": ";
4117 break;
4118 }
4119 $return .= trim($error->message);
4120 if ($error->file) {
4121 $return .= " in " . $error->file;
4122 }
4123 $return .= " on line " . $error->line . "\n";
4124
4125 return $return;
4126 }
4127
4128 /**
4129 * Override method to show the overlay content.
4130 * Used to display the edit form of the raw XML.
4131 * This method echoes the string to be displayed.
4132 *
4133 * @return void
4134 */
4135 public function printOverlayContent()
4136 {
4137 $content = VikRequest::getString('drivercontent', '', 'request');
4138 $einvid = VikRequest::getInt('einvid', 0, 'request');
4139 $envfeebid = VikRequest::getInt('envfeebid', 0, 'request');
4140
4141 if ($content == 'editEInvoice' && !empty($einvid)) {
4142 $einv_data = $this->loadEInvoiceDetails($einvid);
4143 if (!$einv_data) {
4144 return;
4145 }
4146
4147 if ($envfeebid) {
4148 $correlated_invoice = $this->getPreviousCorrelatedInvoiceData($einvid, $envfeebid, $type = 'record');
4149 if ($correlated_invoice) {
4150 $einv_data['correlated_invoice'] = $correlated_invoice;
4151 }
4152 }
4153
4154 // path to edit invoice layout file
4155 $fpath = $this->driverHelperPath . 'editeinvoice.php';
4156
4157 // load helper file and echo its content
4158 echo $this->loadHelperFile($fpath, $einv_data);
4159
4160 return;
4161 }
4162 }
4163
4164 /**
4165 * Updates the XML of an electonic invoice. This is a 'driver action',
4166 * and so it's called before getBookingsData() in the view.
4167 *
4168 * @return bool
4169 */
4170 public function updateXmlEInvoice()
4171 {
4172 $einvid = VikRequest::getInt('einvid', '', 'request');
4173 $newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
4174 $einv_data = $this->loadEInvoiceDetails($einvid);
4175 if (!$einv_data) {
4176 $this->setError('Invoice not found');
4177 return false;
4178 }
4179 if (empty($newxml)) {
4180 $this->setError('Empty XML content');
4181 return false;
4182 }
4183
4184 $jdate = new JDate;
4185 $data = new stdClass;
4186 $data->id = $einv_data['id'];
4187 $data->created_on = $jdate->toSql();
4188 $data->xml = $newxml;
4189
4190 return $this->updateEInvoice($data);
4191 }
4192
4193 /**
4194 * Updates the XML of a correlated electonic invoice. This is a 'driver action',
4195 * and so it's called before getBookingsData() in the view.
4196 *
4197 * @return bool
4198 *
4199 * @since 1.16.7 (J) - 1.6.7 (WP)
4200 */
4201 public function updateCorrelatedXmlEInvoice()
4202 {
4203 $einvid = VikRequest::getInt('einvid', 0, 'request');
4204 $envfeebid = VikRequest::getInt('envfeebid', 0, 'request');
4205 $newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
4206
4207 // get the invoice record
4208 $correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einvid, $envfeebid), []);
4209
4210 if (!$correlated_inv_raw_data) {
4211 return false;
4212 }
4213
4214 // update the XML source code
4215 $correlated_inv_raw_data['xml'] = $newxml;
4216
4217 // update record
4218 VBOFactory::getConfig()->set($this->getCorrelatedInvoiceParamName($einvid, $envfeebid), $correlated_inv_raw_data);
4219
4220 return true;
4221 }
4222
4223 /**
4224 * Extracts only numbers from a given string, by optionally
4225 * stripping the current year. Useful to find an invoice number.
4226 *
4227 * @param string $str the string to look for numbers
4228 * @param boolean $stripy whether to strip the current year
4229 *
4230 * @return string either an empty string or all numbers as a concatenated string
4231 */
4232 protected function getOnlyNumbers($str, $stripy = false)
4233 {
4234 if ($stripy) {
4235 $str = str_replace(date('Y'), '', $str);
4236 }
4237
4238 preg_match_all('/\d+/', $str, $matches);
4239
4240 return implode('', $matches[0]);
4241 }
4242 }
4243