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

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

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