PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.6
VikBooking Hotel Booking Engine & PMS v1.6.6
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.6, at admin/helpers/einvoicing/drivers/mydata_aade.php

3,037 lines 115.4 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 - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. 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 * Class constructor should define the name of the driver and
57 * other vars. Call the parent constructor to define the DB object.
58 */
59 function __construct()
60 {
61 $this->driverFile = basename(__FILE__, '.php');
62 $this->driverName = "myDATA - ΑΑΔΕ Greece";
63 $this->driverFilters = array();
64 $this->driverButtons = array();
65
66 // driver helper dir path
67 $this->driverHelperPath = dirname(__FILE__) . DIRECTORY_SEPARATOR . str_replace(' ', '', ucwords(str_replace('_', ' ', $this->driverFile))) . DIRECTORY_SEPARATOR;
68
69 // this driver has settings
70 $this->hasSettings = true;
71
72 // reset session filters
73 $this->sessionFilters = array();
74
75 // reset bookings array
76 $this->bookings = array();
77
78 $this->cols = array();
79 $this->rows = array();
80 $this->footerRow = array();
81
82 // require class constants
83 $this->importHelper($this->driverHelperPath . 'constants.php');
84
85 parent::__construct();
86 }
87
88 /**
89 * Returns the name of this file without .php.
90 *
91 * @return string
92 */
93 public function getFileName()
94 {
95 return $this->driverFile;
96 }
97
98 /**
99 * Returns the name of this driver.
100 *
101 * @return string
102 */
103 public function getName()
104 {
105 return $this->driverName;
106 }
107
108 /**
109 * Returns the filters of this driver.
110 *
111 * @return array
112 */
113 public function getFilters()
114 {
115 if (count($this->driverFilters)) {
116 // do not run this method twice, as it could load JS and CSS files.
117 return $this->driverFilters;
118 }
119
120 // session filters
121 $sessfilters = $this->loadSessionFilters();
122
123 // get VBO Application Object
124 $vbo_app = VikBooking::getVboApplication();
125
126 // load the jQuery UI Datepicker
127 $this->loadDatePicker();
128
129 // date format
130 $df = $this->getDateFormat();
131
132 // request variables
133 $pfromdate = VikRequest::getString('fromdate', '', 'request');
134 $ptodate = VikRequest::getString('todate', '', 'request');
135 $peinvtype = VikRequest::getInt('einvtype', 0, 'request');
136 $peinvkword = VikRequest::getString('einvkword', '', 'request');
137 $pdatetype = VikRequest::getString('datetype', $this->getSessionFilter('datetype', ''), 'request');
138
139 // js lang vars
140 JText::script('VBDELCONFIRM');
141
142 // From Date Filter
143 $filter_opt = array(
144 'label' => '<label for="fromdate">'.JText::translate('VBOREPORTSDATEFROM').'</label>',
145 'html' => '<input type="text" id="fromdate" name="fromdate" value="" class="vbo-einvoicing-datepicker vbo-einvoicing-datepicker-from" size="12" autocomplete="off" />',
146 'type' => 'calendar',
147 'name' => 'fromdate'
148 );
149 array_push($this->driverFilters, $filter_opt);
150
151 // To Date Filter
152 $filter_opt = array(
153 'label' => '<label for="todate">'.JText::translate('VBOREPORTSDATETO').'</label>',
154 'html' => '<input type="text" id="todate" name="todate" value="" class="vbo-einvoicing-datepicker vbo-einvoicing-datepicker-to" size="12" autocomplete="off" />',
155 'type' => 'calendar',
156 'name' => 'todate'
157 );
158 array_push($this->driverFilters, $filter_opt);
159
160 // jQuery code for the datepicker calendars and other events
161 if (empty($pfromdate) && empty($ptodate)) {
162 // if both request values are empty, take them from the session
163 $pfromdate = $this->getSessionFilter('fromdate');
164 $ptodate = $this->getSessionFilter('todate');
165 }
166 $js = '
167 jQuery(function() {
168 jQuery(".vbo-einvoicing-datepicker:input").datepicker({
169 maxDate: "+1y",
170 dateFormat: "'.$this->getDateFormat('jui').'",
171 onSelect: vboEInvoicingCheckDates
172 });
173 '.(!empty($pfromdate) && empty($peinvkword) ? 'jQuery(".vbo-einvoicing-datepicker-from").datepicker("setDate", "'.$pfromdate.'");' : '').'
174 '.(!empty($ptodate) && empty($peinvkword) ? 'jQuery(".vbo-einvoicing-datepicker-to").datepicker("setDate", "'.$ptodate.'");' : '').'
175 jQuery("#monyear").change(function() {
176 var monopt = jQuery(this).find("option:selected");
177 if (monopt && monopt.length && monopt.val().length) {
178 var from = monopt.attr("data-from");
179 var to = monopt.attr("data-to");
180 jQuery(".vbo-einvoicing-datepicker-from").datepicker("setDate", from);
181 jQuery(".vbo-einvoicing-datepicker-to").datepicker("setDate", to);
182 jQuery("#einvkword").val("");
183 }
184 });
185 jQuery(".vbo-einvoicing-selaction").change(function() {
186 var prop = "excludebid"+jQuery(this).attr("data-bid");
187 var pobj = {};
188 var actval = parseInt(jQuery(this).val());
189 pobj[prop] = actval;
190 vboSetFilters(pobj, false);
191 if (actval > 0) {
192 // update cell data attribute for CSS to not-generate
193 jQuery(this).closest("td").attr("data-einvaction", 0);
194 } else {
195 // update cell data attribute for CSS to generate
196 jQuery(this).closest("td").attr("data-einvaction", 1);
197 }
198 });
199 jQuery(".vbo-einvoicing-existaction").change(function() {
200 var prop = "regeneratebid"+jQuery(this).attr("data-bid");
201 var propexcl = "excludesendbid"+jQuery(this).attr("data-bid");
202 var einvid = parseInt(jQuery(this).val());
203 var pobj = {};
204 if (einvid > 0) {
205 // update cell data attribute for CSS to generate
206 jQuery(this).closest("td").attr("data-einvaction", 1);
207 // set re-generate and exclude send
208 pobj[prop] = einvid;
209 pobj[propexcl] = 1;
210 } else {
211 if (einvid < 0) {
212 // update cell data attribute for CSS to not-transmit
213 jQuery(this).closest("td").attr("data-einvaction", 0);
214 // set exclude send and not re-generate
215 pobj[prop] = 0;
216 pobj[propexcl] = 1;
217 } else {
218 // update cell data attribute for CSS to transmit (value = 0)
219 jQuery(this).closest("td").attr("data-einvaction", -2);
220 // set send and not re-generate
221 pobj[prop] = 0;
222 pobj[propexcl] = 0;
223 }
224 }
225 vboSetFilters(pobj, false);
226 });
227 jQuery(".vbo-einvoicing-sentaction").change(function() {
228 var propregen = "regeneratebid"+jQuery(this).attr("data-bid");
229 var propresend = "resendbid"+jQuery(this).attr("data-bid");
230 var curval = jQuery(this).val();
231 var splitval = curval.split("-");
232 var einvid = parseInt(splitval[0]);
233 var pobj = {};
234 if (einvid === 0) {
235 // update cell data attribute for CSS to transmitted
236 jQuery(this).closest("td").attr("data-einvaction", -1);
237 pobj[propregen] = einvid;
238 pobj[propresend] = einvid;
239 } else {
240 if (splitval[1] == "regen") {
241 // update cell data attribute for CSS to generate
242 jQuery(this).closest("td").attr("data-einvaction", 1);
243 pobj[propregen] = einvid;
244 pobj[propresend] = 0;
245 } else if (splitval[1] == "resend") {
246 // update cell data attribute for CSS to transmitted
247 jQuery(this).closest("td").attr("data-einvaction", -1);
248 pobj[propregen] = 0;
249 pobj[propresend] = einvid;
250 }
251 }
252 vboSetFilters(pobj, false);
253 });
254 jQuery(".vbo-driver-output-vieweinv").click(function() {
255 var id = jQuery(this).attr("data-einvid");
256 vboSetFilters({einvid: id}, false);
257 vboDriverDoAction("viewEInvoice", true);
258 });
259 jQuery(".vbo-driver-output-editeinv").click(function() {
260 var id = jQuery(this).attr("data-einvid");
261 vboSetFilters({drivercontent: "editEInvoice", einvid: id}, true);
262 });
263 jQuery(".vbo-driver-output-rmeinv").click(function() {
264 var id = jQuery(this).attr("data-einvid");
265 if (confirm(Joomla.JText._("VBDELCONFIRM"))) {
266 vboSetFilters({einvid: id}, false);
267 vboDriverDoAction("removeEInvoice", false);
268 }
269 });
270 });
271 function vboEInvoicingCheckDates(selectedDate, inst) {
272 if (selectedDate === null || inst === null) {
273 return;
274 }
275 jQuery("#monyear").val("");
276 jQuery("#einvkword").val("");
277 var cur_from_date = jQuery(this).val();
278 if (jQuery(this).hasClass("vbo-einvoicing-datepicker-from") && cur_from_date.length) {
279 var nowstart = jQuery(this).datepicker("getDate");
280 var nowstartdate = new Date(nowstart.getTime());
281 jQuery(".vbo-einvoicing-datepicker-to").datepicker("option", {minDate: nowstartdate});
282 }
283 }';
284 $this->setScript($js);
285
286 // month-year filter
287 $q = "SELECT MIN(`for_date`) AS `mindate`, MAX(`for_date`) AS `maxdate` FROM `#__vikbooking_einvoicing_data`;";
288 $this->dbo->setQuery($q);
289 $this->dbo->execute();
290 if ($this->dbo->getNumRows()) {
291 $minmax = $this->dbo->loadAssoc();
292 if (!empty($minmax['mindate']) && !empty($minmax['maxdate'])) {
293 $infomin = getdate(strtotime($minmax['mindate']));
294 $infomax = getdate(strtotime($minmax['maxdate']));
295 $startts = mktime(0, 0, 0, $infomin['mon'], 1, $infomin['year']);
296 $lastts = mktime(23, 59, 59, $infomax['mon'], date('t', $infomax[0]), $infomax['year']);
297 $monthys = array();
298 while ($startts < $lastts) {
299 array_push($monthys, array(
300 'mon' => $infomin['mon'],
301 'year' => $infomin['year'],
302 'from' => $startts,
303 'to' => mktime(0, 0, 0, $infomin['mon'], date('t', $infomin[0]), $infomin['year'])
304 ));
305 $startts = mktime(0, 0, 0, ($infomin['mon'] + 1), 1, $infomin['year']);
306 $infomin = getdate($startts);
307 }
308 $opts = '';
309 foreach ($monthys as $my) {
310 $dfrom = date($df, $my['from']);
311 $dto = date($df, $my['to']);
312 $selectedstat = $pfromdate == $dfrom && $ptodate == $dto ? ' selected="selected"' : '';
313 $opts .= '<option value="'.$my['from'].'" data-from="'.$dfrom.'" data-to="'.$dto.'"'.$selectedstat.'>'.$this->getMonthString($my['mon']).' '.$my['year'].'</option>';
314 }
315 $filter_opt = array(
316 'label' => '<label for="monyear">' . JText::translate('VBPVIEWRESTRICTIONSTWO') . '</label>',
317 'html' => '<select name="monyear" id="monyear"><option value=""></option>'.$opts.'</select>',
318 'type' => 'select',
319 'name' => 'monyear'
320 );
321 array_push($this->driverFilters, $filter_opt);
322 }
323 }
324
325 // date type filter
326 $filter_opt = array(
327 'label' => '<label for="datetype">' . JText::translate('VBPVIEWORDERSONE') . '</label>',
328 'html' => '<select name="datetype" id="datetype">
329 <option value="ts"'.($pdatetype == 'ts' ? ' selected="selected"' : '').'>' . JText::translate('VBRENTALORD') . '</option>
330 <option value="checkin"'.($pdatetype == 'checkin' ? ' selected="selected"' : '').'>' . JText::translate('VBPICKUPAT') . '</option>
331 <option value="checkout"'.($pdatetype == 'checkout' ? ' selected="selected"' : '').'>' . JText::translate('VBRELEASEAT') . '</option>
332 </select>',
333 'type' => 'select',
334 'name' => 'datetype'
335 );
336 array_push($this->driverFilters, $filter_opt);
337
338 // invoice type filter
339 $filter_opt = array(
340 'label' => '<label for="einvtype">Show</label>',
341 'html' => '<select name="einvtype" id="einvtype">
342 <option value="0">All reservations</option>
343 <option value="1"'.($peinvtype == 1 ? ' selected="selected"' : '').'>- To be invoiced</option>
344 <option value="-1"'.($peinvtype == -1 ? ' selected="selected"' : '').'>- To be transmitted</option>
345 <option value="-2"'.($peinvtype == -2 ? ' selected="selected"' : '').'>- Trasmitted</option>
346 </select>',
347 'type' => 'select',
348 'name' => 'einvtype'
349 );
350 array_push($this->driverFilters, $filter_opt);
351
352 // search invoice filter
353 $filter_opt = array(
354 'label' => '<label for="einvkword">' . JText::translate('VBODASHSEARCHKEYS') . '</label>',
355 '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>',
356 'type' => 'text',
357 'name' => 'einvkword'
358 );
359 array_push($this->driverFilters, $filter_opt);
360
361 return $this->driverFilters;
362 }
363
364 /**
365 * Whether there are enough filters in the session to render data when the page loads.
366 *
367 * @return boolean
368 */
369 public function hasFiltersSet()
370 {
371 return count($this->loadSessionFilters());
372 }
373
374 /**
375 * Returns the current filters saved in the session.
376 * This protected method is only used by this class.
377 *
378 * @return array
379 */
380 protected function loadSessionFilters()
381 {
382 if (count($this->sessionFilters)) {
383 return $this->sessionFilters;
384 }
385
386 $session = JFactory::getSession();
387 $sessfilters = $session->get($this->getFileName().'Filt', '');
388 $sessfilters = empty($sessfilters) || !is_array($sessfilters) ? array() : $sessfilters;
389
390 $this->sessionFilters = $sessfilters;
391
392 return $this->sessionFilters;
393 }
394
395 /**
396 * Returns the current session filter for the given name.
397 * This protected method is only used by this class.
398 *
399 * @param string the name of the filter to fetch
400 * @param mixed the default filter value if empty
401 *
402 * @return mixed the current session filter requested, or a default empty value
403 */
404 protected function getSessionFilter($name, $def = '')
405 {
406 if (isset($this->sessionFilters[$name])) {
407 return $this->sessionFilters[$name];
408 }
409
410 return $def;
411 }
412
413 /**
414 * Sets and updates the session filters.
415 *
416 * @param string the name of the filter to set
417 * @param mixed the value to set for the filter
418 *
419 * @return void
420 */
421 protected function setSessionFilter($name, $val)
422 {
423 $this->sessionFilters[$name] = $val;
424
425 // update session
426 $session = JFactory::getSession();
427 $sessfilters = $session->set($this->getFileName().'Filt', $this->sessionFilters);
428
429 return;
430 }
431
432 /**
433 * Returns the buttons for the driver actions.
434 *
435 * @return array
436 */
437 public function getButtons()
438 {
439 // generate invoices button
440 array_push($this->driverButtons, '
441 <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>
442 ');
443
444 // transmit invoices button
445 array_push($this->driverButtons, '
446 <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>
447 ');
448
449 // download invoices button
450 array_push($this->driverButtons, '
451 <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>
452 ');
453
454 return $this->driverButtons;
455 }
456
457 /**
458 * Prepares the data for saving the driver settings.
459 * Validate post vars to make sure they are correct.
460 *
461 * @return stdClass
462 */
463 protected function prepareSavingSettings()
464 {
465 $data = new stdClass;
466 $params = new stdClass;
467
468 // settings vars
469 $automatic = VikRequest::getInt('automatic', 0, 'request');
470 $progcount = VikRequest::getInt('progcount', 1, 'request');
471 $invoiceinum = VikRequest::getInt('invoiceinum', 1, 'request');
472 $invoiceinum = $invoiceinum < 1 ? 1 : $invoiceinum;
473 // we lower the next invoice num because VikBooking::getNextInvoiceNumber() returns increased by 1
474 $invoiceinum--;
475
476 $einvdttype = VikRequest::getString('einvdttype', 'today', 'request');
477 $einvexnumdt = VikRequest::getString('einvexnumdt', 'new', 'request');
478 $einvtypecode = VikRequest::getString('einvtypecode', '1.1', 'request');
479 $vat_exempt_cat = VikRequest::getString('vat_exempt_cat', '1', 'request');
480 $einv_paymethod = VikRequest::getString('einv_paymethod', '1', 'request');
481 $einv_inc_class_type = VikRequest::getString('einv_inc_class_type', '', 'request');
482 $einv_inc_class_cat = VikRequest::getString('einv_inc_class_cat', '', 'request');
483 $schema_validate = VikRequest::getInt('schema_validate', 0, 'request');
484
485 $aade_user_id = VikRequest::getString('aade_user_id', '', 'request');
486 $aade_subscription_key = VikRequest::getString('aade_subscription_key', '', 'request');
487 $test_mode = VikRequest::getInt('test_mode', 0, 'request');
488 $mydata_endpoint_url = VikRequest::getString('mydata_endpoint_url', '', 'request');
489
490 $companyname = VikRequest::getString('companyname', '', 'request');
491 $vatid = VikRequest::getString('vatid', '', 'request');
492 $country = VikRequest::getString('country', '', 'request');
493 $address = VikRequest::getString('address', '', 'request');
494 $streetnumber = VikRequest::getString('streetnumber', '', 'request');
495 $zip = VikRequest::getString('zip', '', 'request');
496 $city = VikRequest::getString('city', '', 'request');
497
498 // fields validation
499 $mandatory = array(
500 $companyname,
501 $vatid,
502 $country,
503 $address,
504 $streetnumber,
505 $zip,
506 $city,
507 $aade_user_id,
508 $aade_subscription_key,
509 );
510 foreach ($mandatory as $field) {
511 if (empty($field)) {
512 $this->setError(JText::translate('VBO_PLEASE_FILL_FIELDS'));
513 return false;
514 }
515 }
516
517 // update the global configuration setting 'invoiceinum'
518 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$this->dbo->quote((string)$invoiceinum)." WHERE `param`='invoiceinum';";
519 $this->dbo->setQuery($q);
520 $this->dbo->execute();
521
522 // build data for saving
523 $params->einvdttype = $einvdttype;
524 $params->einvexnumdt = $einvexnumdt;
525 $params->einvtypecode = $einvtypecode;
526 $params->vat_exempt_cat = $vat_exempt_cat;
527 $params->einv_paymethod = $einv_paymethod;
528 $params->einv_inc_class_type = $einv_inc_class_type;
529 $params->einv_inc_class_cat = $einv_inc_class_cat;
530 $params->schema_validate = $schema_validate;
531
532 $params->aade_user_id = $aade_user_id;
533 $params->aade_subscription_key = $aade_subscription_key;
534 $params->test_mode = $test_mode;
535 $params->mydata_endpoint_url = $mydata_endpoint_url;
536
537 $params->companyname = $companyname;
538 $params->vatid = $vatid;
539 $params->country = $country;
540 $params->address = $address;
541 $params->streetnumber = $streetnumber;
542 $params->zip = $zip;
543 $params->city = $city;
544
545 $data->driver = $this->getFileName();
546 $data->params = json_encode($params);
547 $data->automatic = $automatic;
548 $data->progcount = $progcount;
549
550 return $data;
551 }
552
553 /**
554 * Gets an array with the default settings.
555 *
556 * @return array
557 */
558 protected function getDefaultSettings()
559 {
560 return [
561 'id' => -1,
562 'driver' => $this->getFileName(),
563 'params' => array(),
564 'automatic' => 0
565 ];
566 }
567
568 /**
569 * Echoes the HTML required for the driver settings form.
570 *
571 * @return void
572 */
573 public function printSettings()
574 {
575 // load current driver settings
576 $settings = $this->loadSettings();
577 if ($settings === false) {
578 $settings = $this->getDefaultSettings();
579 /**
580 * it's the first time we run the driver, so we print a warning message
581 * with some instructions for generating the invoices and to transmit them.
582 */
583 $this->displayInstructions();
584 }
585
586 // settings layout file
587 $fpath = $this->driverHelperPath . 'settings.php';
588
589 // load helper file and echo its content
590 echo $this->loadHelperFile($fpath, $settings);
591 }
592
593 /**
594 * Sets some warning messages.
595 *
596 * @return array
597 */
598 protected function displayInstructions()
599 {
600 $this->setWarning('Driver settings not available. Make sure to save your personal myDATA information, or the data transmission will not work.');
601 $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.');
602 }
603
604 /**
605 * This method converts each booking array into a matrix with one room-booking per index.
606 * It also adds information about the customer and the invoices generated for each booking.
607 *
608 * @param array $records the array containing the bookings before nesting
609 *
610 * @return array
611 */
612 protected function nestBookingsData($records)
613 {
614 // to avoid heavy and extra joins, we load all customers for the returned booking ids
615 $allids = array();
616 foreach ($records as $b) {
617 if (!isset($b['customer']) && !in_array($b['id'], $allids)) {
618 array_push($allids, $b['id']);
619 }
620 }
621 $customers_books = array();
622 if (count($allids)) {
623 $q = "SELECT `c`.*,`co`.`idorder`,`cy`.`country_name`,`cy`.`country_2_code` FROM `#__vikbooking_customers` AS `c`
624 LEFT JOIN `#__vikbooking_customers_orders` `co` ON `c`.`id`=`co`.`idcustomer`
625 LEFT JOIN `#__vikbooking_countries` AS `cy` ON `c`.`country`=`cy`.`country_3_code`
626 WHERE `co`.`idorder`".(count($allids) === 1 ? "=".(int)$allids[0] : " IN (".implode(', ', $allids).")").";";
627 $this->dbo->setQuery($q);
628 $this->dbo->execute();
629 if ($this->dbo->getNumRows() > 0) {
630 $allcustomers = $this->dbo->loadAssocList();
631 foreach ($allcustomers as $customer) {
632 $customers_books[$customer['idorder']] = $customer;
633 }
634 }
635 }
636
637 // nest records with multiple rooms booked inside sub-array
638 $bookings = array();
639 foreach ($records as $v) {
640 if (!isset($bookings[$v['id']])) {
641 $bookings[$v['id']] = array();
642 }
643 // to avoid heavy joins, we put the customer record onto the first nested room booked
644 if (!isset($v['customer']) && !count($bookings[$v['id']])) {
645 $v['customer'] = isset($customers_books[$v['id']]) ? $customers_books[$v['id']] : array();
646 }
647
648 // push room sub-array
649 array_push($bookings[$v['id']], $v);
650 }
651
652 return $bookings;
653 }
654
655 /**
656 * Loads the bookings from the DB according to the filters set.
657 * Gathers the information for the electronic invoices generation.
658 * Sets the columns and rows for the page and commands to be displayed.
659 * Updates the internal bookings array for any custom action.
660 *
661 * @return boolean
662 */
663 public function getBookingsData()
664 {
665 if (strlen($this->getError())) {
666 // other methods may set errors rather than exiting the process, and the View may continue the execution to attempt to render the page.
667 return false;
668 }
669
670 if (count($this->bookings)) {
671 // this method may be called by other generation methods, so it's useless to run it twice
672 return true;
673 }
674
675 $cpin = VikBooking::getCPinIstance();
676 $customsq = '';
677 // input fields and other vars
678 $pdatetype = VikRequest::getString('datetype', $this->getSessionFilter('datetype', 'ts'), 'request');
679 $peinvtype = VikRequest::getInt('einvtype', 0, 'request');
680 $peinvkword = VikRequest::getString('einvkword', '', 'request');
681 $pfromdate = VikRequest::getString('fromdate', '', 'request');
682 $ptodate = VikRequest::getString('todate', '', 'request');
683 if (empty($pfromdate) && empty($ptodate)) {
684 // if both request values are empty, take them from the session
685 $pfromdate = $this->getSessionFilter('fromdate');
686 $ptodate = $this->getSessionFilter('todate');
687 }
688 $pkrsort = VikRequest::getString('krsort', $this->defaultKeySort, 'request');
689 $pkrsort = empty($pkrsort) ? $this->defaultKeySort : $pkrsort;
690 $pkrorder = VikRequest::getString('krorder', $this->defaultKeyOrder, 'request');
691 $pkrorder = empty($pkrorder) ? $this->defaultKeyOrder : $pkrorder;
692 $pkrorder = $pkrorder == 'DESC' ? 'DESC' : 'ASC';
693 $currency_symb = VikBooking::getCurrencySymb();
694 $df = $this->getDateFormat();
695 $datesep = VikBooking::getDateSeparator();
696 if (empty($ptodate)) {
697 $ptodate = $pfromdate;
698 }
699 // get dates timestamps
700 $from_ts = VikBooking::getDateTimestamp($pfromdate, 0, 0);
701 $to_ts = VikBooking::getDateTimestamp($ptodate, 23, 59, 59);
702 if (empty($peinvkword) && (empty($pfromdate) || empty($from_ts) || empty($to_ts) || $from_ts > $to_ts)) {
703 $this->setError('Please select the dates to filter invoices and reservations.');
704 return false;
705 }
706
707 // update session filters
708 $this->setSessionFilter('fromdate', $pfromdate);
709 $this->setSessionFilter('todate', $ptodate);
710 $this->setSessionFilter('datetype', $pdatetype);
711
712 // query to obtain the records
713 $records = array();
714 if (!empty($peinvkword)) {
715 // search invoice requires a different query
716 $seekclauses = array();
717 $maybenum = $this->getOnlyNumbers($peinvkword, true);
718 $maybevat = $this->getOnlyNumbers($peinvkword);
719 if (!empty($maybenum)) {
720 // try to seek for this invoice number
721 array_push($seekclauses, '`ei`.`number`='.(int)$maybenum);
722 }
723 if (!empty($maybevat)) {
724 // customer vat number
725 array_push($seekclauses, "`cust`.`vat` LIKE ".$this->dbo->quote("%".$maybevat."%"));
726 }
727 // customer company name
728 array_push($seekclauses, "`cust`.`company` LIKE ".$this->dbo->quote("%".$peinvkword."%"));
729 // customer full name
730 array_push($seekclauses, "CONCAT_WS(' ', `cust`.`first_name`, `cust`.`last_name`) LIKE ".$this->dbo->quote("%".$peinvkword."%"));
731 // customer email
732 if (strpos($peinvkword, '@') !== false) {
733 // customer email
734 array_push($seekclauses, "`cust`.`email`=".$this->dbo->quote($peinvkword));
735 }
736 // customer fiscal code
737 array_push($seekclauses, "`cust`.`fisccode`=".$this->dbo->quote($peinvkword));
738
739 // find first the booking IDs with a specific query given the filters
740 $oidsfound = array();
741 $q = "SELECT `ei`.`id`,`ei`.`idorder` FROM `#__vikbooking_einvoicing_data` AS `ei` ".
742 "LEFT JOIN `#__vikbooking_customers` AS `cust` ON `ei`.`idcustomer` = `cust`.`id` ".
743 "WHERE `ei`.`obliterated`=0 AND (".implode(' OR ', $seekclauses).") ".
744 "GROUP BY `ei`.`driverid`,`ei`.`number`;";
745 $this->dbo->setQuery($q);
746 $this->dbo->execute();
747 if (!$this->dbo->getNumRows()) {
748 $this->setError('No invoice found with the specified filters');
749 return false;
750 }
751 $results = $this->dbo->loadAssocList();
752 $mergecustoms = false;
753 $customsids = array();
754 foreach ($results as $res) {
755 if ($res['idorder'] < 0) {
756 $mergecustoms = true;
757 array_push($customsids, $res['id']);
758 }
759 array_push($oidsfound, $res['idorder']);
760 }
761 // we make the same query but by passing the IDs of the bookings found according to the filters
762 $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`,".
763 "`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` ".
764 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
765 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
766 "LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
767 "LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
768 "WHERE `o`.`id` IN (".implode(', ', array_unique($oidsfound)).") ".
769 "ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
770 // check if we need to merge custom (manual) invoices
771 if ($mergecustoms) {
772 $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` ".
773 "FROM `#__vikbooking_einvoicing_data` AS `ei` ".
774 "LEFT JOIN `#__vikbooking_invoices` AS `inv` ON `ei`.`idorder`=`inv`.`idorder` ".
775 "WHERE `ei`.`idorder` < 0 AND `ei`.`obliterated`=0 AND `ei`.`id` IN (".implode(', ', $customsids).");";
776 }
777 } else {
778 // use date filters for the regular query
779 $mergecustoms = false;
780 $typeclause = '';
781 // filter by type
782 switch ($peinvtype) {
783 case 1:
784 $typeclause = '`ei`.`id` IS NULL AND ';
785 break;
786 case -1:
787 $mergecustoms = true;
788 $typeclause = '`ei`.`id` IS NOT NULL AND `ei`.`transmitted`=0 AND ';
789 break;
790 case -2:
791 $mergecustoms = true;
792 $typeclause = '`ei`.`id` IS NOT NULL AND `ei`.`transmitted`=1 AND ';
793 break;
794 default:
795 // when no e-invoice type filter set, try to merge custom (manual) invoices
796 $mergecustoms = true;
797 break;
798 }
799 $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`,".
800 "`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` ".
801 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
802 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
803 "LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
804 "LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
805 "WHERE ".$typeclause.
806 "(`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." ".
807 "ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
808 // 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`)
809 if ($mergecustoms) {
810 $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` ".
811 "FROM `#__vikbooking_einvoicing_data` AS `ei` ".
812 "LEFT JOIN `#__vikbooking_invoices` AS `inv` ON `ei`.`idorder`=`inv`.`idorder` ".
813 "WHERE `ei`.`idorder` < 0 AND `ei`.`obliterated`=0 AND {$typeclause}".
814 "( (`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 ".
815 "(`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 ".
816 /**
817 * We need to add also the following clause in order to not get multiple records with equal invoice numbers for manual bookings.
818 */
819 "( (`inv`.`created_on`>={$from_ts} AND `inv`.`created_on`<={$to_ts}) OR ".
820 "(`inv`.`for_date`>={$from_ts} AND `inv`.`for_date`<={$to_ts}) );";
821 }
822 }
823 $this->dbo->setQuery($q);
824 $this->dbo->execute();
825 if ($this->dbo->getNumRows() > 0) {
826 $records = $this->dbo->loadAssocList();
827 }
828
829 if (!empty($customsq)) {
830 // we make a query to fetch the custom (manual) invoices to merge them with the real bookings
831 $this->dbo->setQuery($customsq);
832 $this->dbo->execute();
833 if ($this->dbo->getNumRows() > 0) {
834 $custom_records = $this->dbo->loadAssocList();
835 foreach ($custom_records as $customrec) {
836 $custom_data = $this->prepareCustomInvoiceData($customrec, $cpin->getCustomerByID($customrec['idcustomer']));
837 // push the prepared custom invoice array to the global records array
838 array_push($records, $custom_data[0]);
839 }
840 }
841 }
842
843 if (!count($records)) {
844 $this->setError('No reservation or invoice found with the specified filters.');
845 return false;
846 }
847
848 // nest records with multiple rooms booked inside sub-array
849 $bookings = $this->nestBookingsData($records);
850
851 // define the columns of the page
852 $this->cols = array(
853 // id
854 array(
855 'key' => 'id',
856 'sortable' => 1,
857 'label' => 'ID'
858 ),
859 // date
860 array(
861 'key' => 'ts',
862 'attr' => array(
863 'class="center"'
864 ),
865 'sortable' => 1,
866 'label' => JText::translate('VBPVIEWORDERSONE')
867 ),
868 // checkin
869 array(
870 'key' => 'checkin',
871 'sortable' => 1,
872 'label' => JText::translate('VBPICKUPAT')
873 ),
874 // checkout
875 array(
876 'key' => 'checkout',
877 'sortable' => 1,
878 'label' => JText::translate('VBRELEASEAT')
879 ),
880 // customer
881 array(
882 'key' => 'customer',
883 'sortable' => 1,
884 'label' => JText::translate('VBOCUSTOMER')
885 ),
886 // country
887 array(
888 'key' => 'country',
889 'sortable' => 1,
890 'label' => JText::translate('ORDER_STATE')
891 ),
892 // city
893 array(
894 'key' => 'city',
895 'attr' => array(
896 'class="center"'
897 ),
898 'sortable' => 1,
899 'label' => JText::translate('ORDER_CITY')
900 ),
901 // vat
902 array(
903 'key' => 'vat',
904 'attr' => array(
905 'class="center"'
906 ),
907 'sortable' => 1,
908 'label' => JText::translate('VBCUSTOMERCOMPANYVAT')
909 ),
910 // counterpart company name
911 array(
912 'key' => 'company',
913 'sortable' => 1,
914 'label' => JText::translate('VBCUSTOMERCOMPANY')
915 ),
916 // total
917 array(
918 'key' => 'tot',
919 'attr' => array(
920 'class="center"'
921 ),
922 'sortable' => 1,
923 'label' => JText::translate('VBPVIEWORDERSSEVEN')
924 ),
925 // commands
926 array(
927 'key' => 'commands',
928 'attr' => array(
929 'class="center"'
930 ),
931 'label' => ''
932 ),
933 // action
934 array(
935 'key' => 'action',
936 'attr' => array(
937 'class="center"'
938 ),
939 'sortable' => 1,
940 'label' => JText::translate('VBO_BACKUP_ACTION_LABEL')
941 ),
942 );
943
944 // build the rows of the page
945 foreach ($bookings as $bk => $gbook) {
946 $bid = $gbook[0]['id'];
947 $analog_id = isset($gbook[0]['invid']) && !empty($gbook[0]['invid']) ? $gbook[0]['invid'] : null;
948 /**
949 * Manual invoices could have the same number and so negative id order across multiple years.
950 * For this reason, searching for an invoice by number may display invalid links to the manual
951 * invoices, and so we build a list of invoice IDs with related dates to be displayed.
952 */
953 $multi_analog_ids = array();
954 if (!empty($analog_id) && count($gbook) > 1) {
955 $all_analog_ids = array();
956 foreach ($gbook as $subinv) {
957 if (!isset($subinv['invid']) || !isset($subinv['inv_fordate_ts'])) {
958 continue;
959 }
960 $inv_key_identifier = $subinv['invid'] . $subinv['inv_fordate_ts'];
961 if (in_array($inv_key_identifier, $all_analog_ids)) {
962 continue;
963 }
964 array_push($all_analog_ids, $inv_key_identifier);
965 array_push($multi_analog_ids, array(
966 'invid' => $subinv['invid'],
967 'for_date' => date(str_replace("/", $datesep, $df), $subinv['inv_fordate_ts']),
968 ));
969 }
970 }
971 //
972 $tsinfo = getdate($gbook[0]['ts']);
973 $tswday = $this->getWdayString($tsinfo['wday'], 'short');
974 $ininfo = getdate($gbook[0]['checkin']);
975 $inwday = $this->getWdayString($ininfo['wday'], 'short');
976 $outinfo = getdate($gbook[0]['checkout']);
977 $outwday = $this->getWdayString($outinfo['wday'], 'short');
978 $customer = $gbook[0]['customer'];
979 $country3 = $gbook[0]['country'];
980 $country2 = $gbook[0]['country_2_code'];
981 $countryfull = $gbook[0]['country_name'];
982 if (empty($country3) && count($customer) && !empty($customer['country'])) {
983 $country3 = $customer['country'];
984 $gbook[0]['country'] = $country3;
985 }
986 if (empty($country2) && count($customer) && !empty($customer['country_2_code'])) {
987 $country2 = $customer['country_2_code'];
988 $gbook[0]['country_2_code'] = $country2;
989 }
990 if (empty($countryfull) && count($customer) && !empty($customer['country_name'])) {
991 $countryfull = $customer['country_name'];
992 $gbook[0]['country_name'] = $countryfull;
993 }
994 $totguests = 0;
995 $rooms_map = array();
996 $rooms_str = array();
997 foreach ($gbook as $book) {
998 $totguests += $book['adults'] + $book['children'];
999 if (!isset($book['room_name'])) {
1000 // custom (manual) invoice records may be missing this property
1001 continue;
1002 }
1003 if (!isset($rooms_map[$book['room_name']])) {
1004 $rooms_map[$book['room_name']] = 0;
1005 }
1006 $rooms_map[$book['room_name']]++;
1007 }
1008 foreach ($rooms_map as $rname => $rcount) {
1009 array_push($rooms_str, $rname . ($rcount > 1 ? ' x'.$rcount : ''));
1010 }
1011 $rooms_str = implode(', ', $rooms_str);
1012
1013 // einvnum (if exists)
1014 $einvnum = !empty($gbook[0]['einvnum']) ? $gbook[0]['einvnum'] : 0;
1015
1016 // always update the main array reference
1017 $bookings[$bk] = $gbook;
1018
1019 // check whether the invoice can be issued
1020 list($canbeinvoiced, $noinvoicereason) = $this->canBookingBeInvoiced($bookings[$bk]);
1021
1022 // push fields in the rows array as a new row
1023 array_push($this->rows, array(
1024 array(
1025 'key' => 'id',
1026 'callback' => function ($val) use ($analog_id, $multi_analog_ids) {
1027 if ($val < 0 && !empty($analog_id)) {
1028 // custom (manual) invoices have a negative idorder (-number)
1029 $returi = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1030 if (count($multi_analog_ids) < 2) {
1031 // just one manual invoice found
1032 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>';
1033 }
1034 /**
1035 * There can be conflictual manual invoices with the same number and negative order
1036 * across multiple years, so we print a link to display them all with an alert.
1037 * @since 1.13.5
1038 */
1039 $all_links = array();
1040 foreach ($multi_analog_ids as $analog_info) {
1041 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>');
1042 }
1043 return implode('<br/>', $all_links);
1044 }
1045 return '<a href="index.php?option=com_vikbooking&task=editorder&cid[]='.$val.'" target="_blank"><i class="'.VikBookingIcons::i('external-link').'"></i> '.$val.'</a>';
1046 },
1047 'value' => $bid
1048 ),
1049 array(
1050 'key' => 'ts',
1051 'attr' => array(
1052 'class="center"'
1053 ),
1054 'callback' => function ($val) use ($df, $datesep, $tswday) {
1055 return $tswday.', '.date(str_replace("/", $datesep, $df), $val);
1056 },
1057 'value' => $gbook[0]['ts']
1058 ),
1059 array(
1060 'key' => 'checkin',
1061 'callback' => function ($val) use ($df, $datesep, $inwday) {
1062 if (empty($val)) {
1063 // custom (manual) invoices have an empty timestamp
1064 return '-----';
1065 }
1066 return $inwday.', '.date(str_replace("/", $datesep, $df), $val);
1067 },
1068 'value' => $gbook[0]['checkin']
1069 ),
1070 array(
1071 'key' => 'checkout',
1072 'callback' => function ($val) use ($df, $datesep, $outwday) {
1073 if (empty($val)) {
1074 // custom (manual) invoices have an empty timestamp
1075 return '-----';
1076 }
1077 return $outwday.', '.date(str_replace("/", $datesep, $df), $val);
1078 },
1079 'value' => $gbook[0]['checkout']
1080 ),
1081 array(
1082 'key' => 'customer',
1083 'callback' => function ($val) use ($customer, $bid) {
1084 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1085 if (!empty($val)) {
1086 $cont = count($customer) ? '<a href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">'.$val.'</a>' : $val;
1087 if (count($customer) && !empty($customer['country'])) {
1088 if (is_file(VBO_ADMIN_PATH.DS.'resources'.DS.'countries'.DS.$customer['country'].'.png')) {
1089 $cont .= '<img src="'.VBO_ADMIN_URI.'resources/countries/'.$customer['country'].'.png'.'" title="'.$customer['country'].'" class="vbo-country-flag vbo-country-flag-left"/>';
1090 }
1091 }
1092 } else {
1093 // if empty customer ($val) print danger button to assign a customer to this booking ID
1094 $cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=newcustomer&bid='.$bid.'&goto='.$goto.'">' . JText::translate('VBOCREATENEWCUST') . '</a>';
1095 }
1096 return $cont;
1097 },
1098 'value' => (count($customer) ? $customer['first_name'].' '.$customer['last_name'] : '')
1099 ),
1100 array(
1101 'key' => 'country',
1102 'callback' => function ($val) {
1103 return !empty($val) ? $val : '-----';
1104 },
1105 'value' => $countryfull
1106 ),
1107 array(
1108 'key' => 'city',
1109 'attr' => array(
1110 'class="center"'
1111 ),
1112 'callback' => function ($val) use ($customer) {
1113 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1114 if (empty($val)) {
1115 if (count($customer) && !empty($customer['id'])) {
1116 // just an empty City, edit the customer
1117 $cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
1118 } else {
1119 $cont = '-----';
1120 }
1121 return $cont;
1122 }
1123 if (count($customer) && empty($customer['zip'])) {
1124 // postal code is mandatory
1125 return '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">No Postal Code</a>';
1126 }
1127 if (count($customer) && empty($customer['address'])) {
1128 // address is mandatory
1129 return '<a class="btn btn-secondary" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">No Address</a>';
1130 }
1131 return $val;
1132 },
1133 'value' => (count($customer) && !empty($customer['city']) ? $customer['city'] : '')
1134 ),
1135 array(
1136 'key' => 'vat',
1137 'attr' => array(
1138 'class="center"'
1139 ),
1140 'callback' => function ($val) use ($customer, $bid) {
1141 if (!empty($val)) {
1142 $cont = $val;
1143 } else {
1144 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1145 if (count($customer) && !empty($customer['id'])) {
1146 // empty VAT Number, which is mandatory for both issuer and counterpart
1147 $cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
1148 } else {
1149 // if empty customer ($val) print danger button to assign a customer to this booking ID
1150 $cont = '<a class="btn btn-danger" href="index.php?option=com_vikbooking&task=newcustomer&bid='.$bid.'&goto='.$goto.'">' . JText::translate('VBCONFIGCLOSINGDATEADD') . '</a>';
1151 }
1152 }
1153 return $cont;
1154 },
1155 'value' => (count($customer) && !empty($customer['vat']) ? $customer['vat'] : '')
1156 ),
1157 array(
1158 'key' => 'company',
1159 'callback' => function ($val) use ($customer) {
1160 $cont = !empty($val) ? $val : '-----';
1161 if (count($customer)) {
1162 $goto = base64_encode('index.php?option=com_vikbooking&task=einvoicing');
1163 $cont = '<a href="index.php?option=com_vikbooking&task=editcustomer&cid[]='.$customer['id'].'&goto='.$goto.'">'.$cont.'</a>';
1164 }
1165 return $cont;
1166 },
1167 'value' => (count($customer) && !empty($customer['company']) ? $customer['company'] : '')
1168 ),
1169 array(
1170 'key' => 'tot',
1171 'attr' => array(
1172 'class="center"'
1173 ),
1174 'callback' => function ($val) use ($currency_symb) {
1175 return $currency_symb.' '.VikBooking::numberFormat($val);
1176 },
1177 'value' => $gbook[0]['total']
1178 ),
1179 array(
1180 'key' => 'commands',
1181 'attr' => array(
1182 'class="center"'
1183 ),
1184 'callback' => function ($val) use ($bid, $noinvoicereason) {
1185 if ($val === 0 || $val === 1) {
1186 // invoice cannot be issued or is about to be issued
1187 return '';
1188 }
1189 $buttons = array();
1190 if ($val === -1 || $val === -2) {
1191 // invoice generated or generated and transmitted
1192 array_push($buttons, '<i class="vboicn-eye icn-nomargin vbo-driver-customoutput vbo-driver-output-vieweinv" title="View invoice" data-einvid="'.$noinvoicereason.'"></i>');
1193 array_push($buttons, '<i class="vboicn-pencil2 icn-nomargin vbo-driver-customoutput vbo-driver-output-editeinv" title="Edit invoice" data-einvid="'.$noinvoicereason.'"></i>');
1194 array_push($buttons, '<i class="vboicn-bin icn-nomargin vbo-driver-customoutput vbo-driver-output-rmeinv" title="Delete invoice" data-einvid="'.$noinvoicereason.'"></i>');
1195 }
1196 return implode("\n", $buttons);
1197 },
1198 'value' => $canbeinvoiced
1199 ),
1200 array(
1201 'key' => 'action',
1202 'attr' => array(
1203 'class="center vbo-einvoicing-cellaction"',
1204 'data-einvaction="'.$canbeinvoiced.'"'
1205 ),
1206 'callback' => function ($val) use ($bid, $noinvoicereason, $einvnum) {
1207 if ($val === 0) {
1208 // invoice cannot be issued
1209 $noinvoicereason = empty($noinvoicereason) ? 'Missing data to generate the invoice' : $noinvoicereason;
1210 return '<button type="button" class="btn btn-secondary" onclick="alert(\''.addslashes($noinvoicereason).'\');"><i class="vboicn-blocked icn-nomargin"></i> Not billable</button>';
1211 }
1212 if ($val === -1) {
1213 // e-invoice already issued and transmitted: print drop down to let the customer regenerate this invoice and obliterate the other or to re-send
1214 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>';
1215 }
1216 if ($val === -2) {
1217 // e-invoice already issued but NOT transmitted: print drop down to let the customer regenerate this invoice and obliterate the other
1218 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>';
1219 }
1220 // invoice can be issued: print drop down to let the customer skip this generation
1221 return '<select class="vbo-einvoicing-selaction" data-bid="'.$bid.'"><option value="0">Generate invoice</option><option value="1">- Do NOT generate invoice</option></select>';
1222 },
1223 'value' => $canbeinvoiced
1224 ),
1225 ));
1226 }
1227
1228 // sort rows
1229 $this->sortRows($pkrsort, $pkrorder);
1230
1231 // build footer rows
1232 $totcols = count($this->cols);
1233 $footerstats = array();
1234 foreach ($this->rows as $k => $row) {
1235 foreach ($row as $col) {
1236 if ($col['key'] != 'action') {
1237 continue;
1238 }
1239 if (!isset($footerstats[$col['value']])) {
1240 $footerstats[$col['value']] = 0;
1241 }
1242 $footerstats[$col['value']]++;
1243 }
1244 }
1245 $avgcolspan = floor($totcols / count($footerstats));
1246 $footercells = array();
1247 foreach ($footerstats as $canbeinvoiced => $tot) {
1248 switch ($canbeinvoiced) {
1249 case 1:
1250 $descr = 'To be invoiced';
1251 break;
1252 case -1:
1253 $descr = 'Transmitted invoices';
1254 break;
1255 case -2:
1256 $descr = 'Generated invoices';
1257 break;
1258 default:
1259 $descr = 'Not billable';
1260 break;
1261 }
1262 array_push($footercells, array(
1263 'attr' => array(
1264 'class="vbo-report-total vbo-driver-total"',
1265 'colspan="'.$avgcolspan.'"'
1266 ),
1267 'value' => '<h3>'.$descr.': '.$tot.'</h3>'
1268 ));
1269 }
1270 $this->footerRow[0] = $footercells;
1271 $missingcols = $totcols - ($avgcolspan * count($footerstats));
1272 if ($missingcols > 0) {
1273 array_push($this->footerRow[0], array(
1274 'attr' => array(
1275 'class="vbo-report-total vbo-driver-total"',
1276 'colspan="'.$missingcols.'"'
1277 ),
1278 'value' => ''
1279 ));
1280 }
1281
1282 // update bookings array for the other methods to avoid double executions
1283 $this->bookings = $bookings;
1284
1285 return true;
1286 }
1287
1288 /**
1289 * Checks whether an e-invoice can be issued for this booking.
1290 *
1291 * @param array the booking array with one array-room per array value
1292 *
1293 * @return array to be used with list(): 0 => (int) can be invoiced, 1 => (string) reason message
1294 *
1295 * @see https://www.aade.gr/sites/default/files/2020-04/myDATA%20API%20Documentation%20v0%206b_eng.pdf
1296 */
1297 protected function canBookingBeInvoiced($booking)
1298 {
1299 if (empty($booking[0]['customer']) || empty($booking[0]['customer']['vat'])) {
1300 // the VAT number is a mandatory field for both issuer and counterpart
1301 return array(0, 'Missing VAT Number');
1302 }
1303
1304 if (empty($booking[0]['customer']) || empty($booking[0]['customer']['country']) || empty($booking[0]['customer']['country_2_code'])) {
1305 return array(0, 'Missing country');
1306 }
1307
1308 if (empty($booking[0]['customer']) || empty($booking[0]['customer']['city'])) {
1309 return array(0, 'Missing City');
1310 }
1311
1312 if (empty($booking[0]['customer']) || empty($booking[0]['customer']['zip'])) {
1313 return array(0, 'Missing Postal Code');
1314 }
1315
1316 // check if an electronic invoice was already issued for this booking ID by this driver
1317 if (!empty($booking[0]['einvid']) && $booking[0]['einvdriver'] == $this->getDriverId()) {
1318 if ($booking[0]['einvsent'] > 0) {
1319 // 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
1320 return array(-1, $booking[0]['einvid']);
1321 }
1322 // 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
1323 return array(-2, $booking[0]['einvid']);
1324 }
1325
1326 return array(1, '');
1327 }
1328
1329 /**
1330 * Generates the electronic invoices according to the input parameters.
1331 * This is a 'driver action', and so it's called before getBookingsData()
1332 * in the view. This method will save/update records in the DB so that when
1333 * the view re-calls getBookingsData(), the information will be up to date.
1334 *
1335 * @return boolean True if at least one e-invoice was generated
1336 */
1337 public function generateEInvoices()
1338 {
1339 // call the main method to generate rows, cols and bookings array
1340 $this->getBookingsData();
1341
1342 if (strlen($this->getError()) || !count($this->bookings)) {
1343 return false;
1344 }
1345
1346 $generated = 0;
1347
1348 foreach ($this->bookings as $gbook) {
1349 // check whether this booking ID was set to be skipped
1350 $exclude = VikRequest::getInt('excludebid'.$gbook[0]['id'], 0, 'request');
1351 if ($exclude > 0) {
1352 // skipping this invoice
1353 continue;
1354 }
1355
1356 // check if an electronic invoice was already issued for this booking ID by this driver
1357 if (!empty($gbook[0]['einvid']) && $gbook[0]['einvdriver'] == $this->getDriverId()) {
1358 $regenerate = VikRequest::getInt('regeneratebid'.$gbook[0]['id'], 0, 'request');
1359 if (!($regenerate > 0)) {
1360 // we do not re-generate an invoice for this booking ID
1361 continue;
1362 }
1363 }
1364
1365 // generate invoice
1366 if ($this->generateEInvoice($gbook)) {
1367 $generated++;
1368 }
1369 }
1370
1371 // we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
1372 $this->bookings = array();
1373 // unset also cols, rows and footer row to not merge data
1374 $this->cols = array();
1375 $this->rows = array();
1376 $this->footerRow = array();
1377
1378 // set info message
1379 $this->setInfo('Invoices generated: '.$generated);
1380
1381 return ($generated > 0);
1382 }
1383
1384 /**
1385 * Given two arguments, the current analogic invoice record and the customer record, this
1386 * method should prepare and return an array that can be later passed onto generateEInvoice().
1387 * This originally abstract method must be implemented for the generation of the custom (manual) invoices
1388 * that are not related to any bookings (idorder = -number), that were manually created for certain customers.
1389 *
1390 * @param array $invoice the analogic invoice record
1391 * @param array $customer the customer record obtained through VikBookingCustomersPin::getCustomerByID()
1392 *
1393 * @return array the data array compatible with generateEInvoice()
1394 *
1395 * @see generateEInvoice()
1396 */
1397 public function prepareCustomInvoiceData($invoice, $customer)
1398 {
1399 if (!isset($invoice['number']) && !empty($invoice['einvnum'])) {
1400 // getBookingsData() may call this method by knowing only the electronic invoice number
1401 $invoice['number'] = $invoice['einvnum'];
1402 }
1403
1404 // make sure to get an integer value from the invoice number, which is a string with a probable suffix
1405 $numnumber = intval(preg_replace("/[^\d]+/", '', $invoice['number']));
1406
1407 // make sure the key rawcont is an array
1408 if (!is_array($invoice['rawcont'])) {
1409 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
1410 $rawcont = is_array($rawcont) ? $rawcont : array();
1411 $invoice['rawcont'] = $rawcont;
1412 }
1413
1414 // build necessary data array compatible with generateEInvoice()
1415 $data = array(
1416 'id' => ($numnumber - ($numnumber * 2)),
1417 'ts' => (isset($invoice['created_on']) ? strtotime($invoice['created_on']) : time()),
1418 'checkin' => 0,
1419 'checkout' => 0,
1420 'adults' => 0,
1421 'children' => 0,
1422 'total' => $invoice['rawcont']['totaltot'],
1423 'country' => $customer['country'],
1424 'country_name' => $customer['country_name'],
1425 'country_2_code' => (isset($customer['country_2_code']) ? $customer['country_2_code'] : null),
1426 'tot_taxes' => $invoice['rawcont']['totaltax'],
1427 'tot_city_taxes' => 0,
1428 'tot_fees' => 0,
1429 'customer' => $customer,
1430 'pkg' => null,
1431 'einvid' => (isset($invoice['einvid']) ? $invoice['einvid'] : null),
1432 'einvdriver' => (isset($invoice['einvdriver']) ? $invoice['einvdriver'] : null),
1433 'einvdate' => (isset($invoice['einvdate']) ? $invoice['einvdate'] : null),
1434 'einvnum' => (isset($invoice['einvnum']) ? $invoice['einvnum'] : null),
1435 'einvsent' => (isset($invoice['einvsent']) ? $invoice['einvsent'] : null),
1436 // this could be the ID of the analogic invoice
1437 'invid' => (isset($invoice['invid']) ? $invoice['invid'] : null),
1438 // this could be the for date timestamp of the analogic invoice
1439 'inv_fordate_ts' => (isset($invoice['inv_fordate_ts']) ? $invoice['inv_fordate_ts'] : null),
1440 );
1441
1442 // make sure to inject the raw content of the custom invoice
1443 $this->externalData['einvrawcont'] = $invoice['rawcont'];
1444
1445 // original data array contains nested rooms booked so we need to return it as the 0th value
1446 return array($data);
1447 }
1448
1449 /**
1450 * Checks whether an active electronic invoice already exists from the given details.
1451 *
1452 * @param mixed $data array or StdClass object with properties to identify the e-invoice
1453 *
1454 * @return mixed False if the invoice does not exist, its ID otherwise.
1455 */
1456 public function eInvoiceExists($data)
1457 {
1458 if (is_object($data)) {
1459 // cast to array
1460 $data = (array)$data;
1461 }
1462
1463 // allowed properties to check
1464 $properties = array(
1465 'id' => 'einvid',
1466 'idorder' => 'idorder',
1467 'number' => 'number',
1468 );
1469
1470 $filters = array();
1471 foreach ($properties as $k => $v) {
1472 if (isset($data[$v]) && !empty($data[$v])) {
1473 $filters[$k] = $data[$v];
1474 } elseif (isset($data[$k]) && !empty($data[$k])) {
1475 $filters[$k] = $data[$k];
1476 }
1477 }
1478
1479 if (empty($filters)) {
1480 return false;
1481 }
1482
1483 $clauses = array();
1484 foreach ($filters as $col => $val) {
1485 array_push($clauses, "`{$col}`=".$this->dbo->quote($val));
1486 }
1487
1488 $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;";
1489 $this->dbo->setQuery($q);
1490 $this->dbo->execute();
1491 if (!$this->dbo->getNumRows()) {
1492 return false;
1493 }
1494
1495 return $this->dbo->loadResult();
1496 }
1497
1498 /**
1499 * Attempts to set one e-invoice to obliterated.
1500 *
1501 * @param mixed $data array or StdClass object with properties to identify the e-invoice
1502 *
1503 * @return void
1504 */
1505 public function obliterateEInvoice($data)
1506 {
1507 if (is_object($data)) {
1508 // cast to array
1509 $data = (array)$data;
1510 }
1511
1512 // allowed properties to check
1513 $properties = array(
1514 'id' => 'einvid',
1515 'idorder' => 'idorder',
1516 'number' => 'number',
1517 );
1518
1519 $filters = array();
1520 foreach ($properties as $k => $v) {
1521 if (isset($data[$v]) && !empty($data[$v])) {
1522 $filters[$k] = $data[$v];
1523 } elseif (isset($data[$k]) && !empty($data[$k])) {
1524 $filters[$k] = $data[$k];
1525 }
1526 }
1527
1528 if (empty($filters)) {
1529 return;
1530 }
1531
1532 $clauses = array();
1533 foreach ($filters as $col => $val) {
1534 array_push($clauses, "`{$col}`=".$this->dbo->quote($val));
1535 }
1536
1537 $q = "UPDATE `#__vikbooking_einvoicing_data` SET `obliterated`=1 WHERE `driverid`=".(int)$this->getDriverId()." AND ".implode(' AND ', $clauses).";";
1538 $this->dbo->setQuery($q);
1539 $this->dbo->execute();
1540 }
1541
1542 /**
1543 * Generates one single electronic invoice. If no array data provided, the booking ID should
1544 * be passed as argument. In this case the method would fetch and nest the booking data.
1545 *
1546 * @param mixed $data either the booking ID or the booking array (one room info per index)
1547 *
1548 * @return boolean True if the e-invoice was generated
1549 */
1550 public function generateEInvoice($data)
1551 {
1552 // load driver settings
1553 $settings = $this->loadSettings();
1554 if ($settings === false || !count($settings['params'])) {
1555 $this->setError('Missing driver settings. Please set up the driver first.');
1556 return false;
1557 }
1558
1559 if (is_int($data)) {
1560 // query to obtain the booking records
1561 $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`,".
1562 "`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` ".
1563 "FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_ordersrooms` AS `or` ON `or`.`idorder`=`o`.`id` ".
1564 "LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` ".
1565 "LEFT JOIN `#__vikbooking_countries` AS `c` ON `o`.`country`=`c`.`country_3_code` ".
1566 "LEFT JOIN `#__vikbooking_einvoicing_data` AS `ei` ON `o`.`id`=`ei`.`idorder` AND `ei`.`obliterated`=0 ".
1567 "WHERE ".
1568 "(`o`.`status`='confirmed' OR (`o`.`status`='cancelled' AND `o`.`totpaid`>0)) AND `o`.`closure`=0 AND `o`.`id`=".$this->dbo->quote($data)." ".
1569 "ORDER BY `o`.`ts` ASC, `o`.`id` ASC;";
1570 $this->dbo->setQuery($q);
1571 $this->dbo->execute();
1572 if (!$this->dbo->getNumRows()) {
1573 $this->setError('Could not find the booking information');
1574 return false;
1575 }
1576 $record = $this->dbo->loadAssocList();
1577
1578 // nest records with multiple rooms booked inside sub-array
1579 $record = $this->nestBookingsData($record);
1580 $data = $record[$data];
1581 }
1582
1583 if (!is_array($data) || empty($data)) {
1584 $this->setError('No bookings found');
1585 return false;
1586 }
1587
1588 // check whether the invoice can be issued
1589 list($canbeinvoiced, $noinvoicereason) = $this->canBookingBeInvoiced($data);
1590 if ($canbeinvoiced === 0) {
1591 /**
1592 * IMPORTANT: if this method is not called by generateEInvoices(), then the script should
1593 * make sure that an e-invoice is not already available for this booking ID because
1594 * here we skip only if $canbeinvoiced=0 and when e-invoices exist, the code is -1 or -2.
1595 */
1596
1597 // do not raise any errors unless called externally, we just skip this booking because it cannot be invoiced
1598 if ($this->externalCall) {
1599 if ($data[0]['id'] < 0) {
1600 $message = "Could not generate electronic invoice from custom invoice: {$noinvoicereason}";
1601 } else {
1602 $message = "Could not generate electronic invoice for booking ID {$data[0]['id']} ({$noinvoicereason})";
1603 }
1604 $this->setError($message);
1605 }
1606
1607 return false;
1608 }
1609
1610 // counterpart branch number
1611 $branch = '0';
1612
1613 // counterpart name must not be submitted if entity is from Greece
1614 $client_name = '';
1615 if ((!empty($data[0]['customer']['first_name']) || !empty($data[0]['customer']['last_name'])) && $data[0]['customer']['country'] != 'GRC') {
1616 $client_name = $data[0]['customer']['first_name'] . ' ' . $data[0]['customer']['last_name'];
1617 }
1618
1619 // invoice date and number (suffix not supported for AA serial number)
1620 if (!empty($data[0]['einvnum']) && $settings['params']['einvexnumdt'] == 'old') {
1621 // if an invoice already exists, we re-use the same number also because the setting said so
1622 $invnum = $data[0]['einvnum'];
1623 $invdate = $data[0]['einvdate'];
1624 } else {
1625 // get a new invoice number
1626 $invnum = VikBooking::getNextInvoiceNumber();
1627 $invdate = $settings['params']['einvdttype'] == 'ts' ? date('Y-m-d', $data[0]['ts']) : date('Y-m-d');
1628 }
1629 if (isset($this->externalData['einvnum']) && intval($this->externalData['einvnum']) > 0) {
1630 // external calls may inject the invoice number to use
1631 $invnum = (int)$this->externalData['einvnum'];
1632 }
1633 if (isset($this->externalData['einvdate']) && !empty($this->externalData['einvdate'])) {
1634 // external calls may inject the invoice date to use
1635 $invdate = is_int($this->externalData['einvdate']) ? date('Y-m-d', $this->externalData['einvdate']) : $this->externalData['einvdate'];
1636 }
1637
1638 // invoice series ("in case of non-issuance of series of an invoice, the series field must have a value of 0")
1639 $series = '0';
1640 // 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')
1641 $aa_serial_number = $invnum;
1642 // invoice type
1643 $invtype = !empty($settings['params']['einvtypecode']) ? $settings['params']['einvtypecode'] : VikBookingMydataAadeConstants::DEFAULT_INVOICE_TYPE;
1644
1645 // invoice total paid amount
1646 $inv_tot_paid = empty($data[0]['totpaid']) ? $data[0]['total'] : $data[0]['totpaid'];
1647
1648 // invoice payment method
1649 $inv_pay_method = '';
1650 if (!empty($data[0]['idpayment'])) {
1651 $pay_info_parts = explode('=', $data[0]['idpayment']);
1652 $inv_pay_method = !empty($pay_info_parts[1]) ? $pay_info_parts[1] : $inv_pay_method;
1653 }
1654
1655 // compose the invoice UID
1656 $invoice_uid_parts = [
1657 $settings['params']['vatid'],
1658 $invdate,
1659 $branch,
1660 $invtype,
1661 $series,
1662 $aa_serial_number,
1663 ];
1664 $invoice_uid = sha1(implode('', $invoice_uid_parts));
1665
1666 // invoice details and summaries
1667 $invoice_details = [];
1668 $summaries = [];
1669 $summariesvat = [];
1670 $rounded_nets = [];
1671
1672 // whether to include "incomeClassification" nodes
1673 $use_income_classf = (!empty($settings['params']['einv_inc_class_type']) && !empty($settings['params']['einv_inc_class_cat']));
1674
1675 $is_package = (!empty($data[0]['pkg']));
1676 $isdue = 0;
1677 $extralinenum = 0;
1678 $discountval = 0;
1679 if ($data[0]['id'] < 0 && isset($this->externalData['einvrawcont'])) {
1680 // custom (manual) invoice, get the raw content of the invoice
1681 foreach ($this->externalData['einvrawcont']['rows'] as $ind => $row) {
1682 if (!isset($summariesvat[$row['aliq']])) {
1683 $summariesvat[$row['aliq']] = array('net' => 0, 'tax' => 0);
1684 $rounded_nets[$row['aliq']] = 0;
1685 }
1686 $summariesvat[$row['aliq']]['net'] += $row['net'];
1687 $summariesvat[$row['aliq']]['tax'] += $row['tax'];
1688 $rounded_nets[$row['aliq']] += (float)number_format($row['net'], 2, '.', '');
1689
1690 // income classification
1691 $inc_classf_nodes = '';
1692 if ($use_income_classf) {
1693 $inc_classf_nodes = '<incomeClassification>
1694 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
1695 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
1696 <N1:amount>' . number_format($row['net'], 2, '.', '') . '</N1:amount>
1697 </incomeClassification>';
1698 }
1699
1700 // push invoice details node
1701 $vat_category = VikBookingMydataAadeConstants::getVatCategory($row['aliq']);
1702 array_push($invoice_details, '
1703 <invoiceDetails>
1704 <lineNumber>' . ($ind + 1) . '</lineNumber>
1705 <quantity>1.00</quantity>
1706 <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
1707 <netValue>' . number_format($row['net'], 2, '.', '') . '</netValue>
1708 <vatCategory>' . $vat_category . '</vatCategory>
1709 <vatAmount>' . number_format($row['tax'], 2, '.', '') . '</vatAmount>
1710 ' . ((int)$row['aliq'] === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
1711 <lineComments>'.$this->convertSpecials($row['service']).'</lineComments>
1712 ' . $inc_classf_nodes . '
1713 </invoiceDetails>');
1714
1715 }
1716 } else {
1717 // invoice for a regular booking
1718 $tars = $this->getBookingTariffs($data);
1719
1720 // check discount (coupon and/or refund)
1721 $discount_nodes = '';
1722 if (isset($data[0]['coupon']) && strlen($data[0]['coupon']) > 0) {
1723 $expcoupon = explode(";", $data[0]['coupon']);
1724 $discountval += (float)$expcoupon[1];
1725 }
1726 if (isset($data[0]['refund']) && $data[0]['refund'] > 0) {
1727 $discountval += $data[0]['refund'];
1728 }
1729 if ($discountval > 0) {
1730 $discount_nodes = '
1731 <discountOption>true</discountOption>
1732 <deductionsAmount>' . number_format($discountval, 2, '.', '') . '</deductionsAmount>';
1733 }
1734
1735 foreach ($data as $kor => $or) {
1736 $num = $kor + 1;
1737 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1738 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
1739 $descr = $is_package ? sprintf(VikBookingMydataAadeConstants::DESCRPACKAGENIGHTS, $or['days']) : sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
1740 $cost_minus_tax = VikBooking::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
1741 $cost_tax_amount = (VikBooking::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']) - $cost_minus_tax);
1742 $aliq = $this->getAliquoteById($or['cust_idiva']);
1743 if (!isset($summariesvat[$aliq])) {
1744 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
1745 $rounded_nets[$aliq] = 0;
1746 }
1747 $summariesvat[$aliq]['net'] += $cost_minus_tax;
1748 $summariesvat[$aliq]['tax'] += $cost_tax_amount;
1749 $rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
1750
1751 // income classification
1752 $inc_classf_nodes = '';
1753 if ($use_income_classf) {
1754 $inc_classf_nodes = '<incomeClassification>
1755 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
1756 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
1757 <N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
1758 </incomeClassification>';
1759 }
1760
1761 // push invoice details node
1762 array_push($invoice_details, '
1763 <invoiceDetails>
1764 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
1765 <quantity>1.00</quantity>
1766 <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
1767 <netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
1768 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
1769 <vatAmount>' . number_format($cost_tax_amount, 2, '.', '') . '</vatAmount>
1770 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
1771 ' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
1772 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
1773 ' . $inc_classf_nodes . '
1774 </invoiceDetails>');
1775 } elseif (isset($tars[$num]) && is_array($tars[$num])) {
1776 // regular tariff
1777 $descr = sprintf(VikBookingMydataAadeConstants::DESCRSTAYROOMNIGHTS, $or['days'], strtoupper($or['room_name']));
1778 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
1779 $calctar = VikBooking::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
1780 $aliq = $this->getAliquoteFromPriceId($tars[$num]['idprice']);
1781 $isdue += $calctar;
1782 if ($calctar == $display_rate) {
1783 $cost_minus_tax = VikBooking::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
1784 $tax = ($display_rate - $cost_minus_tax);
1785 } else {
1786 $cost_minus_tax = $display_rate;
1787 $tax = ($calctar - $display_rate);
1788 }
1789 if (!isset($summariesvat[$aliq])) {
1790 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
1791 $rounded_nets[$aliq] = 0;
1792 }
1793 $summariesvat[$aliq]['net'] += $cost_minus_tax;
1794 $summariesvat[$aliq]['tax'] += $tax;
1795 $rounded_nets[$aliq] += (float)number_format($cost_minus_tax, 2, '.', '');
1796
1797 // income classification
1798 $inc_classf_nodes = '';
1799 if ($use_income_classf) {
1800 $inc_classf_nodes = '<incomeClassification>
1801 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
1802 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
1803 <N1:amount>' . number_format($cost_minus_tax, 2, '.', '') . '</N1:amount>
1804 </incomeClassification>';
1805 }
1806
1807 // push invoice details node
1808 array_push($invoice_details, '
1809 <invoiceDetails>
1810 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
1811 <quantity>1.00</quantity>
1812 <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
1813 <netValue>' . number_format($cost_minus_tax, 2, '.', '') . '</netValue>
1814 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
1815 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
1816 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
1817 ' . (($num + $extralinenum) == 1 ? $discount_nodes : '') . '
1818 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
1819 ' . $inc_classf_nodes . '
1820 </invoiceDetails>');
1821 }
1822
1823 // room options
1824 if (!empty($or['optionals'])) {
1825 $stepo = explode(";", $or['optionals']);
1826 foreach ($stepo as $roptkey => $oo) {
1827 if (empty($oo)) {
1828 continue;
1829 }
1830 $stept = explode(":", $oo);
1831 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $this->dbo->quote($stept[0]) . ";";
1832 $this->dbo->setQuery($q);
1833 $this->dbo->execute();
1834 if (!$this->dbo->getNumRows()) {
1835 continue;
1836 }
1837 $actopt = $this->dbo->loadAssocList();
1838 $chvar = '';
1839 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
1840 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
1841 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
1842 $optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
1843 $child_num = VikBooking::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
1844 $optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
1845 $agestept = explode('-', $stept[1]);
1846 $stept[1] = $agestept[0];
1847 $chvar = $agestept[1];
1848 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
1849 // percentage value of the adults tariff
1850 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1851 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
1852 } else {
1853 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
1854 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
1855 }
1856 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
1857 // percentage value of room base cost
1858 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1859 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
1860 } else {
1861 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
1862 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
1863 }
1864 }
1865 $actopt[0]['chageintv'] = $chvar;
1866 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
1867 $actopt[0]['quan'] = $stept[1];
1868 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $or['days'] * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
1869 } else {
1870 $actopt[0]['quan'] = $stept[1];
1871 // VBO 1.11 - options percentage cost of the room total fee
1872 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1873 $deftar_basecosts = $or['cust_cost'];
1874 } else {
1875 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
1876 }
1877 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
1878 //
1879 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $or['days'] * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
1880 }
1881 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
1882 $realcost = $actopt[0]['maxprice'];
1883 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
1884 $realcost = $actopt[0]['maxprice'] * $stept[1];
1885 }
1886 }
1887 if ($actopt[0]['perperson'] == 1) {
1888 $realcost = $realcost * $or['adults'];
1889 }
1890 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
1891 $isdue += $tmpopr;
1892 // increase line number
1893 $extralinenum++;
1894 //
1895 $aliq = $this->getAliquoteById($actopt[0]['idiva']);
1896 if ($tmpopr == $realcost) {
1897 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
1898 $tax = ($realcost - $opt_minus_tax);
1899 } else {
1900 $opt_minus_tax = $realcost;
1901 $tax = ($tmpopr - $realcost);
1902 }
1903 $descr = $actopt[0]['is_citytax'] == 1 ? VikBookingMydataAadeConstants::DESCRTOURISTTAX : sprintf(VikBookingMydataAadeConstants::DESCRROOMOPTION, strtoupper($actopt[0]['name']));
1904 if (!isset($summariesvat[$aliq])) {
1905 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
1906 $rounded_nets[$aliq] = 0;
1907 }
1908
1909 $summariesvat[$aliq]['net'] += $opt_minus_tax;
1910 $summariesvat[$aliq]['tax'] += $tax;
1911 $rounded_nets[$aliq] += (float)number_format($opt_minus_tax, 2, '.', '');
1912
1913 // income classification
1914 $inc_classf_nodes = '';
1915 if ($use_income_classf) {
1916 $inc_classf_nodes = '<incomeClassification>
1917 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
1918 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
1919 <N1:amount>' . number_format($opt_minus_tax, 2, '.', '') . '</N1:amount>
1920 </incomeClassification>';
1921 }
1922
1923 // push invoice details node
1924 array_push($invoice_details, '
1925 <invoiceDetails>
1926 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
1927 <quantity>1.00</quantity>
1928 <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
1929 <netValue>' . number_format($opt_minus_tax, 2, '.', '') . '</netValue>
1930 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
1931 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
1932 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
1933 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
1934 ' . $inc_classf_nodes . '
1935 </invoiceDetails>');
1936 }
1937 }
1938
1939 // custom extra costs
1940 if (!empty($or['extracosts'])) {
1941 $cur_extra_costs = json_decode($or['extracosts'], true);
1942 foreach ($cur_extra_costs as $eck => $ecv) {
1943 // increase line number
1944 $extralinenum++;
1945 //
1946 $ecplustax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
1947 $isdue += $ecplustax;
1948 $descr = sprintf(VikBookingMydataAadeConstants::DESCRROOMEXTRACOST, strtoupper($ecv['name']));
1949 if ($ecplustax == $ecv['cost']) {
1950 $ec_minus_tax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
1951 $tax = ($ecv['cost'] - $ec_minus_tax);
1952 } else {
1953 $ec_minus_tax = ($ecplustax - $ecv['cost']);
1954 $tax = ($ecplustax - $ecv['cost']);
1955 }
1956 $aliq = $this->getAliquoteById($ecv['idtax']);
1957 if (!isset($summariesvat[$aliq])) {
1958 $summariesvat[$aliq] = array('net' => 0, 'tax' => 0);
1959 $rounded_nets[$aliq] = 0;
1960 }
1961
1962 $summariesvat[$aliq]['net'] += $ec_minus_tax;
1963 $summariesvat[$aliq]['tax'] += $tax;
1964 $rounded_nets[$aliq] += (float)number_format($ec_minus_tax, 2, '.', '');
1965
1966 // income classification
1967 $inc_classf_nodes = '';
1968 if ($use_income_classf) {
1969 $inc_classf_nodes = '<incomeClassification>
1970 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
1971 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
1972 <N1:amount>' . number_format($ec_minus_tax, 2, '.', '') . '</N1:amount>
1973 </incomeClassification>';
1974 }
1975
1976 // push invoice details node
1977 array_push($invoice_details, '
1978 <invoiceDetails>
1979 <lineNumber>' . ($num + $extralinenum) . '</lineNumber>
1980 <quantity>1.00</quantity>
1981 <measurementUnit>' . VikBookingMydataAadeConstants::DEFAULT_MEAS_UNIT . '</measurementUnit>
1982 <netValue>' . number_format($ec_minus_tax, 2, '.', '') . '</netValue>
1983 <vatCategory>' . VikBookingMydataAadeConstants::getVatCategory($aliq) . '</vatCategory>
1984 <vatAmount>' . number_format($tax, 2, '.', '') . '</vatAmount>
1985 ' . ((int)$aliq === 0 && !empty($settings['params']['vat_exempt_cat']) ? '<vatExemptionCategory>' . $settings['params']['vat_exempt_cat'] . '</vatExemptionCategory>' : '') . '
1986 <lineComments>' . $this->convertSpecials($descr) . '</lineComments>
1987 ' . $inc_classf_nodes . '
1988 </invoiceDetails>');
1989 }
1990 }
1991 }
1992 }
1993
1994 // build riepiloghi IVA
1995 $grand_total_net = 0;
1996 $grand_total_vat = 0;
1997 $grand_total_tax_no_rate = 0;
1998 foreach ($summariesvat as $aliq => $vat_summary) {
1999 $totnet = number_format($vat_summary['net'], 2, '.', '');
2000 $tottax = number_format($vat_summary['tax'], 2, '.', '');
2001 if (isset($rounded_nets[$aliq]) && (float)$totnet != $rounded_nets[$aliq]) {
2002 /**
2003 * In case of several rows in the invoice, maybe a lot of Extra Services,
2004 * there can be a discrepancy between the sum of the <netValue> nodes
2005 * in the <invoiceDetails> nodes, and the <taxAmount> in <taxesTotals>.
2006 * We need to prevent the amounts to be different because of number_format and
2007 * adjust the amounts and obtain the same value as the sum of the nets in the lines.
2008 * The issue was reproduced with 9 Extra Services, one Room, one Tourist Tax (Option).
2009 *
2010 * @see sandbox booking ID 1140
2011 */
2012 if ($rounded_nets[$aliq] > (float)$totnet) {
2013 $diff = $rounded_nets[$aliq] - (float)$totnet;
2014 $totnet = number_format($rounded_nets[$aliq], 2, '.', '');
2015 $tottax = number_format(((float)$tottax - $diff), 2, '.', '');
2016 } else {
2017 $diff = (float)$totnet - $rounded_nets[$aliq];
2018 $totnet = number_format($rounded_nets[$aliq], 2, '.', '');
2019 $tottax = number_format(((float)$tottax + $diff), 2, '.', '');
2020 }
2021 }
2022
2023 // sum grand total values
2024 $grand_total_net += $totnet;
2025 if ((int)$aliq > 0) {
2026 $grand_total_vat += $tottax;
2027 } else {
2028 /**
2029 * @todo are we doing good by summing this kind of tax, which is not VAT because
2030 * the tax rate is 0%, to the "total withheld amount"? Or should we use the
2031 * node <totalOtherTaxesAmount> instead?
2032 */
2033 $grand_total_tax_no_rate += $tottax;
2034 }
2035
2036 /**
2037 * For the moment we ingore completely the <taxesTotals> node and sub-nodes. Docs say:
2038 * "Field taxesTotals contains all taxes except VAT. If user users this element,
2039 * taxes will not exist in invoiceDetails".
2040 * However, here we have a sum of tax amounts for any aliquote (tax rate) involved.
2041 *
2042 * @todo check if these nodes should be somehow composed even if they are optional.
2043 */
2044 }
2045
2046 /**
2047 * Address element is forbidden for issuer from Greece.
2048 */
2049 $issuer_address_nodes = '';
2050 if (strcasecmp($settings['params']['country'], 'GR')) {
2051 // issuer not from Greece, compose address
2052 $issuer_address_nodes = '<address>
2053 <street>' . $this->convertSpecials($settings['params']['address']) . '</street>
2054 <number>' . $settings['params']['streetnumber'] . '</number>
2055 <postalCode>' . $settings['params']['zip'] . '</postalCode>
2056 <city>' . $this->convertSpecials($settings['params']['city']) . '</city>
2057 </address>';
2058 }
2059
2060 // total income classification
2061 $inc_classf_nodes = '';
2062 if ($use_income_classf) {
2063 $inc_classf_nodes = '<incomeClassification>
2064 <N1:classificationType>' . $settings['params']['einv_inc_class_type'] . '</N1:classificationType>
2065 <N1:classificationCategory>' . $settings['params']['einv_inc_class_cat'] . '</N1:classificationCategory>
2066 <N1:amount>' . number_format($grand_total_net, 2, '.', '') . '</N1:amount>
2067 </incomeClassification>';
2068 }
2069
2070 // build XML
2071 $root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
2072 $xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
2073 <InvoicesDoc ' . $root_namespaces . '>
2074 <invoice>
2075 <uid>' . $invoice_uid . '</uid>
2076 <mark>' . $settings['progcount'] . '</mark>
2077 <issuer>
2078 <vatNumber>' . $settings['params']['vatid'] . '</vatNumber>
2079 <country>' . $settings['params']['country'] . '</country>
2080 <branch>0</branch>
2081 ' . (strcasecmp($settings['params']['country'], 'GR') ? '<name>' . $this->convertSpecials($settings['params']['companyname']) . '</name>' : '') . '
2082 ' . (!empty($issuer_address_nodes) ? $issuer_address_nodes : '') . '
2083 </issuer>
2084 <counterpart>
2085 ' . (!empty($data[0]['customer']['vat']) ? '<vatNumber>' . $this->convertSpecials($data[0]['customer']['vat']) . '</vatNumber>' : '') . '
2086 ' . (!empty($data[0]['customer']['country_2_code']) ? '<country>' . $this->convertSpecials($data[0]['customer']['country_2_code']) . '</country>' : '') . '
2087 <branch>' . $branch . '</branch>
2088 ' . (!empty($client_name) ? '<name>' . $this->convertSpecials($client_name) . '</name>' : '') . '
2089 <address>
2090 <street>' . $this->convertSpecials(preg_replace("/[0-9]/", '', $data[0]['customer']['address'])) . '</street>
2091 <number>' . preg_replace("/[^0-9]/", '', $data[0]['customer']['address']) . '</number>
2092 <postalCode>' . $this->convertSpecials($data[0]['customer']['zip']) . '</postalCode>
2093 <city>' . $this->convertSpecials($data[0]['customer']['city']) . '</city>
2094 </address>
2095 </counterpart>
2096 <invoiceHeader>
2097 <series>' . $series . '</series>
2098 <aa>' . $aa_serial_number . '</aa>
2099 <issueDate>' . $invdate . '</issueDate>
2100 <invoiceType>' . $invtype . '</invoiceType>
2101 <currency>' . VikBooking::getCurrencyName() . '</currency>
2102 </invoiceHeader>
2103 <paymentMethods>
2104 <paymentMethodDetails>
2105 <type>' . $settings['params']['einv_paymethod'] . '</type>
2106 <amount>' . number_format($inv_tot_paid, 2, '.', '') . '</amount>
2107 ' . (!empty($inv_pay_method) ? '<paymentMethodInfo>' . $this->convertSpecials($inv_pay_method) . '</paymentMethodInfo>' : '') . '
2108 </paymentMethodDetails>
2109 </paymentMethods>
2110 ' . implode("\n", $invoice_details) . '
2111 <invoiceSummary>
2112 <totalNetValue>' . number_format($grand_total_net, 2, '.', '') . '</totalNetValue>
2113 <totalVatAmount>' . number_format($grand_total_vat, 2, '.', '') . '</totalVatAmount>
2114 <totalWithheldAmount>' . number_format($grand_total_tax_no_rate, 2, '.', '') . '</totalWithheldAmount>
2115 <totalFeesAmount>0.00</totalFeesAmount>
2116 <totalStampDutyAmount>0.00</totalStampDutyAmount>
2117 <totalOtherTaxesAmount>0.00</totalOtherTaxesAmount>
2118 <totalDeductionsAmount>' . number_format($discountval, 2, '.', '') . '</totalDeductionsAmount>
2119 <totalGrossValue>' . number_format($data[0]['total'], 2, '.', '') . '</totalGrossValue>
2120 ' . $inc_classf_nodes . '
2121 </invoiceSummary>
2122 </invoice>
2123 </InvoicesDoc>';
2124
2125 // attempt to properly format the XML string
2126 $this->formatXmlString($xml);
2127
2128 // check if we need to validate the XML against the official schema
2129 if (!empty($settings['params']['schema_validate'])) {
2130 /**
2131 * It may not be possible to validate the XML against the schema, as on
2132 * some environments this process may run out of execution time.
2133 */
2134 try {
2135 $schema_validation = $this->validateXmlAgainstSchema($xml);
2136 if ($schema_validation === null) {
2137 // display warning
2138 $this->setWarning('Missing PHP libraries for DOMDocument to validate the XML invoice against the official schema.');
2139 }
2140 } catch (Exception $e) {
2141 // display warning
2142 $this->setWarning('Could not validate the XML invoice against the official Schema - process failed with no response.');
2143 }
2144 }
2145
2146 if ($this->debugging()) {
2147 $this->setWarning('<pre>'.htmlentities($xml).'</pre><br/>');
2148 // break the process when in debug mode
2149 return false;
2150 }
2151
2152 // we proceed with the generation
2153
2154 // invoice name (transmission date-time string + auto-increment registration value just for our internal purpose)
2155 $einvname = date('YmdHis') . '_' . $settings['progcount'] . '.xml';
2156
2157 // get current datetime object in local format
2158 $date_obj = JFactory::getDate();
2159 $date_obj->setTimezone(new DateTimeZone(date_default_timezone_get()));
2160
2161 // prepare object for storing the invoice
2162 $jdate = new JDate;
2163 $einvobj = new stdClass;
2164 $einvobj->driverid = $settings['id'];
2165 $einvobj->created_on = $date_obj->toSql($local = true);
2166 $einvobj->for_date = $invdate;
2167 $einvobj->filename = $einvname;
2168 $einvobj->number = $invnum;
2169 $einvobj->idorder = $data[0]['id'];
2170 $einvobj->idcustomer = !empty($data[0]['customer']['id']) ? $data[0]['customer']['id'] : 0;
2171 $einvobj->country = !empty($data[0]['customer']['country']) ? $data[0]['customer']['country'] : null;
2172 // this column is not needed in this driver, but we give it a default value
2173 $einvobj->recipientcode = '';
2174 $einvobj->xml = $xml;
2175 // always reset transmitted and obliterated values for new e-invoices
2176 $einvobj->transmitted = 0;
2177 $einvobj->obliterated = 0;
2178
2179 $newinvid = $this->storeEInvoice($einvobj);
2180 if ($newinvid === false) {
2181 $this->setError('Error storing the electronic invoice for the reservation ID '.$data[0]['id']);
2182 return false;
2183 }
2184
2185 if ($canbeinvoiced < 0) {
2186 // log event history when regenerating an e-invoice
2187 VikBooking::getBookingHistoryInstance()->setBid($data[0]['id'])->store('BI', ($this->getName() . ' #' . $invnum));
2188 }
2189
2190 // update settings before generating the analogic invoice in PDF format to prevent exceptions to be thrown or exit/die calls.
2191 // update configuration setting for VikBooking::getNextInvoiceNumber()
2192 if ($data[0]['id'] > 0) {
2193 // we exclude custom (manual) invoices which would have a booking ID set to -number
2194 $this->updateInvoiceNumber($invnum);
2195 }
2196 // update ProgressivoInvio driver setting by increasing it for the next run
2197 $this->updateProgressiveNumber(++$settings['progcount']);
2198
2199 if (!$this->hasAnalogicInvoice($data[0]['id'])) {
2200 // no analogic invoice in PDF available, so we create it
2201 if (!$this->generateAnalogicInvoice($data[0]['id'], $invnum, $invdate)) {
2202 // raise warning in case of error
2203 $this->setWarning('It was not possible to generate the courtesy PDF version of the invoice for the reservation ID '.$data[0]['id']);
2204 }
2205 }
2206
2207 return true;
2208 }
2209
2210 /**
2211 * Returns the calculated tariffs given their IDs per room booked.
2212 *
2213 * @param array the booking array with one array-room per array value
2214 *
2215 * @return array associative array of tariffs for each room booked
2216 */
2217 protected function getBookingTariffs($booking)
2218 {
2219 $tars = array();
2220
2221 $is_package = (!empty($booking[0]['pkg']));
2222
2223 foreach ($booking as $kor => $or) {
2224 $num = $kor + 1;
2225 if ($is_package || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2226 // package or custom cost set from the back-end does not need calculation
2227 continue;
2228 }
2229 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=".(int)$or['idtar'].";";
2230 $this->dbo->setQuery($q);
2231 $this->dbo->execute();
2232 if ($this->dbo->getNumRows() > 0) {
2233 $tar = $this->dbo->loadAssocList();
2234 $tar = VikBooking::applySeasonsRoom($tar, $or['checkin'], $or['checkout']);
2235
2236 // apply OBP rules
2237 $tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $or, $or['adults']);
2238
2239 $tars[$num] = $tar[0];
2240 }
2241 }
2242
2243 return $tars;
2244 }
2245
2246 /**
2247 * Transmits the electronic invoices to the SdI according to the input parameters.
2248 * This is a 'driver action', and so it's called before getBookingsData()
2249 * in the view. This method will save/update records in the DB so that when
2250 * the view re-calls getBookingsData(), the information will be up to date.
2251 *
2252 * @return boolean True if at least one e-invoice was transmitted
2253 */
2254 public function transmitEInvoices()
2255 {
2256 // make sure the transmission settings are not empty
2257 $settings = $this->loadSettings();
2258 if ($settings === false || !count($settings['params'])) {
2259 $this->setError('Missing settings to transmit the invoices. Please set up the driver settings first.');
2260 return false;
2261 }
2262 // make sure the settings we need are not empty
2263 $required = [
2264 $settings['params']['aade_user_id'],
2265 $settings['params']['aade_subscription_key'],
2266 ];
2267 foreach ($required as $reqset) {
2268 if (empty($reqset)) {
2269 $this->setError('Invalid settings to transmit the invoices. Please make sure to provide all the information from the driver settings.');
2270 return false;
2271 }
2272 }
2273
2274 // call the main method to generate rows, cols and bookings array
2275 $this->getBookingsData();
2276
2277 if (strlen($this->getError()) || !count($this->bookings)) {
2278 return false;
2279 }
2280
2281 // pool of e-invoice IDs to transmit
2282 $einvspool = [];
2283 $einvnumbs = [];
2284
2285 foreach ($this->bookings as $gbook) {
2286 // check whether this booking ID was set to be skipped from transmission
2287 $exclude = VikRequest::getInt('excludesendbid'.$gbook[0]['id'], 0, 'request');
2288 if ($exclude > 0) {
2289 // skipping this invoice from transmission
2290 continue;
2291 }
2292
2293 // make sure an electronic invoice was already issued for this booking ID by this driver
2294 if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
2295 // no e-invoices available for this booking, skipping
2296 continue;
2297 }
2298
2299 // check if an e-invoice was already sent for this booking
2300 if ($gbook[0]['einvsent'] > 0) {
2301 $resend = VikRequest::getInt('resendbid'.$gbook[0]['id'], 0, 'request');
2302 if (!($resend > 0)) {
2303 // we do not re-send the invoice for this booking ID
2304 continue;
2305 }
2306 }
2307
2308 // push e-invoice ID to the pool
2309 array_push($einvspool, $gbook[0]['einvid']);
2310 // push also the corresponding invoice number
2311 array_push($einvnumbs, $gbook[0]['einvnum']);
2312 }
2313
2314 if (!count($einvspool)) {
2315 // no e-invoices generated or ready to be transmitted
2316 $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.');
2317 return false;
2318 }
2319
2320 // build one XML file for all XML e-invoices (if more than one)
2321 $einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
2322 if ($einv_xml_body === false) {
2323 // something went wrong with the creation of the XML file
2324 $this->setError('Error creating the XML file for the request. Unable to proceed.');
2325 return false;
2326 }
2327
2328 if ($this->debugging()) {
2329 // when in debug mode, the raw XML request is sent to output
2330 $this->setWarning('Raw XML request for Debug Mode');
2331 $this->setWarning('<pre> ' . htmlentities($einv_xml_body) . ' </pre>');
2332 }
2333
2334 // transmit e-invoices to myDATA
2335 $response = $this->myDATARequestPOST('SendInvoices', $einv_xml_body, $settings);
2336 if ($response->code != 200) {
2337 // the request was not successful, and the XML invoices were not parsed at all by myDATA
2338 $this->setError(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
2339 $this->setError('Could not send the invoice(s) to myDATA.');
2340 return false;
2341 }
2342
2343 if ($this->debugging()) {
2344 // when in debug mode, the raw XML response is sent to output
2345 $this->setWarning('Raw XML response for Debug Mode');
2346 $this->setWarning('<pre> ' . htmlentities($response->body) . ' </pre>');
2347 }
2348
2349 // check if the XML response contains errors, and adjust the e-invoices that succeeded
2350 list($success, $valid_einv_marks, $valid_einv_uids) = $this->myDATAParseXMLResponse($response->body, $einvspool, $einvnumbs);
2351
2352 if (!$success) {
2353 // some errors occurred
2354 if (!is_array($valid_einv_marks) || !count($valid_einv_marks)) {
2355 $this->setError('Could not send the invoice(s) to myDATA.');
2356 return false;
2357 } else {
2358 // some e-invoices were transmitted successfully
2359 $einvspool = array_keys($valid_einv_marks);
2360 }
2361 }
2362
2363 // update ProgressivoInvio driver setting by increasing it for the next run
2364 $this->updateProgressiveNumber(++$settings['progcount']);
2365
2366 // set to transmitted=1 all e-invoice IDs that were transmitted with success
2367 foreach ($einvspool as $einvid) {
2368 // prepare "transmission data" object
2369 $trans_data = new stdClass;
2370 $trans_data->invoice_uid = (isset($valid_einv_uids[$einvid]) && $einvid != $valid_einv_uids[$einvid] ? $valid_einv_uids[$einvid] : null);
2371 $trans_data->invoice_mark = (isset($valid_einv_marks[$einvid]) && $einvid != $valid_einv_marks[$einvid] ? $valid_einv_marks[$einvid] : null);
2372 $trans_data->trans_dtime = date('Y-m-d H:i:s');
2373
2374 // build e-invoice object for update (with "transmission data")
2375 $data = new stdClass;
2376 $data->id = $einvid;
2377 $data->transmitted = 1;
2378 $data->trans_data = json_encode($trans_data);
2379
2380 $this->updateEInvoice($data);
2381 }
2382
2383 // display info message
2384 $this->setInfo('Electronic invoices transmitted: ' . count($einvspool));
2385
2386 // we need to unset the bookings var so that the later call to getBookingsData() made by the View will reload the information
2387 $this->bookings = array();
2388 // unset also cols, rows and footer row to not merge data
2389 $this->cols = array();
2390 $this->rows = array();
2391 $this->footerRow = array();
2392
2393 return true;
2394 }
2395
2396 /**
2397 * Generates one XML string for the request body to myDATA. If more
2398 * than one e-invoice ID passed, attempts to parse all XML files for
2399 * the already generated e-invoices in order to compose one single
2400 * XML request body that contains all invoices. Every e-invoice has
2401 * got an XML file compliant for the transmission, but when we need
2402 * to transmit in mass multiple e-invoices, we try to use just one
2403 * HTTP request by merging all e-invoices into one single XML file.
2404 *
2405 * @param array $einvspool an array of e-invoice IDs
2406 * @param array $settings the driver settings
2407 *
2408 * @return bool|string false on failure or XML request body string.
2409 */
2410 protected function buildTransmissionXMLBody($einvspool, $settings)
2411 {
2412 if (!is_array($einvspool) || !count($einvspool)) {
2413 return false;
2414 }
2415
2416 // the list of XML strings
2417 $xml_strings = [];
2418
2419 // generate XML files for the requested e-invoice IDs
2420 foreach ($einvspool as $einvid) {
2421 // load e-invoice details
2422 $einv_data = $this->loadEInvoiceDetails($einvid);
2423 if (!$einv_data || !is_array($einv_data) || empty($einv_data['xml'])) {
2424 // all e-invoices must exist as they will be set to transmitted=1 so we break the process
2425 $this->setError('Unable to load data for the electronic invoice ID ' . $einvid);
2426 return false;
2427 }
2428
2429 // push e-invoice content
2430 $xml_strings[] = $einv_data['xml'];
2431 }
2432
2433 if (!count($xml_strings)) {
2434 // no XML files created, break the process
2435 return false;
2436 }
2437
2438 if (count($xml_strings) === 1) {
2439 // just one XML file, no need to build an XML container
2440 return $xml_strings[0];
2441 }
2442
2443 // return one whole XML request body for all e-invoices
2444 return $this->mergeXMLInvoices($xml_strings);
2445 }
2446
2447 /**
2448 * Given a list of XML e-invoice strings, attempts to merge them
2449 * into one single XML to avoid making one HTTP request per e-invoice.
2450 *
2451 * @param array $xml_strings list of XML strings for each e-invoice.
2452 *
2453 * @return bool|string false or whole XML string for all e-invoices.
2454 */
2455 protected function mergeXMLInvoices($xml_strings)
2456 {
2457 if (!is_array($xml_strings) || !count($xml_strings)) {
2458 return false;
2459 }
2460
2461 if (count($xml_strings) === 1) {
2462 return $xml_strings[0];
2463 }
2464
2465 if (!class_exists('SimpleXMLElement')) {
2466 /**
2467 * We cannot afford to do a string manipulation only because SimpleXMLElement
2468 * is missing on the server. It has to be available, it's a native library.
2469 */
2470 $this->setError('SimpleXMLElement is missing on your server.');
2471 $this->setError('This is unusual, and you should contact your hosting company to enable this native PHP library.');
2472 $this->setError('You can only transmit single e-invoices, not more than one because SimpleXMLElement is missing');
2473
2474 return false;
2475 }
2476
2477 /**
2478 * Define the XML root element for the InvoicesDoc message.
2479 * Namespace attributes will affect the incomeClassification sub nodes.
2480 *
2481 * @see VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
2482 * @see https://mydata-dev.portal.azure-api.net/issues/5f3c411ac75730207831ead4
2483 */
2484 $root_namespaces = VikBookingMydataAadeConstants::getInvoiceNamespaceAttributes();
2485 $xml_root = <<<XML
2486 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
2487 <InvoicesDoc $root_namespaces>
2488 </InvoicesDoc>
2489 XML;
2490 // get the SimpleXMLElement object
2491 $xml = new SimpleXMLElement($xml_root);
2492
2493 // define the namespace rules for the children elements of <incomeClassification>
2494 $child_nmspaces = [
2495 'incomeClassification' => VikBookingMydataAadeConstants::getInvoiceChildrenNamespace()
2496 ];
2497
2498 // parse all e-invoices
2499 $parsed = 0;
2500 foreach ($xml_strings as $k => $einvoice) {
2501 $xml_einvoice = simplexml_load_string($einvoice);
2502 if (!is_object($xml_einvoice)) {
2503 $this->setWarning('Unable to parse the XML of the e-invoice index ' . ($k + 1));
2504 $this->setWarning($this->libxml_display_errors());
2505 continue;
2506 }
2507 // append XML tree to a new <invoice> node
2508 $invoice_node = $xml->addChild('invoice');
2509 $this->simpleXmlAppendTree($invoice_node, $xml_einvoice->invoice, $child_nmspaces);
2510 // increase parsed invoices
2511 $parsed++;
2512 }
2513
2514 if (!$parsed) {
2515 $this->setError('No e-invoices could be parsed to merge the XML trees and related nodes into one single XML body.');
2516 return false;
2517 }
2518
2519 // get the whole XML request body just built from all e-invoices
2520 $full_xml = $xml->asXML();
2521
2522 /**
2523 * When appending child nodes with namespaces to "<incomeClassification>", these may be added as
2524 * "<N1:classificationCategory xmlns:N1="N1">category1_3</N1:classificationCategory>" so with both
2525 * the proper namespace in the node name, but also with the attribute 'xmlns:N1="N1"' which is making
2526 * the whole XML failing according to the schema. Therefore, we manipulate the string to remove such attributes.
2527 */
2528 if (!empty($child_nmspaces['incomeClassification'])) {
2529 $seek_pattern = $child_nmspaces['incomeClassification'];
2530 $full_xml = str_replace('xmlns:' . $seek_pattern . '="' . $seek_pattern . '"', '', $full_xml);
2531 }
2532
2533 // attempt to properly format the XML string
2534 $this->formatXmlString($full_xml);
2535
2536 // return the whole XML request body containing all the e-invoices
2537 return $full_xml;
2538 }
2539
2540 /**
2541 * Recursive method to append a SimpleXMLElement tree node, and
2542 * related children nodes, to another SimpleXMLElement. Used to
2543 * dinamically add an entire tree of a single e-invoice XML file
2544 * under a single node <invoice> of the whole XML request body.
2545 *
2546 * @param SimpleXMLElement $xml_to the node where the tree will be appended.
2547 * @param SimpleXMLElement $xml_from the element to append with all its children.
2548 * @param array $child_nmspaces associative list of children namespaces.
2549 *
2550 * @return void
2551 */
2552 protected function simpleXmlAppendTree(&$xml_to, &$xml_from, $child_nmspaces = [])
2553 {
2554 $child_nmspace = null;
2555 $child_isprefix = false;
2556
2557 $node_name = $xml_to->getName();
2558 if (!empty($child_nmspaces[$node_name])) {
2559 $child_nmspace = $child_nmspaces[$node_name];
2560 $child_isprefix = true;
2561 }
2562
2563 foreach ($xml_from->children($child_nmspace, $child_isprefix) as $xml_child) {
2564 $add_node_name = $xml_child->getName();
2565 if (!empty($child_nmspace)) {
2566 $add_node_name = "$child_nmspace:$add_node_name";
2567 }
2568 $xml_temp = $xml_to->addChild($add_node_name, (string)$xml_child, $child_nmspace);
2569 foreach ($xml_child->attributes() as $attr_key => $attr_value) {
2570 $xml_temp->addAttribute($attr_key, $attr_value);
2571 }
2572 $this->simpleXmlAppendTree($xml_temp, $xml_child, $child_nmspaces);
2573 }
2574 }
2575
2576 /**
2577 * Loads the details of the given e-invoice ID. The given ID should not be obliterated.
2578 *
2579 * @param int $einvid the ID of the e-invoice
2580 *
2581 * @return mixed array if the e-invoice exists and is not obliterated, false otherwise.
2582 */
2583 protected function loadEInvoiceDetails($einvid)
2584 {
2585 if (empty($einvid)) {
2586 return false;
2587 }
2588
2589 $q = "SELECT * FROM `#__vikbooking_einvoicing_data` WHERE `id`=".$einvid." AND `obliterated`=0;";
2590 $this->dbo->setQuery($q);
2591 $this->dbo->execute();
2592 if ($this->dbo->getNumRows()) {
2593 return $this->dbo->loadAssoc();
2594 }
2595
2596 return false;
2597 }
2598
2599 /**
2600 * Performs a POST request to the myDATA infrastructure.
2601 *
2602 * @param string $url_path the path to append to the base endpoint URI.
2603 * @param mixed $body the request body.
2604 * @param array $settings driver settings or any other option to inject.
2605 *
2606 * @return JHttpResponse object with code and body properties
2607 */
2608 protected function myDATARequestPOST($url_path = '', $body = null, $settings = [])
2609 {
2610 if (empty($settings)) {
2611 $settings = $this->loadSettings();
2612 }
2613
2614 $aade_user_id = $settings['params']['aade_user_id'];
2615 $aade_subscription_key = $settings['params']['aade_subscription_key'];
2616 $aade_endp_url = $settings['params']['mydata_endpoint_url'];
2617 if (!empty($settings['params']['test_mode'])) {
2618 $aade_endp_url = VikBookingMydataAadeConstants::getDevEndpointBaseUrl();
2619 }
2620
2621 if (!empty($url_path)) {
2622 $aade_endp_url .= ltrim($url_path, '/');
2623 }
2624
2625 // build request headers
2626 $headers = [
2627 'Content-Type' => 'application/xml',
2628 'aade-user-id' => $aade_user_id,
2629 'Ocp-Apim-Subscription-Key' => $aade_subscription_key,
2630 ];
2631
2632 // invoke CMS native transporter
2633 $transporter = new JHttp;
2634 $response = $transporter->post($aade_endp_url, $body, $headers);
2635
2636 if ($response->code != 200) {
2637 $this->setError('Erroneous response with HTTP code ' . $response->code);
2638 $this->setError(htmlentities($response->body));
2639 }
2640
2641 return $response;
2642 }
2643
2644 /**
2645 * Checks if the XML response string from myDATA contains errors.
2646 * Returns an array with boolean "success" and array with "einv_id => einv_mark".
2647 *
2648 * @param string $body the raw response body from the request.
2649 * @param array $einvspool list of e-invoice ids in VBO just transmitted.
2650 * @param array $einvnumbs list of e-invoice numbers in VBO just transmitted.
2651 *
2652 * @return array to be used with list($success, $valid_einv_marks, $valid_einv_uids).
2653 */
2654 protected function myDATAParseXMLResponse($body, $einvspool = [], $einvnumbs = [])
2655 {
2656 // the default information to return
2657 $success = false;
2658 $valid_einv_marks = [];
2659 $valid_einv_uids = [];
2660
2661 $res_obj = $body;
2662 if (!is_object($res_obj)) {
2663 $res_obj = simplexml_load_string($body);
2664 }
2665
2666 if (!is_object($res_obj) || !isset($res_obj->response)) {
2667 $this->setError('Could not parse XML response');
2668 $this->setError('<pre>' . htmlentities($body) . '</pre>');
2669 return [$success, $valid_einv_marks, $valid_einv_uids];
2670 }
2671
2672 // errors counter
2673 $errors_found = 0;
2674
2675 // loop through each response node
2676 foreach ($res_obj->response as $invoice_resp) {
2677 if (!isset($invoice_resp->statusCode)) {
2678 $this->setError('Unexpected nodes in XML response (missing statusCode)');
2679 $this->setError('<pre>' . htmlentities($body) . '</pre>');
2680 return [$success, $valid_einv_marks, $valid_einv_uids];
2681 }
2682 /**
2683 * Endpoint POST /SendInvoices only (/CancelInvoice would not return this data).
2684 * Get the index of the current invoice response (line-number starts from 1).
2685 */
2686 $invoice_index = isset($invoice_resp->index) ? (int)$invoice_resp->index : 0;
2687 // check if we have a successful status code for this invoice
2688 if (!strcasecmp((string)$invoice_resp->statusCode, 'Success')) {
2689 // success!
2690 if ($invoice_index > 0 && isset($einvspool[($invoice_index - 1)])) {
2691 // push successful invoice
2692 $einv_id = $einvspool[($invoice_index - 1)];
2693 // get the invoice UID
2694 $invoice_uid = isset($invoice_resp->invoiceUid) ? (string)$invoice_resp->invoiceUid : $einv_id;
2695 $valid_einv_uids[$einv_id] = $invoice_uid;
2696 // get the invoice mark (needed for a later cancellation)
2697 $invoice_mark = isset($invoice_resp->invoiceMark) ? (string)$invoice_resp->invoiceMark : $einv_id;
2698 $valid_einv_marks[$einv_id] = $invoice_mark;
2699 }
2700 continue;
2701 }
2702 // at this point we expect an error
2703 if (!isset($invoice_resp->errors) || !isset($invoice_resp->errors->error)) {
2704 // errors should be set, but if they aren't, this is unexpected
2705 $this->setError('Unexpected nodes in XML response (missing errors or error)');
2706 $this->setError('<pre>' . htmlentities($body) . '</pre>');
2707 return [$success, $valid_einv_marks, $valid_einv_uids];
2708 }
2709 // loop through the errors
2710 foreach ($invoice_resp->errors->error as $resp_err) {
2711 $errors_found++;
2712 $err_code = isset($resp_err->code) ? (string)$resp_err->code : '0';
2713 $err_mess = isset($resp_err->message) ? (string)$resp_err->message : '???';
2714 $inv_numb = isset($einvnumbs[($invoice_index - 1)]) ? $einvnumbs[($invoice_index - 1)] : '???';
2715 $this->setError(sprintf('Error (%s) in invoice index %d (#%s): %s', $err_code, $invoice_index, $inv_numb, $err_mess));
2716 }
2717 }
2718
2719 // if we had no errors at all, the response was successful
2720 $success = (!$errors_found);
2721
2722 return [$success, $valid_einv_marks, $valid_einv_uids];
2723 }
2724
2725 /**
2726 * Downloads the electronic invoices by storing temporary files.
2727 * This is a 'driver action', and so it's called before getBookingsData()
2728 * in the view. This method will not save/update records in the DB.
2729 *
2730 * @return void
2731 */
2732 public function downloadEInvoices()
2733 {
2734 // make sure the transmission settings are not empty
2735 $settings = $this->loadSettings();
2736 if ($settings === false || !count($settings['params'])) {
2737 $this->setError('Missing settings. Please set up the driver first.');
2738 return false;
2739 }
2740
2741 // call the main method to generate rows, cols and bookings array
2742 $this->getBookingsData();
2743
2744 if (strlen($this->getError()) || !count($this->bookings)) {
2745 return false;
2746 }
2747
2748 // pool of e-invoice IDs to download
2749 $einvspool = array();
2750
2751 foreach ($this->bookings as $gbook) {
2752 // make sure an electronic invoice was already issued for this booking ID by this driver
2753 if (empty($gbook[0]['einvid']) || $gbook[0]['einvdriver'] != $this->getDriverId()) {
2754 // no e-invoices available for this booking, skipping
2755 continue;
2756 }
2757
2758 // push e-invoice ID to the pool
2759 array_push($einvspool, $gbook[0]['einvid']);
2760 }
2761
2762 if (!count($einvspool)) {
2763 // no e-invoices generated
2764 $this->setWarning('No electronic invoices can be downloaded. Please generate them first.');
2765 return false;
2766 }
2767
2768 // build one whole XML file
2769 $einv_xml_body = $this->buildTransmissionXMLBody($einvspool, $settings);
2770 if ($einv_xml_body === false) {
2771 // something went wrong with the creation of the file to download
2772 $this->setError('Could not generate the XML file containing all the electronic invoices.');
2773 return false;
2774 }
2775
2776 // force the download of the XML string
2777 header('Content-Disposition: attachment; filename="mydata-aade-einvoices' . date('Y-m-d') . '.xml"');
2778 header("Content-Type: text/xml");
2779 header("Content-Length:" . strlen($einv_xml_body));
2780 header('Connection: close');
2781 echo $einv_xml_body;
2782
2783 exit;
2784 }
2785
2786 /**
2787 * Forces the display of an electronic invoice. This is a 'driver action', and so it's called
2788 * before getBookingsData() in the view. This method will not save/update records in the DB.
2789 * This method truncates the execution of the script to read the XML data.
2790 *
2791 * @return void
2792 */
2793 public function viewEInvoice()
2794 {
2795 $einvid = VikRequest::getInt('einvid', 0, 'request');
2796 $einv_data = $this->loadEInvoiceDetails($einvid);
2797 if (!$einv_data) {
2798 die('Missing e-invoice ID');
2799 }
2800
2801 // force the output
2802 header("Content-type:text/xml");
2803 echo $einv_data['xml'];
2804
2805 exit;
2806 }
2807
2808 /**
2809 * Removes an electonic invoice. This is a 'driver action',
2810 * and so it's called before getBookingsData() in the view.
2811 * It also removes the analogic version in PDF of the invoice.
2812 *
2813 * @return void
2814 */
2815 public function removeEInvoice()
2816 {
2817 $einvid = VikRequest::getInt('einvid', '', 'request');
2818 $einv_data = $this->loadEInvoiceDetails($einvid);
2819 if (!$einv_data) {
2820 $this->setError('Missing e-invoice ID. Unable to delete the e-invoice.');
2821 return false;
2822 }
2823
2824 // get "transmission data" (if any)
2825 $trans_data = !empty($einv_data['trans_data']) ? json_decode($einv_data['trans_data']) : null;
2826 if (is_object($trans_data) && !empty($trans_data->invoice_mark)) {
2827 /**
2828 * This invoice was transmitted before, make sure to cancel it also from myDATA.
2829 * However, the endpoint requires a "mark" value for the invoice, which could be the
2830 * invoiceMark property upon a successful submission or the number we pass to compose
2831 * the XML of the electronic invoice (our progressive number). There are two "mark"
2832 * values, but we got errors for both, hence we don't know which one to use. We always
2833 * check if $trans_data->invoice_mark is not empty so that we know the invoice was
2834 * already transmitted before to myDATA and accepted.
2835 *
2836 * @todo what's the right invoice mark? the "number" is inside the XML that we generate
2837 * even before the transmission, while "invoice_mark" is returned in the myDATA response.
2838 */
2839 $mydata_invoice_mark = $einv_data['number'];
2840 $mydata_invoice_mark = $trans_data->invoice_mark;
2841
2842 // make the POST request
2843 $response = $this->myDATARequestPOST('CancelInvoice?mark=' . $mydata_invoice_mark);
2844 if ($response->code != 200) {
2845 // the request was not successful, and the XML invoices were not parsed at all by myDATA
2846 $this->setWarning(sprintf('Invalid response (code %s): %s', $response->code, htmlspecialchars($response->body)));
2847 $this->setWarning('Could not cancel the invoice from myDATA.');
2848 } else {
2849 // check the XML response
2850 $xml_result = $this->myDATAParseXMLResponse($response->body);
2851 if ($xml_result[0]) {
2852 // success! the invoice was cancelled from myDATA
2853 $this->setInfo(sprintf('Invoice mark %s (#%s) successfully cancelled from myDATA', $trans_data->invoice_mark, $einv_data['number']));
2854 }
2855 }
2856 }
2857
2858 // remove e-invoice
2859 $q = "DELETE FROM `#__vikbooking_einvoicing_data` WHERE `id`=".$einv_data['id'].";";
2860 $this->dbo->setQuery($q);
2861 $this->dbo->execute();
2862
2863 // remove analogic invoice for this booking
2864 $pdfremoved = false;
2865 if (!empty($einv_data['idorder'])) {
2866 $pdfname = '';
2867 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
2868 $this->dbo->setQuery($q);
2869 $this->dbo->execute();
2870 if ($this->dbo->getNumRows()) {
2871 $analogic = $this->dbo->loadAssoc();
2872 $pdfname = $analogic['file_name'];
2873 $q = "DELETE FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$einv_data['idorder'].";";
2874 $this->dbo->setQuery($q);
2875 $this->dbo->execute();
2876
2877 }
2878 $pdfpath = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . $pdfname;
2879 if (!empty($pdfname) && is_file($pdfpath)) {
2880 $pdfremoved = true;
2881 @unlink($pdfpath);
2882 }
2883 }
2884
2885 $this->setInfo(($pdfremoved ? 'Electronic and PDF invoices deleted' : 'Electronic invoice deleted'));
2886 }
2887
2888 /**
2889 * Validates the XML against the Schema.
2890 *
2891 * @param string $xml the xml string to validate
2892 *
2893 * @return null|boolean
2894 */
2895 protected function validateXmlAgainstSchema($xml) {
2896 if (!class_exists('DOMDocument')) {
2897 // we cannot validate the XML because DOMDocument is missing
2898 return null;
2899 }
2900
2901 $schema_path = VikBookingMydataAadeConstants::getSchemaPath();
2902
2903 libxml_use_internal_errors(true);
2904
2905 $dom = new DOMDocument();
2906 $dom->load($xml);
2907 if (!$dom->schemaValidate($schema_path)) {
2908 $this->setWarning('The schema validation of the electronic XML invoice returned errors, but they may be related to an unreadable schema.');
2909 $this->setWarning($this->libxml_display_errors());
2910 return false;
2911 }
2912
2913 return true;
2914 }
2915
2916 /**
2917 * Formats the XML errors occurred
2918 *
2919 * @return string the error string
2920 */
2921 protected function libxml_display_errors() {
2922 $errorstr = "";
2923 $errors = libxml_get_errors();
2924 foreach ($errors as $error) {
2925 $errorstr .= $this->libxml_display_error($error);
2926 }
2927 libxml_clear_errors();
2928
2929 return $errorstr;
2930 }
2931
2932 /**
2933 * Explanation of the XML error
2934 *
2935 * @param object $error the libxml error object
2936 *
2937 * @return string the explained error occurred
2938 */
2939 protected function libxml_display_error($error) {
2940 $return = "\n";
2941 switch ($error->level) {
2942 case LIBXML_ERR_WARNING :
2943 $return .= "Warning ".$error->code.": ";
2944 break;
2945 case LIBXML_ERR_ERROR :
2946 $return .= "Error ".$error->code.": ";
2947 break;
2948 case LIBXML_ERR_FATAL :
2949 $return .= "Fatal Error ".$error->code.": ";
2950 break;
2951 }
2952 $return .= trim($error->message);
2953 if ($error->file) {
2954 $return .= " in " . $error->file;
2955 }
2956 $return .= " on line " . $error->line . "\n";
2957
2958 return $return;
2959 }
2960
2961 /**
2962 * Override method to show the overlay content.
2963 * Used to display the edit form of the raw XML.
2964 * This method echoes the string to be displayed.
2965 *
2966 * @return void
2967 */
2968 public function printOverlayContent()
2969 {
2970 $content = VikRequest::getString('drivercontent', '', 'request');
2971 $einvid = VikRequest::getInt('einvid', '', 'request');
2972 if ($content == 'editEInvoice' && !empty($einvid)) {
2973 $einv_data = $this->loadEInvoiceDetails($einvid);
2974 if (!$einv_data) {
2975 return;
2976 }
2977
2978 // path to edit invoice layout file
2979 $fpath = $this->driverHelperPath . 'editeinvoice.php';
2980
2981 // load helper file and echo its content
2982 echo $this->loadHelperFile($fpath, $einv_data);
2983
2984 return;
2985 }
2986 }
2987
2988 /**
2989 * Updates the XML of an electonic invoice. This is a 'driver action',
2990 * and so it's called before getBookingsData() in the view.
2991 *
2992 * @return boolean
2993 */
2994 public function updateXmlEInvoice()
2995 {
2996 $einvid = VikRequest::getInt('einvid', '', 'request');
2997 $newxml = VikRequest::getString('newxml', '', 'request', VIKREQUEST_ALLOWRAW);
2998 $einv_data = $this->loadEInvoiceDetails($einvid);
2999 if (!$einv_data) {
3000 $this->setError('Invoice not found');
3001 return false;
3002 }
3003 if (empty($newxml)) {
3004 $this->setError('Empty XML content');
3005 return false;
3006 }
3007
3008 $jdate = new JDate;
3009 $data = new stdClass;
3010 $data->id = $einv_data['id'];
3011 $data->created_on = $jdate->toSql();
3012 $data->xml = $newxml;
3013
3014 return $this->updateEInvoice($data);
3015 }
3016
3017 /**
3018 * Extracts only numbers from a given string, by optionally
3019 * stripping the current year. Useful to find an invoice number.
3020 *
3021 * @param string $str the string to look for numbers
3022 * @param boolean $stripy whether to strip the current year
3023 *
3024 * @return string either an empty string or all numbers as a concatenated string
3025 */
3026 protected function getOnlyNumbers($str, $stripy = false)
3027 {
3028 if ($stripy) {
3029 $str = str_replace(date('Y'), '', $str);
3030 }
3031
3032 preg_match_all('/\d+/', $str, $matches);
3033
3034 return implode('', $matches[0]);
3035 }
3036 }
3037