PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / einvoicing / drivers / mydata_aade.php

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

4,246 lines 158.3 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 } elseif (!$correlated && $inv_tot_paid > $data[0]['total']) {
1958 // use the calculated booking total amount minus the environmental fees
1959 $inv_tot_paid = $data[0]['total'];
1960 }
1961
1962 // invoice payment method
1963 $inv_pay_method = '';
1964 if (!empty($data[0]['idpayment'])) {
1965 $pay_info_parts = explode('=', $data[0]['idpayment']);
1966 $inv_pay_method = !empty($pay_info_parts[1]) ? $pay_info_parts[1] : $inv_pay_method;
1967 }
1968
1969 // compose the invoice UID
1970 $invoice_uid_parts = [
1971 $settings['params']['vatid'],
1972 $invdate,
1973 $branch,
1974 $invtype,
1975 $series,
1976 $aa_serial_number,
1977 ];
1978 $invoice_uid = sha1(implode('', $invoice_uid_parts));
1979
1980 // invoice details and summaries
1981 $invoice_details = [];
1982 $summaries = [];
1983 $summariesvat = [];
1984 $rounded_nets = [];
1985
1986 // whether to include "incomeClassification" nodes
1987 $use_income_classf = (!empty($settings['params']['einv_inc_class_type']) && !empty($settings['params']['einv_inc_class_cat']));
1988
1989 $is_package = (!empty($data[0]['pkg']));
1990 $isdue = 0;
1991 $extralinenum = 0;
1992 $discountval = 0;
1993 if ($data[0]['id'] < 0 && isset($this->externalData['einvrawcont'])) {
1994 // custom (manual) invoice, get the raw content of the invoice
1995 foreach ($this->externalData['einvrawcont']['rows'] as $ind => $row) {
1996 if (!isset($summariesvat[$row['aliq']])) {
1997 $summariesvat[$row['aliq']] = array('net' => 0, 'tax' => 0);
1998 $rounded_nets[$row['aliq']] = 0;
1999 }
2000 $summariesvat[$row['aliq']]['net'] += $row['net'];
2001 $summariesvat[$row['aliq']]['tax'] += $row['tax'];
2002 $rounded_nets[$row['aliq']] += (float)number_format($row['net'], 2, '.', '');
2003
2004 // income classification
2005 $inc_classf_nodes = '';
2006 if ($use_income_classf) {
2007 $inc_classf_nodes = '<incomeClassification>
2008 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2009 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2010 <N1:amount>' . number_format($row['net'], 2, '.', '') . '</N1:amount>
2011 </incomeClassification>';
2012 }
2013
2014 // push invoice details node
2015 $vat_category = VikBookingMydataAadeConstants::getVatCategory($row['aliq']);
2016 array_push($invoice_details, '
2017 <invoiceDetails>
2018 <lineNumber>' . ($ind + 1) . '</lineNumber>
2019 <netValue>' . number_format($row['net'], 2, '.', '') . '</netValue>
2020 <vatCategory>' . $vat_category . '</vatCategory>
2021 <vatAmount>' . number_format($row['tax'], 2, '.', '') . '</vatAmount>
2022 ' . ((int)$row['aliq'] === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2023 <lineComments>'.$this->convertSpecials($row['service']).'</lineComments>
2024 ' . $inc_classf_nodes . '
2025 </invoiceDetails>');
2026
2027 }
2028 } else {
2029 // invoice for a regular booking
2030 $tars = $this->getBookingTariffs($data);
2031
2032 // check discount (coupon and/or refund)
2033 $discount_nodes = '';
2034 if (isset($data[0]['coupon']) && strlen($data[0]['coupon']) > 0) {
2035 $expcoupon = explode(";", $data[0]['coupon']);
2036 $discountval += (float)$expcoupon[1];
2037 }
2038 if (isset($data[0]['refund']) && $data[0]['refund'] > 0) {
2039 $discountval += $data[0]['refund'];
2040 }
2041 if ($discountval > 0) {
2042 $discount_nodes = '
2043 <discountOption>true</discountOption>
2044 <deductionsAmount>' . number_format($discountval, 2, '.', '') . '</deductionsAmount>';
2045 }
2046
2047 foreach ($data as $kor => $or) {
2048 $num = $kor + 1;
2049
2050 if ($correlated) {
2051 // push invoice details node
2052 array_push($invoice_details, '
2053 <invoiceDetails>
2054 <lineNumber>1</lineNumber>
2055 <netValue>0.00</netValue>
2056 <vatCategory>8</vatCategory>
2057 <vatAmount>0.00</vatAmount>
2058 <incomeClassification>
2059 <N1:classificationCategory>category1_95</N1:classificationCategory>
2060 <N1:amount>0</N1:amount>
2061 </incomeClassification>
2062 </invoiceDetails>
2063 <taxesTotals>
2064 <taxes>
2065 <taxType>3</taxType>
2066 <taxCategory>10</taxCategory>
2067 <underlyingValue>' . number_format($or['total'], 2, '.', '') . '</underlyingValue>
2068 <taxAmount>' . number_format($this->environmental_fee_details['fee_cost'], 2, '.', '') . '</taxAmount>
2069 </taxes>
2070 </taxesTotals>');
2071 // break the loop for the single environmental fee
2072 break;
2073 }
2074
2075 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2076 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
2077 $descr = $is_package ? sprintf(VikBookingMydataAadeConstants::DESCRPACKAGENIGHTS, $or['days']) : sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
2078 $cost_minus_tax = VikBooking::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
2079 $cost_tax_amount = (VikBooking::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']) - $cost_minus_tax);
2080 $aliq = $this->getAliquoteById($or['cust_idiva']);
2081 if (!isset($summariesvat[$aliq])) {
2082 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2083 $rounded_nets[$aliq] = 0;
2084 }
2085 $summariesvat[$aliq]['net'] += $cost_minus_tax;
2086 $summariesvat[$aliq]['tax'] += $cost_tax_amount;
2087 $rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
2088
2089 // income classification
2090 $inc_classf_nodes = '';
2091 if ($use_income_classf) {
2092 $inc_classf_nodes = '<incomeClassification>
2093 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2094 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2095 <N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
2096 </incomeClassification>';
2097 }
2098
2099 // push invoice details node
2100 array_push($invoice_details, '
2101 <invoiceDetails>
2102 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2103 <netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
2104 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2105 <vatAmount>' . number_format($cost_tax_amount, 2, '.', '') . '</vatAmount>
2106 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2107 ' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
2108 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2109 ' . $inc_classf_nodes . '
2110 </invoiceDetails>');
2111 } elseif (isset($tars[$num]) && is_array($tars[$num])) {
2112 // regular tariff
2113 $descr = sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
2114 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
2115 $calctar = VikBooking::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
2116 $aliq = $this->getAliquoteFromPriceId($tars[$num]['idprice']);
2117 $isdue += $calctar;
2118 if ($calctar == $display_rate) {
2119 $cost_minus_tax = VikBooking::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
2120 $tax = ($display_rate - $cost_minus_tax);
2121 } else {
2122 $cost_minus_tax = $display_rate;
2123 $tax = ($calctar - $display_rate);
2124 }
2125 if (!isset($summariesvat[$aliq])) {
2126 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2127 $rounded_nets[$aliq] = 0;
2128 }
2129 $summariesvat[$aliq]['net'] += $cost_minus_tax;
2130 $summariesvat[$aliq]['tax'] += $tax;
2131 $rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
2132
2133 // income classification
2134 $inc_classf_nodes = '';
2135 if ($use_income_classf) {
2136 $inc_classf_nodes = '<incomeClassification>
2137 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2138 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2139 <N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
2140 </incomeClassification>';
2141 }
2142
2143 // push invoice details node
2144 array_push($invoice_details, '
2145 <invoiceDetails>
2146 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2147 <netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
2148 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2149 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
2150 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2151 ' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
2152 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2153 ' . $inc_classf_nodes . '
2154 </invoiceDetails>');
2155 }
2156
2157 // room options
2158 if (!empty($or['optionals']) && !$correlated) {
2159 $stepo = explode(";", $or['optionals']);
2160 foreach ($stepo as $roptkey => $oo) {
2161 if (empty($oo)) {
2162 continue;
2163 }
2164 $stept = explode(":", $oo);
2165 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $this->dbo->quote($stept[0]) . ";";
2166 $this->dbo->setQuery($q);
2167 $actopt = $this->dbo->loadAssocList();
2168 if (!$actopt) {
2169 continue;
2170 }
2171 $chvar = '';
2172 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
2173 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
2174 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
2175 $optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
2176 $child_num = VikBooking::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
2177 $optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
2178 $agestept = explode('-', $stept[1]);
2179 $stept[1] = $agestept[0];
2180 $chvar = $agestept[1];
2181 $realcost = 0;
2182 if (!empty($chvar)) {
2183 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
2184 // percentage value of the adults tariff
2185 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2186 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
2187 } else {
2188 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
2189 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
2190 }
2191 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
2192 // percentage value of room base cost
2193 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2194 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
2195 } else {
2196 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
2197 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
2198 }
2199 }
2200 $actopt[0]['chageintv'] = $chvar;
2201 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
2202 $actopt[0]['quan'] = $stept[1];
2203 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $or['days'] * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
2204 }
2205 } else {
2206 $actopt[0]['quan'] = $stept[1];
2207 // VBO 1.11 - options percentage cost of the room total fee
2208 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2209 $deftar_basecosts = $or['cust_cost'];
2210 } else {
2211 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
2212 }
2213 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
2214 //
2215 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $or['days'] * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
2216 }
2217 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
2218 $realcost = $actopt[0]['maxprice'];
2219 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
2220 $realcost = $actopt[0]['maxprice'] * $stept[1];
2221 }
2222 }
2223 if ($actopt[0]['perperson'] == 1) {
2224 $realcost = $realcost * $or['adults'];
2225 }
2226
2227 /**
2228 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
2229 *
2230 * @since 1.17.7 (J) - 1.7.7 (WP)
2231 */
2232 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $or, $or]);
2233 if ($custom_calculation) {
2234 $realcost = (float) $custom_calculation[0];
2235 }
2236
2237 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
2238 $isdue += $tmpopr;
2239 // increase line number
2240 $extralinenum++;
2241 //
2242 $aliq = $this->getAliquoteById($actopt[0]['idiva']);
2243 if ($tmpopr == $realcost) {
2244 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
2245 $tax = ($realcost - $opt_minus_tax);
2246 } else {
2247 $opt_minus_tax = $realcost;
2248 $tax = ($tmpopr - $realcost);
2249 }
2250 $descr = $actopt[0]['is_citytax'] == 1 ? VikBookingMydataAadeConstants::DESCRTOURISTTAX : sprintf(VikBookingMydataAadeConstants::DESCRROOMOPTION, strtoupper($actopt[0]['name']));
2251 if (!isset($summariesvat[$aliq])) {
2252 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2253 $rounded_nets[$aliq] = 0;
2254 }
2255
2256 $summariesvat[$aliq]['net'] += $opt_minus_tax;
2257 $summariesvat[$aliq]['tax'] += $tax;
2258 $rounded_nets[$aliq] += (float)number_format($opt_minus_tax, 2, '.', '');
2259
2260 // income classification
2261 $inc_classf_nodes = '';
2262 if ($use_income_classf) {
2263 $inc_classf_nodes = '<incomeClassification>
2264 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2265 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2266 <N1:amount>' . number_format($opt_minus_tax, 2, '.', '') . '</N1:amount>
2267 </incomeClassification>';
2268 }
2269
2270 // push invoice details node
2271 array_push($invoice_details, '
2272 <invoiceDetails>
2273 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2274 <netValue>' . number_format($opt_minus_tax, 2, '.', '') . '</netValue>
2275 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2276 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
2277 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2278 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2279 ' . $inc_classf_nodes . '
2280 </invoiceDetails>');
2281 }
2282 }
2283
2284 // custom extra costs
2285 if (!empty($or['extracosts']) && !$correlated) {
2286 $cur_extra_costs = json_decode($or['extracosts'], true);
2287 foreach ($cur_extra_costs as $eck => $ecv) {
2288 // increase line number
2289 $extralinenum++;
2290 //
2291 $ecplustax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
2292 $isdue += $ecplustax;
2293 $descr = sprintf(VikBookingMydataAadeConstants::DESCRROOMEXTRACOST, strtoupper($ecv['name']));
2294 if ($ecplustax == $ecv['cost']) {
2295 $ec_minus_tax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
2296 $tax = ($ecv['cost'] - $ec_minus_tax);
2297 } else {
2298 $ec_minus_tax = ($ecplustax - $ecv['cost']);
2299 $tax = ($ecplustax - $ecv['cost']);
2300 }
2301 $aliq = $this->getAliquoteById($ecv['idtax']);
2302 if (!isset($summariesvat[$aliq])) {
2303 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
2304 $rounded_nets[$aliq] = 0;
2305 }
2306
2307 $summariesvat[$aliq]['net'] += $ec_minus_tax;
2308 $summariesvat[$aliq]['tax'] += $tax;
2309 $rounded_nets[$aliq] += (float)number_format($ec_minus_tax, 2, '.', '');
2310
2311 // income classification
2312 $inc_classf_nodes = '';
2313 if ($use_income_classf) {
2314 $inc_classf_nodes = '<incomeClassification>
2315 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2316 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2317 <N1:amount>' . number_format($ec_minus_tax, 2, '.', '') . '</N1:amount>
2318 </incomeClassification>';
2319 }
2320
2321 /**
2322 * Removed "quantity" and "measurementUnit" nodes from every "invoiceDetails" node.
2323 *
2324 * <quantity>1.00</quantity>
2325 * <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
2326 *
2327 * @since 1.16.7 (J) - 1.6.7 (WP)
2328 */
2329
2330 // push invoice details node
2331 array_push($invoice_details, '
2332 <invoiceDetails>
2333 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
2334 <netValue>' . number_format($ec_minus_tax, 2, '.', '') . '</netValue>
2335 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
2336 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
2337 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
2338 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
2339 ' . $inc_classf_nodes . '
2340 </invoiceDetails>');
2341 }
2342 }
2343 }
2344 }
2345
2346 // build riepiloghi IVA
2347 $grand_total_net = 0;
2348 $grand_total_vat = 0;
2349 $grand_total_tax_no_rate = 0;
2350 $environmental_fee_amount = $correlated ? $this->environmental_fee_details['fee_cost'] : 0;
2351 foreach ($summariesvat as $aliq => $vat_summary) {
2352 $totnet = number_format($vat_summary['net'], 2, '.', '');
2353 $tottax = number_format($vat_summary['tax'], 2, '.', '');
2354 if (isset($rounded_nets[$aliq]) && (float)$totnet != $rounded_nets[$aliq]) {
2355 /**
2356 * In case of several rows in the invoice, maybe a lot of Extra Services,
2357 * there can be a discrepancy between the sum of the <netValue> nodes
2358 * in the <invoiceDetails> nodes, and the <taxAmount> in <taxesTotals>.
2359 * We need to prevent the amounts to be different because of number_format and
2360 * adjust the amounts and obtain the same value as the sum of the nets in the lines.
2361 * The issue was reproduced with 9 Extra Services, one Room, one Tourist Tax (Option).
2362 *
2363 * @see sandbox booking ID 1140
2364 */
2365 if ($rounded_nets[$aliq] > (float)$totnet) {
2366 $diff = $rounded_nets[$aliq] - (float)$totnet;
2367 $totnet = number_format($rounded_nets[$aliq], 2, '.', '');
2368 $tottax = number_format(((float)$tottax - $diff), 2, '.', '');
2369 } else {
2370 $diff = (float)$totnet - $rounded_nets[$aliq];
2371 $totnet = number_format($rounded_nets[$aliq], 2, '.', '');
2372 $tottax = number_format(((float)$tottax + $diff), 2, '.', '');
2373 }
2374 }
2375
2376 // sum grand total values
2377 $grand_total_net += $totnet;
2378 if ((int)$aliq > 0) {
2379 $grand_total_vat += $tottax;
2380 } else {
2381 /**
2382 * @todo are we doing good by summing this kind of tax, which is not VAT because
2383 * the tax rate is 0%, to the "total withheld amount"? Or should we use the
2384 * node <totalOtherTaxesAmount> instead?
2385 */
2386 $grand_total_tax_no_rate += $tottax;
2387 }
2388
2389 /**
2390 * For the moment we ingore completely the <taxesTotals> node and sub-nodes. Docs say:
2391 * "Field taxesTotals contains all taxes except VAT. If user users this element,
2392 * taxes will not exist in invoiceDetails".
2393 * However, here we have a sum of tax amounts for any aliquote (tax rate) involved.
2394 *
2395 * @todo check if these nodes should be somehow composed even if they are optional.
2396 */
2397 }
2398
2399 /**
2400 * Address element is forbidden for issuer from Greece.
2401 */
2402 $issuer_address_nodes = '';
2403 if (strcasecmp($settings['params']['country'], 'GR')) {
2404 // issuer not from Greece, compose address
2405 $issuer_address_nodes = '<address>
2406 <street>' . $this->convertSpecials($settings['params']['address']) . '</street>
2407 <number>' . $settings['params']['streetnumber'] . '</number>
2408 <postalCode>' . $settings['params']['zip'] . '</postalCode>
2409 <city>' . $this->convertSpecials($settings['params']['city']) . '</city>
2410 </address>';
2411 }
2412
2413 // total income classification
2414 $inc_classf_nodes = '';
2415 if ($use_income_classf) {
2416 if ($correlated) {
2417 $inc_classf_nodes = '<incomeClassification>
2418 <N1:classificationCategory>category1_95</N1:classificationCategory>
2419 <N1:amount>' . number_format(0, 2, '.', '') . '</N1:amount>
2420 </incomeClassification>';
2421 } else {
2422 $inc_classf_nodes = '<incomeClassification>
2423 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2424 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2425 <N1:amount>' . number_format($grand_total_net, 2, '.', '') . '</N1:amount>
2426 </incomeClassification>';
2427 }
2428 }
2429
2430 /**
2431 * The "Counterpart" node is forbidden for certain invoice types.
2432 *
2433 * @since 1.18.2 (J) - 1.8.2 (WP)
2434 */
2435 $inv_types_forbid_counterpart = [
2436 '11.1',
2437 '11.2',
2438 ];
2439
2440 // build counterpart node with the customer information
2441 $counterpartNode = '';
2442 if (!in_array($orig_invtype, $inv_types_forbid_counterpart)) {
2443 $counterpartNode = '<counterpart>
2444 <vatNumber>' . $this->convertSpecials($data[0]['customer']['vat'] ?: '') . '</vatNumber>
2445 ' . (!empty($data[0]['customer']['country_2_code']) ? '<country>' . $this->convertSpecials($data[0]['customer']['country_2_code']) . '</country>' : '') . '
2446 <branch>' . $branch . '</branch>
2447 ' . (!empty($client_name) ? '<name>' . $this->convertSpecials($client_name) . '</name>' : '') . '
2448 <address>
2449 <street>' . $this->convertSpecials(preg_replace("/[0-9]/", '', $data[0]['customer']['address'])) . '</street>
2450 <number>' . preg_replace("/[^0-9]/", '', $data[0]['customer']['address']) . '</number>
2451 <postalCode>' . $this->convertSpecials($data[0]['customer']['zip']) . '</postalCode>
2452 <city>' . $this->convertSpecials($data[0]['customer']['city']) . '</city>
2453 </address>
2454 </counterpart>';
2455 }
2456
2457 // build XML
2458 $root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
2459 $xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
2460 <InvoicesDoc ' . $root_namespaces . '>
2461 <invoice>
2462 <uid>' . $invoice_uid . '</uid>
2463 <mark>' . ($correlated ? $correlated_invnum : $settings['progcount']) . '</mark>
2464 <issuer>
2465 <vatNumber>' . $settings['params']['vatid'] . '</vatNumber>
2466 <country>' . $settings['params']['country'] . '</country>
2467 <branch>0</branch>
2468 ' . (strcasecmp($settings['params']['country'], 'GR') ? '<name>' . $this->convertSpecials($settings['params']['companyname']) . '</name>' : '') . '
2469 ' . (!empty($issuer_address_nodes) ? $issuer_address_nodes : '') . '
2470 </issuer>
2471 ' . $counterpartNode . '
2472 <invoiceHeader>
2473 <series>' . $series . '</series>
2474 <aa>' . $aa_serial_number . '</aa>
2475 <issueDate>' . $invdate . '</issueDate>
2476 <invoiceType>' . $invtype . '</invoiceType>
2477 <currency>' . VikBooking::getCurrencyName() . '</currency>
2478 ' . ($correlated ? '<correlatedInvoices>{main_invoice_mark}</correlatedInvoices>' : '') . '
2479 </invoiceHeader>
2480 <paymentMethods>
2481 <paymentMethodDetails>
2482 <type>' . $settings['params']['einv_paymethod'] . '</type>
2483 <amount>' . number_format($inv_tot_paid, 2, '.', '') . '</amount>
2484 ' . (!empty($inv_pay_method) ? '<paymentMethodInfo>' . $this->convertSpecials($inv_pay_method) . '</paymentMethodInfo>' : '') . '
2485 </paymentMethodDetails>
2486 </paymentMethods>
2487 ' . implode("\n", $invoice_details) . '
2488 <invoiceSummary>
2489 <totalNetValue>' . number_format($grand_total_net, 2, '.', '') . '</totalNetValue>
2490 <totalVatAmount>' . number_format($grand_total_vat, 2, '.', '') . '</totalVatAmount>
2491 <totalWithheldAmount>' . number_format($grand_total_tax_no_rate, 2, '.', '') . '</totalWithheldAmount>
2492 <totalFeesAmount>0.00</totalFeesAmount>
2493 <totalStampDutyAmount>0.00</totalStampDutyAmount>
2494 <totalOtherTaxesAmount>' . number_format($environmental_fee_amount, 2, '.', '') . '</totalOtherTaxesAmount>
2495 <totalDeductionsAmount>' . number_format(($correlated ? 0 : $discountval), 2, '.', '') . '</totalDeductionsAmount>
2496 <totalGrossValue>' . number_format(($correlated ? $this->environmental_fee_details['fee_cost'] : $data[0]['total']), 2, '.', '') . '</totalGrossValue>
2497 ' . $inc_classf_nodes . '
2498 </invoiceSummary>
2499 </invoice>
2500 </InvoicesDoc>';
2501
2502 // attempt to properly format the XML string
2503 $this->formatXmlString($xml);
2504
2505 if ($correlated) {
2506 /**
2507 * Trigger event to allow third party plugins to apply a custom eco-fee e-invoice auto-increment number.
2508 *
2509 * @since 1.18.6 (J) - 1.8.6 (WP)
2510 */
2511 $custom_einv_data = VBOFactory::getPlatform()->getDispatcher()->filter('onMydataUpdateEcofeeEinvoiceAutoincrementNumber', [$correlated_invnum, $settings, $data]);
2512 if (intval($custom_einv_data[0] ?? '')) {
2513 // overwrite correlated e-invoice auto-increment number
2514 $correlated_invnum = (int) $custom_einv_data[0];
2515 }
2516
2517 // update driver setting
2518 $this->updateDriverSetting('envfeeinvoiceinum', $correlated_invnum);
2519
2520 // return the raw XML for the correlated invoice just built
2521 return $xml;
2522 }
2523
2524 // check if we need to validate the XML against the official schema
2525 if (!empty($settings['params']['schema_validate'])) {
2526 /**
2527 * It may not be possible to validate the XML against the schema, as on
2528 * some environments this process may run out of execution time.
2529 */
2530 try {
2531 $schema_validation = $this->validateXmlAgainstSchema($xml);
2532 if ($schema_validation === null) {
2533 // display warning
2534 $this->setWarning('Missing PHP libraries for DOMDocument to validate the XML invoice against the official schema.');
2535 }
2536 } catch (Exception $e) {
2537 // display warning
2538 $this->setWarning('Could not validate the XML invoice against the official Schema - process failed with no response.');
2539 }
2540 }
2541
2542 if ($this->debugging()) {
2543 $this->setWarning('<pre>'.htmlentities($xml).'</pre><br/>');
2544 // break the process when in debug mode
2545 return false;
2546 }
2547
2548 // we proceed with the generation
2549
2550 // invoice name (transmission date-time string + auto-increment registration value just for our internal purpose)
2551 $einvname = date('YmdHis') . '_' . $settings['progcount'] . '.xml';
2552
2553 // get current datetime object in local format
2554 $date_obj = JFactory::getDate();
2555 $date_obj->setTimezone(new DateTimeZone(date_default_timezone_get()));
2556
2557 // prepare object for storing the invoice
2558 $einvobj = new stdClass;
2559 $einvobj->driverid = $settings['id'];
2560 $einvobj->created_on = $date_obj->toSql($local = true);
2561 $einvobj->for_date = $invdate;
2562 $einvobj->filename = $einvname;
2563 $einvobj->number = $invnum;
2564 $einvobj->idorder = $data[0]['id'];
2565 $einvobj->idcustomer = !empty($data[0]['customer']['id']) ? $data[0]['customer']['id'] : 0;
2566 $einvobj->country = !empty($data[0]['customer']['country']) ? $data[0]['customer']['country'] : null;
2567 // this column is not needed in this driver, but we give it a default value
2568 $einvobj->recipientcode = '';
2569 $einvobj->xml = $xml;
2570 // always reset transmitted and obliterated values for new e-invoices
2571 $einvobj->transmitted = 0;
2572 $einvobj->obliterated = 0;
2573
2574 $newinvid = $this->storeEInvoice($einvobj);
2575 if ($newinvid === false) {
2576 $this->setError('Error storing the electronic invoice for the reservation ID '.$data[0]['id']);
2577 return false;
2578 }
2579
2580 if ($canbeinvoiced < 0) {
2581 // log event history when regenerating an e-invoice
2582 VikBooking::getBookingHistoryInstance()->setBid($data[0]['id'])->store('BI', ($this->getName() . ' #' . $invnum));
2583 }
2584
2585 // update settings before generating the analogic invoice in PDF format to prevent exceptions to be thrown or exit/die calls.
2586 // update configuration setting for VikBooking::getNextInvoiceNumber()
2587 if ($data[0]['id'] > 0) {
2588 // we exclude custom (manual) invoices which would have a booking ID set to -number
2589 $this->updateInvoiceNumber($invnum);
2590 }
2591 // update auto-increment driver setting by increasing it for the next run
2592 $this->updateProgressiveNumber(++$settings['progcount']);
2593
2594 /**
2595 * Check if we should generate another, correlated, invoice.
2596 */
2597 if (!$correlated && $this->environmental_fee_details) {
2598 // re-call the same method to generate the invoice for the environmental fee
2599 $correlated_inv_xml = $this->generateEInvoice($data, true);
2600 if ($correlated_inv_xml) {
2601 // let the method store the raw XML for the correlated invoice
2602 $this->prepareCorrelatedInvoice($einvobj, $data, $correlated_inv_xml);
2603 }
2604 }
2605
2606 if (!$correlated && !$this->hasAnalogicInvoice($data[0]['id'])) {
2607 // no analogic invoice in PDF available, so we create it
2608 if (!$this->generateAnalogicInvoice($data[0]['id'], $invnum, $invdate)) {
2609 // raise warning in case of error
2610 $this->setWarning('It was not possible to generate the courtesy PDF version of the invoice for the reservation ID '.$data[0]['id']);
2611 }
2612 }
2613
2614 return true;
2615 }
2616
2617 /**
2618 * Builds the param name to read the correlated e-invoice data from the db settings.
2619 *
2620 * @param int $einv_id the e-invoice record ID.
2621 * @param int $booking_id the reservation record ID.
2622 *
2623 * @return string
2624 *
2625 * @since 1.16.7 (J) - 1.6.7 (WP)
2626 */
2627 public function getCorrelatedInvoiceParamName($einv_id, $booking_id)
2628 {
2629 $driver_id = $this->getDriverId();
2630
2631 return "envfee_invoice_{$driver_id}_{$einv_id}_{$booking_id}";
2632 }
2633
2634 /**
2635 * Stores a record with the information to create the environmental fee invoice (correlated invoice).
2636 * The actual invoice will be created upon transmitting the main one for the reservation because
2637 * the environmental fee invoice requires the correlated number to be the Mark of the main invoice.
2638 *
2639 * @param object $einvobj the main e-invoice record.
2640 * @param array $data the nested booking record.
2641 * @param string $xml the raw XML generated.
2642 *
2643 * @return bool
2644 *
2645 * @since 1.16.7 (J) - 1.6.7 (WP)
2646 */
2647 public function prepareCorrelatedInvoice($einvobj, array $data, $xml)
2648 {
2649 if (!is_object($einvobj) || empty($einvobj->id) || !$data) {
2650 return false;
2651 }
2652
2653 $booking_id = $data[0]['id'];
2654 $einv_id = $einvobj->id;
2655 $driver_id = $this->getDriverId();
2656
2657 $config_param_name = $this->getCorrelatedInvoiceParamName($einv_id, $booking_id);
2658
2659 $config_param_value = [
2660 'bid' => $booking_id,
2661 'einvid' => $einv_id,
2662 'envfee' => $this->environmental_fee_details,
2663 'xml' => $xml,
2664 ];
2665
2666 VBOFactory::getConfig()->set($config_param_name, $config_param_value);
2667
2668 return true;
2669 }
2670
2671 /**
2672 * Attempts to get the previous correlated invoice number.
2673 * Useful when re-generating an invoice already transmitted.
2674 *
2675 * @param int $einv_id the main invoice ID.
2676 * @param int $bid the VBO booking ID.
2677 * @param string $type the type of data to fetch.
2678 *
2679 * @return string|array
2680 */
2681 public function getPreviousCorrelatedInvoiceData($einv_id, $bid, $type = '')
2682 {
2683 $driver_id = $this->getDriverId();
2684
2685 $correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einv_id, $bid), []);
2686
2687 if (!$correlated_inv_raw_data || empty($correlated_inv_raw_data['xml'])) {
2688 return '';
2689 }
2690
2691 $xml_inv = simplexml_load_string($correlated_inv_raw_data['xml']);
2692
2693 if (!$xml_inv) {
2694 return '';
2695 }
2696
2697 if (!strcasecmp($type, 'date')) {
2698 // previous invoice date
2699 return (string)$xml_inv->invoice->invoiceHeader->issueDate;
2700 }
2701
2702 if (!strcasecmp($type, 'xml')) {
2703 // return the plain XML
2704 return $correlated_inv_raw_data['xml'];
2705 }
2706
2707 if (!strcasecmp($type, 'record')) {
2708 // return the whole array record
2709 return $correlated_inv_raw_data;
2710 }
2711
2712 if (!strcasecmp($type, 'transmission')) {
2713 // return the whole transmission array, if available
2714 return $correlated_inv_raw_data['transmission'] ?? [];
2715 }
2716
2717 // default to previous invoice number
2718 return (string)$xml_inv->invoice->mark;
2719 }
2720
2721 /**
2722 * Manipulates the previously created correlated invoice by adding the proper invoice mark,
2723 * then transmits the second e-invoice to myDATA.
2724 *
2725 * @param object|array $main_invoice_data the main e-invoice transaction information object (or associative array).
2726 * @param array $env_fee_data the prepared data for the correlated invoice and fee.
2727 * @param array $extras associative array to pass extra information.
2728 *
2729 * @return bool
2730 *
2731 * @since 1.16.7 (J) - 1.6.7 (WP)
2732 */
2733 public function transmitCorrelatedInvoice($main_invoice_data, array $env_fee_data, array $extras)
2734 {
2735 // always cast main invoice data to an object
2736 $main_invoice_data = (object) $main_invoice_data;
2737
2738 /**
2739 * Customers have reported an update to myDATA that no longer accepts the XML nodes
2740 * <uid> and <mark> (right under the node <invoice>) otherwise errors with code 273
2741 * will be raised stating that such details will be generated and provided by myDATA.
2742 *
2743 * @since 1.18.6 (J) - 1.8.6 (WP)
2744 */
2745 $xmlObj = simplexml_load_string($env_fee_data['xml']);
2746 if ($xmlObj->invoice->uid ?? null) {
2747 // delete nodes, will be added back in case of successful transmission
2748 unset($xmlObj->invoice->uid, $xmlObj->invoice->mark);
2749
2750 // re-build XML string
2751 $env_fee_data['xml'] = $xmlObj->asXML();
2752 }
2753
2754 // first off, set the proper correlated invoice number by using the mark
2755 // we use a regex that will capture two groups to avoid problems with the number-value for replacement
2756 $correlated_einv_final_xml = preg_replace("/<(correlatedInvoices)>(\{[a-z_]*\})?<\/correlatedInvoices>/i", '<$1>' . $main_invoice_data->invoice_mark . '</$1>', $env_fee_data['xml']);
2757
2758 // transmit the XML correlated e-invoice to myDATA
2759 $response = $this->myDATARequestPOST('SendInvoices', $correlated_einv_final_xml);
2760 if ($response->code != 200) {
2761 // the request was not successful, and the XML invoices were not parsed at all by myDATA
2762 $this->setError(sprintf('Correlated e-invoice - Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
2763 $this->setError('Correlated e-invoice - Could not send the invoice to myDATA.');
2764 return false;
2765 }
2766
2767 // process the transmission response
2768 $res_obj = simplexml_load_string($response->body);
2769 if (!is_object($res_obj) || !isset($res_obj->response)) {
2770 $this->setError('Could not parse XML response');
2771 $this->setError('<pre>' . htmlentities($response->body) . '</pre>');
2772 return false;
2773 }
2774
2775 if (!isset($res_obj->response->statusCode)) {
2776 $this->setError('Unexpected nodes in XML response (missing statusCode)');
2777 $this->setError('<pre>' . htmlentities($response->body) . '</pre>');
2778 return false;
2779 }
2780
2781 // check if we have a successful status code for this invoice
2782 if (!strcasecmp((string)$res_obj->response->statusCode, 'Success')) {
2783 // get the invoice UID
2784 $invoice_uid = isset($res_obj->response->invoiceUid) ? (string)$res_obj->response->invoiceUid : null;
2785
2786 // get the invoice mark (needed for a later cancellation)
2787 $invoice_mark = isset($res_obj->response->invoiceMark) ? (string)$res_obj->response->invoiceMark : null;
2788
2789 // get the invoice QRCode URL
2790 $invoice_qrcode = isset($res_obj->response->qrUrl) ? (string)$res_obj->response->qrUrl : null;
2791 $invoice_qrcode = !isset($res_obj->response->qrUrl) && isset($res_obj->response->qrCodeUrl) ? (string)$res_obj->response->qrCodeUrl : $invoice_qrcode;
2792
2793 /**
2794 * Update the original XML on main invoice to set the content of the nodes UID and Mark, because
2795 * they were removed at runtime before the transmission after the myDATA update (Jan 2026).
2796 *
2797 * @since 1.18.6 (J) - 1.8.6 (WP)
2798 */
2799 if ($invoice_uid && $invoice_mark) {
2800 // load XML e-invoice
2801 $dom = new DOMDocument();
2802 $dom->preserveWhiteSpace = false;
2803 $dom->formatOutput = true;
2804 $dom->loadXML($correlated_einv_final_xml);
2805
2806 // locate the parent <invoice> node
2807 $invoiceNode = $dom->getElementsByTagName('invoice')->item(0);
2808
2809 // locate the <issuer> node
2810 $issuerNode = $invoiceNode->getElementsByTagName('issuer')->item(0);
2811
2812 // create the two element nodes that should be added
2813 $uidNode = $dom->createElement('uid', $invoice_uid);
2814 $markNode = $dom->createElement('mark', $invoice_mark);
2815
2816 // take care of the mark node first, because it will go after uid
2817 $invoiceNode->insertBefore($markNode, $issuerNode);
2818
2819 // take care of the uid node, by placing it before the mark node
2820 $invoiceNode->insertBefore($uidNode, $markNode);
2821
2822 // update the final XML content before it gets saved
2823 $correlated_einv_final_xml = $dom->saveXML();
2824 }
2825
2826 // prepare data transmission values to be updated
2827 $config_param_name = $this->getCorrelatedInvoiceParamName($extras['einvid'], $extras['bid']);
2828 $env_fee_data['xml'] = $correlated_einv_final_xml;
2829 $env_fee_data['transmission'] = [
2830 'ts' => time(),
2831 'uid' => $invoice_uid,
2832 'mark' => $invoice_mark,
2833 'qrurl' => $invoice_qrcode,
2834 'qrcode_img' => '',
2835 'pdf' => '',
2836 ];
2837
2838 if ($invoice_qrcode) {
2839 // generate the QR Code for the environmental fee invoice as well
2840
2841 // the QR Code PNG file name
2842 $filename = "aade_qrcode_env_{$extras['bid']}_{$extras['einvid']}.png";
2843
2844 if ($this->generateQRCodeImage($invoice_qrcode, VikBookingMydataAadeConstants::getQRCodeBase('path', $filename))) {
2845 // set the QR Code image property
2846 $env_fee_data['transmission']['qrcode_img'] = $filename;
2847 }
2848 }
2849
2850 // immediately update data transmission values before generating the PDF invoice
2851 VBOFactory::getConfig()->set($config_param_name, $env_fee_data);
2852
2853 // generate the PDF (courtesy) for the correlated invoice (will set the PDF path in case of success)
2854 if ($this->generateAnalogicEnvFeeInvoice($env_fee_data)) {
2855 // update data transmission values again, as they will contain the path to the PDF file
2856 VBOFactory::getConfig()->set($config_param_name, $env_fee_data);
2857 }
2858
2859 return true;
2860 }
2861
2862 // at this point we expect an error
2863 if (!isset($res_obj->response->errors) || !isset($res_obj->response->errors->error)) {
2864 // errors should be set, but if they aren't, this is unexpected
2865 $this->setError('Unexpected nodes in XML response (missing errors or error)');
2866 $this->setError('<pre>' . htmlentities($response->body) . '</pre>');
2867 return false;
2868 }
2869
2870 // loop through the errors
2871 foreach ($res_obj->response->errors->error as $resp_err) {
2872 $err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
2873 $err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
2874 $this->setError(sprintf('Error (%s): %s', $err_code, $err_mess));
2875 }
2876
2877 return false;
2878 }
2879
2880 /**
2881 * Generates a PDF file for the correlated invoice for the environmental fee.
2882 *
2883 * @param array &$env_fee_data the raw environmental fee information data.
2884 *
2885 * @return bool
2886 *
2887 * @since 1.16.7 (J) - 1.6.7 (WP)
2888 */
2889 public function generateAnalogicEnvFeeInvoice(&$env_fee_data)
2890 {
2891 // get the customer information
2892 $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($env_fee_data['bid']);
2893
2894 // build a dummy invoice associative array with the information required
2895 $invoice = [
2896 'id' => -1,
2897 'number' => $this->getPreviousCorrelatedInvoiceData($env_fee_data['einvid'], $env_fee_data['bid']),
2898 'for_date' => strtotime($this->getPreviousCorrelatedInvoiceData($env_fee_data['einvid'], $env_fee_data['bid'], 'date')),
2899 'rawcont' => [
2900 'totalnet' => 0,
2901 'totaltax' => $env_fee_data['envfee']['fee_cost'],
2902 'totaltot' => $env_fee_data['envfee']['fee_cost'],
2903 'rows' => [
2904 [
2905 'service' => $env_fee_data['envfee']['name'],
2906 'net' => 0,
2907 'tax' => $env_fee_data['envfee']['fee_cost'],
2908 'tot' => $env_fee_data['envfee']['fee_cost'],
2909 ],
2910 ],
2911 ],
2912 'env_fee_data' => $env_fee_data,
2913 'feeseries' => 'C',
2914 ];
2915
2916 // load the custom invoice template file
2917 list($invoice_tmpl, $pdfparams) = VikBooking::loadCustomInvoiceTmpl($invoice, $customer);
2918
2919 // trigger an event to allow third-party plugins to manipulate the content of the custom invoice
2920 VBOFactory::getPlatform()->getDispatcher()->trigger('onMydataBeforeGenerateEnvFeeCourtesyInvoice', [$env_fee_data, $invoice, $invoice_tmpl]);
2921
2922 // parse the content of the template file
2923 $invoice_body = VikBooking::parseCustomInvoiceTemplate($invoice_tmpl, $invoice, $customer);
2924
2925 // reload booking details
2926 $booking_details = VikBooking::getBookingInfoFromID($env_fee_data['bid']);
2927
2928 // force the execution of the conditional text rules
2929 VikBooking::getConditionalRulesInstance()
2930 ->set(
2931 [
2932 'booking',
2933 'rooms',
2934 ],
2935 [
2936 $booking_details,
2937 VikBooking::loadOrdersRoomsData($env_fee_data['bid']),
2938 ]
2939 )
2940 ->parseTokens($invoice_body);
2941
2942 // load dependencies
2943 if (!class_exists('TCPDF')) {
2944 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
2945 }
2946 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
2947
2948 /**
2949 * Trigger event to allow third party plugins to return a specific font name.
2950 */
2951 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
2952 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
2953 $usepdffont = $custom_pdf_font[0];
2954 }
2955
2956 // write the PDF on file
2957 $pdffname = implode('_', ['envfee', $booking_details['id'], ($booking_details['sid'] ?: $booking_details['ts'])]) . '.pdf';
2958 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
2959
2960 if (is_file($pathpdf)) {
2961 @unlink($pathpdf);
2962 }
2963
2964 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
2965
2966 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
2967 $pdf->SetTitle(JText::translate('VBOINVNUM') . ' ' . $invoice['number']);
2968
2969 // header for each page of the pdf
2970 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
2971 $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]);
2972 }
2973
2974 // change some currencies to their unicode (decimal) value
2975 $currencyname = VikBooking::getCurrencyName();
2976 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
2977 if (array_key_exists($currencyname, $unichr_map)) {
2978 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
2979 }
2980
2981 // header and footer fonts
2982 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
2983 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
2984
2985 // margins
2986 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
2987 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
2988 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
2989
2990 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
2991 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
2992 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
2993
2994 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
2995 $pdf->SetPrintHeader(false);
2996 }
2997 if ($pdfparams['show_footer'] == 0) {
2998 $pdf->SetPrintFooter(false);
2999 }
3000
3001 $pdf->AddPage();
3002 $pdf->writeHTML($invoice_body, true, false, true, false, '');
3003 $pdf->lastPage();
3004 $pdf->Output($pathpdf, 'F');
3005
3006 if (!is_file($pathpdf)) {
3007 return false;
3008 }
3009
3010 if (VBOPlatformDetection::isWordPress()) {
3011 /**
3012 * @wponly - trigger files mirroring
3013 */
3014 VikBookingLoader::import('update.manager');
3015 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
3016 }
3017
3018 // set the PDF file name at last
3019 $env_fee_data['transmission']['pdf'] = $pdffname;
3020
3021 return true;
3022 }
3023
3024 /**
3025 * Returns the calculated tariffs given their IDs per room booked.
3026 *
3027 * @param array $booking the booking array with one array-room per array value
3028 *
3029 * @return array associative array of tariffs for each room booked
3030 */
3031 protected function getBookingTariffs($booking)
3032 {
3033 $tars = [];
3034
3035 $is_package = (!empty($booking[0]['pkg']));
3036
3037 foreach ($booking as $kor => $or) {
3038 $num = $kor + 1;
3039 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
3040 // package or custom cost set from the back-end does not need calculation
3041 continue;
3042 }
3043 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=".(int)$or['idtar'].";";
3044 $this->dbo->setQuery($q);
3045 $tar = $this->dbo->loadAssocList();
3046 if ($tar) {
3047 $tar = VikBooking::applySeasonsRoom($tar, $or['checkin'], $or['checkout']);
3048
3049 // apply OBP rules
3050 $tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $or, $or['adults']);
3051
3052 $tars[$num] = $tar[0];
3053 }
3054 }
3055
3056 return $tars;
3057 }
3058
3059 /**
3060 * Transmits the electronic invoices to myDATA according to the input parameters.
3061 * This is a 'driver action', and so it's called before getBookingsData()
3062 * in the view. This method will save/update records in the DB so that when
3063 * the view re-calls getBookingsData(), the information will be up to date.
3064 *
3065 * @return boolean True if at least one e-invoice was transmitted
3066 */
3067 public function transmitEInvoices()
3068 {
3069 // make sure the transmission settings are not empty
3070 $settings = $this->loadSettings();
3071 if ($settings === false || !$settings['params']) {
3072 $this->setError('Missing settings to transmit the invoices. Please set up the driver settings first.');
3073 return false;
3074 }
3075 // make sure the settings we need are not empty
3076 $required = [
3077 $settings['params']['aade_user_id'],
3078 $settings['params']['aade_subscription_key'],
3079 ];
3080 foreach ($required as $reqset) {
3081 if (empty($reqset)) {
3082 $this->setError('Invalid settings to transmit the invoices. Please make sure to provide all the information from the driver settings.');
3083 return false;
3084 }
3085 }
3086
3087 // call the main method to generate rows, cols and bookings array
3088 $this->getBookingsData();
3089
3090 if ($this->getError() || !$this->bookings) {
3091 return false;
3092 }
3093
3094 // get the driver ID
3095 $driver_id = $this->getDriverId();
3096
3097 // pool of e-invoice IDs to transmit
3098 $einvspool = [];
3099 $einvnumbs = [];
3100
3101 // electronic invoices IDs referenced to booking IDs
3102 $einvs_bids_ref = [];
3103
3104 // list of eco-fee-only invoices to be (re-)transmitted
3105 $ecofee_einvs_retn = [];
3106
3107 foreach ($this->bookings as $gbook) {
3108 // check whether this booking ID was set to be skipped from transmission
3109 $exclude = VikRequest::getInt('excludesendbid'.$gbook[0]['id'], 0, 'request');
3110 if ($exclude > 0) {
3111 // skipping this invoice from transmission
3112 continue;
3113 }
3114
3115 // make sure an electronic invoice was already issued for this booking ID by this driver
3116 if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
3117 // no e-invoices available for this booking, skipping
3118 continue;
3119 }
3120
3121 // check if an e-invoice was already sent for this booking
3122 if ($gbook[0]['einvsent'] > 0) {
3123 $resend = VikRequest::getInt('resendbid'.$gbook[0]['id'], 0, 'request');
3124 $resendecofee = VikRequest::getInt('resendecofeebid'.$gbook[0]['id'], 0, 'request');
3125 if ($resendecofee) {
3126 // push record for the eco-fee invoice re-transmit only
3127 $ecofee_einvs_retn[] = $gbook[0];
3128 continue;
3129 }
3130 if (!$resend) {
3131 // we do not re-send the invoice for this booking ID
3132 continue;
3133 }
3134 }
3135
3136 // push e-invoice ID to the pool
3137 array_push($einvspool, $gbook[0]['einvid']);
3138
3139 // push also the corresponding invoice number
3140 array_push($einvnumbs, $gbook[0]['einvnum']);
3141
3142 // set the e-invoice ID/booking ID relation
3143 $einvs_bids_ref[$gbook[0]['einvid']] = $gbook[0]['id'];
3144 }
3145
3146 if ($einvspool && $ecofee_einvs_retn) {
3147 // pre-check: conflict with re-transmission of eco-fee invoice(s) and main invoice first transmission or re-transmission
3148 $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.');
3149 return false;
3150 }
3151
3152 if (!$ecofee_einvs_retn) {
3153 // attempt to transmit or re-transmit the main invoices with their (eventually) related eco-fee invoices
3154 if (!$einvspool) {
3155 // no e-invoices generated or ready to be transmitted
3156 $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.');
3157 return false;
3158 }
3159
3160 // build one XML file for all XML e-invoices (if more than one)
3161 $einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
3162 if ($einv_xml_body === false) {
3163 // something went wrong with the creation of the XML file
3164 $this->setError('Error creating the XML file for the request. Unable to proceed.');
3165 return false;
3166 }
3167
3168 if ($this->debugging()) {
3169 // when in debug mode, the raw XML request is sent to output
3170 $this->setWarning('Raw XML request for Debug Mode');
3171 $this->setWarning('<pre> ' . htmlentities($einv_xml_body) . ' </pre>');
3172 }
3173
3174 // transmit e-invoices to myDATA
3175 $response = $this->myDATARequestPOST('SendInvoices', $einv_xml_body, $settings);
3176 if ($response->code != 200) {
3177 // the request was not successful, and the XML invoices were not parsed at all by myDATA
3178 $this->setError(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
3179 $this->setError('Could not send the invoice(s) to myDATA.');
3180 return false;
3181 }
3182
3183 if ($this->debugging()) {
3184 // when in debug mode, the raw XML response is sent to output
3185 $this->setWarning('Raw XML response for Debug Mode');
3186 $this->setWarning('<pre> ' . htmlentities($response->body) . ' </pre>');
3187 }
3188
3189 // check if the XML response contains errors, and adjust the e-invoices that succeeded
3190 list($success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls) = $this->myDATAParseXMLResponse($response->body, $einvspool, $einvnumbs);
3191
3192 if (!$success) {
3193 // some errors occurred
3194 if (!is_array($valid_einv_marks) || !$valid_einv_marks) {
3195 $this->setError('Could not send the invoice(s) to myDATA.');
3196 return false;
3197 } else {
3198 // some e-invoices were transmitted successfully
3199 $einvspool = array_keys($valid_einv_marks);
3200 }
3201 }
3202
3203 // update ProgressivoInvio driver setting by increasing it for the next run
3204 $this->updateProgressiveNumber(++$settings['progcount']);
3205
3206 // set to transmitted=1 all e-invoice IDs that were transmitted with success
3207 foreach ($einvspool as $einvid) {
3208 // find the corresponding booking ID
3209 $einv_bid = $einvs_bids_ref[$einvid] ?? 0;
3210
3211 // flag to check if the PDF invoice should be refreshed
3212 $qrcode_fname = null;
3213
3214 // prepare "transmission data" object
3215 $trans_data = new stdClass;
3216 $trans_data->invoice_uid = (isset($valid_einv_uids[$einvid]) && $einvid != $valid_einv_uids[$einvid] ? $valid_einv_uids[$einvid] : null);
3217 $trans_data->invoice_mark = (isset($valid_einv_marks[$einvid]) && $einvid != $valid_einv_marks[$einvid] ? $valid_einv_marks[$einvid] : null);
3218 $trans_data->invoice_qrcode = (isset($valid_qrcode_urls[$einvid]) && $einvid != $valid_qrcode_urls[$einvid] ? $valid_qrcode_urls[$einvid] : null);
3219 $trans_data->qrcode_img = null;
3220 $trans_data->trans_dtime = date('Y-m-d H:i:s');
3221
3222 if ($trans_data->invoice_qrcode) {
3223 /**
3224 * Attempt to generate the QR Code image file for the current invoice correctly transmitted.
3225 *
3226 * @since 1.16.7 (J) - 1.6.7 (WP)
3227 */
3228 $qrcode_fname = $this->generateInvoiceQRCode($einvid, $einv_bid, $trans_data);
3229 if ($qrcode_fname) {
3230 $trans_data->qrcode_img = $qrcode_fname;
3231 }
3232 }
3233
3234 /**
3235 * Update the original XML on main invoice to change the content of the nodes UID and Mark, even
3236 * if they will be removed at runtime before the transmission after the myDATA update (Jan 2026).
3237 *
3238 * @since 1.18.6 (J) - 1.8.6 (WP)
3239 */
3240 $updatedEinvoiceXML = null;
3241 $prev_einv_data = $this->loadEInvoiceDetails($einvid);
3242 if ($trans_data->invoice_uid && $trans_data->invoice_mark && !empty($prev_einv_data['xml'])) {
3243 // load XML e-invoice
3244 $dom = new DOMDocument();
3245 $dom->preserveWhiteSpace = false;
3246 $dom->formatOutput = true;
3247 $dom->loadXML($prev_einv_data['xml']);
3248
3249 // locate the parent <invoice> node
3250 $invoiceNode = $dom->getElementsByTagName('invoice')->item(0);
3251
3252 // locate the <uid> node
3253 $uidNode = $invoiceNode->getElementsByTagName('uid')->item(0);
3254 if ($uidNode) {
3255 // replace text content
3256 $uidNode->nodeValue = $trans_data->invoice_uid;
3257 }
3258
3259 // locate the <mark> node
3260 $markNode = $invoiceNode->getElementsByTagName('mark')->item(0);
3261 if ($markNode) {
3262 // replace text content
3263 $markNode->nodeValue = $trans_data->invoice_mark;
3264 }
3265
3266 // turn flag on for updating the XML content
3267 $updatedEinvoiceXML = $dom->saveXML();
3268 }
3269
3270 // build e-invoice object for update (with "transmission data")
3271 $data = new stdClass;
3272 $data->id = $einvid;
3273 $data->transmitted = 1;
3274 $data->trans_data = json_encode($trans_data);
3275
3276 if ($updatedEinvoiceXML) {
3277 // e-invoice XML content should be updated
3278 $data->xml = $updatedEinvoiceXML;
3279 }
3280
3281 // update e-invoice record
3282 $this->updateEInvoice($data);
3283
3284 // check if the PDF invoice requires a refresh to let the conditional text rules run after having updated the e-invoice
3285 if ($qrcode_fname) {
3286 // attempt to refresh the PDF invoice, if available, in case it uses the Conditional Text Rules
3287 if ($this->refreshPdfInvoice($einv_bid)) {
3288 $qrcode_url = VikBookingMydataAadeConstants::getQRCodeBase('uri', $qrcode_fname);
3289 // trigger an event to allow third-party plugins to do something, like sending the invoice via email
3290 VBOFactory::getPlatform()->getDispatcher()->trigger('onMydataAfterQrcodeInvoiceSubmitted', [$einv_bid, $einvid, $trans_data, $qrcode_url]);
3291 }
3292 }
3293
3294 /**
3295 * Check if an e-invoice for the environmental fee was prepared to be generated and transmitted as well.
3296 *
3297 * @since 1.16.7 (J) - 1.6.7 (WP)
3298 */
3299 $env_fee_inv_details = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einvid, $einv_bid), []);
3300 if ($env_fee_inv_details && $trans_data->invoice_mark) {
3301 /**
3302 * It is recommended to sleep at least one second after the main invoice has been
3303 * transmitted and before the correlated invoice gets transmitted to ensure the
3304 * main invoice mark is registered on the myDATA platform.
3305 */
3306 sleep(1);
3307
3308 // use the obtained invoice mark to adjust and transmit the environmental fee invoice
3309 $env_fee_tn_result = $this->transmitCorrelatedInvoice(
3310 $trans_data,
3311 $env_fee_inv_details,
3312 [
3313 'einvid' => $einvid,
3314 'bid' => $einv_bid,
3315 ]
3316 );
3317
3318 /**
3319 * Store within the main e-invoice record the transmission result and timestamp for the eco-fee invoice.
3320 * This is useful for eventually allowing to re-transmit just the eco-fee invoice in case of errors, usually
3321 * caused by a premature transmission of the eco-fee invoice before the main invoice mark is registered.
3322 *
3323 * @since 1.18.3 (J) - 1.8.3 (WP)
3324 */
3325 if (is_object($data ?? null) && is_object($trans_data ?? null)) {
3326 // set the eco-fee invoice transmission result within the main invoice transaction data object
3327 $trans_data->env_fee_tn_res = (int) $env_fee_tn_result;
3328 $trans_data->env_fee_tn_ts = time();
3329
3330 // update object property
3331 $data->trans_data = json_encode($trans_data);
3332
3333 // update main e-invoice record
3334 $this->updateEInvoice($data);
3335 }
3336 }
3337 }
3338
3339 // display info message
3340 $this->setInfo('Electronic invoices transmitted: ' . count($einvspool));
3341 } else {
3342 /**
3343 * Retry to (re-)transmit just the selected eco-fee invoice(s).
3344 *
3345 * @since 1.18.3 (J) - 1.8.3 (WP)
3346 */
3347 $eco_fee_invs_retn_attempts = 0;
3348 $eco_fee_invs_retn_success = 0;
3349 foreach ($ecofee_einvs_retn as $ecofee_einv_data) {
3350 // get the eco-fee e-invoice details
3351 $env_fee_inv_details = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($ecofee_einv_data['einvid'], $ecofee_einv_data['id']), []);
3352 // get the main e-invoice transaction data
3353 $trans_data = $ecofee_einv_data['einvtndata'] ?? null;
3354 if ($env_fee_inv_details && is_array($trans_data) && ($trans_data['invoice_mark'] ?? null)) {
3355 // increase global retry counter
3356 $eco_fee_invs_retn_attempts++;
3357
3358 // use the previous invoice mark to adjust and transmit the environmental fee invoice
3359 $env_fee_tn_result = $this->transmitCorrelatedInvoice(
3360 $trans_data,
3361 $env_fee_inv_details,
3362 [
3363 'einvid' => $ecofee_einv_data['einvid'],
3364 'bid' => $ecofee_einv_data['id'],
3365 ]
3366 );
3367
3368 if ($env_fee_tn_result) {
3369 // increase success counter
3370 $eco_fee_invs_retn_success++;
3371 }
3372
3373 // always update the transmission result for the eco-fee invoice within the main invoice transaction data object
3374 $trans_data['env_fee_tn_res'] = (int) $env_fee_tn_result;
3375 $trans_data['env_fee_tn_ts'] = time();
3376
3377 // build the object record for update
3378 $main_record = new stdClass;
3379 $main_record->id = $ecofee_einv_data['einvid'];
3380 $main_record->trans_data = json_encode($trans_data);
3381
3382 // update main e-invoice record
3383 $this->updateEInvoice($main_record);
3384 }
3385 }
3386
3387 // display info message
3388 $this->setInfo('Electronic invoices (eco-fee) re-transmitted: ' . $eco_fee_invs_retn_attempts . ' - Success counter: ' . $eco_fee_invs_retn_success);
3389 }
3390
3391 // we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
3392 $this->bookings = [];
3393
3394 // unset also cols, rows and footer row to not merge data
3395 $this->cols = [];
3396 $this->rows = [];
3397 $this->footerRow = [];
3398
3399 return true;
3400 }
3401
3402 /**
3403 * Generates one XML string for the request body to myDATA. If more
3404 * than one e-invoice ID passed, attempts to parse all XML files for
3405 * the already generated e-invoices in order to compose one single
3406 * XML request body that contains all invoices. Every e-invoice has
3407 * got an XML file compliant for the transmission, but when we need
3408 * to transmit in mass multiple e-invoices, we try to use just one
3409 * HTTP request by merging all e-invoices into one single XML file.
3410 *
3411 * @param array $einvspool an array of e-invoice IDs
3412 * @param array $settings the driver settings
3413 *
3414 * @return bool|string false on failure or XML request body string.
3415 */
3416 protected function buildTransmissionXMLBody($einvspool, $settings)
3417 {
3418 if (!is_array($einvspool) || !$einvspool) {
3419 return false;
3420 }
3421
3422 // the list of XML strings
3423 $xml_strings = [];
3424
3425 // generate XML files for the requested e-invoice IDs
3426 foreach ($einvspool as $einvid) {
3427 // load e-invoice details
3428 $einv_data = $this->loadEInvoiceDetails($einvid);
3429 if (!$einv_data || !is_array($einv_data) || empty($einv_data['xml'])) {
3430 // all e-invoices must exist as they will be set to transmitted=1 so we break the process
3431 $this->setError('Unable to load data for the electronic invoice ID ' . $einvid);
3432 return false;
3433 }
3434
3435 /**
3436 * Customers have reported an update to myDATA that no longer accepts the XML nodes
3437 * <uid> and <mark> (right under the node <invoice>) otherwise errors with code 273
3438 * will be raised stating that such details will be generated and provided by myDATA.
3439 *
3440 * @since 1.18.6 (J) - 1.8.6 (WP)
3441 */
3442 $xmlObj = simplexml_load_string($einv_data['xml']);
3443 if ($xmlObj->invoice->uid ?? null) {
3444 // delete nodes
3445 unset($xmlObj->invoice->uid, $xmlObj->invoice->mark);
3446
3447 // re-build XML string
3448 $einv_data['xml'] = $xmlObj->asXML();
3449 }
3450
3451 // push e-invoice content
3452 $xml_strings[] = $einv_data['xml'];
3453 }
3454
3455 if (!$xml_strings) {
3456 // no XML files created, break the process
3457 return false;
3458 }
3459
3460 if (count($xml_strings) === 1) {
3461 // just one XML file, no need to build an XML container
3462 return $xml_strings[0];
3463 }
3464
3465 // return one whole XML request body for all e-invoices
3466 return $this->mergeXMLInvoices($xml_strings);
3467 }
3468
3469 /**
3470 * Given a list of XML e-invoice strings, attempts to merge them
3471 * into one single XML to avoid making one HTTP request per e-invoice.
3472 *
3473 * @param array $xml_strings list of XML strings for each e-invoice.
3474 *
3475 * @return bool|string false or whole XML string for all e-invoices.
3476 */
3477 protected function mergeXMLInvoices($xml_strings)
3478 {
3479 if (!is_array($xml_strings) || !$xml_strings) {
3480 return false;
3481 }
3482
3483 if (count($xml_strings) === 1) {
3484 return $xml_strings[0];
3485 }
3486
3487 if (!class_exists('SimpleXMLElement')) {
3488 /**
3489 * We cannot afford to do a string manipulation only because SimpleXMLElement
3490 * is missing on the server. It has to be available, it's a native library.
3491 */
3492 $this->setError('SimpleXMLElement is missing on your server.');
3493 $this->setError('This is unusual, and you should contact your hosting company to enable this native PHP library.');
3494 $this->setError('You can only transmit single e-invoices, not more than one because SimpleXMLElement is missing');
3495
3496 return false;
3497 }
3498
3499 /**
3500 * Define the XML root element for the InvoicesDoc message.
3501 * Namespace attributes will affect the incomeClassification sub nodes.
3502 *
3503 * @see VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
3504 * @see https://mydata-dev.portal.azure-api.net/issues/5f3c411ac75730207831ead4
3505 */
3506 $root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
3507 $xml_root = <<<XML
3508 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
3509 <InvoicesDoc $root_namespaces>
3510 </InvoicesDoc>
3511 XML;
3512 // get the SimpleXMLElement object
3513 $xml = new SimpleXMLElement($xml_root);
3514
3515 // define the namespace rules for the children elements of <incomeClassification>
3516 $child_nmspaces = [
3517 'incomeClassification' => VikBookingMydataAadeConstants::getInvoiceChildrenNamespace()
3518 ];
3519
3520 // parse all e-invoices
3521 $parsed = 0;
3522 foreach ($xml_strings as $k => $einvoice) {
3523 $xml_einvoice = simplexml_load_string($einvoice);
3524 if (!is_object($xml_einvoice)) {
3525 $this->setWarning('Unable to parse the XML of the e-invoice index ' . ($k + 1));
3526 $this->setWarning($this->libxml_display_errors());
3527 continue;
3528 }
3529 // append XML tree to a new <invoice> node
3530 $invoice_node = $xml->addChild('invoice');
3531 $this->simpleXmlAppendTree($invoice_node, $xml_einvoice->invoice, $child_nmspaces);
3532 // increase parsed invoices
3533 $parsed++;
3534 }
3535
3536 if (!$parsed) {
3537 $this->setError('No e-invoices could be parsed to merge the XML trees and related nodes into one single XML body.');
3538 return false;
3539 }
3540
3541 // get the whole XML request body just built from all e-invoices
3542 $full_xml = $xml->asXML();
3543
3544 /**
3545 * When appending child nodes with namespaces to "<incomeClassification>", these may be added as
3546 * "<N1:classificationCategory xmlns:N1="N1">category1_3</N1:classificationCategory>" so with both
3547 * the proper namespace in the node name, but also with the attribute 'xmlns:N1="N1"' which is making
3548 * the whole XML failing according to the schema. Therefore, we manipulate the string to remove such attributes.
3549 */
3550 if (!empty($child_nmspaces['incomeClassification'])) {
3551 $seek_pattern = $child_nmspaces['incomeClassification'];
3552 $full_xml = str_replace('xmlns:' . $seek_pattern . '="' . $seek_pattern . '"', '', $full_xml);
3553 }
3554
3555 // attempt to properly format the XML string
3556 $this->formatXmlString($full_xml);
3557
3558 // return the whole XML request body containing all the e-invoices
3559 return $full_xml;
3560 }
3561
3562 /**
3563 * Recursive method to append a SimpleXMLElement tree node, and
3564 * related children nodes, to another SimpleXMLElement. Used to
3565 * dinamically add an entire tree of a single e-invoice XML file
3566 * under a single node <invoice> of the whole XML request body.
3567 *
3568 * @param SimpleXMLElement $xml_to the node where the tree will be appended.
3569 * @param SimpleXMLElement $xml_from the element to append with all its children.
3570 * @param array $child_nmspaces associative list of children namespaces.
3571 *
3572 * @return void
3573 */
3574 protected function simpleXmlAppendTree(&$xml_to, &$xml_from, $child_nmspaces = [])
3575 {
3576 $child_nmspace = null;
3577 $child_isprefix = false;
3578
3579 $node_name = $xml_to->getName();
3580 if (!empty($child_nmspaces[$node_name])) {
3581 $child_nmspace = $child_nmspaces[$node_name];
3582 $child_isprefix = true;
3583 }
3584
3585 foreach ($xml_from->children($child_nmspace, $child_isprefix) as $xml_child) {
3586 $add_node_name = $xml_child->getName();
3587 if (!empty($child_nmspace)) {
3588 $add_node_name = "$child_nmspace:$add_node_name";
3589 }
3590 $xml_temp = $xml_to->addChild($add_node_name, (string)$xml_child, $child_nmspace);
3591 foreach ($xml_child->attributes() as $attr_key => $attr_value) {
3592 $xml_temp->addAttribute($attr_key, $attr_value);
3593 }
3594 $this->simpleXmlAppendTree($xml_temp, $xml_child, $child_nmspaces);
3595 }
3596 }
3597
3598 /**
3599 * Loads the details of the given e-invoice ID. The given ID should not be obliterated.
3600 *
3601 * @param int $einvid the ID of the e-invoice
3602 *
3603 * @return mixed array if the e-invoice exists and is not obliterated, false otherwise.
3604 */
3605 protected function loadEInvoiceDetails($einvid)
3606 {
3607 if (empty($einvid)) {
3608 return false;
3609 }
3610
3611 $q = "SELECT * FROM `#__vikbooking_einvoicing_data` WHERE `id`=" . (int)$einvid . " AND `obliterated`=0;";
3612 $this->dbo->setQuery($q);
3613 $einv = $this->dbo->loadAssoc();
3614
3615 return $einv ? $einv : false;
3616 }
3617
3618 /**
3619 * Performs a POST request to the myDATA infrastructure.
3620 *
3621 * @param string $url_path the path to append to the base endpoint URI.
3622 * @param mixed $body the request body.
3623 * @param array $settings driver settings or any other option to inject.
3624 *
3625 * @return JHttpResponse object with code and body properties
3626 */
3627 protected function myDATARequestPOST($url_path = '', $body = null, $settings = [])
3628 {
3629 if (empty($settings)) {
3630 $settings = $this->loadSettings();
3631 }
3632
3633 $aade_user_id = $settings['params']['aade_user_id'];
3634 $aade_subscription_key = $settings['params']['aade_subscription_key'];
3635 $aade_endp_url = $settings['params']['mydata_endpoint_url'];
3636 if (!empty($settings['params']['test_mode'])) {
3637 $aade_endp_url = VikBookingMydataAadeConstants::getDevEndpointBaseUrl();
3638 }
3639
3640 if (!empty($url_path)) {
3641 $aade_endp_url .= ltrim($url_path, '/');
3642 }
3643
3644 // build request headers
3645 $headers = [
3646 'Content-Type' => 'application/xml',
3647 'aade-user-id' => $aade_user_id,
3648 'Ocp-Apim-Subscription-Key' => $aade_subscription_key,
3649 ];
3650
3651 // invoke CMS native transporter
3652 $transporter = new JHttp;
3653 $response = $transporter->post($aade_endp_url, $body, $headers);
3654
3655 if ($response->code != 200) {
3656 $this->setError('Erroneous response with HTTP code ' . $response->code);
3657 $this->setError(htmlentities($response->body));
3658 }
3659
3660 return $response;
3661 }
3662
3663 /**
3664 * Checks if the XML response string from myDATA contains errors.
3665 * Returns an array with boolean "success" and array with "einv_id => einv_mark".
3666 *
3667 * @param string $body the raw response body from the request.
3668 * @param array $einvspool list of e-invoice ids in VBO just transmitted.
3669 * @param array $einvnumbs list of e-invoice numbers in VBO just transmitted.
3670 *
3671 * @return array to be used with list($success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls).
3672 */
3673 protected function myDATAParseXMLResponse($body, $einvspool = [], $einvnumbs = [])
3674 {
3675 // the default information to return
3676 $success = false;
3677 $valid_einv_marks = [];
3678 $valid_einv_uids = [];
3679 $valid_qrcode_urls = [];
3680
3681 $res_obj = $body;
3682 if (!is_object($res_obj)) {
3683 $res_obj = simplexml_load_string($body);
3684 }
3685
3686 if (!is_object($res_obj) || !isset($res_obj->response)) {
3687 $this->setError('Could not parse XML response');
3688 $this->setError('<pre>' . htmlentities($body) . '</pre>');
3689 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3690 }
3691
3692 // errors counter
3693 $errors_found = 0;
3694
3695 // loop through each response node
3696 foreach ($res_obj->response as $invoice_resp) {
3697 if (!isset($invoice_resp->statusCode)) {
3698 $this->setError('Unexpected nodes in XML response (missing statusCode)');
3699 $this->setError('<pre>' . htmlentities($body) . '</pre>');
3700 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3701 }
3702
3703 /**
3704 * Endpoint POST /SendInvoices only (/CancelInvoice would not return this data).
3705 * Get the index of the current invoice response (line-number starts from 1).
3706 */
3707 $invoice_index = isset($invoice_resp->index) ? (int)$invoice_resp->index : 0;
3708
3709 // check if we have a successful status code for this invoice
3710 if (!strcasecmp((string)$invoice_resp->statusCode, 'Success')) {
3711 // success!
3712 if ($invoice_index > 0 && isset($einvspool[($invoice_index - 1)])) {
3713 // push successful invoice
3714 $einv_id = $einvspool[($invoice_index - 1)];
3715
3716 // get the invoice UID
3717 $invoice_uid = isset($invoice_resp->invoiceUid) ? (string)$invoice_resp->invoiceUid : $einv_id;
3718 $valid_einv_uids[$einv_id] = $invoice_uid;
3719
3720 // get the invoice mark (needed for a later cancellation)
3721 $invoice_mark = isset($invoice_resp->invoiceMark) ? (string)$invoice_resp->invoiceMark : $einv_id;
3722 $valid_einv_marks[$einv_id] = $invoice_mark;
3723
3724 /**
3725 * Check if a QRCode URL is available for the electronic invoice.
3726 * Valid property name should be "qrUrl".
3727 *
3728 * @since 1.16.7 (J) - 1.6.7 (WP)
3729 */
3730 $invoice_qrcode = isset($invoice_resp->qrUrl) ? (string)$invoice_resp->qrUrl : $einv_id;
3731 $invoice_qrcode = !isset($invoice_resp->qrUrl) && isset($invoice_resp->qrCodeUrl) ? (string)$invoice_resp->qrCodeUrl : $invoice_qrcode;
3732 $valid_qrcode_urls[$einv_id] = $invoice_qrcode;
3733 }
3734 continue;
3735 }
3736
3737 // at this point we expect an error
3738 if (!isset($invoice_resp->errors) || !isset($invoice_resp->errors->error)) {
3739 // errors should be set, but if they aren't, this is unexpected
3740 $this->setError('Unexpected nodes in XML response (missing errors or error)');
3741 $this->setError('<pre>' . htmlentities($body) . '</pre>');
3742 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3743 }
3744
3745 // loop through the errors
3746 foreach ($invoice_resp->errors->error as $resp_err) {
3747 $errors_found++;
3748 $err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
3749 $err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
3750 $inv_numb = isset($einvnumbs[($invoice_index - 1)]) ? $einvnumbs[($invoice_index - 1)] : '???';
3751 $this->setError(sprintf('Error (%s) in invoice index %d (#%s): %s', $err_code, $invoice_index, $inv_numb, $err_mess));
3752 }
3753 }
3754
3755 // if we had no errors at all, the response was successful
3756 $success = (!$errors_found);
3757
3758 return [$success, $valid_einv_marks, $valid_einv_uids, $valid_qrcode_urls];
3759 }
3760
3761 /**
3762 * Downloads the electronic invoices by storing temporary files.
3763 * This is a 'driver action', and so it's called before getBookingsData()
3764 * in the view. This method will not save/update records in the DB.
3765 *
3766 * @return void
3767 */
3768 public function downloadEInvoices()
3769 {
3770 // make sure the transmission settings are not empty
3771 $settings = $this->loadSettings();
3772 if ($settings === false || !$settings['params']) {
3773 $this->setError('Missing settings. Please set up the driver first.');
3774 return false;
3775 }
3776
3777 // call the main method to generate rows, cols and bookings array
3778 $this->getBookingsData();
3779
3780 if (strlen($this->getError()) || !$this->bookings) {
3781 return false;
3782 }
3783
3784 // pool of e-invoice IDs to download
3785 $einvspool = [];
3786
3787 foreach ($this->bookings as $gbook) {
3788 // make sure an electronic invoice was already issued for this booking ID by this driver
3789 if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
3790 // no e-invoices available for this booking, skipping
3791 continue;
3792 }
3793
3794 // push e-invoice ID to the pool
3795 array_push($einvspool, $gbook[0]['einvid']);
3796 }
3797
3798 if (!$einvspool) {
3799 // no e-invoices generated
3800 $this->setWarning('No electronic invoices can be downloaded. Please generate them first.');
3801 return false;
3802 }
3803
3804 // build one whole XML file
3805 $einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
3806 if ($einv_xml_body === false) {
3807 // something went wrong with the creation of the file to download
3808 $this->setError('Could not generate the XML file containing all the electronic invoices.');
3809 return false;
3810 }
3811
3812 // force the download of the XML string
3813 header('Content-Disposition: attachment; filename="mydata-aade-einvoices' . date('Y-m-d') . '.xml"');
3814 header("Content-Type: text/xml");
3815 header("Content-Length:" . strlen($einv_xml_body));
3816 header('Connection: close');
3817 echo $einv_xml_body;
3818
3819 exit;
3820 }
3821
3822 /**
3823 * Forces the display of an electronic invoice. This is a 'driver action', and so it's called
3824 * before getBookingsData() in the view. This method will not save/update records in the DB.
3825 * This method truncates the execution of the script to read the XML data.
3826 *
3827 * @return void
3828 */
3829 public function viewEInvoice()
3830 {
3831 $einvid = VikRequest::getInt('einvid', 0, 'request');
3832 $einv_data = $this->loadEInvoiceDetails($einvid);
3833 if (!$einv_data) {
3834 die('Missing e-invoice ID');
3835 }
3836
3837 // force the output
3838 header("Content-type:text/xml");
3839 echo $einv_data['xml'];
3840
3841 exit;
3842 }
3843
3844 /**
3845 * Removes an electonic invoice. This is a 'driver action',
3846 * and so it's called before getBookingsData() in the view.
3847 * It also removes the analogic version in PDF of the invoice.
3848 *
3849 * @return void
3850 */
3851 public function removeEInvoice()
3852 {
3853 $einvid = VikRequest::getInt('einvid', '', 'request');
3854 $einv_data = $this->loadEInvoiceDetails($einvid);
3855 if (!$einv_data) {
3856 $this->setError('Missing e-invoice ID. Unable to delete the e-invoice.');
3857 return false;
3858 }
3859
3860 // get "transmission data" (if any)
3861 $trans_data = !empty($einv_data['trans_data']) ? json_decode($einv_data['trans_data']) : null;
3862 if (is_object($trans_data) && !empty($trans_data->invoice_mark)) {
3863 /**
3864 * This invoice was transmitted before, make sure to cancel it also from myDATA.
3865 * However, the endpoint requires a "mark" value for the invoice, which could be the
3866 * invoiceMark property upon a successful submission or the number we pass to compose
3867 * the XML of the electronic invoice (our progressive number). There are two "mark"
3868 * values, but we got errors for both, hence we don't know which one to use. We always
3869 * check if $trans_data->invoice_mark is not empty so that we know the invoice was
3870 * already transmitted before to myDATA and accepted.
3871 *
3872 * @todo what's the right invoice mark? the "number" is inside the XML that we generate
3873 * even before the transmission, while "invoice_mark" is returned in the myDATA response.
3874 */
3875 $mydata_invoice_mark = $einv_data['number'];
3876 $mydata_invoice_mark = $trans_data->invoice_mark;
3877
3878 /**
3879 * Check if a correlated invoice was transmitted, because it should be removed first.
3880 *
3881 * @since 1.16.7 (J) - 1.6.7 (WP)
3882 */
3883 $correlated_inv_data_tn = $this->getPreviousCorrelatedInvoiceData($einv_data['id'], $einv_data['idorder'], 'transmission');
3884 if ($correlated_inv_data_tn && !empty($correlated_inv_data_tn['mark'])) {
3885 // delete the correlated invoice first, by making the POST request
3886 $response = $this->myDATARequestPOST('CancelInvoice?mark=' . $correlated_inv_data_tn['mark']);
3887 if ($response->code != 200) {
3888 // the request was not successful
3889 $this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
3890 $this->setWarning('Could not cancel the correlated invoice from myDATA with mark ' . $correlated_inv_data_tn['mark']);
3891 } else {
3892 // check the XML response
3893 $xml_result = $this->myDATAParseXMLResponse($response->body);
3894 if ($xml_result[0]) {
3895 // success! the correlated invoice was cancelled from myDATA
3896 $this->setInfo(sprintf('Correlated invoice mark %s successfully cancelled from myDATA', $correlated_inv_data_tn['mark']));
3897
3898 /**
3899 * IMPORTANT: sleep for 2 seconds, or deleting immediately the main invoice below may
3900 * result into an error like "Error (255) in invoice index 0 (#???): Invoice with MARK 400001924172498
3901 * cannot be cancelled because it is connected with active invoice with MARK 400001924172499"
3902 */
3903 sleep(2);
3904 }
3905 }
3906
3907 // always delete the record for the correlated invoice
3908 VBOFactory::getConfig()->remove($this->getCorrelatedInvoiceParamName($einv_data['id'], $einv_data['idorder']));
3909
3910 // check if the PDF version of the environmental fee exists
3911 if (!empty($correlated_inv_data_tn['pdf'])) {
3912 $envfee_pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $correlated_inv_data_tn['pdf'];
3913 if (is_file($envfee_pathpdf)) {
3914 @unlink($envfee_pathpdf);
3915 }
3916 }
3917 }
3918
3919 // make the POST request
3920 $response = $this->myDATARequestPOST('CancelInvoice?mark=' . $mydata_invoice_mark);
3921 if ($response->code != 200) {
3922 // the request was not successful, and the XML invoices were not parsed at all by myDATA
3923 $this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
3924 $this->setWarning('Could not cancel the invoice from myDATA.');
3925 } else {
3926 // check the XML response
3927 $xml_result = $this->myDATAParseXMLResponse($response->body);
3928 if ($xml_result[0]) {
3929 // success! the invoice was cancelled from myDATA
3930 $this->setInfo(sprintf('Invoice mark %s (#%s) successfully cancelled from myDATA', $trans_data->invoice_mark, $einv_data['number']));
3931 }
3932 }
3933 }
3934
3935 // remove e-invoice
3936 $q = "DELETE FROM `#__vikbooking_einvoicing_data` WHERE `id`=".$einv_data['id'].";";
3937 $this->dbo->setQuery($q);
3938 $this->dbo->execute();
3939
3940 // remove analogic invoice for this booking
3941 $pdfremoved = false;
3942 if (!empty($einv_data['idorder'])) {
3943 $pdfname = '';
3944 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
3945 $this->dbo->setQuery($q);
3946 $analogic = $this->dbo->loadAssoc();
3947 if ($analogic) {
3948 $pdfname = $analogic['file_name'];
3949 $q = "DELETE FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
3950 $this->dbo->setQuery($q);
3951 $this->dbo->execute();
3952
3953 }
3954 $pdfpath = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . $pdfname;
3955 if (!empty($pdfname) && is_file($pdfpath)) {
3956 $pdfremoved = true;
3957 @unlink($pdfpath);
3958 }
3959 }
3960
3961 $this->setInfo(($pdfremoved ? 'Electronic and PDF invoices deleted' : 'Electronic invoice deleted'));
3962 }
3963
3964 /**
3965 * Attempts to generate a QR Code PNG image file with the e-invoice URL.
3966 *
3967 * @param int $einv_id the generated e-invoice record ID.
3968 * @param int $bid the reservation record ID.
3969 * @param object $data transaction data object with myDATA values.
3970 *
3971 * @return string empty string in case of failure, or generated QR Code file name.
3972 *
3973 * @since 1.16.7 (J) - 1.6.7 (WP)
3974 */
3975 protected function generateInvoiceQRCode($einv_id, $bid, $data)
3976 {
3977 if (!is_object($data) || empty($data->invoice_qrcode)) {
3978 return '';
3979 }
3980
3981 // the QR Code PNG file name
3982 $filename = "aade_qrcode_{$bid}_{$einv_id}.png";
3983
3984 // generate the image
3985 if ($this->generateQRCodeImage($data->invoice_qrcode, VikBookingMydataAadeConstants::getQRCodeBase('path', $filename))) {
3986 // file was written successfully
3987 return $filename;
3988 }
3989
3990 // an error has occurred
3991 return '';
3992 }
3993
3994 /**
3995 * Generates a QR Code image with the given URL in the given path.
3996 *
3997 * @param string $url the URL to be assigned (content) to the QR Code.
3998 * @param string $path the full path where the file should be saved.
3999 *
4000 * @return bool
4001 */
4002 protected function generateQRCodeImage($url, $path)
4003 {
4004 try {
4005 // require the TCPDF 2D Barcode library
4006 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf_barcodes_2d.php';
4007
4008 // set the barcode content and type
4009 $barCode = new TCPDF2DBarcode($url, 'QRCODE,H');
4010
4011 // generate the QR code as PNG image
4012 $qr = $barCode->getBarcodePngData(
4013 VikBookingMydataAadeConstants::QRCODE_PNG_WIDTH,
4014 VikBookingMydataAadeConstants::QRCODE_PNG_HEIGHT,
4015 explode(',', preg_replace("/[^0-9\.\,]/", '', VikBookingMydataAadeConstants::QRCODE_PNG_COLOR_RGB))
4016 );
4017
4018 // write the image on disk
4019 return (bool)JFile::write($path, $qr);
4020 } catch (Throwable $t) {
4021 // do nothing
4022 }
4023
4024 return false;
4025 }
4026
4027 /**
4028 * Right after obtaining a QR Code for an electronic invoice, the driver
4029 * calls this method to refresh the PDF (courtesy) invoice so that any
4030 * conditional text rule used on the invoice template file will run correctly.
4031 *
4032 * @param int $bid the reservation record ID for which the invoice should be refreshed.
4033 *
4034 * @return bool
4035 *
4036 * @since 1.16.7 (J) - 1.6.7 (WP)
4037 */
4038 protected function refreshPdfInvoice($bid)
4039 {
4040 return (bool)VikBooking::generateBookingInvoice(
4041 // load booking details
4042 VikBooking::getBookingInfoFromID($bid),
4043 // do not set an invoice number, because the previous one must be used
4044 $invoice_num = 0,
4045 // do not set an invoice suffix, because the previous one must be used
4046 $invoice_suff = '',
4047 // do not set an invoice date, because the previous one must be used
4048 $invoice_date = '',
4049 // company information will be re-fetched
4050 $company_info = '',
4051 // translation is not needed
4052 $translate = false,
4053 // set the argument to request a re-generation (refresh) of the existing invoice
4054 $refresh_pdf = true
4055 );
4056 }
4057
4058 /**
4059 * Validates the XML against the Schema.
4060 *
4061 * @param string $xml the xml string to validate
4062 *
4063 * @return null|boolean
4064 */
4065 protected function validateXmlAgainstSchema($xml) {
4066 if (!class_exists('DOMDocument')) {
4067 // we cannot validate the XML because DOMDocument is missing
4068 return null;
4069 }
4070
4071 $schema_path = VikBookingMydataAadeConstants::getSchemaPath();
4072
4073 libxml_use_internal_errors(true);
4074
4075 $dom = new DOMDocument();
4076 $dom->load($xml);
4077 if (!$dom->schemaValidate($schema_path)) {
4078 $this->setWarning('The schema validation of the electronic XML invoice returned errors, but they may be related to an unreadable schema.');
4079 $this->setWarning($this->libxml_display_errors());
4080 return false;
4081 }
4082
4083 return true;
4084 }
4085
4086 /**
4087 * Formats the XML errors occurred
4088 *
4089 * @return string the error string
4090 */
4091 protected function libxml_display_errors() {
4092 $errorstr = "";
4093 $errors = libxml_get_errors();
4094 foreach ($errors as $error) {
4095 $errorstr .= $this->libxml_display_error($error);
4096 }
4097 libxml_clear_errors();
4098
4099 return $errorstr;
4100 }
4101
4102 /**
4103 * Explanation of the XML error
4104 *
4105 * @param object $error the libxml error object
4106 *
4107 * @return string the explained error occurred
4108 */
4109 protected function libxml_display_error($error) {
4110 $return = "\n";
4111 switch ($error->level) {
4112 case LIBXML_ERR_WARNING :
4113 $return .= "Warning ".$error->code.": ";
4114 break;
4115 case LIBXML_ERR_ERROR :
4116 $return .= "Error ".$error->code.": ";
4117 break;
4118 case LIBXML_ERR_FATAL :
4119 $return .= "Fatal Error ".$error->code.": ";
4120 break;
4121 }
4122 $return .= trim($error->message);
4123 if ($error->file) {
4124 $return .= " in " . $error->file;
4125 }
4126 $return .= " on line " . $error->line . "\n";
4127
4128 return $return;
4129 }
4130
4131 /**
4132 * Override method to show the overlay content.
4133 * Used to display the edit form of the raw XML.
4134 * This method echoes the string to be displayed.
4135 *
4136 * @return void
4137 */
4138 public function printOverlayContent()
4139 {
4140 $content = VikRequest::getString('drivercontent', '', 'request');
4141 $einvid = VikRequest::getInt('einvid', 0, 'request');
4142 $envfeebid = VikRequest::getInt('envfeebid', 0, 'request');
4143
4144 if ($content == 'editEInvoice' && !empty($einvid)) {
4145 $einv_data = $this->loadEInvoiceDetails($einvid);
4146 if (!$einv_data) {
4147 return;
4148 }
4149
4150 if ($envfeebid) {
4151 $correlated_invoice = $this->getPreviousCorrelatedInvoiceData($einvid, $envfeebid, $type = 'record');
4152 if ($correlated_invoice) {
4153 $einv_data['correlated_invoice'] = $correlated_invoice;
4154 }
4155 }
4156
4157 // path to edit invoice layout file
4158 $fpath = $this->driverHelperPath . 'editeinvoice.php';
4159
4160 // load helper file and echo its content
4161 echo $this->loadHelperFile($fpath, $einv_data);
4162
4163 return;
4164 }
4165 }
4166
4167 /**
4168 * Updates the XML of an electonic invoice. This is a 'driver action',
4169 * and so it's called before getBookingsData() in the view.
4170 *
4171 * @return bool
4172 */
4173 public function updateXmlEInvoice()
4174 {
4175 $einvid = VikRequest::getInt('einvid', '', 'request');
4176 $newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
4177 $einv_data = $this->loadEInvoiceDetails($einvid);
4178 if (!$einv_data) {
4179 $this->setError('Invoice not found');
4180 return false;
4181 }
4182 if (empty($newxml)) {
4183 $this->setError('Empty XML content');
4184 return false;
4185 }
4186
4187 $jdate = new JDate;
4188 $data = new stdClass;
4189 $data->id = $einv_data['id'];
4190 $data->created_on = $jdate->toSql();
4191 $data->xml = $newxml;
4192
4193 return $this->updateEInvoice($data);
4194 }
4195
4196 /**
4197 * Updates the XML of a correlated electonic invoice. This is a 'driver action',
4198 * and so it's called before getBookingsData() in the view.
4199 *
4200 * @return bool
4201 *
4202 * @since 1.16.7 (J) - 1.6.7 (WP)
4203 */
4204 public function updateCorrelatedXmlEInvoice()
4205 {
4206 $einvid = VikRequest::getInt('einvid', 0, 'request');
4207 $envfeebid = VikRequest::getInt('envfeebid', 0, 'request');
4208 $newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
4209
4210 // get the invoice record
4211 $correlated_inv_raw_data = VBOFactory::getConfig()->getArray($this->getCorrelatedInvoiceParamName($einvid, $envfeebid), []);
4212
4213 if (!$correlated_inv_raw_data) {
4214 return false;
4215 }
4216
4217 // update the XML source code
4218 $correlated_inv_raw_data['xml'] = $newxml;
4219
4220 // update record
4221 VBOFactory::getConfig()->set($this->getCorrelatedInvoiceParamName($einvid, $envfeebid), $correlated_inv_raw_data);
4222
4223 return true;
4224 }
4225
4226 /**
4227 * Extracts only numbers from a given string, by optionally
4228 * stripping the current year. Useful to find an invoice number.
4229 *
4230 * @param string $str the string to look for numbers
4231 * @param boolean $stripy whether to strip the current year
4232 *
4233 * @return string either an empty string or all numbers as a concatenated string
4234 */
4235 protected function getOnlyNumbers($str, $stripy = false)
4236 {
4237 if ($stripy) {
4238 $str = str_replace(date('Y'), '', $str);
4239 }
4240
4241 preg_match_all('/\d+/', $str, $matches);
4242
4243 return implode('', $matches[0]);
4244 }
4245 }
4246